pinomad 0.13.0 → 0.14.0
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/chunk-0dh2k3er.js +43 -0
- package/dist/{chunk-kmzzjv53.js → chunk-1mjbt2z2.js} +143 -10
- package/dist/{chunk-s0h7c7da.js → chunk-1rdbnvy6.js} +1 -1
- package/dist/{chunk-53xv3fny.js → chunk-35szq73k.js} +2446 -416
- package/dist/{chunk-aapsc9g0.js → chunk-5j7ybhjj.js} +1952 -295
- package/dist/{chunk-m51mq8vh.js → chunk-5pv0g1nj.js} +12 -10
- package/dist/{chunk-7mn38n4f.js → chunk-63ymn5c9.js} +2 -2
- package/dist/{chunk-563fnad9.js → chunk-6j6hkfbj.js} +8 -4
- package/dist/{chunk-pqe7q1s7.js → chunk-6vbp1vva.js} +2 -1
- package/dist/{chunk-g3ahp7b9.js → chunk-9trbbz4r.js} +1 -1
- package/dist/{chunk-d9mq5d46.js → chunk-b0nwq1dt.js} +10 -8
- package/dist/chunk-dsr19q5h.js +23 -0
- package/dist/{chunk-6th9xv39.js → chunk-e53c0s8z.js} +9 -7
- package/dist/{chunk-4fferbxw.js → chunk-e59k3bmz.js} +213 -30
- package/dist/chunk-eerpgvy9.js +315 -0
- package/dist/chunk-eewxy4cp.js +240 -0
- package/dist/{chunk-maqv34cs.js → chunk-evbv1fkn.js} +3 -3
- package/dist/{chunk-ra86wjp5.js → chunk-ex5yr09h.js} +11 -6
- package/dist/{chunk-t91zt5f0.js → chunk-hg9gs396.js} +1 -1
- package/dist/{chunk-90g1cg9n.js → chunk-mtkvft88.js} +1 -1
- package/dist/{chunk-wd7aracm.js → chunk-mxwcnnkg.js} +1 -303
- package/dist/{chunk-d4tq62yr.js → chunk-w927q9m1.js} +12 -11
- package/dist/pinomad.js +12 -12
- package/package.json +5 -1
- package/web/assets/{core-BHm2gdHL.js → core-BWHGsFpV.js} +1 -1
- package/web/assets/{highlighted-diff-DrHvSLK9.js → highlighted-diff-DJ7KZCpS.js} +2 -2
- package/web/assets/{highlights-1aLwssz-.js → highlights-CyEmo6OI.js} +1 -1
- package/web/assets/{highlights-themes-C1p4pFYS.js → highlights-themes-CSNkBojy.js} +1 -1
- package/web/assets/index-C_qkbyL7.css +1 -0
- package/web/assets/index-DefQtCI2.js +396 -0
- package/web/assets/{shiki-BwyCm3kZ.js → shiki-BzCmRG-v.js} +2 -2
- package/web/assets/{shiki-langs-CMlALLSg.js → shiki-langs-DOkjyV4d.js} +1 -1
- package/web/assets/{stream-tokenizer-BlgIhw0y.js → stream-tokenizer-CBPqFe4Z.js} +1 -1
- package/web/assets/{theming-themes-DDvoqEqT.js → theming-themes-wKVca0aB.js} +1 -1
- package/web/assets/{xterm-view-BnKOnF3C.js → xterm-view-CKa1-QCM.js} +12 -12
- package/web/index.html +3 -2
- package/web/theme-boot.js +11 -0
- package/dist/chunk-sd640dt7.js +0 -22
- package/dist/chunk-t6fw0wa9.js +0 -96
- package/web/assets/index-A_AUJoO6.css +0 -1
- package/web/assets/index-BafYMKiN.js +0 -396
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
openHostSettings,
|
|
6
6
|
sameList,
|
|
7
7
|
settingsDefaults
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-6j6hkfbj.js";
|
|
9
9
|
import {
|
|
10
10
|
DevicesDoc,
|
|
11
11
|
RevocationsDoc,
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
loadHostKey,
|
|
15
15
|
loadRelayKey,
|
|
16
16
|
pairingOffers
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-hg9gs396.js";
|
|
18
18
|
import {
|
|
19
19
|
REMOVAL_NOTICE_FILE,
|
|
20
20
|
markRemoved,
|
|
@@ -30,9 +30,21 @@ import {
|
|
|
30
30
|
relayHostId,
|
|
31
31
|
relayWsBase,
|
|
32
32
|
startRelayLink
|
|
33
|
-
} from "./chunk-
|
|
33
|
+
} from "./chunk-9trbbz4r.js";
|
|
34
|
+
import {
|
|
35
|
+
installCommand,
|
|
36
|
+
npmBeside,
|
|
37
|
+
outputTail,
|
|
38
|
+
packagePrefix,
|
|
39
|
+
parseRelease,
|
|
40
|
+
viewCommand
|
|
41
|
+
} from "./chunk-0dh2k3er.js";
|
|
42
|
+
import {
|
|
43
|
+
serviceManager
|
|
44
|
+
} from "./chunk-mxwcnnkg.js";
|
|
34
45
|
import {
|
|
35
46
|
ATTACHMENT_TTL_MS,
|
|
47
|
+
HOST_UPGRADING_ERROR,
|
|
36
48
|
INVALID_FRAME_CLOSE_CODE,
|
|
37
49
|
MAX_ATTACHMENTS_PER_MESSAGE,
|
|
38
50
|
MAX_ATTACHMENT_NAME_LENGTH,
|
|
@@ -40,9 +52,12 @@ import {
|
|
|
40
52
|
MAX_PENDING_ATTACHMENT_BYTES,
|
|
41
53
|
MAX_TRANSCRIPT_PAGE,
|
|
42
54
|
PROTOCOL,
|
|
55
|
+
QuestionItemSchema,
|
|
56
|
+
QuestionSchema,
|
|
43
57
|
RTC_DATA_CHANNEL_LABEL,
|
|
44
58
|
RTC_DEFAULT_STUN,
|
|
45
59
|
RTC_MAX_RECORD_BYTES,
|
|
60
|
+
TodoItemSchema,
|
|
46
61
|
UNAUTHORIZED_CLOSE_CODE,
|
|
47
62
|
attachmentBeginProblem,
|
|
48
63
|
attachmentBlock,
|
|
@@ -50,6 +65,7 @@ import {
|
|
|
50
65
|
base64DecodedLength,
|
|
51
66
|
isAttachmentId,
|
|
52
67
|
isCustomProviderApi,
|
|
68
|
+
isProcessActive,
|
|
53
69
|
organize,
|
|
54
70
|
parseAttachmentBlock,
|
|
55
71
|
parseClientFrame,
|
|
@@ -57,7 +73,7 @@ import {
|
|
|
57
73
|
rtcCandidateType,
|
|
58
74
|
sdpCandidates,
|
|
59
75
|
withRtcCandidates
|
|
60
|
-
} from "./chunk-
|
|
76
|
+
} from "./chunk-1mjbt2z2.js";
|
|
61
77
|
import {
|
|
62
78
|
ENDPOINT_SET_SCHEMA_VERSION,
|
|
63
79
|
MAX_HOST_ENDPOINT_URL_LENGTH,
|
|
@@ -73,29 +89,348 @@ import {
|
|
|
73
89
|
parseEndpointSet,
|
|
74
90
|
respondIK,
|
|
75
91
|
toBase64Url
|
|
76
|
-
} from "./chunk-
|
|
92
|
+
} from "./chunk-6vbp1vva.js";
|
|
77
93
|
import {
|
|
78
94
|
DEFAULT_DATA_DIR
|
|
79
95
|
} from "./chunk-ngf1zatm.js";
|
|
80
96
|
import {
|
|
81
97
|
packagedVersion,
|
|
82
98
|
webRoot
|
|
83
|
-
} from "./chunk-
|
|
99
|
+
} from "./chunk-1rdbnvy6.js";
|
|
84
100
|
import {
|
|
85
101
|
__require
|
|
86
102
|
} from "./chunk-df32avw6.js";
|
|
87
103
|
|
|
88
104
|
// apps/host/src/main.ts
|
|
89
|
-
import { randomUUID as
|
|
105
|
+
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
90
106
|
import { mkdir as mkdir6, readFile as readFile7, writeFile as writeFile5 } from "node:fs/promises";
|
|
91
107
|
import { homedir as homedir3 } from "node:os";
|
|
92
|
-
import { join as
|
|
108
|
+
import { join as join13, resolve as resolve7 } from "node:path";
|
|
93
109
|
import { parseArgs } from "node:util";
|
|
94
110
|
import { createModels } from "@earendil-works/pi-ai/models";
|
|
95
111
|
import { fauxAssistantMessage, fauxProvider } from "@earendil-works/pi-ai/providers/faux";
|
|
96
112
|
import { ModelRuntime, SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
97
113
|
import { createNodePhotonImages } from "@earendil-works/pi-durable/images/node";
|
|
98
114
|
|
|
115
|
+
// apps/host/src/upgrade/host-upgrader.ts
|
|
116
|
+
import { execFile, spawn } from "node:child_process";
|
|
117
|
+
import { promisify } from "node:util";
|
|
118
|
+
|
|
119
|
+
// apps/host/src/upgrade/upgrader.ts
|
|
120
|
+
var CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000;
|
|
121
|
+
var AUTO_TICK_MS = 60 * 1000;
|
|
122
|
+
var AUTO_MIN_AGE_MS = 3 * 24 * 60 * 60 * 1000;
|
|
123
|
+
var AUTO_QUIET_MS = 15 * 60 * 1000;
|
|
124
|
+
var sameUpgrade = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
125
|
+
var message = (error) => error instanceof Error ? error.message : String(error);
|
|
126
|
+
function compareVersions(a, b) {
|
|
127
|
+
const parse = (version) => {
|
|
128
|
+
const [core = "", pre] = version.split("-", 2);
|
|
129
|
+
return { parts: core.split(".").map((part) => Number.parseInt(part, 10) || 0), pre };
|
|
130
|
+
};
|
|
131
|
+
const x = parse(a);
|
|
132
|
+
const y = parse(b);
|
|
133
|
+
for (let i = 0;i < Math.max(x.parts.length, y.parts.length); i++) {
|
|
134
|
+
const diff = (x.parts[i] ?? 0) - (y.parts[i] ?? 0);
|
|
135
|
+
if (diff !== 0)
|
|
136
|
+
return diff;
|
|
137
|
+
}
|
|
138
|
+
if (x.pre === y.pre)
|
|
139
|
+
return 0;
|
|
140
|
+
if (x.pre === undefined)
|
|
141
|
+
return 1;
|
|
142
|
+
if (y.pre === undefined)
|
|
143
|
+
return -1;
|
|
144
|
+
return x.pre < y.pre ? -1 : 1;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
class HostUpgrader {
|
|
148
|
+
#options;
|
|
149
|
+
#published;
|
|
150
|
+
#stops = [];
|
|
151
|
+
#check;
|
|
152
|
+
#lastActivity;
|
|
153
|
+
#admitting = 0;
|
|
154
|
+
constructor(options) {
|
|
155
|
+
this.#options = options;
|
|
156
|
+
this.#lastActivity = options.now();
|
|
157
|
+
this.#published = new Published({
|
|
158
|
+
current: options.current,
|
|
159
|
+
available: null,
|
|
160
|
+
blocked: null,
|
|
161
|
+
phase: "idle",
|
|
162
|
+
target: null,
|
|
163
|
+
attempt: 0,
|
|
164
|
+
checking: false,
|
|
165
|
+
checkedAt: null,
|
|
166
|
+
checkError: null,
|
|
167
|
+
failure: null
|
|
168
|
+
}, sameUpgrade);
|
|
169
|
+
}
|
|
170
|
+
get value() {
|
|
171
|
+
return this.#published.value;
|
|
172
|
+
}
|
|
173
|
+
subscribe(listener) {
|
|
174
|
+
return this.#published.subscribe(listener);
|
|
175
|
+
}
|
|
176
|
+
get installing() {
|
|
177
|
+
return this.value.phase === "installing";
|
|
178
|
+
}
|
|
179
|
+
async begin() {
|
|
180
|
+
const { busy, mode, every } = this.#options;
|
|
181
|
+
this.#stops.push(busy.subscribe((value) => {
|
|
182
|
+
this.touch();
|
|
183
|
+
if (!value)
|
|
184
|
+
this.#installIfIdle();
|
|
185
|
+
}), mode.subscribe((value) => {
|
|
186
|
+
if (value !== "off")
|
|
187
|
+
this.check().catch(() => {});
|
|
188
|
+
}), every(CHECK_INTERVAL_MS, () => void this.check().catch(() => {})), every(AUTO_TICK_MS, () => this.#autoTick()));
|
|
189
|
+
await this.#refreshBlocked();
|
|
190
|
+
if (mode.value !== "off")
|
|
191
|
+
this.check().catch(() => {});
|
|
192
|
+
}
|
|
193
|
+
close() {
|
|
194
|
+
for (const stop of this.#stops.splice(0))
|
|
195
|
+
stop();
|
|
196
|
+
}
|
|
197
|
+
touch() {
|
|
198
|
+
this.#lastActivity = this.#options.now();
|
|
199
|
+
}
|
|
200
|
+
admit() {
|
|
201
|
+
if (this.installing)
|
|
202
|
+
throw new Error(HOST_UPGRADING_ERROR);
|
|
203
|
+
this.touch();
|
|
204
|
+
this.#admitting++;
|
|
205
|
+
let released = false;
|
|
206
|
+
return () => {
|
|
207
|
+
if (released)
|
|
208
|
+
return;
|
|
209
|
+
released = true;
|
|
210
|
+
this.#admitting--;
|
|
211
|
+
this.touch();
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
check() {
|
|
215
|
+
if (this.#options.mode.value === "off")
|
|
216
|
+
return Promise.reject(new Error("Upgrade checks are off on this host"));
|
|
217
|
+
this.#check ??= this.#runCheck().finally(() => {
|
|
218
|
+
this.#check = undefined;
|
|
219
|
+
});
|
|
220
|
+
return this.#check;
|
|
221
|
+
}
|
|
222
|
+
async start(when) {
|
|
223
|
+
await this.#refreshBlocked();
|
|
224
|
+
const { blocked, available, phase } = this.value;
|
|
225
|
+
if (blocked !== null)
|
|
226
|
+
throw new Error(`This host can't upgrade itself (${blocked})`);
|
|
227
|
+
if (phase !== "idle")
|
|
228
|
+
throw new Error("An upgrade is already in progress");
|
|
229
|
+
if (available === null)
|
|
230
|
+
throw new Error("No newer release");
|
|
231
|
+
this.#begin(when, available.version);
|
|
232
|
+
}
|
|
233
|
+
#begin(when, version) {
|
|
234
|
+
this.#set({ attempt: this.value.attempt + 1 });
|
|
235
|
+
if (when === "now" || !this.#busy())
|
|
236
|
+
this.#install(version);
|
|
237
|
+
else
|
|
238
|
+
this.#set({ phase: "waiting", target: version });
|
|
239
|
+
}
|
|
240
|
+
#busy() {
|
|
241
|
+
return this.#options.busy.value || this.#admitting > 0;
|
|
242
|
+
}
|
|
243
|
+
#installIfIdle() {
|
|
244
|
+
if (this.value.phase === "waiting" && !this.#busy())
|
|
245
|
+
this.#install(this.value.target);
|
|
246
|
+
}
|
|
247
|
+
cancel() {
|
|
248
|
+
if (this.value.phase === "installing")
|
|
249
|
+
throw new Error("The upgrade is installing and can't be cancelled");
|
|
250
|
+
if (this.value.phase === "waiting")
|
|
251
|
+
this.#set({ phase: "idle", target: null });
|
|
252
|
+
}
|
|
253
|
+
async#runCheck() {
|
|
254
|
+
this.#set({ checking: true });
|
|
255
|
+
try {
|
|
256
|
+
const release = await this.#options.latest();
|
|
257
|
+
const newer = compareVersions(release.version, this.#options.current) > 0;
|
|
258
|
+
this.#set({
|
|
259
|
+
checking: false,
|
|
260
|
+
checkedAt: this.#options.now(),
|
|
261
|
+
checkError: null,
|
|
262
|
+
available: newer ? {
|
|
263
|
+
version: release.version,
|
|
264
|
+
publishedAt: release.publishedAt,
|
|
265
|
+
crossesMajor: release.protocolMajor !== this.#options.protocolMajor
|
|
266
|
+
} : null
|
|
267
|
+
});
|
|
268
|
+
} catch (error) {
|
|
269
|
+
this.#set({ checking: false, checkedAt: this.#options.now(), checkError: message(error) });
|
|
270
|
+
throw error;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
async#refreshBlocked() {
|
|
274
|
+
this.#set({ blocked: await this.#options.blocked() });
|
|
275
|
+
}
|
|
276
|
+
#autoTick() {
|
|
277
|
+
if (this.#busy())
|
|
278
|
+
this.touch();
|
|
279
|
+
this.#installIfIdle();
|
|
280
|
+
const candidate = this.#autoCandidate();
|
|
281
|
+
if (candidate === undefined)
|
|
282
|
+
return;
|
|
283
|
+
this.#refreshBlocked().then(() => {
|
|
284
|
+
if (this.#autoCandidate() === candidate)
|
|
285
|
+
this.#begin("idle", candidate);
|
|
286
|
+
}, () => {});
|
|
287
|
+
}
|
|
288
|
+
#autoCandidate() {
|
|
289
|
+
const { mode, now } = this.#options;
|
|
290
|
+
const { available, blocked, phase, failure } = this.value;
|
|
291
|
+
if (mode.value !== "auto" || phase !== "idle" || blocked !== null || available === null)
|
|
292
|
+
return;
|
|
293
|
+
if (available.crossesMajor || failure?.version === available.version)
|
|
294
|
+
return;
|
|
295
|
+
if (now() - available.publishedAt < AUTO_MIN_AGE_MS || this.#busy() || now() - this.#lastActivity < AUTO_QUIET_MS) {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
return available.version;
|
|
299
|
+
}
|
|
300
|
+
async#install(version) {
|
|
301
|
+
this.#set({ phase: "installing", target: version });
|
|
302
|
+
try {
|
|
303
|
+
await this.#options.drain?.();
|
|
304
|
+
} catch (error) {
|
|
305
|
+
this.#set({ phase: "idle", target: null, failure: { version, message: message(error), attempt: this.value.attempt } });
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
let result;
|
|
309
|
+
try {
|
|
310
|
+
result = await this.#options.install(version);
|
|
311
|
+
} catch (error) {
|
|
312
|
+
result = { ok: false, message: message(error) };
|
|
313
|
+
}
|
|
314
|
+
if (result.ok) {
|
|
315
|
+
this.#options.restart();
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
this.#set({ phase: "idle", target: null, failure: { version, message: result.message, attempt: this.value.attempt } });
|
|
319
|
+
}
|
|
320
|
+
#set(patch) {
|
|
321
|
+
this.#published.set({ ...this.value, ...patch });
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// apps/host/src/upgrade/host-upgrader.ts
|
|
326
|
+
var exec = promisify(execFile);
|
|
327
|
+
var withPath = (npm) => npm.binDir === undefined ? process.env : { ...process.env, PATH: `${npm.binDir}:${process.env.PATH ?? ""}` };
|
|
328
|
+
var UPGRADE_EXIT_CODE = 75;
|
|
329
|
+
async function upgradeBlocker(dataDir, {
|
|
330
|
+
packaged = packagedVersion !== undefined && packagePrefix() !== undefined,
|
|
331
|
+
manager = serviceManager
|
|
332
|
+
} = {}) {
|
|
333
|
+
if (!packaged)
|
|
334
|
+
return "source";
|
|
335
|
+
try {
|
|
336
|
+
const service = manager();
|
|
337
|
+
const spec = await service.read();
|
|
338
|
+
if (spec === undefined || spec.dataDir !== dataDir)
|
|
339
|
+
return "no-service";
|
|
340
|
+
const state = await service.state();
|
|
341
|
+
if (!state.installed)
|
|
342
|
+
return "no-service";
|
|
343
|
+
if (state.stopped)
|
|
344
|
+
return "stopped";
|
|
345
|
+
return state.running ? null : "no-service";
|
|
346
|
+
} catch {
|
|
347
|
+
return "no-service";
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
async function latestRelease() {
|
|
351
|
+
const npm = viewCommand(npmBeside(process.execPath));
|
|
352
|
+
const { stdout } = await exec(npm.command, [...npm.args], { env: withPath(npm), timeout: 60000 });
|
|
353
|
+
return parseRelease(stdout);
|
|
354
|
+
}
|
|
355
|
+
function installRelease(version) {
|
|
356
|
+
const prefix = packagePrefix();
|
|
357
|
+
if (prefix === undefined)
|
|
358
|
+
return Promise.resolve({ ok: false, message: "pinomad isn't a global npm install" });
|
|
359
|
+
const npm = installCommand(npmBeside(process.execPath), prefix, `pinomad@${version}`);
|
|
360
|
+
return new Promise((resolve) => {
|
|
361
|
+
let output = "";
|
|
362
|
+
const child = spawn(npm.command, [...npm.args], { env: withPath(npm), stdio: ["ignore", "pipe", "pipe"] });
|
|
363
|
+
const collect = (chunk) => {
|
|
364
|
+
output = (output + chunk.toString("utf8")).slice(-64 * 1024);
|
|
365
|
+
};
|
|
366
|
+
child.stdout.on("data", collect);
|
|
367
|
+
child.stderr.on("data", collect);
|
|
368
|
+
child.once("error", (error) => resolve({ ok: false, message: error.message }));
|
|
369
|
+
child.once("exit", (code, signal) => resolve(code === 0 ? { ok: true } : { ok: false, message: outputTail(output) || `npm exited with ${signal ?? code}` }));
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
async function busyValue(harness, context) {
|
|
373
|
+
const watch = await harness.watchTaskGraph(context);
|
|
374
|
+
const busy = new Published(Object.keys(watch.value.tasks).length > 0, (a, b) => a === b);
|
|
375
|
+
watch.start((call) => {
|
|
376
|
+
if (call.kind === "value")
|
|
377
|
+
busy.set(Object.keys(call.value.tasks).length > 0);
|
|
378
|
+
return Promise.resolve();
|
|
379
|
+
});
|
|
380
|
+
return Object.assign(busy, { stop: () => watch.stop() });
|
|
381
|
+
}
|
|
382
|
+
function mergedBusy(tasks, processes) {
|
|
383
|
+
if (processes === undefined)
|
|
384
|
+
return tasks;
|
|
385
|
+
const busy = new Published(tasks.value || processes.value, (a, b) => a === b);
|
|
386
|
+
const stopTasks = tasks.subscribe((value) => busy.set(value || processes.value));
|
|
387
|
+
const stopProcesses = processes.subscribe((value) => busy.set(tasks.value || value));
|
|
388
|
+
return Object.assign(busy, {
|
|
389
|
+
stop: () => {
|
|
390
|
+
stopTasks();
|
|
391
|
+
stopProcesses();
|
|
392
|
+
tasks.stop();
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
function modeValue(settings) {
|
|
397
|
+
const mode = new Published(settings.value.autoUpgrade ?? "notify", (a, b) => a === b);
|
|
398
|
+
const stop = settings.subscribe((value) => mode.set(value.autoUpgrade ?? "notify"));
|
|
399
|
+
return Object.assign(mode, { stop });
|
|
400
|
+
}
|
|
401
|
+
var every = (ms, run) => {
|
|
402
|
+
const timer = setInterval(run, ms);
|
|
403
|
+
timer.unref();
|
|
404
|
+
return () => clearInterval(timer);
|
|
405
|
+
};
|
|
406
|
+
async function startHostUpgrader(options) {
|
|
407
|
+
const taskBusy = await busyValue(options.harness, options.context);
|
|
408
|
+
const busy = mergedBusy(taskBusy, options.processesBusy);
|
|
409
|
+
const mode = modeValue(options.settings);
|
|
410
|
+
const { seams } = options;
|
|
411
|
+
const upgrader = new HostUpgrader({
|
|
412
|
+
current: options.current,
|
|
413
|
+
protocolMajor: PROTOCOL.major,
|
|
414
|
+
blocked: seams.blocked ?? (() => upgradeBlocker(options.dataDir)),
|
|
415
|
+
latest: seams.latest ?? latestRelease,
|
|
416
|
+
install: seams.install ?? installRelease,
|
|
417
|
+
restart: seams.restart,
|
|
418
|
+
...options.drain === undefined ? {} : { drain: options.drain },
|
|
419
|
+
busy,
|
|
420
|
+
mode,
|
|
421
|
+
now: Date.now,
|
|
422
|
+
every: seams.every ?? every
|
|
423
|
+
});
|
|
424
|
+
const close = upgrader.close.bind(upgrader);
|
|
425
|
+
upgrader.close = () => {
|
|
426
|
+
close();
|
|
427
|
+
busy.stop();
|
|
428
|
+
mode.stop();
|
|
429
|
+
};
|
|
430
|
+
await upgrader.begin();
|
|
431
|
+
return upgrader;
|
|
432
|
+
}
|
|
433
|
+
|
|
99
434
|
// apps/host/src/extensions/question.ts
|
|
100
435
|
import {
|
|
101
436
|
defineDoc,
|
|
@@ -103,55 +438,7 @@ import {
|
|
|
103
438
|
defineTask,
|
|
104
439
|
defineTool
|
|
105
440
|
} from "@earendil-works/pi-durable";
|
|
106
|
-
import { Type as Type2 } from "typebox";
|
|
107
|
-
|
|
108
|
-
// packages/protocol/src/presentation.ts
|
|
109
441
|
import { Type } from "typebox";
|
|
110
|
-
import { Value } from "typebox/value";
|
|
111
|
-
var TodoItemSchema = Type.Object({
|
|
112
|
-
text: Type.String({ minLength: 1 }),
|
|
113
|
-
status: Type.Union([Type.Literal("pending"), Type.Literal("in_progress"), Type.Literal("completed")])
|
|
114
|
-
});
|
|
115
|
-
var TodoSchema = Type.Object({ items: Type.Array(TodoItemSchema) });
|
|
116
|
-
var QuestionOptionSchema = Type.Object({
|
|
117
|
-
label: Type.String({ minLength: 1 }),
|
|
118
|
-
description: Type.Optional(Type.String())
|
|
119
|
-
});
|
|
120
|
-
var QuestionItemSchema = Type.Object({
|
|
121
|
-
question: Type.String({ minLength: 1 }),
|
|
122
|
-
header: Type.String({ minLength: 1, maxLength: 16 }),
|
|
123
|
-
options: Type.Array(QuestionOptionSchema, { minItems: 2, maxItems: 4 }),
|
|
124
|
-
multiSelect: Type.Optional(Type.Boolean())
|
|
125
|
-
});
|
|
126
|
-
var QuestionAnswerSchema = Type.Object({
|
|
127
|
-
selected: Type.Array(Type.String()),
|
|
128
|
-
other: Type.Optional(Type.String())
|
|
129
|
-
});
|
|
130
|
-
var QuestionResolutionSchema = Type.Union([
|
|
131
|
-
Type.Object({ outcome: Type.Literal("answered"), answers: Type.Array(QuestionAnswerSchema), at: Type.Number() }),
|
|
132
|
-
Type.Object({ outcome: Type.Literal("dismissed"), at: Type.Number() }),
|
|
133
|
-
Type.Object({ outcome: Type.Literal("cancelled"), at: Type.Number() })
|
|
134
|
-
]);
|
|
135
|
-
var QuestionRequestSchema = Type.Object({
|
|
136
|
-
id: Type.String({ minLength: 1 }),
|
|
137
|
-
questions: Type.Array(QuestionItemSchema, { minItems: 1, maxItems: 4 }),
|
|
138
|
-
askedAt: Type.Number(),
|
|
139
|
-
resolution: Type.Optional(QuestionResolutionSchema)
|
|
140
|
-
});
|
|
141
|
-
var QuestionSchema = Type.Object({ requests: Type.Array(QuestionRequestSchema) });
|
|
142
|
-
var DiffSchema = Type.Object({ patch: Type.String() });
|
|
143
|
-
var CodemodeCallSchema = Type.Object({
|
|
144
|
-
id: Type.Optional(Type.Number()),
|
|
145
|
-
name: Type.String(),
|
|
146
|
-
args: Type.String(),
|
|
147
|
-
status: Type.Union([Type.Literal("running"), Type.Literal("ok"), Type.Literal("error"), Type.Literal("cancelled")]),
|
|
148
|
-
durationMs: Type.Optional(Type.Number()),
|
|
149
|
-
error: Type.Optional(Type.String()),
|
|
150
|
-
details: Type.Optional(Type.Unknown())
|
|
151
|
-
});
|
|
152
|
-
var CodemodeSchema = Type.Object({ code: Type.Optional(Type.String()), calls: Type.Array(CodemodeCallSchema) });
|
|
153
|
-
|
|
154
|
-
// apps/host/src/extensions/question.ts
|
|
155
442
|
var QuestionDoc = defineDoc({
|
|
156
443
|
kind: "question.requests",
|
|
157
444
|
version: 1,
|
|
@@ -227,8 +514,8 @@ var askUserQuestion = defineTool({
|
|
|
227
514
|
name: "ask_user_question",
|
|
228
515
|
callers: ["model"],
|
|
229
516
|
description: "Ask the user 1–4 multiple-choice questions when their decision is needed to proceed (ambiguous requirements, choosing between approaches, scope). Blocks until answered. The user can always type a custom answer, so never add an 'Other' option. Keep everything short: headers a word or two, option labels a few words. Usually leave option descriptions out; add one only when the label alone is ambiguous, as one line of at most ~60 characters.",
|
|
230
|
-
parameters:
|
|
231
|
-
questions:
|
|
517
|
+
parameters: Type.Object({
|
|
518
|
+
questions: Type.Array(QuestionItemSchema, { minItems: 1, maxItems: 4 })
|
|
232
519
|
}),
|
|
233
520
|
replay: "safe",
|
|
234
521
|
execute: async (args, api, context) => {
|
|
@@ -297,31 +584,31 @@ function createCodemode() {
|
|
|
297
584
|
}
|
|
298
585
|
|
|
299
586
|
// apps/host/src/extensions/context.ts
|
|
300
|
-
import { dirname as dirname2, sep as
|
|
587
|
+
import { dirname as dirname2, sep as sep5 } from "node:path";
|
|
301
588
|
import { defineExtension as defineExtension2, section } from "@earendil-works/pi-durable";
|
|
302
589
|
import { formatSkillsForPrompt, loadProjectContextFiles } from "@earendil-works/pi-coding-agent";
|
|
303
590
|
|
|
304
591
|
// apps/host/src/skills.ts
|
|
305
|
-
import { execFile as
|
|
592
|
+
import { execFile as execFile3 } from "node:child_process";
|
|
306
593
|
import { existsSync } from "node:fs";
|
|
307
594
|
import { realpath as realpath4, stat as stat4 } from "node:fs/promises";
|
|
308
|
-
import { dirname, join as join4, relative as relative2, resolve as resolve3, sep as
|
|
309
|
-
import { promisify as
|
|
595
|
+
import { dirname, join as join4, relative as relative2, resolve as resolve3, sep as sep4 } from "node:path";
|
|
596
|
+
import { promisify as promisify3 } from "node:util";
|
|
310
597
|
import { AgentDoc as AgentDoc3 } from "@earendil-works/pi-durable";
|
|
311
598
|
import { loadSkills } from "@earendil-works/pi-coding-agent";
|
|
312
599
|
|
|
313
600
|
// apps/host/src/checkout.ts
|
|
314
|
-
import { execFile, spawn } from "node:child_process";
|
|
601
|
+
import { execFile as execFile2, spawn as spawn2 } from "node:child_process";
|
|
315
602
|
import { randomBytes } from "node:crypto";
|
|
316
603
|
import { realpath, rm, stat } from "node:fs/promises";
|
|
317
604
|
import { tmpdir } from "node:os";
|
|
318
605
|
import { join, relative, sep } from "node:path";
|
|
319
|
-
import { promisify } from "node:util";
|
|
320
|
-
var run =
|
|
606
|
+
import { promisify as promisify2 } from "node:util";
|
|
607
|
+
var run = promisify2(execFile2);
|
|
321
608
|
var git = (dir, args, env) => run("git", ["-C", dir, ...args], env === undefined ? {} : { env: { ...process.env, ...env } });
|
|
322
609
|
function gitCapped(dir, args, maxBytes, env) {
|
|
323
610
|
return new Promise((resolve, reject) => {
|
|
324
|
-
const child =
|
|
611
|
+
const child = spawn2("git", ["-C", dir, ...args], env === undefined ? {} : { env: { ...process.env, ...env } });
|
|
325
612
|
const chunks = [];
|
|
326
613
|
let size = 0;
|
|
327
614
|
let killed = false;
|
|
@@ -446,14 +733,14 @@ async function changesOf(dir, base, maxBytes = 1024 * 1024) {
|
|
|
446
733
|
}
|
|
447
734
|
|
|
448
735
|
// apps/host/src/files.ts
|
|
449
|
-
import { spawn as
|
|
736
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
450
737
|
import { opendir, realpath as realpath3, stat as stat3 } from "node:fs/promises";
|
|
451
|
-
import { join as join3, resolve as resolve2, sep as
|
|
738
|
+
import { join as join3, resolve as resolve2, sep as sep3 } from "node:path";
|
|
452
739
|
import { AgentDoc as AgentDoc2 } from "@earendil-works/pi-durable";
|
|
453
740
|
|
|
454
741
|
// apps/host/src/organization.ts
|
|
455
742
|
import { mkdir, realpath as realpath2, stat as stat2 } from "node:fs/promises";
|
|
456
|
-
import { basename, join as join2, resolve } from "node:path";
|
|
743
|
+
import { basename, join as join2, resolve, sep as sep2 } from "node:path";
|
|
457
744
|
import { clampThinkingLevel } from "@earendil-works/pi-ai";
|
|
458
745
|
import {
|
|
459
746
|
AgentDoc,
|
|
@@ -759,6 +1046,17 @@ async function familyRoot(harness, id, context) {
|
|
|
759
1046
|
}
|
|
760
1047
|
return current;
|
|
761
1048
|
}
|
|
1049
|
+
async function subagentWorktreeBase(harness, dataDir, dispatcher, context) {
|
|
1050
|
+
const unavailable = (why) => new Error(`checkout "worktree" needs ${why}; use "shared"`);
|
|
1051
|
+
const cwd = (await harness.snapshot(AgentDoc, dispatcher, context))?.cwd;
|
|
1052
|
+
if (cwd === undefined || cwd.startsWith(join2(dataDir, "chats") + sep2))
|
|
1053
|
+
throw unavailable("a project conversation, not a Chat");
|
|
1054
|
+
const head = await gitBase(cwd);
|
|
1055
|
+
if (head === undefined)
|
|
1056
|
+
throw unavailable("a Git repository with at least one commit");
|
|
1057
|
+
const record = checkoutAt((await harness.snapshot(IndexDoc, context))?.checkouts, cwd);
|
|
1058
|
+
return record === undefined ? head : { repo: record.repo, subdir: record.subdir, base: head.base };
|
|
1059
|
+
}
|
|
762
1060
|
async function cleanupArchivedWorktrees(harness, rootId, context) {
|
|
763
1061
|
const index = await harness.snapshot(IndexDoc, context);
|
|
764
1062
|
const checkouts = index?.checkouts ?? [];
|
|
@@ -830,7 +1128,7 @@ function createCandidateLister(enumerate, options = {}) {
|
|
|
830
1128
|
}
|
|
831
1129
|
function gitLines(cwd, args, limits) {
|
|
832
1130
|
return new Promise((resolvePromise, rejectPromise) => {
|
|
833
|
-
const child =
|
|
1131
|
+
const child = spawn3("git", ["-C", cwd, ...args], { stdio: ["ignore", "pipe", "pipe"] });
|
|
834
1132
|
const chunks = [];
|
|
835
1133
|
let bytes = 0;
|
|
836
1134
|
let truncated = false;
|
|
@@ -878,7 +1176,7 @@ function gitLines(cwd, args, limits) {
|
|
|
878
1176
|
}
|
|
879
1177
|
async function inWorkTree(root) {
|
|
880
1178
|
return await new Promise((resolvePromise) => {
|
|
881
|
-
const child =
|
|
1179
|
+
const child = spawn3("git", ["-C", root, "rev-parse", "--is-inside-work-tree"], { stdio: ["ignore", "pipe", "ignore"] });
|
|
882
1180
|
let out = "";
|
|
883
1181
|
child.stdout.on("data", (chunk) => out += chunk.toString("utf8"));
|
|
884
1182
|
child.on("error", () => resolvePromise(false));
|
|
@@ -887,7 +1185,7 @@ async function inWorkTree(root) {
|
|
|
887
1185
|
}
|
|
888
1186
|
async function hasHead(root) {
|
|
889
1187
|
return await new Promise((resolvePromise) => {
|
|
890
|
-
const child =
|
|
1188
|
+
const child = spawn3("git", ["-C", root, "rev-parse", "--verify", "-q", "HEAD"], { stdio: ["ignore", "ignore", "ignore"] });
|
|
891
1189
|
child.on("error", () => resolvePromise(false));
|
|
892
1190
|
child.on("close", (code) => resolvePromise(code === 0));
|
|
893
1191
|
});
|
|
@@ -1008,7 +1306,7 @@ async function listFiles(harness, dataDir, enumerate, scope, query, context) {
|
|
|
1008
1306
|
const cwd = agent.cwd;
|
|
1009
1307
|
if (cwd === undefined)
|
|
1010
1308
|
throw new Error(`Conversation ${scope.conversationId} has no checkout`);
|
|
1011
|
-
if (cwd.startsWith(join3(dataDir, "chats") +
|
|
1309
|
+
if (cwd.startsWith(join3(dataDir, "chats") + sep3))
|
|
1012
1310
|
return { files: [], truncated: false };
|
|
1013
1311
|
const record = checkoutAt((await harness.snapshot(IndexDoc, context))?.checkouts, cwd);
|
|
1014
1312
|
const ready = record === undefined || await worktreeSettled(record);
|
|
@@ -1040,7 +1338,7 @@ async function listFiles(harness, dataDir, enumerate, scope, query, context) {
|
|
|
1040
1338
|
}
|
|
1041
1339
|
|
|
1042
1340
|
// apps/host/src/skills.ts
|
|
1043
|
-
var
|
|
1341
|
+
var exec2 = promisify3(execFile3);
|
|
1044
1342
|
function invocableSkillName(name) {
|
|
1045
1343
|
return name.length <= 64 && /^[a-z0-9-]+$/.test(name) && !name.startsWith("-") && !name.endsWith("-") && !name.includes("--");
|
|
1046
1344
|
}
|
|
@@ -1104,10 +1402,10 @@ async function committedSkillFiles(gitRoot, layers) {
|
|
|
1104
1402
|
if (!await inWorkTree(gitRoot) || !await hasHead(gitRoot))
|
|
1105
1403
|
return null;
|
|
1106
1404
|
const pathspecs = layers.map((dir) => {
|
|
1107
|
-
const rel = relative2(gitRoot, dir).split(
|
|
1405
|
+
const rel = relative2(gitRoot, dir).split(sep4).join("/");
|
|
1108
1406
|
return rel === "" ? ".agents/skills" : `${rel}/.agents/skills`;
|
|
1109
1407
|
});
|
|
1110
|
-
const { stdout } = await
|
|
1408
|
+
const { stdout } = await exec2("git", ["-C", gitRoot, "ls-tree", "-r", "-z", "--full-name", "--name-only", "HEAD", "--", ...pathspecs], { maxBuffer: 4 * 1024 * 1024 });
|
|
1111
1409
|
return new Set(stdout.split("\x00").filter((path) => path !== ""));
|
|
1112
1410
|
}
|
|
1113
1411
|
async function enumerateSkills(root, mode, agentsHome) {
|
|
@@ -1119,7 +1417,7 @@ async function enumerateSkills(root, mode, agentsHome) {
|
|
|
1119
1417
|
if (mode === "head" && gitRoot !== undefined) {
|
|
1120
1418
|
const committed = await committedSkillFiles(gitRoot, layers.dirs);
|
|
1121
1419
|
if (committed !== null) {
|
|
1122
|
-
project = project.filter((scoped) => committed.has(relative2(gitRoot, scoped.skill.filePath).split(
|
|
1420
|
+
project = project.filter((scoped) => committed.has(relative2(gitRoot, scoped.skill.filePath).split(sep4).join("/")));
|
|
1123
1421
|
}
|
|
1124
1422
|
}
|
|
1125
1423
|
}
|
|
@@ -1253,7 +1551,7 @@ function createContext(options) {
|
|
|
1253
1551
|
const seen = new Set;
|
|
1254
1552
|
files = [
|
|
1255
1553
|
...loadProjectContextFiles({ cwd: dirname2(checkout.repo), agentDir: agentsHome }),
|
|
1256
|
-
...loadProjectContextFiles({ cwd, agentDir: agentsHome }).filter((file) => file.path.startsWith(checkout.worktreeRoot +
|
|
1554
|
+
...loadProjectContextFiles({ cwd, agentDir: agentsHome }).filter((file) => file.path.startsWith(checkout.worktreeRoot + sep5))
|
|
1257
1555
|
].filter((file) => {
|
|
1258
1556
|
if (seen.has(file.path))
|
|
1259
1557
|
return false;
|
|
@@ -1280,21 +1578,336 @@ ${file.content}`).join(`
|
|
|
1280
1578
|
};
|
|
1281
1579
|
}
|
|
1282
1580
|
|
|
1581
|
+
// apps/host/src/extensions/context-breakdown.ts
|
|
1582
|
+
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
|
1583
|
+
import { defineDoc as defineDoc4, defineExtension as defineExtension3, GenerationTask, hook } from "@earendil-works/pi-durable";
|
|
1584
|
+
|
|
1585
|
+
// packages/client/src/context-usage.ts
|
|
1586
|
+
var contextTokens = (usage) => usage.totalTokens || usage.input + usage.output + usage.cacheRead + usage.cacheWrite;
|
|
1587
|
+
|
|
1588
|
+
// apps/host/src/context-breakdown.ts
|
|
1589
|
+
var CHARS_PER_TOKEN = 4;
|
|
1590
|
+
var tokensOf = (text) => Math.ceil(text.length / CHARS_PER_TOKEN);
|
|
1591
|
+
var MCP_TOOL = /^mcp__/;
|
|
1592
|
+
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1593
|
+
var arrayOf = (value) => Array.isArray(value) ? value : [];
|
|
1594
|
+
function textOf(value) {
|
|
1595
|
+
if (typeof value === "string")
|
|
1596
|
+
return value;
|
|
1597
|
+
if (isObject(value) && "parts" in value)
|
|
1598
|
+
return textOf(value.parts);
|
|
1599
|
+
return arrayOf(value).map((block) => isObject(block) && typeof block.text === "string" ? block.text : "").join(`
|
|
1600
|
+
`);
|
|
1601
|
+
}
|
|
1602
|
+
var sourcesOf = (payload) => [payload, payload.config, payload.context].filter(isObject);
|
|
1603
|
+
var systemItemsOf = (source) => [...arrayOf(source.messages), ...arrayOf(source.input)].filter((item) => isObject(item) && (item.role === "system" || item.role === "developer"));
|
|
1604
|
+
function systemTextOf(payload) {
|
|
1605
|
+
return sourcesOf(payload).flatMap((source) => [
|
|
1606
|
+
textOf(source.system),
|
|
1607
|
+
textOf(source.instructions),
|
|
1608
|
+
textOf(source.systemInstruction),
|
|
1609
|
+
...systemItemsOf(source).flatMap((item) => [
|
|
1610
|
+
textOf(item.content),
|
|
1611
|
+
...isObject(item.sections) ? Object.values(item.sections).filter((section2) => typeof section2 === "string") : []
|
|
1612
|
+
])
|
|
1613
|
+
]).filter((text) => text !== "").join(`
|
|
1614
|
+
`);
|
|
1615
|
+
}
|
|
1616
|
+
function toolsOf(payload) {
|
|
1617
|
+
const listed = sourcesOf(payload).flatMap((source) => [
|
|
1618
|
+
...arrayOf(source.tools),
|
|
1619
|
+
...arrayOf(isObject(source.toolConfig) ? source.toolConfig.tools : undefined),
|
|
1620
|
+
...systemItemsOf(source).flatMap((item) => [...arrayOf(item.tools), ...arrayOf(item.toolsAdded)])
|
|
1621
|
+
]);
|
|
1622
|
+
return listed.flatMap((tool) => isObject(tool) && Array.isArray(tool.functionDeclarations) ? tool.functionDeclarations.filter(isObject) : isObject(tool) ? [tool] : []);
|
|
1623
|
+
}
|
|
1624
|
+
function toolName(tool) {
|
|
1625
|
+
for (const holder of [tool, tool.function, tool.toolSpec]) {
|
|
1626
|
+
if (isObject(holder) && typeof holder.name === "string")
|
|
1627
|
+
return holder.name;
|
|
1628
|
+
}
|
|
1629
|
+
return;
|
|
1630
|
+
}
|
|
1631
|
+
function cut(text, tag) {
|
|
1632
|
+
const open = text.indexOf(`<${tag}>`);
|
|
1633
|
+
const close = open < 0 ? -1 : text.indexOf(`</${tag}>`, open);
|
|
1634
|
+
if (close < 0)
|
|
1635
|
+
return { inside: "", rest: text };
|
|
1636
|
+
return { inside: text.slice(open, close + tag.length + 3), rest: text.slice(0, open) + text.slice(close + tag.length + 3) };
|
|
1637
|
+
}
|
|
1638
|
+
function splitCodemode(section2, namespaces) {
|
|
1639
|
+
const starts = namespaces.map(({ name }) => section2.search(new RegExp(`\\n## ${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:\\n| \\(|$)`))).filter((index) => index >= 0);
|
|
1640
|
+
if (starts.length === 0)
|
|
1641
|
+
return { builtin: section2, mcp: "" };
|
|
1642
|
+
const first = Math.min(...starts);
|
|
1643
|
+
return { builtin: section2.slice(0, first), mcp: section2.slice(first) };
|
|
1644
|
+
}
|
|
1645
|
+
function estimateRequest(payload, namespaces) {
|
|
1646
|
+
if (!isObject(payload))
|
|
1647
|
+
return;
|
|
1648
|
+
const system = systemTextOf(payload);
|
|
1649
|
+
const tools = toolsOf(payload);
|
|
1650
|
+
if (system === "" && tools.length === 0)
|
|
1651
|
+
return;
|
|
1652
|
+
const skills = cut(system, "available_skills");
|
|
1653
|
+
const codemode = cut(skills.rest, "codemode");
|
|
1654
|
+
const { builtin, mcp } = splitCodemode(codemode.inside, namespaces);
|
|
1655
|
+
let toolTokens = 0;
|
|
1656
|
+
let mcpTokens = tokensOf(mcp);
|
|
1657
|
+
for (const tool of tools) {
|
|
1658
|
+
const cost = tokensOf(JSON.stringify(tool));
|
|
1659
|
+
if (MCP_TOOL.test(toolName(tool) ?? ""))
|
|
1660
|
+
mcpTokens += cost;
|
|
1661
|
+
else
|
|
1662
|
+
toolTokens += cost;
|
|
1663
|
+
}
|
|
1664
|
+
return {
|
|
1665
|
+
system: tokensOf(codemode.rest) + tokensOf(builtin),
|
|
1666
|
+
skills: tokensOf(skills.inside),
|
|
1667
|
+
tools: toolTokens,
|
|
1668
|
+
mcp: mcpTokens
|
|
1669
|
+
};
|
|
1670
|
+
}
|
|
1671
|
+
function breakdownOf(estimate, used) {
|
|
1672
|
+
const fixed = estimate.system + estimate.skills + estimate.tools + estimate.mcp;
|
|
1673
|
+
const scale = fixed > used ? used / fixed : 1;
|
|
1674
|
+
const system = Math.floor(estimate.system * scale);
|
|
1675
|
+
const skills = Math.floor(estimate.skills * scale);
|
|
1676
|
+
const tools = Math.floor(estimate.tools * scale);
|
|
1677
|
+
const mcp = Math.floor(estimate.mcp * scale);
|
|
1678
|
+
return { used, system, skills, tools, mcp, messages: used - system - skills - tools - mcp };
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
// apps/host/src/extensions/context-breakdown.ts
|
|
1682
|
+
var ContextBreakdownDoc = defineDoc4({
|
|
1683
|
+
kind: "context.breakdown",
|
|
1684
|
+
version: 1,
|
|
1685
|
+
scope: "conversation",
|
|
1686
|
+
history: "latest",
|
|
1687
|
+
fork: "current",
|
|
1688
|
+
initial: () => ({ used: 0, system: 0, skills: 0, tools: 0, mcp: 0, messages: 0 }),
|
|
1689
|
+
checkpointWhen: () => false
|
|
1690
|
+
});
|
|
1691
|
+
function createContextBreakdown(options) {
|
|
1692
|
+
let namespaces = [];
|
|
1693
|
+
const pending = new Map;
|
|
1694
|
+
const build = () => defineExtension3({
|
|
1695
|
+
name: "context-breakdown",
|
|
1696
|
+
hooks: [
|
|
1697
|
+
hook(GenerationTask, {
|
|
1698
|
+
beforeProviderRequest: (request, api) => {
|
|
1699
|
+
const estimate = estimateRequest(request.payload, namespaces);
|
|
1700
|
+
if (estimate === undefined)
|
|
1701
|
+
pending.delete(api.taskId);
|
|
1702
|
+
else
|
|
1703
|
+
pending.set(api.taskId, estimate);
|
|
1704
|
+
return;
|
|
1705
|
+
},
|
|
1706
|
+
afterResponse: (message2, api) => {
|
|
1707
|
+
const estimate = pending.get(api.taskId);
|
|
1708
|
+
pending.delete(api.taskId);
|
|
1709
|
+
if (estimate === undefined || message2.stopReason === "aborted" || message2.stopReason === "error")
|
|
1710
|
+
return;
|
|
1711
|
+
const used = message2.usage ? contextTokens(message2.usage) : 0;
|
|
1712
|
+
if (!(used > 0))
|
|
1713
|
+
return;
|
|
1714
|
+
const value = breakdownOf(estimate, used);
|
|
1715
|
+
options.harness().commit(async (tx) => {
|
|
1716
|
+
Object.assign(await tx.doc(ContextBreakdownDoc, api.conversationId), value);
|
|
1717
|
+
}, BACKGROUND_CONTEXT).catch((error) => {
|
|
1718
|
+
console.error(`context-breakdown: conversation ${api.conversationId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1719
|
+
});
|
|
1720
|
+
}
|
|
1721
|
+
})
|
|
1722
|
+
]
|
|
1723
|
+
});
|
|
1724
|
+
return {
|
|
1725
|
+
extension: build(),
|
|
1726
|
+
docs: [{ token: ContextBreakdownDoc, presentation: "pinomad.context" }],
|
|
1727
|
+
namespaced: (next) => {
|
|
1728
|
+
namespaces = next;
|
|
1729
|
+
return build();
|
|
1730
|
+
}
|
|
1731
|
+
};
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
// apps/host/src/extensions/processes.ts
|
|
1735
|
+
import { randomUUID } from "node:crypto";
|
|
1736
|
+
import { defineExtension as defineExtension4, defineTool as defineTool2 } from "@earendil-works/pi-durable";
|
|
1737
|
+
import { Type as Type2 } from "typebox";
|
|
1738
|
+
|
|
1739
|
+
// packages/client/src/processes.ts
|
|
1740
|
+
function processStatusText(state) {
|
|
1741
|
+
if (state === "starting")
|
|
1742
|
+
return "Starting";
|
|
1743
|
+
if (state === "running")
|
|
1744
|
+
return "Running";
|
|
1745
|
+
if (state === "lost")
|
|
1746
|
+
return "Lost track after a host restart; it may still be running";
|
|
1747
|
+
if (typeof state === "object" && "stopped" in state) {
|
|
1748
|
+
switch (state.stopped) {
|
|
1749
|
+
case "agent":
|
|
1750
|
+
return "Stopped by the agent";
|
|
1751
|
+
case "user":
|
|
1752
|
+
return "Stopped by the user";
|
|
1753
|
+
case "archive":
|
|
1754
|
+
return "Stopped when the conversation was archived";
|
|
1755
|
+
case "shutdown":
|
|
1756
|
+
return "Stopped when the host shut down";
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
if (typeof state === "object" && "failed" in state)
|
|
1760
|
+
return `Failed to start: ${state.failed}`;
|
|
1761
|
+
if (typeof state === "object" && "exited" in state) {
|
|
1762
|
+
if (state.signal !== undefined)
|
|
1763
|
+
return `Exited after signal ${state.signal}`;
|
|
1764
|
+
if (state.exited === null)
|
|
1765
|
+
return "Exited";
|
|
1766
|
+
return `Exited with code ${state.exited}`;
|
|
1767
|
+
}
|
|
1768
|
+
return "Unknown";
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
// apps/host/src/process-doc.ts
|
|
1772
|
+
import { defineDoc as defineDoc5 } from "@earendil-works/pi-durable";
|
|
1773
|
+
var ProcessDoc = defineDoc5({
|
|
1774
|
+
kind: "pinomad.processes",
|
|
1775
|
+
version: 1,
|
|
1776
|
+
scope: "conversation",
|
|
1777
|
+
history: "latest",
|
|
1778
|
+
fork: "initial",
|
|
1779
|
+
initial: () => ({ processes: [] }),
|
|
1780
|
+
checkpointWhen: () => false
|
|
1781
|
+
});
|
|
1782
|
+
|
|
1783
|
+
// apps/host/src/extensions/processes.ts
|
|
1784
|
+
var ID = Type2.String({ minLength: 1, description: "The process id returned by process_start." });
|
|
1785
|
+
function createProcesses(options) {
|
|
1786
|
+
const { runtime } = options;
|
|
1787
|
+
if (runtime === undefined) {
|
|
1788
|
+
return {
|
|
1789
|
+
extension: defineExtension4({ name: "processes", tools: [] }),
|
|
1790
|
+
docs: [{ token: ProcessDoc, presentation: "pinomad.processes" }]
|
|
1791
|
+
};
|
|
1792
|
+
}
|
|
1793
|
+
const start = defineTool2({
|
|
1794
|
+
name: "process_start",
|
|
1795
|
+
callers: ["model"],
|
|
1796
|
+
description: "Start a long-running command in the background and return its id. The command runs in your conversation's working directory under a pty, so dev servers and monitors keep running after the call returns. " + "It cannot receive input. Use process_output to read it, process_stop to end it, and process_list to recover ids after a context reset. " + "The host keeps at most 8 unfinished processes per conversation family. Write monitors as commands that exit when the event happens (for example a one-shot grep) so the exit report wakes you; line-by-line callbacks are not supported.",
|
|
1797
|
+
parameters: Type2.Object({
|
|
1798
|
+
command: Type2.String({ minLength: 1, description: "Shell command; it is run with the login shell's -c." }),
|
|
1799
|
+
name: Type2.Optional(Type2.String({ description: `Short label for lists, e.g. "dev server". Defaults to the command's first line.` }))
|
|
1800
|
+
}),
|
|
1801
|
+
replay: "unsafe",
|
|
1802
|
+
execute: async (args, api, context) => {
|
|
1803
|
+
const agent = await api.agent(context);
|
|
1804
|
+
const cwd = agent.cwd;
|
|
1805
|
+
if (cwd === undefined)
|
|
1806
|
+
throw new Error("This conversation has no working directory");
|
|
1807
|
+
const name = cleanName(args.name) ?? defaultName(args.command);
|
|
1808
|
+
const memo = await api.memo("process.start", { id: randomUUID(), name }, context);
|
|
1809
|
+
const process2 = await runtime.start({
|
|
1810
|
+
conversationId: api.conversationId,
|
|
1811
|
+
command: args.command,
|
|
1812
|
+
name: memo.name,
|
|
1813
|
+
cwd,
|
|
1814
|
+
id: memo.id
|
|
1815
|
+
}, context);
|
|
1816
|
+
return result2(processResult("Started", process2), { id: process2.id, name: process2.name });
|
|
1817
|
+
}
|
|
1818
|
+
});
|
|
1819
|
+
const output = defineTool2({
|
|
1820
|
+
name: "process_output",
|
|
1821
|
+
callers: ["model"],
|
|
1822
|
+
description: `Read a background process's output after a raw character position. Omit "since" for the retained output; pass the previous result's "next" to read only what is new. ` + 'Returns at most 16 KiB of text with ANSI escapes removed; "next" stays a raw output position. "truncated" is true when the start of the retained output was already dropped. ' + "If the process ended, status and exitCode say how; output may be unavailable after it falls out of history or the host restarts.",
|
|
1823
|
+
parameters: Type2.Object({
|
|
1824
|
+
id: ID,
|
|
1825
|
+
since: Type2.Optional(Type2.Integer({ minimum: 0, description: "The previous result's next position." }))
|
|
1826
|
+
}),
|
|
1827
|
+
replay: "safe",
|
|
1828
|
+
execute: async (args, api, context) => {
|
|
1829
|
+
const value = await runtime.output({
|
|
1830
|
+
conversationId: api.conversationId,
|
|
1831
|
+
id: args.id,
|
|
1832
|
+
...args.since === undefined ? {} : { since: args.since }
|
|
1833
|
+
}, context);
|
|
1834
|
+
const heading = statusLine(value.status);
|
|
1835
|
+
const body = value.released === true ? "Output is no longer available." : value.text === "" ? "(no new output)" : value.text;
|
|
1836
|
+
const truncated = value.truncated && value.released !== true ? `
|
|
1837
|
+
(the beginning of the retained output was truncated)` : "";
|
|
1838
|
+
return result2(`${heading}
|
|
1839
|
+
${body}${truncated}`, value);
|
|
1840
|
+
}
|
|
1841
|
+
});
|
|
1842
|
+
const stop = defineTool2({
|
|
1843
|
+
name: "process_stop",
|
|
1844
|
+
callers: ["model"],
|
|
1845
|
+
description: "Stop a background process and wait until it has exited. The host sends SIGTERM to the whole process group, then SIGKILL after 5 seconds. " + "A process that exits after this call is treated as stopped, so it will not report back to you; the result carries its final state. Stopping an already ended process returns that state.",
|
|
1846
|
+
parameters: Type2.Object({ id: ID }),
|
|
1847
|
+
replay: "safe",
|
|
1848
|
+
execute: async (args, api, context) => {
|
|
1849
|
+
const process2 = await runtime.stop({
|
|
1850
|
+
conversationId: api.conversationId,
|
|
1851
|
+
id: args.id,
|
|
1852
|
+
reason: "agent"
|
|
1853
|
+
}, context);
|
|
1854
|
+
return result2(processResult("Stopped", process2), { state: process2.state });
|
|
1855
|
+
}
|
|
1856
|
+
});
|
|
1857
|
+
const list = defineTool2({
|
|
1858
|
+
name: "process_list",
|
|
1859
|
+
callers: ["model"],
|
|
1860
|
+
description: "List this conversation family's background processes, running and recently ended, with ids and states. Use it to recover ids after a context reset. Output and stop work only while the host still has the process's pty.",
|
|
1861
|
+
parameters: Type2.Object({}),
|
|
1862
|
+
replay: "safe",
|
|
1863
|
+
execute: async (_args, api, context) => {
|
|
1864
|
+
const processes = await runtime.list(api.conversationId, context);
|
|
1865
|
+
const text = processes.length === 0 ? "No background processes." : processes.map((process2) => `${process2.id} ${process2.name} [${statusLine(process2.state)}] ${process2.command}`).join(`
|
|
1866
|
+
`);
|
|
1867
|
+
return result2(text, { processes });
|
|
1868
|
+
}
|
|
1869
|
+
});
|
|
1870
|
+
return {
|
|
1871
|
+
extension: defineExtension4({ name: "processes", tools: [start, output, stop, list] }),
|
|
1872
|
+
docs: [{ token: ProcessDoc, presentation: "pinomad.processes" }]
|
|
1873
|
+
};
|
|
1874
|
+
}
|
|
1875
|
+
function result2(text, details) {
|
|
1876
|
+
return { output: [{ type: "text", text }], details };
|
|
1877
|
+
}
|
|
1878
|
+
function processResult(verb, process2) {
|
|
1879
|
+
return `${verb} ${process2.name} (${process2.id}): ${statusLine(process2.state)}`;
|
|
1880
|
+
}
|
|
1881
|
+
function statusLine(state) {
|
|
1882
|
+
return processStatusText(state);
|
|
1883
|
+
}
|
|
1884
|
+
function cleanName(name) {
|
|
1885
|
+
const trimmed = name?.trim();
|
|
1886
|
+
return trimmed === undefined || trimmed === "" ? undefined : trimmed;
|
|
1887
|
+
}
|
|
1888
|
+
function defaultName(command) {
|
|
1889
|
+
const line = command.trim().split(`
|
|
1890
|
+
`, 1)[0].trim();
|
|
1891
|
+
if (line === "")
|
|
1892
|
+
return "Background process";
|
|
1893
|
+
return line.length <= 60 ? line : `${line.slice(0, 59)}…`;
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1283
1896
|
// apps/host/src/extensions/subagent.ts
|
|
1897
|
+
import { join as join6 } from "node:path";
|
|
1284
1898
|
import { clampThinkingLevel as clampThinkingLevel2 } from "@earendil-works/pi-ai";
|
|
1285
1899
|
import {
|
|
1286
1900
|
AgentDoc as AgentDoc4,
|
|
1287
|
-
AssistantEntry,
|
|
1288
1901
|
configure as configure2,
|
|
1289
|
-
defineExtension as
|
|
1290
|
-
defineTool as
|
|
1902
|
+
defineExtension as defineExtension5,
|
|
1903
|
+
defineTool as defineTool3,
|
|
1291
1904
|
section as section2
|
|
1292
1905
|
} from "@earendil-works/pi-durable";
|
|
1293
1906
|
import { Type as Type3 } from "typebox";
|
|
1294
1907
|
|
|
1295
1908
|
// apps/host/src/subagent-doc.ts
|
|
1296
|
-
import { defineDoc as
|
|
1297
|
-
var SubagentDoc =
|
|
1909
|
+
import { defineDoc as defineDoc6 } from "@earendil-works/pi-durable";
|
|
1910
|
+
var SubagentDoc = defineDoc6({
|
|
1298
1911
|
kind: "pinomad.subagent",
|
|
1299
1912
|
version: 1,
|
|
1300
1913
|
scope: "conversation",
|
|
@@ -1304,8 +1917,273 @@ var SubagentDoc = defineDoc4({
|
|
|
1304
1917
|
checkpointWhen: () => false
|
|
1305
1918
|
});
|
|
1306
1919
|
|
|
1920
|
+
// apps/host/src/subagents.ts
|
|
1921
|
+
import {
|
|
1922
|
+
AssistantEntry,
|
|
1923
|
+
defineDoc as defineDoc7,
|
|
1924
|
+
defineTask as defineTask2
|
|
1925
|
+
} from "@earendil-works/pi-durable";
|
|
1926
|
+
var SubagentsDoc = defineDoc7({
|
|
1927
|
+
kind: "pinomad.subagents",
|
|
1928
|
+
version: 1,
|
|
1929
|
+
scope: "conversation",
|
|
1930
|
+
history: "latest",
|
|
1931
|
+
fork: "initial",
|
|
1932
|
+
initial: () => ({ agents: {} }),
|
|
1933
|
+
checkpointWhen: () => false
|
|
1934
|
+
});
|
|
1935
|
+
var MAX_WORKING_SUBAGENTS = 8;
|
|
1936
|
+
var NAME = /^[\p{L}\p{N}][\p{L}\p{N}_-]{0,39}$/u;
|
|
1937
|
+
async function familyRootIn(tx, id) {
|
|
1938
|
+
let current = id;
|
|
1939
|
+
for (let depth = 0;depth < 256; depth++) {
|
|
1940
|
+
const record = await tx.conversation(current);
|
|
1941
|
+
const up = record?.parent?.conversationId ?? record?.owner?.conversationId;
|
|
1942
|
+
if (up === undefined)
|
|
1943
|
+
return current;
|
|
1944
|
+
current = up;
|
|
1945
|
+
}
|
|
1946
|
+
return current;
|
|
1947
|
+
}
|
|
1948
|
+
function nameFromDescription(description) {
|
|
1949
|
+
const base = (description ?? "").trim().toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "-").replace(/^-+/, "").slice(0, 32).replace(/-+$/, "");
|
|
1950
|
+
return base === "" ? "subagent" : base;
|
|
1951
|
+
}
|
|
1952
|
+
function uniqueName(base, taken) {
|
|
1953
|
+
if (!taken(base))
|
|
1954
|
+
return base;
|
|
1955
|
+
for (let n = 2;; n++) {
|
|
1956
|
+
const candidate = `${base}-${n}`;
|
|
1957
|
+
if (!taken(candidate))
|
|
1958
|
+
return candidate;
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
function assertValidName(name) {
|
|
1962
|
+
if (!NAME.test(name)) {
|
|
1963
|
+
throw new Error(`Invalid subagent name "${name}": use 1-40 letters, digits, "-" or "_", starting with a letter or digit`);
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
async function isLive(tx, id) {
|
|
1967
|
+
if (id === undefined)
|
|
1968
|
+
return false;
|
|
1969
|
+
const task = await tx.task(id);
|
|
1970
|
+
return task !== undefined && task.state.status !== "terminal";
|
|
1971
|
+
}
|
|
1972
|
+
async function inCall(tx, record) {
|
|
1973
|
+
return await isLive(tx, record.call);
|
|
1974
|
+
}
|
|
1975
|
+
async function statusOf(tx, record) {
|
|
1976
|
+
if (record.status !== "working" || record.reporters.length > 0 || await inCall(tx, record))
|
|
1977
|
+
return record.status;
|
|
1978
|
+
return "stopped";
|
|
1979
|
+
}
|
|
1980
|
+
async function workingCount(tx, root) {
|
|
1981
|
+
let count = 0;
|
|
1982
|
+
for (const record of Object.values((await tx.doc(SubagentsDoc, root)).agents)) {
|
|
1983
|
+
if (record.reporters.length > 0 || await inCall(tx, record))
|
|
1984
|
+
count++;
|
|
1985
|
+
}
|
|
1986
|
+
return count;
|
|
1987
|
+
}
|
|
1988
|
+
async function assertCapacity(tx, root) {
|
|
1989
|
+
if (await workingCount(tx, root) >= MAX_WORKING_SUBAGENTS) {
|
|
1990
|
+
throw new Error(`At most ${MAX_WORKING_SUBAGENTS} subagents can work at once in this conversation; wait for one to answer or stop one with subagent_stop`);
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
function recordNamed(agents, name) {
|
|
1994
|
+
const found = Object.hasOwn(agents, name) ? agents[name] : undefined;
|
|
1995
|
+
if (found !== undefined)
|
|
1996
|
+
return found;
|
|
1997
|
+
const names = Object.keys(agents);
|
|
1998
|
+
throw new Error(names.length === 0 ? `No subagent named "${name}"` : `No subagent named "${name}". Subagents: ${names.join(", ")}`);
|
|
1999
|
+
}
|
|
2000
|
+
async function recordOf(tx, childId) {
|
|
2001
|
+
if ((await tx.conversation(childId))?.owner === undefined)
|
|
2002
|
+
return;
|
|
2003
|
+
const root = await familyRootIn(tx, childId);
|
|
2004
|
+
const agents = (await tx.doc(SubagentsDoc, root)).agents;
|
|
2005
|
+
for (const [name, record] of Object.entries(agents)) {
|
|
2006
|
+
if (record.conversationId === childId)
|
|
2007
|
+
return { root, name, record };
|
|
2008
|
+
}
|
|
2009
|
+
return;
|
|
2010
|
+
}
|
|
2011
|
+
var SubagentAnchor = defineTask2({
|
|
2012
|
+
name: "pinomad.subagent-anchor",
|
|
2013
|
+
version: 1,
|
|
2014
|
+
initial: () => ({ phase: "done" }),
|
|
2015
|
+
phases: {
|
|
2016
|
+
done: (_anchor, runtime, context) => runtime.commit(() => ({ status: "terminal", outcome: { status: "completed", result: null } }), context)
|
|
2017
|
+
},
|
|
2018
|
+
abort: (_anchor, runtime, context) => runtime.commit(() => ({ status: "terminal", outcome: { status: "aborted" } }), context)
|
|
2019
|
+
});
|
|
2020
|
+
var SubagentReporter = defineTask2({
|
|
2021
|
+
name: "pinomad.subagent-reporter",
|
|
2022
|
+
version: 1,
|
|
2023
|
+
initial: () => ({ phase: "deliver" }),
|
|
2024
|
+
phases: {
|
|
2025
|
+
deliver: async (reporter, runtime, context) => {
|
|
2026
|
+
const input = reporter.input;
|
|
2027
|
+
const child = await runtime.conversation(input.conversationId, context);
|
|
2028
|
+
const sender = await runtime.conversation(runtime.conversationId, context);
|
|
2029
|
+
await runtime.commit(async (tx) => {
|
|
2030
|
+
const record = (await tx.doc(SubagentsDoc, input.root)).agents[input.name];
|
|
2031
|
+
if (input.message === undefined) {
|
|
2032
|
+
if (record !== undefined)
|
|
2033
|
+
release(record, reporter.id, "idle");
|
|
2034
|
+
return { status: "terminal", outcome: { status: "completed", result: null } };
|
|
2035
|
+
}
|
|
2036
|
+
const message2 = input.message;
|
|
2037
|
+
if (child !== undefined) {
|
|
2038
|
+
if (record?.cancelled?.includes(reporter.id) === true) {
|
|
2039
|
+
await settle(tx, sender, reporter.id, input, "cancelled", runtime.now());
|
|
2040
|
+
return { status: "terminal", outcome: { status: "completed", result: null } };
|
|
2041
|
+
}
|
|
2042
|
+
await child.submit(tx, draftOf(reporter.id, message2));
|
|
2043
|
+
}
|
|
2044
|
+
return { status: "running", checkpoint: { phase: "wait" } };
|
|
2045
|
+
}, context);
|
|
2046
|
+
},
|
|
2047
|
+
wait: async (reporter, runtime, context) => {
|
|
2048
|
+
const input = reporter.input;
|
|
2049
|
+
const child = await runtime.conversation(input.conversationId, context);
|
|
2050
|
+
const message2 = input.message;
|
|
2051
|
+
const settled = child === undefined || message2 === undefined ? undefined : await (await child.submit(draftOf(reporter.id, message2), context)).wait(context);
|
|
2052
|
+
const sender = await runtime.conversation(runtime.conversationId, context);
|
|
2053
|
+
await runtime.commit(async (tx) => {
|
|
2054
|
+
if (message2 === undefined) {
|
|
2055
|
+
const record = (await tx.doc(SubagentsDoc, input.root)).agents[input.name];
|
|
2056
|
+
if (record !== undefined)
|
|
2057
|
+
release(record, reporter.id, "idle");
|
|
2058
|
+
} else {
|
|
2059
|
+
await settle(tx, sender, reporter.id, input, settled, runtime.now());
|
|
2060
|
+
}
|
|
2061
|
+
return { status: "terminal", outcome: { status: "completed", result: null } };
|
|
2062
|
+
}, context);
|
|
2063
|
+
}
|
|
2064
|
+
},
|
|
2065
|
+
abort: (reporter, runtime, context) => runtime.commit(async (tx) => {
|
|
2066
|
+
const record = (await tx.doc(SubagentsDoc, reporter.input.root)).agents[reporter.input.name];
|
|
2067
|
+
if (record !== undefined)
|
|
2068
|
+
release(record, reporter.id, "stopped");
|
|
2069
|
+
return { status: "terminal", outcome: { status: "aborted" } };
|
|
2070
|
+
}, context)
|
|
2071
|
+
});
|
|
2072
|
+
function draftOf(reporter, message2) {
|
|
2073
|
+
return { type: "input", content: message2, whenBusy: "steer", requestId: `subagent:${reporter}` };
|
|
2074
|
+
}
|
|
2075
|
+
function release(record, reporter, status) {
|
|
2076
|
+
record.reporters = record.reporters.filter((id) => id !== reporter);
|
|
2077
|
+
if (record.cancelled !== undefined)
|
|
2078
|
+
record.cancelled = record.cancelled.filter((id) => id !== reporter);
|
|
2079
|
+
if (record.reporters.length === 0)
|
|
2080
|
+
record.status = status;
|
|
2081
|
+
}
|
|
2082
|
+
function stopRecord(record, by) {
|
|
2083
|
+
record.stop = { by, noted: false };
|
|
2084
|
+
record.cancelled = [...record.reporters];
|
|
2085
|
+
}
|
|
2086
|
+
function steer(text) {
|
|
2087
|
+
return { type: "input", content: text, whenBusy: "steer" };
|
|
2088
|
+
}
|
|
2089
|
+
function note(text, now) {
|
|
2090
|
+
return { type: "write", entry: { kind: "pi.user", model: [{ role: "user", content: text, timestamp: now }] } };
|
|
2091
|
+
}
|
|
2092
|
+
async function settle(tx, sender, reporter, input, settled, now) {
|
|
2093
|
+
const record = (await tx.doc(SubagentsDoc, input.root)).agents[input.name];
|
|
2094
|
+
if (record === undefined)
|
|
2095
|
+
return;
|
|
2096
|
+
const name = input.name;
|
|
2097
|
+
let report;
|
|
2098
|
+
let status;
|
|
2099
|
+
if (settled !== "cancelled" && settled?.status === "done" && settled.type === "input") {
|
|
2100
|
+
status = "idle";
|
|
2101
|
+
const key = `${sender.id}:${settled.answer}`;
|
|
2102
|
+
if (record.reported[key] !== "steer") {
|
|
2103
|
+
report = steer(`${answeredHeader(name, record)}
|
|
2104
|
+
|
|
2105
|
+
${await answerText(tx, settled.answer)}`);
|
|
2106
|
+
record.reported[key] = "steer";
|
|
2107
|
+
}
|
|
2108
|
+
} else if (settled === "cancelled" || settled?.status === "unanswered" && settled.reason === "aborted") {
|
|
2109
|
+
status = "stopped";
|
|
2110
|
+
const stop = record.stop ?? { by: "user", noted: false };
|
|
2111
|
+
if (stop.by === "user" && !stop.noted) {
|
|
2112
|
+
report = note(`[The user stopped subagent ${name}.]`, now);
|
|
2113
|
+
stop.noted = true;
|
|
2114
|
+
}
|
|
2115
|
+
record.stop = stop;
|
|
2116
|
+
} else {
|
|
2117
|
+
status = "failed";
|
|
2118
|
+
const reason = settled === undefined ? "its conversation no longer exists" : settled.status === "unanswered" ? settled.reason : "no answer";
|
|
2119
|
+
report = steer(`[subagent ${name} failed: ${reason}]`);
|
|
2120
|
+
}
|
|
2121
|
+
if (report !== undefined)
|
|
2122
|
+
await sender.submit(tx, { ...report, requestId: `subagent-report:${reporter}` });
|
|
2123
|
+
release(record, reporter, status);
|
|
2124
|
+
}
|
|
2125
|
+
function answeredHeader(name, record) {
|
|
2126
|
+
return record?.branch === undefined ? `[subagent ${name} answered]` : `[subagent ${name} answered on branch ${record.branch} (from ${record.base?.slice(0, 12) ?? "?"})]`;
|
|
2127
|
+
}
|
|
2128
|
+
async function answerText(tx, id) {
|
|
2129
|
+
const message2 = (await tx.entry(AssistantEntry, id))?.model?.[0];
|
|
2130
|
+
const text = message2?.role === "assistant" ? message2.content.flatMap((block) => block.type === "text" ? [block.text] : []).join("") : "";
|
|
2131
|
+
return text === "" ? "(The subagent returned no text.)" : text;
|
|
2132
|
+
}
|
|
2133
|
+
async function entriesSince(tx, record) {
|
|
2134
|
+
const entries = [];
|
|
2135
|
+
let cursor;
|
|
2136
|
+
do {
|
|
2137
|
+
const page = await tx.scanEntries({
|
|
2138
|
+
conversationId: record.conversationId,
|
|
2139
|
+
order: "ascending",
|
|
2140
|
+
...record.readUpTo === undefined ? {} : { minEntryId: record.readUpTo + 1 }
|
|
2141
|
+
}, 256, cursor);
|
|
2142
|
+
entries.push(...page.items);
|
|
2143
|
+
cursor = page.next;
|
|
2144
|
+
} while (cursor !== undefined);
|
|
2145
|
+
return entries;
|
|
2146
|
+
}
|
|
2147
|
+
async function assertDispatchable(tx, root, name) {
|
|
2148
|
+
const record = recordNamed((await tx.doc(SubagentsDoc, root)).agents, name);
|
|
2149
|
+
if (await inCall(tx, record)) {
|
|
2150
|
+
throw new Error(`Subagent "${name}" is still answering the subagent call that dispatched it`);
|
|
2151
|
+
}
|
|
2152
|
+
if (record.reporters.length === 0)
|
|
2153
|
+
await assertCapacity(tx, root);
|
|
2154
|
+
return record;
|
|
2155
|
+
}
|
|
2156
|
+
async function dispatch(tx, options) {
|
|
2157
|
+
const record = recordNamed((await tx.doc(SubagentsDoc, options.root)).agents, options.name);
|
|
2158
|
+
const reporter = await tx.createTask(SubagentReporter, {
|
|
2159
|
+
root: options.root,
|
|
2160
|
+
name: options.name,
|
|
2161
|
+
conversationId: record.conversationId,
|
|
2162
|
+
message: options.message
|
|
2163
|
+
}, { ownership: { kind: "conversation" }, conversationId: options.sender, background: true });
|
|
2164
|
+
record.reporters.push(reporter);
|
|
2165
|
+
record.status = "working";
|
|
2166
|
+
delete record.stop;
|
|
2167
|
+
delete record.call;
|
|
2168
|
+
const doc = await tx.doc(SubagentDoc, record.conversationId);
|
|
2169
|
+
if (doc.lifecycle !== "background")
|
|
2170
|
+
doc.lifecycle = "background";
|
|
2171
|
+
return reporter;
|
|
2172
|
+
}
|
|
2173
|
+
async function markStop(tx, childId, by) {
|
|
2174
|
+
const found = await recordOf(tx, childId);
|
|
2175
|
+
if (found === undefined)
|
|
2176
|
+
return false;
|
|
2177
|
+
stopRecord(found.record, by);
|
|
2178
|
+
return true;
|
|
2179
|
+
}
|
|
2180
|
+
|
|
1307
2181
|
// apps/host/src/extensions/subagent.ts
|
|
1308
|
-
var
|
|
2182
|
+
var instructions = (name) => `You are a subagent named "${name}", working on tasks delegated to you. You cannot ask the user questions; if you need a decision or clarification, stop and state exactly what you need in your final answer. Each final answer is returned to whoever sent the message, so make it a complete, self-contained report. You may get more messages later from the delegating agent.`;
|
|
2183
|
+
var READ_LIMIT = 16000;
|
|
2184
|
+
function contentText(content) {
|
|
2185
|
+
return typeof content === "string" ? content : content.flatMap((block) => block.type === "text" && block.text !== undefined ? [block.text] : []).join("");
|
|
2186
|
+
}
|
|
1309
2187
|
var THINKING_LEVELS2 = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
1310
2188
|
function createSubagent(options) {
|
|
1311
2189
|
const available = () => options.modelSummaries().map((model) => `${model.provider}/${model.modelId}`).join(", ");
|
|
@@ -1317,15 +2195,22 @@ function createSubagent(options) {
|
|
|
1317
2195
|
const defined = roles();
|
|
1318
2196
|
throw new Error(defined.length === 0 ? "No named agents are defined" : `Unknown agent "${name}". Available agents: ${defined.map((each) => each.name).join(", ")}`);
|
|
1319
2197
|
};
|
|
1320
|
-
const tool =
|
|
2198
|
+
const tool = defineTool3({
|
|
1321
2199
|
name: "subagent",
|
|
1322
2200
|
callers: ["model"],
|
|
1323
|
-
description: "Delegate a self-contained task to a subagent that runs in its own conversation and returns its
|
|
2201
|
+
description: "Delegate a self-contained task to a named subagent that runs in its own conversation. " + "Without `background` the call blocks until the subagent answers and returns its answer; with `background: true` it returns at once, you keep working, and the answer is reported to you when it arrives. " + "Either way the subagent stays available afterwards: continue with subagent_send, stop it with subagent_stop, read its answers again with subagent_read, and list subagents with subagent_list. " + "The subagent starts with none of this conversation's context, so put everything it needs in `task`. " + '`checkout` is required: "shared" works in your files (use it for read-only work; parallel subagents must not edit the same files), "worktree" gets its own Git worktree for work that edits files. ' + "It cannot ask the user questions. `model` and `thinkingLevel` override what it inherits from you; `role` delegates to a named agent; a model or thinking level it pins cannot be overridden. " + `Available models: ${available()}.`,
|
|
1324
2202
|
parameters: Type3.Object({
|
|
1325
2203
|
task: Type3.String({
|
|
1326
2204
|
minLength: 1,
|
|
1327
2205
|
description: "Complete, self-contained instructions. The subagent sees none of this conversation."
|
|
1328
2206
|
}),
|
|
2207
|
+
checkout: Type3.Union([Type3.Literal("shared"), Type3.Literal("worktree")], {
|
|
2208
|
+
description: '"shared": your working directory. "worktree": its own Git worktree from your HEAD; uncommitted changes are not carried over.'
|
|
2209
|
+
}),
|
|
2210
|
+
name: Type3.Optional(Type3.String({
|
|
2211
|
+
description: 'Optional unique name, e.g. "reviewer"; derived from `description` when omitted. Use it with the other subagent tools.'
|
|
2212
|
+
})),
|
|
2213
|
+
background: Type3.Optional(Type3.Boolean({ description: "Return at once instead of waiting; the answer is reported to you when it arrives." })),
|
|
1329
2214
|
role: Type3.Optional(Type3.String({
|
|
1330
2215
|
description: "Optional named agent to delegate to, from the Named agents list in your instructions. Its prompt applies, plus any model or thinking level it pins."
|
|
1331
2216
|
})),
|
|
@@ -1335,16 +2220,39 @@ function createSubagent(options) {
|
|
|
1335
2220
|
}),
|
|
1336
2221
|
replay: "safe",
|
|
1337
2222
|
execute: async (args, api, context) => {
|
|
2223
|
+
const background = args.background === true;
|
|
2224
|
+
let repo;
|
|
2225
|
+
if (args.checkout === "worktree") {
|
|
2226
|
+
if (options.worktree === undefined)
|
|
2227
|
+
throw new Error('checkout "worktree" is not available on this host; use "shared"');
|
|
2228
|
+
repo = await options.worktree.base(api.conversationId, context);
|
|
2229
|
+
}
|
|
1338
2230
|
const child = await api.commit(async (tx) => {
|
|
1339
|
-
const
|
|
1340
|
-
|
|
1341
|
-
|
|
2231
|
+
const root = await familyRootIn(tx, api.conversationId);
|
|
2232
|
+
const registry = await tx.doc(SubagentsDoc, root);
|
|
2233
|
+
const recorded = Object.entries(registry.agents).find(([, record]) => record.createdBy === api.taskId);
|
|
2234
|
+
const existingId = recorded?.[1].conversationId ?? (await tx.scanConversations({ ownerTaskId: api.taskId }, 1)).items[0]?.id;
|
|
2235
|
+
if (existingId !== undefined) {
|
|
2236
|
+
const doc2 = await tx.doc(SubagentDoc, existingId);
|
|
1342
2237
|
return {
|
|
1343
|
-
id:
|
|
2238
|
+
id: existingId,
|
|
2239
|
+
name: recorded?.[0] ?? doc2?.name,
|
|
2240
|
+
...recorded?.[1].branch === undefined ? {} : { branch: recorded[1].branch, base: recorded[1].base },
|
|
1344
2241
|
...doc2?.model === undefined ? {} : { model: doc2.model },
|
|
1345
2242
|
...doc2?.startedAt === undefined ? {} : { startedAt: doc2.startedAt }
|
|
1346
2243
|
};
|
|
1347
2244
|
}
|
|
2245
|
+
const taken = (name3) => Object.hasOwn(registry.agents, name3);
|
|
2246
|
+
let name2;
|
|
2247
|
+
if (args.name !== undefined) {
|
|
2248
|
+
name2 = args.name.trim();
|
|
2249
|
+
assertValidName(name2);
|
|
2250
|
+
if (taken(name2))
|
|
2251
|
+
throw new Error(`A subagent named "${name2}" already exists; message it with subagent_send or pick another name`);
|
|
2252
|
+
} else {
|
|
2253
|
+
const words = args.description?.trim() || args.task.trim().split(/\s+/).slice(0, 4).join(" ");
|
|
2254
|
+
name2 = uniqueName(nameFromDescription(words), taken);
|
|
2255
|
+
}
|
|
1348
2256
|
const role = args.role === undefined ? undefined : roleNamed(args.role);
|
|
1349
2257
|
if (role?.model !== undefined && args.model !== undefined) {
|
|
1350
2258
|
throw new Error(`Agent "${role.name}" pins model ${role.model}; omit \`model\``);
|
|
@@ -1377,21 +2285,38 @@ function createSubagent(options) {
|
|
|
1377
2285
|
thinkingLevel = clampThinkingLevel2(override, parent.thinkingLevel);
|
|
1378
2286
|
}
|
|
1379
2287
|
}
|
|
2288
|
+
await assertCapacity(tx, root);
|
|
1380
2289
|
const change = {
|
|
1381
2290
|
extensions: { remove: [extension, ...options.exclude] },
|
|
1382
|
-
instructions: role === undefined || role.prompt === "" ?
|
|
2291
|
+
instructions: role === undefined || role.prompt === "" ? instructions(name2) : `${instructions(name2)}
|
|
1383
2292
|
|
|
1384
2293
|
${role.prompt}`,
|
|
1385
2294
|
...model === undefined ? {} : { model },
|
|
1386
2295
|
...thinkingLevel === undefined ? {} : { thinkingLevel }
|
|
1387
2296
|
};
|
|
1388
|
-
const
|
|
1389
|
-
|
|
2297
|
+
const owner = background ? await tx.createTask(SubagentAnchor, null, {
|
|
2298
|
+
ownership: { kind: "conversation" },
|
|
2299
|
+
conversationId: api.conversationId,
|
|
2300
|
+
background: true
|
|
2301
|
+
}) : api.taskId;
|
|
2302
|
+
const created = await tx.createConversation({ ownership: { kind: "task", taskId: owner } });
|
|
2303
|
+
let worktree;
|
|
2304
|
+
if (repo !== undefined) {
|
|
2305
|
+
const path = options.worktree.path(created.id);
|
|
2306
|
+
worktree = { branch: worktreeBranch(created.id, branchSuffix()), base: repo.base, cwd: join6(path, repo.subdir) };
|
|
2307
|
+
const index = await tx.doc(IndexDoc);
|
|
2308
|
+
if (index.checkouts === undefined)
|
|
2309
|
+
index.checkouts = [];
|
|
2310
|
+
index.checkouts.push({ conversationId: created.id, path, repo: repo.repo, subdir: repo.subdir, branch: worktree.branch, base: repo.base });
|
|
2311
|
+
}
|
|
2312
|
+
await configure2(tx, created.id, worktree === undefined ? change : { ...change, cwd: worktree.cwd });
|
|
1390
2313
|
const childAgent = await tx.doc(AgentDoc4, created.id);
|
|
1391
2314
|
const ref = childAgent.model ?? model ?? (await tx.doc(AgentDoc4, api.conversationId)).model;
|
|
1392
2315
|
const startedAt = Date.now();
|
|
1393
2316
|
const doc = await tx.doc(SubagentDoc, created.id);
|
|
1394
2317
|
doc.startedAt = startedAt;
|
|
2318
|
+
doc.name = name2;
|
|
2319
|
+
doc.lifecycle = background ? "background" : "foreground";
|
|
1395
2320
|
if (ref !== undefined)
|
|
1396
2321
|
doc.model = `${ref.provider}/${ref.modelId}`;
|
|
1397
2322
|
if (role !== undefined)
|
|
@@ -1399,33 +2324,183 @@ ${role.prompt}`,
|
|
|
1399
2324
|
const label = args.description?.trim();
|
|
1400
2325
|
if (label !== undefined && label !== "")
|
|
1401
2326
|
doc.label = label;
|
|
1402
|
-
|
|
2327
|
+
registry.agents[name2] = {
|
|
2328
|
+
conversationId: created.id,
|
|
2329
|
+
dispatcher: api.conversationId,
|
|
2330
|
+
createdBy: api.taskId,
|
|
2331
|
+
...background ? {} : { call: api.taskId },
|
|
2332
|
+
checkout: worktree === undefined ? "shared" : "worktree",
|
|
2333
|
+
...worktree === undefined ? {} : { branch: worktree.branch, base: worktree.base },
|
|
2334
|
+
...label === undefined || label === "" ? {} : { description: label },
|
|
2335
|
+
status: "working",
|
|
2336
|
+
reporters: [],
|
|
2337
|
+
reported: {}
|
|
2338
|
+
};
|
|
2339
|
+
if (background) {
|
|
2340
|
+
await dispatch(tx, { root, name: name2, sender: api.conversationId, message: args.task });
|
|
2341
|
+
}
|
|
2342
|
+
return {
|
|
2343
|
+
id: created.id,
|
|
2344
|
+
name: name2,
|
|
2345
|
+
...worktree === undefined ? {} : { branch: worktree.branch, base: worktree.base },
|
|
2346
|
+
...ref === undefined ? {} : { model: doc.model },
|
|
2347
|
+
startedAt
|
|
2348
|
+
};
|
|
1403
2349
|
}, context);
|
|
1404
|
-
|
|
2350
|
+
const details = {
|
|
2351
|
+
conversationId: child.id,
|
|
2352
|
+
...child.name === undefined ? {} : { name: child.name },
|
|
2353
|
+
...background ? { background: true } : {},
|
|
2354
|
+
...child.branch === undefined ? {} : { branch: child.branch, base: child.base },
|
|
2355
|
+
...child.model === undefined ? {} : { model: child.model },
|
|
2356
|
+
...child.startedAt === undefined ? {} : { startedAt: child.startedAt }
|
|
2357
|
+
};
|
|
2358
|
+
const name = child.name ?? "subagent";
|
|
2359
|
+
if (background) {
|
|
2360
|
+
return {
|
|
2361
|
+
output: [
|
|
2362
|
+
{
|
|
2363
|
+
type: "text",
|
|
2364
|
+
text: `Started subagent "${name}" in the background` + (child.branch === undefined ? "" : ` on its own worktree branch ${child.branch} (from ${child.base?.slice(0, 12)})`) + ". Its answer will be reported to you when it arrives; message it with subagent_send."
|
|
2365
|
+
}
|
|
2366
|
+
],
|
|
2367
|
+
details
|
|
2368
|
+
};
|
|
2369
|
+
}
|
|
2370
|
+
await api.details(details, context);
|
|
1405
2371
|
const handle = await api.conversation(child.id, context);
|
|
1406
2372
|
if (handle === undefined)
|
|
1407
2373
|
throw new Error(`Subagent conversation ${child.id} does not exist`);
|
|
1408
2374
|
const request = { type: "input", content: args.task, requestId: `subagent:${api.taskId}` };
|
|
1409
2375
|
const settled = await (await handle.submit(request, context)).wait(context);
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
...child.startedAt === undefined ? {} : { startedAt: child.startedAt }
|
|
2376
|
+
const answered = settled.status === "done" && settled.type === "input";
|
|
2377
|
+
const text = await api.commit(async (tx) => {
|
|
2378
|
+
const found = await recordOf(tx, child.id);
|
|
2379
|
+
if (found !== undefined) {
|
|
2380
|
+
delete found.record.call;
|
|
2381
|
+
if (found.record.reporters.length === 0) {
|
|
2382
|
+
found.record.status = answered ? "idle" : settled.status === "unanswered" && settled.reason === "aborted" ? "stopped" : "failed";
|
|
2383
|
+
}
|
|
2384
|
+
if (answered)
|
|
2385
|
+
found.record.reported[`${api.conversationId}:${settled.answer}`] = "steer";
|
|
1421
2386
|
}
|
|
2387
|
+
return answered ? await answerText(tx, settled.answer) : undefined;
|
|
2388
|
+
}, context);
|
|
2389
|
+
if (text === undefined)
|
|
2390
|
+
throw new Error(`Subagent failed: ${settled.status === "unanswered" ? settled.reason : settled.status}`);
|
|
2391
|
+
return {
|
|
2392
|
+
output: [{ type: "text", text: `${answeredHeader(name, child)}
|
|
2393
|
+
|
|
2394
|
+
${text}` }],
|
|
2395
|
+
details
|
|
1422
2396
|
};
|
|
1423
|
-
return result2;
|
|
1424
2397
|
}
|
|
1425
2398
|
});
|
|
1426
|
-
const
|
|
2399
|
+
const send = defineTool3({
|
|
2400
|
+
name: "subagent_send",
|
|
2401
|
+
callers: ["model"],
|
|
2402
|
+
description: "Send a message to one of your subagents (started with `subagent`), for example a follow-up question or new instructions. Returns at once; its answer is reported to you when it arrives. A message to a subagent that is still working steers its current answer.",
|
|
2403
|
+
parameters: Type3.Object({
|
|
2404
|
+
name: Type3.String({ description: "The subagent's name." }),
|
|
2405
|
+
message: Type3.String({ minLength: 1, description: "Self-contained message; it sees only its own conversation." })
|
|
2406
|
+
}),
|
|
2407
|
+
replay: "unsafe",
|
|
2408
|
+
execute: async (args, api, context) => {
|
|
2409
|
+
const conversationId = await api.commit(async (tx) => {
|
|
2410
|
+
const root = await familyRootIn(tx, api.conversationId);
|
|
2411
|
+
const record = await assertDispatchable(tx, root, args.name);
|
|
2412
|
+
await dispatch(tx, { root, name: args.name, sender: api.conversationId, message: args.message });
|
|
2413
|
+
return record.conversationId;
|
|
2414
|
+
}, context);
|
|
2415
|
+
return {
|
|
2416
|
+
output: [{ type: "text", text: `Sent to subagent "${args.name}". Its answer will be reported to you when it arrives.` }],
|
|
2417
|
+
details: { conversationId, name: args.name }
|
|
2418
|
+
};
|
|
2419
|
+
}
|
|
2420
|
+
});
|
|
2421
|
+
const stop = defineTool3({
|
|
2422
|
+
name: "subagent_stop",
|
|
2423
|
+
callers: ["model"],
|
|
2424
|
+
description: "Stop what one of your subagents is doing now and drop its queued messages. It stays available for subagent_send.",
|
|
2425
|
+
parameters: Type3.Object({ name: Type3.String({ description: "The subagent's name." }) }),
|
|
2426
|
+
replay: "unsafe",
|
|
2427
|
+
execute: async (args, api, context) => {
|
|
2428
|
+
const conversationId = await api.commit(async (tx) => {
|
|
2429
|
+
const root = await familyRootIn(tx, api.conversationId);
|
|
2430
|
+
const record = recordNamed((await tx.doc(SubagentsDoc, root)).agents, args.name);
|
|
2431
|
+
stopRecord(record, "model");
|
|
2432
|
+
return record.conversationId;
|
|
2433
|
+
}, context);
|
|
2434
|
+
const handle = await api.conversation(conversationId, context);
|
|
2435
|
+
if (handle === undefined)
|
|
2436
|
+
throw new Error(`Subagent conversation ${conversationId} does not exist`);
|
|
2437
|
+
await handle.abort(context);
|
|
2438
|
+
return {
|
|
2439
|
+
output: [{ type: "text", text: `Stopped subagent "${args.name}". It keeps its conversation; message it again with subagent_send.` }],
|
|
2440
|
+
details: { conversationId, name: args.name }
|
|
2441
|
+
};
|
|
2442
|
+
}
|
|
2443
|
+
});
|
|
2444
|
+
const read = defineTool3({
|
|
2445
|
+
name: "subagent_read",
|
|
2446
|
+
callers: ["model"],
|
|
2447
|
+
description: "Read the answers one of your subagents gave since you last read it, without its tool calls — for example after your own context was compacted.",
|
|
2448
|
+
parameters: Type3.Object({ name: Type3.String({ description: "The subagent's name." }) }),
|
|
2449
|
+
replay: "unsafe",
|
|
2450
|
+
execute: async (args, api, context) => {
|
|
2451
|
+
const text = await api.commit(async (tx) => {
|
|
2452
|
+
const root = await familyRootIn(tx, api.conversationId);
|
|
2453
|
+
const record = recordNamed((await tx.doc(SubagentsDoc, root)).agents, args.name);
|
|
2454
|
+
const entries = await entriesSince(tx, record);
|
|
2455
|
+
const lines = [];
|
|
2456
|
+
for (const entry of entries) {
|
|
2457
|
+
const message2 = entry.model?.[0];
|
|
2458
|
+
if (entry.kind !== "pi.assistant" || message2?.role !== "assistant")
|
|
2459
|
+
continue;
|
|
2460
|
+
const said = contentText(message2.content);
|
|
2461
|
+
if (said !== "")
|
|
2462
|
+
lines.push(`${args.name}: ${said}`);
|
|
2463
|
+
}
|
|
2464
|
+
const last = entries.at(-1);
|
|
2465
|
+
if (last !== undefined)
|
|
2466
|
+
record.readUpTo = last.id;
|
|
2467
|
+
if (lines.length === 0)
|
|
2468
|
+
return `Nothing new in subagent "${args.name}" since you last read it.`;
|
|
2469
|
+
const joined = lines.join(`
|
|
2470
|
+
|
|
2471
|
+
`);
|
|
2472
|
+
return joined.length <= READ_LIMIT ? joined : `(Earlier answers omitted.)
|
|
2473
|
+
|
|
2474
|
+
${joined.slice(-READ_LIMIT)}`;
|
|
2475
|
+
}, context);
|
|
2476
|
+
return { output: [{ type: "text", text }] };
|
|
2477
|
+
}
|
|
2478
|
+
});
|
|
2479
|
+
const list = defineTool3({
|
|
2480
|
+
name: "subagent_list",
|
|
2481
|
+
callers: ["model"],
|
|
2482
|
+
description: "List the subagents of this conversation with their status: working, idle, stopped, or failed.",
|
|
2483
|
+
parameters: Type3.Object({}),
|
|
2484
|
+
replay: "safe",
|
|
2485
|
+
execute: async (_args, api, context) => {
|
|
2486
|
+
const text = await api.commit(async (tx) => {
|
|
2487
|
+
const root = await familyRootIn(tx, api.conversationId);
|
|
2488
|
+
const lines = [];
|
|
2489
|
+
for (const [name, record] of Object.entries((await tx.doc(SubagentsDoc, root)).agents)) {
|
|
2490
|
+
const label = record.description === undefined ? "" : ` — ${record.description}`;
|
|
2491
|
+
const checkout = record.branch === undefined ? record.checkout : `worktree, branch ${record.branch}`;
|
|
2492
|
+
lines.push(`- ${name}: ${await statusOf(tx, record)}${label} (checkout: ${checkout})`);
|
|
2493
|
+
}
|
|
2494
|
+
return lines.length === 0 ? "No subagents." : lines.join(`
|
|
2495
|
+
`);
|
|
2496
|
+
}, context);
|
|
2497
|
+
return { output: [{ type: "text", text }] };
|
|
2498
|
+
}
|
|
2499
|
+
});
|
|
2500
|
+
const extension = defineExtension5({
|
|
1427
2501
|
name: "subagent",
|
|
1428
|
-
tools: [tool],
|
|
2502
|
+
tools: [tool, send, stop, read, list],
|
|
2503
|
+
tasks: [SubagentAnchor, SubagentReporter],
|
|
1429
2504
|
sections: [
|
|
1430
2505
|
section2("agent_roles", () => {
|
|
1431
2506
|
const defined = roles();
|
|
@@ -1443,9 +2518,9 @@ ${role.prompt}`,
|
|
|
1443
2518
|
}
|
|
1444
2519
|
|
|
1445
2520
|
// apps/host/src/extensions/todo.ts
|
|
1446
|
-
import { defineDoc as
|
|
2521
|
+
import { defineDoc as defineDoc8, defineExtension as defineExtension6, defineTool as defineTool4 } from "@earendil-works/pi-durable";
|
|
1447
2522
|
import { Type as Type4 } from "typebox";
|
|
1448
|
-
var TodoDoc =
|
|
2523
|
+
var TodoDoc = defineDoc8({
|
|
1449
2524
|
kind: "todo.list",
|
|
1450
2525
|
version: 1,
|
|
1451
2526
|
scope: "conversation",
|
|
@@ -1454,7 +2529,7 @@ var TodoDoc = defineDoc5({
|
|
|
1454
2529
|
initial: () => ({ items: [] }),
|
|
1455
2530
|
checkpointWhen: () => false
|
|
1456
2531
|
});
|
|
1457
|
-
var todoWrite =
|
|
2532
|
+
var todoWrite = defineTool4({
|
|
1458
2533
|
name: "todo_write",
|
|
1459
2534
|
description: "Replace the todo list shown to the user with the given items.",
|
|
1460
2535
|
parameters: Type4.Object({ items: Type4.Array(TodoItemSchema) }),
|
|
@@ -1468,7 +2543,7 @@ var todoWrite = defineTool3({
|
|
|
1468
2543
|
}
|
|
1469
2544
|
});
|
|
1470
2545
|
var todo = {
|
|
1471
|
-
extension:
|
|
2546
|
+
extension: defineExtension6({ name: "todo", tools: [todoWrite] }),
|
|
1472
2547
|
docs: [{ token: TodoDoc, presentation: "pinomad.todo" }]
|
|
1473
2548
|
};
|
|
1474
2549
|
|
|
@@ -1476,8 +2551,8 @@ var todo = {
|
|
|
1476
2551
|
import { stat as stat6 } from "node:fs/promises";
|
|
1477
2552
|
import { createServer } from "node:http";
|
|
1478
2553
|
import { homedir, networkInterfaces } from "node:os";
|
|
1479
|
-
import { join as
|
|
1480
|
-
import { BACKGROUND_CONTEXT as
|
|
2554
|
+
import { join as join8, sep as sep6 } from "node:path";
|
|
2555
|
+
import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT4 } from "@earendil-works/chord/context";
|
|
1481
2556
|
import { clampThinkingLevel as clampThinkingLevel3, getSupportedThinkingLevels } from "@earendil-works/pi-ai";
|
|
1482
2557
|
import {
|
|
1483
2558
|
AgentDoc as AgentDoc5,
|
|
@@ -1486,7 +2561,7 @@ import {
|
|
|
1486
2561
|
LiveDoc
|
|
1487
2562
|
} from "@earendil-works/pi-durable";
|
|
1488
2563
|
import { WebSocket as WebSocket2, WebSocketServer } from "ws";
|
|
1489
|
-
import { Value
|
|
2564
|
+
import { Value } from "typebox/value";
|
|
1490
2565
|
|
|
1491
2566
|
// apps/host/src/secure-carrier.ts
|
|
1492
2567
|
import { WebSocket } from "ws";
|
|
@@ -1522,7 +2597,7 @@ function webSocketCarrier(socket) {
|
|
|
1522
2597
|
}
|
|
1523
2598
|
|
|
1524
2599
|
// apps/host/src/rtc.ts
|
|
1525
|
-
import { randomUUID } from "node:crypto";
|
|
2600
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
1526
2601
|
var BUFFER_HIGH_WATER = 256 * 1024;
|
|
1527
2602
|
var BUFFER_LOW_WATER = 64 * 1024;
|
|
1528
2603
|
var PUMP_STALL_MS = 1e4;
|
|
@@ -1781,7 +2856,7 @@ function nodeRtcFactory(loaded) {
|
|
|
1781
2856
|
const max = parseRtcMaxMessageSize(input.offerSdp);
|
|
1782
2857
|
if (max.kind === "invalid")
|
|
1783
2858
|
throw new Error(max.reason);
|
|
1784
|
-
const pc = new ndc.PeerConnection(`pinomad-${
|
|
2859
|
+
const pc = new ndc.PeerConnection(`pinomad-${randomUUID2()}`, { iceServers: [...input.stun] });
|
|
1785
2860
|
const timers = input.timers;
|
|
1786
2861
|
const candidates = new Set;
|
|
1787
2862
|
let localSdp;
|
|
@@ -2040,17 +3115,17 @@ function dataChannelCarrier(channel, timers, log, onClosed) {
|
|
|
2040
3115
|
stallTimer = timers.setTimeout(stall, 1000);
|
|
2041
3116
|
};
|
|
2042
3117
|
channel.onBufferedAmountLow(() => pump());
|
|
2043
|
-
channel.onMessage((
|
|
2044
|
-
if (typeof
|
|
3118
|
+
channel.onMessage((message2) => {
|
|
3119
|
+
if (typeof message2 === "string")
|
|
2045
3120
|
handlers?.text();
|
|
2046
|
-
else if (
|
|
2047
|
-
handlers?.binary(new Uint8Array(
|
|
3121
|
+
else if (message2 instanceof ArrayBuffer)
|
|
3122
|
+
handlers?.binary(new Uint8Array(message2));
|
|
2048
3123
|
else
|
|
2049
|
-
handlers?.binary(
|
|
3124
|
+
handlers?.binary(message2);
|
|
2050
3125
|
});
|
|
2051
3126
|
channel.onClosed(finish);
|
|
2052
|
-
channel.onError((
|
|
2053
|
-
log("rtc-datachannel-error", { message });
|
|
3127
|
+
channel.onError((message2) => {
|
|
3128
|
+
log("rtc-datachannel-error", { message: message2 });
|
|
2054
3129
|
finish();
|
|
2055
3130
|
});
|
|
2056
3131
|
return {
|
|
@@ -2080,12 +3155,218 @@ function dataChannelCarrier(channel, timers, log, onClosed) {
|
|
|
2080
3155
|
}
|
|
2081
3156
|
|
|
2082
3157
|
// apps/host/src/terminals.ts
|
|
2083
|
-
import { randomUUID as
|
|
3158
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
2084
3159
|
import { userInfo } from "node:os";
|
|
2085
|
-
|
|
3160
|
+
|
|
3161
|
+
// apps/host/src/output-buffer.ts
|
|
2086
3162
|
var BUFFER_SIZE = 512 * 1024;
|
|
2087
3163
|
var FLUSH_MS = 8;
|
|
2088
3164
|
var MAX_CHUNK = 16 * 1024;
|
|
3165
|
+
|
|
3166
|
+
class OutputBuffer {
|
|
3167
|
+
#bufferSize;
|
|
3168
|
+
#flushMs;
|
|
3169
|
+
#maxChunk;
|
|
3170
|
+
#onFlush;
|
|
3171
|
+
#viewers = new Set;
|
|
3172
|
+
#buffer = "";
|
|
3173
|
+
#start = 0;
|
|
3174
|
+
#pending = "";
|
|
3175
|
+
#timer;
|
|
3176
|
+
#ended = false;
|
|
3177
|
+
constructor(options = {}) {
|
|
3178
|
+
this.#bufferSize = options.bufferSize ?? BUFFER_SIZE;
|
|
3179
|
+
this.#flushMs = options.flushMs ?? FLUSH_MS;
|
|
3180
|
+
this.#maxChunk = options.maxChunk ?? MAX_CHUNK;
|
|
3181
|
+
this.#onFlush = options.onFlush;
|
|
3182
|
+
}
|
|
3183
|
+
get start() {
|
|
3184
|
+
return this.#start;
|
|
3185
|
+
}
|
|
3186
|
+
get end() {
|
|
3187
|
+
return this.#start + this.#buffer.length;
|
|
3188
|
+
}
|
|
3189
|
+
get length() {
|
|
3190
|
+
return this.#buffer.length;
|
|
3191
|
+
}
|
|
3192
|
+
get ended() {
|
|
3193
|
+
return this.#ended;
|
|
3194
|
+
}
|
|
3195
|
+
push(data) {
|
|
3196
|
+
if (this.#ended || data === "")
|
|
3197
|
+
return;
|
|
3198
|
+
this.#pending += data;
|
|
3199
|
+
if (this.#pending.length >= this.#maxChunk)
|
|
3200
|
+
this.flush();
|
|
3201
|
+
else
|
|
3202
|
+
this.#timer ??= setTimeout(() => this.flush(), this.#flushMs);
|
|
3203
|
+
}
|
|
3204
|
+
flush() {
|
|
3205
|
+
if (this.#timer !== undefined)
|
|
3206
|
+
clearTimeout(this.#timer);
|
|
3207
|
+
this.#timer = undefined;
|
|
3208
|
+
const data = this.#pending;
|
|
3209
|
+
if (data === "")
|
|
3210
|
+
return;
|
|
3211
|
+
this.#pending = "";
|
|
3212
|
+
this.#buffer += data;
|
|
3213
|
+
this.#trim();
|
|
3214
|
+
this.#onFlush?.(data);
|
|
3215
|
+
for (const viewer of this.#viewers)
|
|
3216
|
+
this.#pump(viewer);
|
|
3217
|
+
}
|
|
3218
|
+
discard() {
|
|
3219
|
+
if (this.#timer !== undefined)
|
|
3220
|
+
clearTimeout(this.#timer);
|
|
3221
|
+
this.#timer = undefined;
|
|
3222
|
+
const gap = this.#buffer.length + this.#pending.length + 1;
|
|
3223
|
+
this.#buffer = "";
|
|
3224
|
+
this.#pending = "";
|
|
3225
|
+
this.#start += gap;
|
|
3226
|
+
for (const viewer of this.#viewers)
|
|
3227
|
+
viewer.position = undefined;
|
|
3228
|
+
}
|
|
3229
|
+
close(reason) {
|
|
3230
|
+
if (this.#ended)
|
|
3231
|
+
return;
|
|
3232
|
+
if (this.#timer !== undefined)
|
|
3233
|
+
clearTimeout(this.#timer);
|
|
3234
|
+
this.#timer = undefined;
|
|
3235
|
+
this.#ended = true;
|
|
3236
|
+
for (const viewer of this.#viewers)
|
|
3237
|
+
viewer.sink.ended(reason);
|
|
3238
|
+
this.#viewers.clear();
|
|
3239
|
+
}
|
|
3240
|
+
attach(since, sink) {
|
|
3241
|
+
const position = since !== undefined && since >= this.#start && since <= this.end ? since : undefined;
|
|
3242
|
+
const viewer = { sink, position, sending: false };
|
|
3243
|
+
this.#viewers.add(viewer);
|
|
3244
|
+
this.#pump(viewer);
|
|
3245
|
+
return () => {
|
|
3246
|
+
this.#viewers.delete(viewer);
|
|
3247
|
+
};
|
|
3248
|
+
}
|
|
3249
|
+
snapshot(since, maxChunk = this.#maxChunk) {
|
|
3250
|
+
const end = this.end;
|
|
3251
|
+
const from = since === undefined ? this.#start : Math.min(Math.max(since, this.#start), end);
|
|
3252
|
+
let to = Math.min(end, from + maxChunk);
|
|
3253
|
+
if (to < end) {
|
|
3254
|
+
const partial = partialEscapeStart(this.#buffer, from - this.#start, to - this.#start);
|
|
3255
|
+
if (partial !== undefined && this.#start + partial > from)
|
|
3256
|
+
to = this.#start + partial;
|
|
3257
|
+
}
|
|
3258
|
+
if (to < end && isHighSurrogate(this.#buffer.charCodeAt(to - this.#start - 1)))
|
|
3259
|
+
to--;
|
|
3260
|
+
const text = this.#buffer.slice(from - this.#start, to - this.#start);
|
|
3261
|
+
return {
|
|
3262
|
+
text,
|
|
3263
|
+
next: to,
|
|
3264
|
+
truncated: since === undefined ? this.#start > 0 : since < this.#start
|
|
3265
|
+
};
|
|
3266
|
+
}
|
|
3267
|
+
#trim() {
|
|
3268
|
+
if (this.#buffer.length <= this.#bufferSize)
|
|
3269
|
+
return;
|
|
3270
|
+
let cut2 = this.#buffer.length - this.#bufferSize;
|
|
3271
|
+
const line = this.#buffer.indexOf(`
|
|
3272
|
+
`, cut2);
|
|
3273
|
+
if (line !== -1 && line - cut2 < 1024)
|
|
3274
|
+
cut2 = line + 1;
|
|
3275
|
+
this.#buffer = this.#buffer.slice(cut2);
|
|
3276
|
+
this.#start += cut2;
|
|
3277
|
+
}
|
|
3278
|
+
async#pump(viewer) {
|
|
3279
|
+
if (viewer.sending)
|
|
3280
|
+
return;
|
|
3281
|
+
viewer.sending = true;
|
|
3282
|
+
try {
|
|
3283
|
+
while (this.#viewers.has(viewer)) {
|
|
3284
|
+
const end = this.end;
|
|
3285
|
+
const reset = viewer.position === undefined || viewer.position < this.#start;
|
|
3286
|
+
const from = reset ? this.#start : viewer.position;
|
|
3287
|
+
if (reset && end === this.#start)
|
|
3288
|
+
return;
|
|
3289
|
+
if (!reset && from >= end)
|
|
3290
|
+
return;
|
|
3291
|
+
let to = reset ? end : Math.min(end, from + this.#maxChunk);
|
|
3292
|
+
if (to < end && isHighSurrogate(this.#buffer.charCodeAt(to - this.#start - 1)))
|
|
3293
|
+
to--;
|
|
3294
|
+
const data = this.#buffer.slice(from - this.#start, to - this.#start);
|
|
3295
|
+
viewer.position = to;
|
|
3296
|
+
await viewer.sink.output(reset ? { seq: to, data, reset: true } : { seq: to, data });
|
|
3297
|
+
}
|
|
3298
|
+
} finally {
|
|
3299
|
+
viewer.sending = false;
|
|
3300
|
+
}
|
|
3301
|
+
}
|
|
3302
|
+
}
|
|
3303
|
+
function isHighSurrogate(code) {
|
|
3304
|
+
return code >= 55296 && code <= 56319;
|
|
3305
|
+
}
|
|
3306
|
+
function partialEscapeStart(text, from, to) {
|
|
3307
|
+
let index = from;
|
|
3308
|
+
while (index < to) {
|
|
3309
|
+
if (text.charCodeAt(index) !== 27) {
|
|
3310
|
+
index++;
|
|
3311
|
+
continue;
|
|
3312
|
+
}
|
|
3313
|
+
const end = escapeEnd(text, index, to);
|
|
3314
|
+
if (end === -1)
|
|
3315
|
+
return index;
|
|
3316
|
+
index = end;
|
|
3317
|
+
}
|
|
3318
|
+
return;
|
|
3319
|
+
}
|
|
3320
|
+
function escapeEnd(text, start, limit) {
|
|
3321
|
+
let index = start + 1;
|
|
3322
|
+
if (index >= limit)
|
|
3323
|
+
return -1;
|
|
3324
|
+
const kind = text.charCodeAt(index);
|
|
3325
|
+
if (kind === 91) {
|
|
3326
|
+
index++;
|
|
3327
|
+
while (index < limit) {
|
|
3328
|
+
const code = text.charCodeAt(index);
|
|
3329
|
+
if (code >= 64 && code <= 126)
|
|
3330
|
+
return index + 1;
|
|
3331
|
+
index++;
|
|
3332
|
+
}
|
|
3333
|
+
return -1;
|
|
3334
|
+
}
|
|
3335
|
+
if (kind === 93) {
|
|
3336
|
+
index++;
|
|
3337
|
+
while (index < limit) {
|
|
3338
|
+
const code = text.charCodeAt(index);
|
|
3339
|
+
if (code === 7)
|
|
3340
|
+
return index + 1;
|
|
3341
|
+
if (code === 27 && index + 1 < limit && text.charCodeAt(index + 1) === 92)
|
|
3342
|
+
return index + 2;
|
|
3343
|
+
index++;
|
|
3344
|
+
}
|
|
3345
|
+
return -1;
|
|
3346
|
+
}
|
|
3347
|
+
if (kind === 80 || kind === 88 || kind === 94 || kind === 95) {
|
|
3348
|
+
index++;
|
|
3349
|
+
while (index < limit) {
|
|
3350
|
+
if (text.charCodeAt(index) === 27 && index + 1 < limit && text.charCodeAt(index + 1) === 92) {
|
|
3351
|
+
return index + 2;
|
|
3352
|
+
}
|
|
3353
|
+
index++;
|
|
3354
|
+
}
|
|
3355
|
+
return -1;
|
|
3356
|
+
}
|
|
3357
|
+
while (index < limit) {
|
|
3358
|
+
const code = text.charCodeAt(index);
|
|
3359
|
+
if (code >= 32 && code <= 47) {
|
|
3360
|
+
index++;
|
|
3361
|
+
continue;
|
|
3362
|
+
}
|
|
3363
|
+
return index + 1;
|
|
3364
|
+
}
|
|
3365
|
+
return -1;
|
|
3366
|
+
}
|
|
3367
|
+
|
|
3368
|
+
// apps/host/src/terminals.ts
|
|
3369
|
+
var TERMINAL_LIMIT = 8;
|
|
2089
3370
|
var MAX_TITLE = 1024;
|
|
2090
3371
|
var TITLE = /\x1b\][02];([^\x07\x1b]*)(?:\x07|\x1b\\)/g;
|
|
2091
3372
|
|
|
@@ -2125,7 +3406,7 @@ class Terminals {
|
|
|
2125
3406
|
const number = (this.#numbers.get(owner) ?? 0) + 1;
|
|
2126
3407
|
this.#numbers.set(owner, number);
|
|
2127
3408
|
const terminal = {
|
|
2128
|
-
id:
|
|
3409
|
+
id: randomUUID3(),
|
|
2129
3410
|
owner,
|
|
2130
3411
|
cwd,
|
|
2131
3412
|
number,
|
|
@@ -2136,12 +3417,16 @@ class Terminals {
|
|
|
2136
3417
|
title: undefined,
|
|
2137
3418
|
rename: undefined,
|
|
2138
3419
|
exit: undefined,
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
3420
|
+
output: new OutputBuffer({
|
|
3421
|
+
...this.#options.bufferSize === undefined ? {} : { bufferSize: this.#options.bufferSize },
|
|
3422
|
+
...this.#options.flushMs === undefined ? {} : { flushMs: this.#options.flushMs },
|
|
3423
|
+
...this.#options.maxChunk === undefined ? {} : { maxChunk: this.#options.maxChunk },
|
|
3424
|
+
onFlush: (data) => {
|
|
3425
|
+
if (this.#readTitle(terminal, data))
|
|
3426
|
+
this.#changed(owner);
|
|
3427
|
+
}
|
|
3428
|
+
}),
|
|
3429
|
+
titleCarry: ""
|
|
2145
3430
|
};
|
|
2146
3431
|
this.#terminals.set(terminal.id, terminal);
|
|
2147
3432
|
try {
|
|
@@ -2171,16 +3456,12 @@ class Terminals {
|
|
|
2171
3456
|
restart(id, size) {
|
|
2172
3457
|
const terminal = this.#get(id);
|
|
2173
3458
|
this.#stop(terminal);
|
|
2174
|
-
terminal.
|
|
2175
|
-
terminal.buffer = "";
|
|
2176
|
-
terminal.pending = "";
|
|
3459
|
+
terminal.output.discard();
|
|
2177
3460
|
terminal.titleCarry = "";
|
|
2178
3461
|
terminal.title = undefined;
|
|
2179
3462
|
terminal.exit = undefined;
|
|
2180
3463
|
terminal.cols = size.cols;
|
|
2181
3464
|
terminal.rows = size.rows;
|
|
2182
|
-
for (const viewer of terminal.viewers)
|
|
2183
|
-
viewer.position = undefined;
|
|
2184
3465
|
try {
|
|
2185
3466
|
this.#start(terminal);
|
|
2186
3467
|
} catch (error) {
|
|
@@ -2209,15 +3490,7 @@ class Terminals {
|
|
|
2209
3490
|
this.closeOwners([...this.#terminals.values()].map((terminal) => terminal.owner));
|
|
2210
3491
|
}
|
|
2211
3492
|
attach(id, since, sink) {
|
|
2212
|
-
|
|
2213
|
-
const end = terminal.start + terminal.buffer.length;
|
|
2214
|
-
const position = since !== undefined && since >= terminal.start && since <= end ? since : undefined;
|
|
2215
|
-
const viewer = { sink, position, sending: false };
|
|
2216
|
-
terminal.viewers.add(viewer);
|
|
2217
|
-
this.#pump(terminal, viewer);
|
|
2218
|
-
return () => {
|
|
2219
|
-
terminal.viewers.delete(viewer);
|
|
2220
|
-
};
|
|
3493
|
+
return this.#get(id).output.attach(since, sink);
|
|
2221
3494
|
}
|
|
2222
3495
|
#get(id) {
|
|
2223
3496
|
const terminal = this.#terminals.get(id);
|
|
@@ -2239,25 +3512,19 @@ class Terminals {
|
|
|
2239
3512
|
pty.onData((data) => {
|
|
2240
3513
|
if (terminal.generation !== generation)
|
|
2241
3514
|
return;
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
this.#flush(terminal);
|
|
2245
|
-
else
|
|
2246
|
-
terminal.flushTimer ??= setTimeout(() => this.#flush(terminal), this.#options.flushMs ?? FLUSH_MS);
|
|
3515
|
+
this.#options.onOutput?.();
|
|
3516
|
+
terminal.output.push(data);
|
|
2247
3517
|
});
|
|
2248
3518
|
pty.onExit(({ exitCode, signal }) => {
|
|
2249
3519
|
if (terminal.generation !== generation || !this.#terminals.has(terminal.id))
|
|
2250
3520
|
return;
|
|
2251
|
-
|
|
3521
|
+
terminal.output.flush();
|
|
2252
3522
|
terminal.pty = undefined;
|
|
2253
3523
|
terminal.exit = signal === undefined || signal === 0 ? { code: exitCode } : { code: exitCode, signal };
|
|
2254
3524
|
this.#changed(terminal.owner);
|
|
2255
3525
|
});
|
|
2256
3526
|
}
|
|
2257
3527
|
#stop(terminal) {
|
|
2258
|
-
if (terminal.flushTimer !== undefined)
|
|
2259
|
-
clearTimeout(terminal.flushTimer);
|
|
2260
|
-
terminal.flushTimer = undefined;
|
|
2261
3528
|
terminal.generation++;
|
|
2262
3529
|
const pty = terminal.pty;
|
|
2263
3530
|
terminal.pty = undefined;
|
|
@@ -2267,10 +3534,8 @@ class Terminals {
|
|
|
2267
3534
|
}
|
|
2268
3535
|
#remove(terminal) {
|
|
2269
3536
|
this.#stop(terminal);
|
|
3537
|
+
terminal.output.close("Terminal closed");
|
|
2270
3538
|
this.#terminals.delete(terminal.id);
|
|
2271
|
-
for (const viewer of terminal.viewers)
|
|
2272
|
-
viewer.sink.ended("Terminal closed");
|
|
2273
|
-
terminal.viewers.clear();
|
|
2274
3539
|
}
|
|
2275
3540
|
#resize(terminal, size) {
|
|
2276
3541
|
if (terminal.cols === size.cols && terminal.rows === size.rows)
|
|
@@ -2282,30 +3547,6 @@ class Terminals {
|
|
|
2282
3547
|
} catch {}
|
|
2283
3548
|
this.#changed(terminal.owner);
|
|
2284
3549
|
}
|
|
2285
|
-
#flush(terminal) {
|
|
2286
|
-
if (terminal.flushTimer !== undefined)
|
|
2287
|
-
clearTimeout(terminal.flushTimer);
|
|
2288
|
-
terminal.flushTimer = undefined;
|
|
2289
|
-
const data = terminal.pending;
|
|
2290
|
-
if (data === "")
|
|
2291
|
-
return;
|
|
2292
|
-
terminal.pending = "";
|
|
2293
|
-
terminal.buffer += data;
|
|
2294
|
-
const bufferSize = this.#options.bufferSize ?? BUFFER_SIZE;
|
|
2295
|
-
if (terminal.buffer.length > bufferSize) {
|
|
2296
|
-
let cut = terminal.buffer.length - bufferSize;
|
|
2297
|
-
const line = terminal.buffer.indexOf(`
|
|
2298
|
-
`, cut);
|
|
2299
|
-
if (line !== -1 && line - cut < 1024)
|
|
2300
|
-
cut = line + 1;
|
|
2301
|
-
terminal.buffer = terminal.buffer.slice(cut);
|
|
2302
|
-
terminal.start += cut;
|
|
2303
|
-
}
|
|
2304
|
-
if (this.#readTitle(terminal, data))
|
|
2305
|
-
this.#changed(terminal.owner);
|
|
2306
|
-
for (const viewer of terminal.viewers)
|
|
2307
|
-
this.#pump(terminal, viewer);
|
|
2308
|
-
}
|
|
2309
3550
|
#readTitle(terminal, data) {
|
|
2310
3551
|
const text = terminal.titleCarry + data;
|
|
2311
3552
|
let title;
|
|
@@ -2321,29 +3562,6 @@ class Terminals {
|
|
|
2321
3562
|
terminal.title = title;
|
|
2322
3563
|
return terminal.rename === undefined;
|
|
2323
3564
|
}
|
|
2324
|
-
async#pump(terminal, viewer) {
|
|
2325
|
-
if (viewer.sending)
|
|
2326
|
-
return;
|
|
2327
|
-
viewer.sending = true;
|
|
2328
|
-
try {
|
|
2329
|
-
const maxChunk = this.#options.maxChunk ?? MAX_CHUNK;
|
|
2330
|
-
while (terminal.viewers.has(viewer)) {
|
|
2331
|
-
const end = terminal.start + terminal.buffer.length;
|
|
2332
|
-
const reset = viewer.position === undefined || viewer.position < terminal.start;
|
|
2333
|
-
const from = reset ? terminal.start : viewer.position;
|
|
2334
|
-
if (!reset && from >= end)
|
|
2335
|
-
return;
|
|
2336
|
-
let to = reset ? end : Math.min(end, from + maxChunk);
|
|
2337
|
-
if (to < end && isHighSurrogate(terminal.buffer.charCodeAt(to - terminal.start - 1)))
|
|
2338
|
-
to--;
|
|
2339
|
-
const data = terminal.buffer.slice(from - terminal.start, to - terminal.start);
|
|
2340
|
-
viewer.position = to;
|
|
2341
|
-
await viewer.sink.output(reset ? { seq: to, data, reset: true } : { seq: to, data });
|
|
2342
|
-
}
|
|
2343
|
-
} finally {
|
|
2344
|
-
viewer.sending = false;
|
|
2345
|
-
}
|
|
2346
|
-
}
|
|
2347
3565
|
#changed(owner) {
|
|
2348
3566
|
const watchers = this.#watchers.get(owner);
|
|
2349
3567
|
if (watchers === undefined)
|
|
@@ -2354,20 +3572,20 @@ class Terminals {
|
|
|
2354
3572
|
}
|
|
2355
3573
|
}
|
|
2356
3574
|
async function startTerminals(options = {}) {
|
|
2357
|
-
let
|
|
2358
|
-
if (
|
|
3575
|
+
let spawn4 = options.spawn;
|
|
3576
|
+
if (spawn4 === undefined) {
|
|
2359
3577
|
try {
|
|
2360
3578
|
const loaded = options.load === undefined ? await import("@lydell/node-pty") : await options.load();
|
|
2361
3579
|
if (typeof loaded.spawn !== "function")
|
|
2362
3580
|
throw new Error("the module has no spawn()");
|
|
2363
|
-
|
|
3581
|
+
spawn4 = loaded.spawn;
|
|
2364
3582
|
} catch (error) {
|
|
2365
3583
|
const detail = error instanceof Error ? error.message : String(error);
|
|
2366
3584
|
(options.diagnostic ?? console.error)(`Terminals unavailable: cannot load @lydell/node-pty on ${process.platform} (${process.arch}); missing @lydell/node-pty-${process.platform}-${process.arch}. Install without --omit=optional; the host continues without hello.terminals. (${detail})`);
|
|
2367
3585
|
return;
|
|
2368
3586
|
}
|
|
2369
3587
|
}
|
|
2370
|
-
return new Terminals({ ...options, spawn:
|
|
3588
|
+
return new Terminals({ ...options, spawn: spawn4, shell: options.shell ?? loginShell(), env: options.env ?? shellEnv(process.env) });
|
|
2371
3589
|
}
|
|
2372
3590
|
function loginShell() {
|
|
2373
3591
|
let file = process.env.SHELL;
|
|
@@ -2381,12 +3599,12 @@ function loginShell() {
|
|
|
2381
3599
|
return { file: file === undefined || file === "" ? "/bin/sh" : file, args: ["-l"] };
|
|
2382
3600
|
}
|
|
2383
3601
|
function shellEnv(env) {
|
|
2384
|
-
const
|
|
3602
|
+
const result3 = {};
|
|
2385
3603
|
for (const [key, value] of Object.entries(env)) {
|
|
2386
3604
|
if (value !== undefined && !key.startsWith("PINOMAD_"))
|
|
2387
|
-
|
|
3605
|
+
result3[key] = value;
|
|
2388
3606
|
}
|
|
2389
|
-
return
|
|
3607
|
+
return result3;
|
|
2390
3608
|
}
|
|
2391
3609
|
function info(terminal) {
|
|
2392
3610
|
return {
|
|
@@ -2398,13 +3616,10 @@ function info(terminal) {
|
|
|
2398
3616
|
...terminal.exit === undefined ? {} : { exit: terminal.exit }
|
|
2399
3617
|
};
|
|
2400
3618
|
}
|
|
2401
|
-
function isHighSurrogate(code) {
|
|
2402
|
-
return code >= 55296 && code <= 56319;
|
|
2403
|
-
}
|
|
2404
3619
|
|
|
2405
3620
|
// apps/host/src/folders.ts
|
|
2406
3621
|
import { readdir, realpath as realpath5, stat as stat5 } from "node:fs/promises";
|
|
2407
|
-
import { isAbsolute, join as
|
|
3622
|
+
import { isAbsolute, join as join7 } from "node:path";
|
|
2408
3623
|
var denied = (error) => {
|
|
2409
3624
|
const code = error?.code;
|
|
2410
3625
|
return code === "EACCES" || code === "EPERM";
|
|
@@ -2434,10 +3649,10 @@ async function listFolders(path, home) {
|
|
|
2434
3649
|
continue;
|
|
2435
3650
|
let isDirectory = entry.isDirectory();
|
|
2436
3651
|
if (!isDirectory && entry.isSymbolicLink()) {
|
|
2437
|
-
isDirectory = await stat5(
|
|
3652
|
+
isDirectory = await stat5(join7(dir, entry.name)).then((target) => target.isDirectory(), () => false);
|
|
2438
3653
|
}
|
|
2439
3654
|
if (isDirectory)
|
|
2440
|
-
folders.push({ name: entry.name, path:
|
|
3655
|
+
folders.push({ name: entry.name, path: join7(dir, entry.name) });
|
|
2441
3656
|
}
|
|
2442
3657
|
folders.sort((a, b) => a.name.localeCompare(b.name, "en", { numeric: true, sensitivity: "base" }));
|
|
2443
3658
|
return { path: dir, home: await realpath5(home).catch(() => home), folders };
|
|
@@ -2623,7 +3838,7 @@ function imageProblem(images) {
|
|
|
2623
3838
|
// apps/host/src/image-refs.ts
|
|
2624
3839
|
import { createHash } from "node:crypto";
|
|
2625
3840
|
import { applyImmutable } from "@earendil-works/chord/delta";
|
|
2626
|
-
var
|
|
3841
|
+
var isObject2 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2627
3842
|
var isImageBlock = (value) => value.type === "image" && typeof value.data === "string" && typeof value.mimeType === "string";
|
|
2628
3843
|
var hashed = new WeakMap;
|
|
2629
3844
|
function refOf(block) {
|
|
@@ -2715,7 +3930,7 @@ function rewriteEntries(value) {
|
|
|
2715
3930
|
return out ?? value;
|
|
2716
3931
|
}
|
|
2717
3932
|
function rewriteEntry(value) {
|
|
2718
|
-
if (!
|
|
3933
|
+
if (!isObject2(value) || value.model === undefined)
|
|
2719
3934
|
return value;
|
|
2720
3935
|
const model = rewriteMessages(value.model);
|
|
2721
3936
|
return model === value.model ? value : { ...value, model };
|
|
@@ -2732,7 +3947,7 @@ function rewriteMessages(value) {
|
|
|
2732
3947
|
return out ?? value;
|
|
2733
3948
|
}
|
|
2734
3949
|
function rewriteMessage(value) {
|
|
2735
|
-
if (!
|
|
3950
|
+
if (!isObject2(value) || value.content === undefined)
|
|
2736
3951
|
return value;
|
|
2737
3952
|
const content = rewriteContent(value.content);
|
|
2738
3953
|
return content === value.content ? value : { ...value, content };
|
|
@@ -2743,7 +3958,7 @@ function rewriteContent(value) {
|
|
|
2743
3958
|
let out;
|
|
2744
3959
|
for (let i = 0;i < value.length; i++) {
|
|
2745
3960
|
const block = value[i];
|
|
2746
|
-
const next =
|
|
3961
|
+
const next = isObject2(block) && isImageBlock(block) ? withRef(block) : block;
|
|
2747
3962
|
if (next !== block)
|
|
2748
3963
|
(out ??= value.slice())[i] = next;
|
|
2749
3964
|
}
|
|
@@ -2761,31 +3976,31 @@ function rewriteItems(value) {
|
|
|
2761
3976
|
return out ?? value;
|
|
2762
3977
|
}
|
|
2763
3978
|
function rewriteItem(value) {
|
|
2764
|
-
if (!
|
|
3979
|
+
if (!isObject2(value) || value.content === undefined)
|
|
2765
3980
|
return value;
|
|
2766
3981
|
const content = rewriteContent(value.content);
|
|
2767
3982
|
return content === value.content ? value : { ...value, content };
|
|
2768
3983
|
}
|
|
2769
3984
|
function rewriteInbox(value) {
|
|
2770
|
-
if (!
|
|
3985
|
+
if (!isObject2(value) || value.items === undefined)
|
|
2771
3986
|
return value;
|
|
2772
3987
|
const items = rewriteItems(value.items);
|
|
2773
3988
|
return items === value.items ? value : { ...value, items };
|
|
2774
3989
|
}
|
|
2775
3990
|
function rewriteGeneration(value) {
|
|
2776
|
-
if (!
|
|
3991
|
+
if (!isObject2(value) || value.message === undefined)
|
|
2777
3992
|
return value;
|
|
2778
|
-
const
|
|
2779
|
-
return
|
|
3993
|
+
const message2 = rewriteMessage(value.message);
|
|
3994
|
+
return message2 === value.message ? value : { ...value, message: message2 };
|
|
2780
3995
|
}
|
|
2781
3996
|
function rewriteLive(value) {
|
|
2782
|
-
if (!
|
|
3997
|
+
if (!isObject2(value) || value.generation === undefined)
|
|
2783
3998
|
return value;
|
|
2784
3999
|
const generation = rewriteGeneration(value.generation);
|
|
2785
4000
|
return generation === value.generation ? value : { ...value, generation };
|
|
2786
4001
|
}
|
|
2787
4002
|
function rewriteDocs(value) {
|
|
2788
|
-
if (!
|
|
4003
|
+
if (!isObject2(value))
|
|
2789
4004
|
return value;
|
|
2790
4005
|
let out;
|
|
2791
4006
|
for (const key of ["pi.inbox", "pi.live"]) {
|
|
@@ -2799,7 +4014,7 @@ function rewriteDocs(value) {
|
|
|
2799
4014
|
return out ?? value;
|
|
2800
4015
|
}
|
|
2801
4016
|
function rewriteView(value) {
|
|
2802
|
-
if (!
|
|
4017
|
+
if (!isObject2(value))
|
|
2803
4018
|
return value;
|
|
2804
4019
|
let out;
|
|
2805
4020
|
if (value.entries !== undefined) {
|
|
@@ -2827,7 +4042,7 @@ function rewriteSlot(slot, value) {
|
|
|
2827
4042
|
case "content":
|
|
2828
4043
|
return rewriteContent(value);
|
|
2829
4044
|
case "block":
|
|
2830
|
-
return
|
|
4045
|
+
return isObject2(value) && isImageBlock(value) ? withRef(value) : value;
|
|
2831
4046
|
case "docs":
|
|
2832
4047
|
return rewriteDocs(value);
|
|
2833
4048
|
case "inbox":
|
|
@@ -2892,35 +4107,35 @@ function reachesIntoImage(op, root) {
|
|
|
2892
4107
|
if (slotAt(path.slice(0, length)) !== "block")
|
|
2893
4108
|
continue;
|
|
2894
4109
|
const node = resolve4(root, path, length);
|
|
2895
|
-
if (
|
|
4110
|
+
if (isObject2(node) && isImageBlock(node))
|
|
2896
4111
|
return true;
|
|
2897
4112
|
}
|
|
2898
4113
|
return false;
|
|
2899
4114
|
}
|
|
2900
4115
|
function* contentArrays(view) {
|
|
2901
|
-
if (!
|
|
4116
|
+
if (!isObject2(view))
|
|
2902
4117
|
return;
|
|
2903
4118
|
if (Array.isArray(view.entries)) {
|
|
2904
4119
|
for (const entry of view.entries) {
|
|
2905
|
-
if (!
|
|
4120
|
+
if (!isObject2(entry) || !Array.isArray(entry.model))
|
|
2906
4121
|
continue;
|
|
2907
|
-
for (const
|
|
2908
|
-
if (
|
|
2909
|
-
yield
|
|
4122
|
+
for (const message2 of entry.model) {
|
|
4123
|
+
if (isObject2(message2) && Array.isArray(message2.content))
|
|
4124
|
+
yield message2.content;
|
|
2910
4125
|
}
|
|
2911
4126
|
}
|
|
2912
4127
|
}
|
|
2913
|
-
if (!
|
|
4128
|
+
if (!isObject2(view.docs))
|
|
2914
4129
|
return;
|
|
2915
4130
|
const inbox = view.docs["pi.inbox"];
|
|
2916
|
-
if (
|
|
4131
|
+
if (isObject2(inbox) && Array.isArray(inbox.items)) {
|
|
2917
4132
|
for (const item of inbox.items) {
|
|
2918
|
-
if (
|
|
4133
|
+
if (isObject2(item) && Array.isArray(item.content))
|
|
2919
4134
|
yield item.content;
|
|
2920
4135
|
}
|
|
2921
4136
|
}
|
|
2922
4137
|
const live = view.docs["pi.live"];
|
|
2923
|
-
if (
|
|
4138
|
+
if (isObject2(live) && isObject2(live.generation) && isObject2(live.generation.message) && Array.isArray(live.generation.message.content)) {
|
|
2924
4139
|
yield live.generation.message.content;
|
|
2925
4140
|
}
|
|
2926
4141
|
}
|
|
@@ -2928,7 +4143,7 @@ function imageBlocks(view) {
|
|
|
2928
4143
|
const blocks = [];
|
|
2929
4144
|
for (const content of contentArrays(view)) {
|
|
2930
4145
|
for (const block of content) {
|
|
2931
|
-
if (
|
|
4146
|
+
if (isObject2(block) && isImageBlock(block))
|
|
2932
4147
|
blocks.push(block);
|
|
2933
4148
|
}
|
|
2934
4149
|
}
|
|
@@ -3016,8 +4231,8 @@ class ImageVariants {
|
|
|
3016
4231
|
if (job === undefined)
|
|
3017
4232
|
return;
|
|
3018
4233
|
this.#active += 1;
|
|
3019
|
-
this.#derive(job.key, job.variant, job.original).then((
|
|
3020
|
-
job.resolve(
|
|
4234
|
+
this.#derive(job.key, job.variant, job.original).then((result3) => {
|
|
4235
|
+
job.resolve(result3);
|
|
3021
4236
|
this.#finish();
|
|
3022
4237
|
}, (error) => {
|
|
3023
4238
|
job.reject(error instanceof Error ? error : new Error(String(error)));
|
|
@@ -3038,13 +4253,13 @@ class ImageVariants {
|
|
|
3038
4253
|
if (prepared === undefined || prepared.data === original.data && prepared.mimeType === original.mimeType) {
|
|
3039
4254
|
return { ...original, variant: "original" };
|
|
3040
4255
|
}
|
|
3041
|
-
const
|
|
4256
|
+
const result3 = {
|
|
3042
4257
|
data: prepared.data,
|
|
3043
4258
|
mimeType: prepared.mimeType,
|
|
3044
4259
|
variant
|
|
3045
4260
|
};
|
|
3046
|
-
this.#write(key,
|
|
3047
|
-
return
|
|
4261
|
+
this.#write(key, result3);
|
|
4262
|
+
return result3;
|
|
3048
4263
|
} catch {
|
|
3049
4264
|
return { ...original, variant: "original" };
|
|
3050
4265
|
}
|
|
@@ -3057,8 +4272,8 @@ class ImageVariants {
|
|
|
3057
4272
|
this.#cache.set(key, cached);
|
|
3058
4273
|
return cached;
|
|
3059
4274
|
}
|
|
3060
|
-
#write(key,
|
|
3061
|
-
const bytes =
|
|
4275
|
+
#write(key, result3) {
|
|
4276
|
+
const bytes = result3.data.length;
|
|
3062
4277
|
if (bytes > this.#maxCacheBytes)
|
|
3063
4278
|
return;
|
|
3064
4279
|
while (this.#cacheBytes + bytes > this.#maxCacheBytes) {
|
|
@@ -3069,17 +4284,17 @@ class ImageVariants {
|
|
|
3069
4284
|
this.#cache.delete(oldest);
|
|
3070
4285
|
this.#cacheBytes -= previous.data.length;
|
|
3071
4286
|
}
|
|
3072
|
-
this.#cache.set(key,
|
|
4287
|
+
this.#cache.set(key, result3);
|
|
3073
4288
|
this.#cacheBytes += bytes;
|
|
3074
4289
|
}
|
|
3075
4290
|
}
|
|
3076
4291
|
|
|
3077
4292
|
// apps/host/src/auto-title.ts
|
|
3078
|
-
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
|
4293
|
+
import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT2 } from "@earendil-works/chord/context";
|
|
3079
4294
|
|
|
3080
4295
|
// apps/host/src/title-doc.ts
|
|
3081
|
-
import { defineDoc as
|
|
3082
|
-
var TitleDoc =
|
|
4296
|
+
import { defineDoc as defineDoc9 } from "@earendil-works/pi-durable";
|
|
4297
|
+
var TitleDoc = defineDoc9({
|
|
3083
4298
|
kind: "pinomad.title",
|
|
3084
4299
|
version: 1,
|
|
3085
4300
|
scope: "conversation",
|
|
@@ -3090,11 +4305,11 @@ var TitleDoc = defineDoc6({
|
|
|
3090
4305
|
});
|
|
3091
4306
|
|
|
3092
4307
|
// apps/host/src/auto-title.ts
|
|
3093
|
-
var context =
|
|
4308
|
+
var context = BACKGROUND_CONTEXT2;
|
|
3094
4309
|
var TITLE_PROMPT = "You name conversations between a user and a coding agent. Reply with only the title: at most 6 words" + " (about 20 characters for Chinese or Japanese), in the same language as the user's message, no quotes," + " no trailing punctuation. Name the task, not the agent's answer.";
|
|
3095
4310
|
var TITLE_INPUT_LIMIT = 2000;
|
|
3096
4311
|
var TITLE_MAX_LENGTH = 80;
|
|
3097
|
-
var
|
|
4312
|
+
var textOf2 = (content) => {
|
|
3098
4313
|
if (typeof content === "string")
|
|
3099
4314
|
return content;
|
|
3100
4315
|
if (!Array.isArray(content))
|
|
@@ -3151,7 +4366,7 @@ function attachAutoTitle(runs, deps) {
|
|
|
3151
4366
|
return;
|
|
3152
4367
|
const firstMessage = first.model?.[0];
|
|
3153
4368
|
const firstText = (stripUserBlocks(firstMessage?.content ?? "").text || "(images or files only)").slice(0, TITLE_INPUT_LIMIT);
|
|
3154
|
-
const answer =
|
|
4369
|
+
const answer = textOf2(last.content).slice(0, TITLE_INPUT_LIMIT);
|
|
3155
4370
|
const request = {
|
|
3156
4371
|
systemPrompt: TITLE_PROMPT,
|
|
3157
4372
|
messages: [
|
|
@@ -3198,8 +4413,8 @@ ${answer}`}`,
|
|
|
3198
4413
|
|
|
3199
4414
|
// apps/host/src/push.ts
|
|
3200
4415
|
import { createHash as createHash2 } from "node:crypto";
|
|
3201
|
-
import { BACKGROUND_CONTEXT as
|
|
3202
|
-
import { defineDoc as
|
|
4416
|
+
import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT3 } from "@earendil-works/chord/context";
|
|
4417
|
+
import { defineDoc as defineDoc10 } from "@earendil-works/pi-durable";
|
|
3203
4418
|
|
|
3204
4419
|
// packages/client/src/family.ts
|
|
3205
4420
|
var SUBAGENT_TASK_PREFIX = /^\s*in the working directory\s+\S+,\s*/i;
|
|
@@ -3256,8 +4471,8 @@ function diffRunNotices(previous, organized) {
|
|
|
3256
4471
|
}
|
|
3257
4472
|
|
|
3258
4473
|
// apps/host/src/push.ts
|
|
3259
|
-
var context2 =
|
|
3260
|
-
var PushDoc =
|
|
4474
|
+
var context2 = BACKGROUND_CONTEXT3;
|
|
4475
|
+
var PushDoc = defineDoc10({
|
|
3261
4476
|
kind: "pinomad.push",
|
|
3262
4477
|
version: 2,
|
|
3263
4478
|
scope: "session",
|
|
@@ -3394,18 +4609,18 @@ async function attachPushSender(options) {
|
|
|
3394
4609
|
return await new Promise((resolve5) => {
|
|
3395
4610
|
const waiters = titleWaiters.get(notice.id) ?? new Set;
|
|
3396
4611
|
titleWaiters.set(notice.id, waiters);
|
|
3397
|
-
const
|
|
3398
|
-
waiters.delete(
|
|
4612
|
+
const settle2 = (title) => {
|
|
4613
|
+
waiters.delete(settle2);
|
|
3399
4614
|
if (waiters.size === 0)
|
|
3400
4615
|
titleWaiters.delete(notice.id);
|
|
3401
4616
|
timers.clearTimeout(timer);
|
|
3402
4617
|
resolve5(title);
|
|
3403
4618
|
};
|
|
3404
|
-
const timer = timers.setTimeout(() =>
|
|
3405
|
-
waiters.add(
|
|
4619
|
+
const timer = timers.setTimeout(() => settle2(FALLBACK_TITLE), TITLE_WAIT_MS);
|
|
4620
|
+
waiters.add(settle2);
|
|
3406
4621
|
namedTitle(notice.id).then((title) => {
|
|
3407
4622
|
if (title !== undefined)
|
|
3408
|
-
|
|
4623
|
+
settle2(title);
|
|
3409
4624
|
});
|
|
3410
4625
|
});
|
|
3411
4626
|
};
|
|
@@ -3545,8 +4760,8 @@ async function attachPushSender(options) {
|
|
|
3545
4760
|
continue;
|
|
3546
4761
|
const waiters = titleWaiters.get(id);
|
|
3547
4762
|
titleWaiters.delete(id);
|
|
3548
|
-
for (const
|
|
3549
|
-
|
|
4763
|
+
for (const settle2 of waiters ?? [])
|
|
4764
|
+
settle2(title);
|
|
3550
4765
|
}
|
|
3551
4766
|
},
|
|
3552
4767
|
end: () => {}
|
|
@@ -3602,7 +4817,7 @@ function toolTarget(args) {
|
|
|
3602
4817
|
}
|
|
3603
4818
|
|
|
3604
4819
|
// apps/host/src/gateway.ts
|
|
3605
|
-
var context3 =
|
|
4820
|
+
var context3 = BACKGROUND_CONTEXT4;
|
|
3606
4821
|
var forkTails = new WeakMap;
|
|
3607
4822
|
function assertQueuedInput(record, conversationId) {
|
|
3608
4823
|
if (record.conversationId !== conversationId) {
|
|
@@ -3615,7 +4830,7 @@ async function dismissPendingQuestions(tx, docs, conversationId) {
|
|
|
3615
4830
|
let dismissed = false;
|
|
3616
4831
|
for (const doc of docs) {
|
|
3617
4832
|
const value = await tx.doc(doc.token, conversationId);
|
|
3618
|
-
if (!
|
|
4833
|
+
if (!Value.Check(QuestionSchema, value))
|
|
3619
4834
|
continue;
|
|
3620
4835
|
for (const request of value.requests) {
|
|
3621
4836
|
if (request.resolution === undefined) {
|
|
@@ -3667,6 +4882,19 @@ function submitResult(record) {
|
|
|
3667
4882
|
...record.entry === undefined ? {} : { entry: record.entry }
|
|
3668
4883
|
};
|
|
3669
4884
|
}
|
|
4885
|
+
var AGENT_WORK_CALLS = new Set(["submit", "createConversation", "runCommand", "compact", "answer"]);
|
|
4886
|
+
var NO_UPGRADES = constantValue({
|
|
4887
|
+
current: packagedVersion ?? null,
|
|
4888
|
+
available: null,
|
|
4889
|
+
blocked: "source",
|
|
4890
|
+
phase: "idle",
|
|
4891
|
+
target: null,
|
|
4892
|
+
attempt: 0,
|
|
4893
|
+
checking: false,
|
|
4894
|
+
checkedAt: null,
|
|
4895
|
+
checkError: null,
|
|
4896
|
+
failure: null
|
|
4897
|
+
});
|
|
3670
4898
|
var LOOPBACK_SECURE_PATH = "/secure";
|
|
3671
4899
|
async function startGateway(options) {
|
|
3672
4900
|
if (options.secure?.lan?.publicUrl !== undefined)
|
|
@@ -3702,10 +4930,16 @@ async function startGateway(options) {
|
|
|
3702
4930
|
const clients = new Set;
|
|
3703
4931
|
let rtc;
|
|
3704
4932
|
const rtcHello = () => rtc?.hello();
|
|
3705
|
-
const terminals = await startTerminals(
|
|
3706
|
-
|
|
4933
|
+
const terminals = await startTerminals({
|
|
4934
|
+
...options.terminals,
|
|
4935
|
+
onOutput: () => {
|
|
4936
|
+
options.terminals?.onOutput?.();
|
|
4937
|
+
options.upgrade?.touch();
|
|
4938
|
+
}
|
|
4939
|
+
});
|
|
4940
|
+
const broadcast = (level, message2) => {
|
|
3707
4941
|
for (const client of clients)
|
|
3708
|
-
client.send({ type: "notice", level, message });
|
|
4942
|
+
client.send({ type: "notice", level, message: message2 });
|
|
3709
4943
|
};
|
|
3710
4944
|
let remote;
|
|
3711
4945
|
let relay;
|
|
@@ -3740,6 +4974,7 @@ async function startGateway(options) {
|
|
|
3740
4974
|
devicePublicKey,
|
|
3741
4975
|
rtcHello,
|
|
3742
4976
|
terminals,
|
|
4977
|
+
...options.processes === undefined ? {} : { processes: options.processes },
|
|
3743
4978
|
closed: (self) => {
|
|
3744
4979
|
clients.delete(self);
|
|
3745
4980
|
connections.delete(self);
|
|
@@ -3977,6 +5212,7 @@ async function startGateway(options) {
|
|
|
3977
5212
|
await detachPush?.close();
|
|
3978
5213
|
conversations.dispose();
|
|
3979
5214
|
terminals?.closeAll();
|
|
5215
|
+
await options.processes?.close(context3);
|
|
3980
5216
|
await rtc?.close();
|
|
3981
5217
|
await Promise.all([...connections].map((connection) => connection.dispose()));
|
|
3982
5218
|
for (const socket of server.clients)
|
|
@@ -4149,6 +5385,8 @@ function secureHandshake(carrier, secure, policy, handlers) {
|
|
|
4149
5385
|
session = new SecureSession(admitted.result);
|
|
4150
5386
|
stage = "settled";
|
|
4151
5387
|
await carrier.send(admitted.message);
|
|
5388
|
+
if (closed)
|
|
5389
|
+
return;
|
|
4152
5390
|
if (admitted.purpose === "signal")
|
|
4153
5391
|
handlers.signal?.(connection, admitted.deviceKey);
|
|
4154
5392
|
else
|
|
@@ -4209,10 +5447,10 @@ function secureHandshake(carrier, secure, policy, handlers) {
|
|
|
4209
5447
|
if (decision.outcome === "admitted" && decision.requestId !== undefined) {
|
|
4210
5448
|
policy.accountAdmitted(decision.requestId, deviceKey);
|
|
4211
5449
|
}
|
|
4212
|
-
const { message, result:
|
|
5450
|
+
const { message: message2, result: result3 } = responder.writeMessage2(encodeHostHello({ v: 1 }));
|
|
4213
5451
|
return {
|
|
4214
|
-
result:
|
|
4215
|
-
message,
|
|
5452
|
+
result: result3,
|
|
5453
|
+
message: message2,
|
|
4216
5454
|
deviceKey,
|
|
4217
5455
|
purpose,
|
|
4218
5456
|
...decision.outcome === "admitted" ? { joined: decision.entry } : {}
|
|
@@ -4324,7 +5562,7 @@ class ConversationList {
|
|
|
4324
5562
|
facts.tools = live?.tools;
|
|
4325
5563
|
for (const token of this.#questionTokens) {
|
|
4326
5564
|
const value = await harness.snapshot(token, record.id, context3);
|
|
4327
|
-
if (
|
|
5565
|
+
if (Value.Check(QuestionSchema, value) && value.requests.some((request) => request.resolution === undefined)) {
|
|
4328
5566
|
facts.pending.add(token.definition.kind);
|
|
4329
5567
|
}
|
|
4330
5568
|
}
|
|
@@ -4335,6 +5573,9 @@ class ConversationList {
|
|
|
4335
5573
|
if (typeof subagent?.role === "string" && subagent.role !== "")
|
|
4336
5574
|
facts.role = subagent.role;
|
|
4337
5575
|
facts.model = modelRefOf(subagent?.model);
|
|
5576
|
+
if (typeof subagent?.name === "string" && subagent.name !== "")
|
|
5577
|
+
facts.name = subagent.name;
|
|
5578
|
+
facts.lifecycle = subagent?.lifecycle === "background" ? "background" : "foreground";
|
|
4338
5579
|
}
|
|
4339
5580
|
facts.agent = roleNameOf(await harness.snapshot(RoleDoc, record.id, context3));
|
|
4340
5581
|
const named = (await harness.snapshot(TitleDoc, record.id, context3))?.title;
|
|
@@ -4363,10 +5604,10 @@ class ConversationList {
|
|
|
4363
5604
|
continue;
|
|
4364
5605
|
touched.add(entry.conversationId);
|
|
4365
5606
|
facts.newest = entry.id;
|
|
4366
|
-
const
|
|
4367
|
-
facts.updatedAt = typeof
|
|
4368
|
-
facts.failed = entry.kind === "pi.assistant" &&
|
|
4369
|
-
facts.calls = entry.kind === "pi.assistant" ? callsOf(
|
|
5607
|
+
const message2 = entry.model?.[0];
|
|
5608
|
+
facts.updatedAt = typeof message2?.timestamp === "number" ? message2.timestamp : undefined;
|
|
5609
|
+
facts.failed = entry.kind === "pi.assistant" && message2?.role === "assistant" && message2.stopReason === "error";
|
|
5610
|
+
facts.calls = entry.kind === "pi.assistant" ? callsOf(message2) : undefined;
|
|
4370
5611
|
} else if (change.type === "document") {
|
|
4371
5612
|
const id = change.conversationId;
|
|
4372
5613
|
const value = change.kind === "change" ? change.value : null;
|
|
@@ -4386,7 +5627,7 @@ class ConversationList {
|
|
|
4386
5627
|
if (wasRunning && !facts.running && this.#records.get(id)?.owner === undefined)
|
|
4387
5628
|
ended.add(id);
|
|
4388
5629
|
} else if (this.#questionKinds.has(change.record.kind)) {
|
|
4389
|
-
const pending =
|
|
5630
|
+
const pending = Value.Check(QuestionSchema, value) && value.requests.some((request) => request.resolution === undefined);
|
|
4390
5631
|
const next = new Set(facts.pending);
|
|
4391
5632
|
if (pending)
|
|
4392
5633
|
next.add(change.record.kind);
|
|
@@ -4402,6 +5643,8 @@ class ConversationList {
|
|
|
4402
5643
|
facts.label = typeof subagent?.label === "string" && subagent.label !== "" ? subagent.label : undefined;
|
|
4403
5644
|
facts.role = typeof subagent?.role === "string" && subagent.role !== "" ? subagent.role : undefined;
|
|
4404
5645
|
facts.model = modelRefOf(subagent?.model);
|
|
5646
|
+
facts.name = typeof subagent?.name === "string" && subagent.name !== "" ? subagent.name : undefined;
|
|
5647
|
+
facts.lifecycle = subagent?.lifecycle === "background" ? "background" : "foreground";
|
|
4405
5648
|
touched.add(id);
|
|
4406
5649
|
} else if (change.record.kind === TitleDoc.definition.kind) {
|
|
4407
5650
|
const named = value?.title;
|
|
@@ -4569,7 +5812,8 @@ function summaryOf(record, facts, role, currentAgent) {
|
|
|
4569
5812
|
return {
|
|
4570
5813
|
id: record.id,
|
|
4571
5814
|
kind: record.owner !== undefined ? "subagent" : record.parent !== undefined ? "fork" : "conversation",
|
|
4572
|
-
...record.owner === undefined ? {} : { lifecycle: "foreground" },
|
|
5815
|
+
...record.owner === undefined ? {} : { lifecycle: facts.lifecycle ?? "foreground" },
|
|
5816
|
+
...facts.name === undefined ? {} : { name: facts.name },
|
|
4573
5817
|
...facts.model === undefined ? {} : { model: facts.model },
|
|
4574
5818
|
...parent === undefined ? {} : { parent },
|
|
4575
5819
|
...record.parent === undefined ? {} : { forkedAt: String(record.parent.at) },
|
|
@@ -4583,13 +5827,13 @@ function summaryOf(record, facts, role, currentAgent) {
|
|
|
4583
5827
|
};
|
|
4584
5828
|
}
|
|
4585
5829
|
function sameSummary(a, b) {
|
|
4586
|
-
return a.id === b.id && a.kind === b.kind && a.lifecycle === b.lifecycle && a.model?.provider === b.model?.provider && a.model?.modelId === b.model?.modelId && a.parent === b.parent && a.forkedAt === b.forkedAt && a.title === b.title && a.status === b.status && a.updatedAt === b.updatedAt && a.label === b.label && a.role === b.role && a.currentAgent === b.currentAgent && a.activity?.tool === b.activity?.tool && a.activity?.target === b.activity?.target;
|
|
5830
|
+
return a.id === b.id && a.kind === b.kind && a.lifecycle === b.lifecycle && a.name === b.name && a.model?.provider === b.model?.provider && a.model?.modelId === b.model?.modelId && a.parent === b.parent && a.forkedAt === b.forkedAt && a.title === b.title && a.status === b.status && a.updatedAt === b.updatedAt && a.label === b.label && a.role === b.role && a.currentAgent === b.currentAgent && a.activity?.tool === b.activity?.tool && a.activity?.target === b.activity?.target;
|
|
4587
5831
|
}
|
|
4588
5832
|
function titleOf(entry) {
|
|
4589
|
-
const
|
|
4590
|
-
if (
|
|
5833
|
+
const message2 = entry?.model?.[0];
|
|
5834
|
+
if (message2?.role !== "user")
|
|
4591
5835
|
return {};
|
|
4592
|
-
const title = stripUserBlocks(
|
|
5836
|
+
const title = stripUserBlocks(message2.content).text.replace(/\s+/g, " ").trim();
|
|
4593
5837
|
return title === "" ? {} : { title };
|
|
4594
5838
|
}
|
|
4595
5839
|
var WHOLE_TRANSCRIPT = { min: Number.MAX_SAFE_INTEGER, max: Number.MAX_SAFE_INTEGER };
|
|
@@ -4635,21 +5879,21 @@ async function historyFacts(harness, id) {
|
|
|
4635
5879
|
newest ??= page.items.find((entry) => entry.conversationId === id && (entry.kind === "pi.user" || entry.kind === "pi.assistant"));
|
|
4636
5880
|
cursor = page.next;
|
|
4637
5881
|
} while (cursor !== undefined);
|
|
4638
|
-
const
|
|
5882
|
+
const message2 = newest?.model?.[0];
|
|
4639
5883
|
return {
|
|
4640
5884
|
...titleOf(first),
|
|
4641
5885
|
...newest === undefined ? {} : {
|
|
4642
5886
|
newest: newest.id,
|
|
4643
|
-
...typeof
|
|
4644
|
-
failed: newest.kind === "pi.assistant" &&
|
|
4645
|
-
...newest.kind === "pi.assistant" ? { calls: callsOf(
|
|
5887
|
+
...typeof message2?.timestamp === "number" ? { updatedAt: message2.timestamp } : {},
|
|
5888
|
+
failed: newest.kind === "pi.assistant" && message2?.role === "assistant" && message2.stopReason === "error",
|
|
5889
|
+
...newest.kind === "pi.assistant" ? { calls: callsOf(message2) } : {}
|
|
4646
5890
|
}
|
|
4647
5891
|
};
|
|
4648
5892
|
}
|
|
4649
|
-
function callsOf(
|
|
4650
|
-
if (
|
|
5893
|
+
function callsOf(message2) {
|
|
5894
|
+
if (message2?.role !== "assistant")
|
|
4651
5895
|
return;
|
|
4652
|
-
const content =
|
|
5896
|
+
const content = message2.content;
|
|
4653
5897
|
if (!Array.isArray(content))
|
|
4654
5898
|
return;
|
|
4655
5899
|
const calls = new Map;
|
|
@@ -4756,6 +6000,12 @@ class GatewayClient {
|
|
|
4756
6000
|
throw new Error("Provider setup is not available on this host");
|
|
4757
6001
|
return modelAuth;
|
|
4758
6002
|
}
|
|
6003
|
+
#upgrades() {
|
|
6004
|
+
const upgrade = this.#options.upgrade;
|
|
6005
|
+
if (upgrade === undefined)
|
|
6006
|
+
throw new Error("This host can't upgrade itself (source)");
|
|
6007
|
+
return upgrade;
|
|
6008
|
+
}
|
|
4759
6009
|
#hostValues;
|
|
4760
6010
|
#subscriptions = new Map;
|
|
4761
6011
|
#terminalSizes = new Map;
|
|
@@ -4773,7 +6023,8 @@ class GatewayClient {
|
|
|
4773
6023
|
auth: options.modelAuth?.auth ?? constantValue({ logins: [] }),
|
|
4774
6024
|
providers: options.modelAuth?.providers ?? constantValue({ providers: [], logins: [] }),
|
|
4775
6025
|
settings: options.settings,
|
|
4776
|
-
agents: options.agents ?? constantValue({ roles: [], problems: [] })
|
|
6026
|
+
agents: options.agents ?? constantValue({ roles: [], problems: [] }),
|
|
6027
|
+
upgrade: options.upgrade ?? NO_UPGRADES
|
|
4777
6028
|
};
|
|
4778
6029
|
this.#hooks = hooks;
|
|
4779
6030
|
this.devicePublicKey = hooks.devicePublicKey;
|
|
@@ -4822,7 +6073,8 @@ class GatewayClient {
|
|
|
4822
6073
|
toolPresentations: options.toolPresentations ?? {},
|
|
4823
6074
|
endpointSet: this.#hooks.endpointSet(),
|
|
4824
6075
|
...rtc === undefined ? {} : { rtc },
|
|
4825
|
-
...this.#hooks.terminals === undefined ? {} : { terminals: { limit: this.#hooks.terminals.limit } }
|
|
6076
|
+
...this.#hooks.terminals === undefined ? {} : { terminals: { limit: this.#hooks.terminals.limit } },
|
|
6077
|
+
...this.#hooks.processes === undefined ? {} : { processes: this.#hooks.processes.hello }
|
|
4826
6078
|
});
|
|
4827
6079
|
}
|
|
4828
6080
|
send(frame) {
|
|
@@ -4909,6 +6161,18 @@ class GatewayClient {
|
|
|
4909
6161
|
});
|
|
4910
6162
|
return { stop: detach };
|
|
4911
6163
|
}
|
|
6164
|
+
if (stream.startsWith("process:")) {
|
|
6165
|
+
const processStream = stream;
|
|
6166
|
+
const detach = this.#processes().attach(processStream.slice("process:".length), state?.since, {
|
|
6167
|
+
output: (frame) => send({ type: "output", stream: processStream, ...frame }),
|
|
6168
|
+
ended: (reason) => {
|
|
6169
|
+
send({ type: "ended", stream, reason });
|
|
6170
|
+
if (this.#subscriptions.get(stream) === state)
|
|
6171
|
+
this.#subscriptions.delete(stream);
|
|
6172
|
+
}
|
|
6173
|
+
});
|
|
6174
|
+
return { stop: detach };
|
|
6175
|
+
}
|
|
4912
6176
|
if (stream.startsWith("doc:")) {
|
|
4913
6177
|
const at = stream.lastIndexOf(":");
|
|
4914
6178
|
return this.#openDoc(stream, stream.slice("doc:".length, at), Number(stream.slice(at + 1)), send);
|
|
@@ -5054,8 +6318,11 @@ class GatewayClient {
|
|
|
5054
6318
|
#submissionByRequest(conversationId, requestId) {
|
|
5055
6319
|
return this.#options.harness.commit((tx) => tx.submissionByRequest(conversationId, requestId), context3);
|
|
5056
6320
|
}
|
|
5057
|
-
async#
|
|
5058
|
-
|
|
6321
|
+
async#isSubagent(id) {
|
|
6322
|
+
return await this.#options.harness.commit(async (tx) => (await tx.conversation(id))?.owner !== undefined, context3);
|
|
6323
|
+
}
|
|
6324
|
+
async#compact(conversation, instructions2) {
|
|
6325
|
+
const taskId = await conversation.compact(instructions2, context3);
|
|
5059
6326
|
this.#options.harness.waitForTask(taskId, context3).then((receipt) => void this.send({ type: "notice", level: "info", message: `Compaction ${receipt.state.outcome.status}.` }), () => {});
|
|
5060
6327
|
return taskId;
|
|
5061
6328
|
}
|
|
@@ -5073,6 +6340,17 @@ class GatewayClient {
|
|
|
5073
6340
|
}
|
|
5074
6341
|
}
|
|
5075
6342
|
async#call(method, args) {
|
|
6343
|
+
if (AGENT_WORK_CALLS.has(method) && this.#options.upgrade !== undefined) {
|
|
6344
|
+
const release2 = this.#options.upgrade.admit();
|
|
6345
|
+
try {
|
|
6346
|
+
return await this.#dispatch(method, args);
|
|
6347
|
+
} finally {
|
|
6348
|
+
release2();
|
|
6349
|
+
}
|
|
6350
|
+
}
|
|
6351
|
+
return this.#dispatch(method, args);
|
|
6352
|
+
}
|
|
6353
|
+
async#dispatch(method, args) {
|
|
5076
6354
|
switch (method) {
|
|
5077
6355
|
case "terminalInput": {
|
|
5078
6356
|
const input = args;
|
|
@@ -5102,6 +6380,11 @@ class GatewayClient {
|
|
|
5102
6380
|
this.#terminals().close(args.terminalId);
|
|
5103
6381
|
this.#terminalSizes.delete(args.terminalId);
|
|
5104
6382
|
return null;
|
|
6383
|
+
case "stopProcess": {
|
|
6384
|
+
const input = args;
|
|
6385
|
+
const process2 = await this.#processes().stop({ conversationId: input.conversationId, id: input.id, reason: "user" }, context3);
|
|
6386
|
+
return { state: process2.state };
|
|
6387
|
+
}
|
|
5105
6388
|
case "addProject":
|
|
5106
6389
|
return addProject(this.#options.harness, args.path, context3);
|
|
5107
6390
|
case "removeProject":
|
|
@@ -5114,6 +6397,9 @@ class GatewayClient {
|
|
|
5114
6397
|
const existing = await this.#submissionByRequest(input.conversationId, input.requestId);
|
|
5115
6398
|
if (existing !== undefined)
|
|
5116
6399
|
return submitResult(existing);
|
|
6400
|
+
if (await this.#isSubagent(input.conversationId)) {
|
|
6401
|
+
throw new Error("Subagents take no messages from the user: tell the conversation that dispatched it");
|
|
6402
|
+
}
|
|
5117
6403
|
const resolved = images();
|
|
5118
6404
|
const conversation2 = await this.#conversation(input.conversationId);
|
|
5119
6405
|
const attachments = await this.#attachmentText(input.attachments);
|
|
@@ -5163,13 +6449,27 @@ class GatewayClient {
|
|
|
5163
6449
|
}
|
|
5164
6450
|
case "archive": {
|
|
5165
6451
|
const input = args;
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
this.#hooks.
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
|
|
5172
|
-
|
|
6452
|
+
if (!input.archived) {
|
|
6453
|
+
await archive(this.#options.harness, input.conversationId, false, context3);
|
|
6454
|
+
this.#hooks.processes?.unseal(await conversationSubtree(this.#options.harness, input.conversationId, context3));
|
|
6455
|
+
return null;
|
|
6456
|
+
}
|
|
6457
|
+
const subtree = await conversationSubtree(this.#options.harness, input.conversationId, context3);
|
|
6458
|
+
this.#hooks.processes?.seal(subtree);
|
|
6459
|
+
try {
|
|
6460
|
+
await archive(this.#options.harness, input.conversationId, true, context3);
|
|
6461
|
+
} catch (error) {
|
|
6462
|
+
this.#hooks.processes?.unseal(subtree);
|
|
6463
|
+
throw error;
|
|
6464
|
+
}
|
|
6465
|
+
for (const id of subtree) {
|
|
6466
|
+
await (await this.#options.harness.conversation(id, context3))?.abort(context3, { background: true });
|
|
6467
|
+
}
|
|
6468
|
+
await this.#hooks.processes?.closeOwners(subtree, "archive", context3);
|
|
6469
|
+
this.#hooks.terminals?.closeOwners(subtree);
|
|
6470
|
+
const kept = await cleanupArchivedWorktrees(this.#options.harness, input.conversationId, context3);
|
|
6471
|
+
for (const { record, reason } of kept) {
|
|
6472
|
+
await this.send({ type: "notice", level: "warning", message: `Kept worktree ${record.path} (branch ${record.branch}): ${reason}` });
|
|
5173
6473
|
}
|
|
5174
6474
|
return null;
|
|
5175
6475
|
}
|
|
@@ -5238,6 +6538,15 @@ class GatewayClient {
|
|
|
5238
6538
|
await this.#options.settings.update(args.patch);
|
|
5239
6539
|
return null;
|
|
5240
6540
|
}
|
|
6541
|
+
case "startUpgrade":
|
|
6542
|
+
await this.#upgrades().start(args.when);
|
|
6543
|
+
return null;
|
|
6544
|
+
case "cancelUpgrade":
|
|
6545
|
+
this.#upgrades().cancel();
|
|
6546
|
+
return null;
|
|
6547
|
+
case "checkUpgrade":
|
|
6548
|
+
await this.#upgrades().check();
|
|
6549
|
+
return null;
|
|
5241
6550
|
case "createAgent": {
|
|
5242
6551
|
const store = this.#options.agents;
|
|
5243
6552
|
if (store === undefined)
|
|
@@ -5402,17 +6711,20 @@ class GatewayClient {
|
|
|
5402
6711
|
return null;
|
|
5403
6712
|
}
|
|
5404
6713
|
case "abort":
|
|
6714
|
+
if (await this.#isSubagent(conversation.id)) {
|
|
6715
|
+
await this.#options.harness.commit((tx) => markStop(tx, conversation.id, "user"), context3);
|
|
6716
|
+
}
|
|
5405
6717
|
await conversation.abort(context3);
|
|
5406
6718
|
return null;
|
|
5407
6719
|
case "compact": {
|
|
5408
|
-
const { instructions } = args;
|
|
5409
|
-
return { taskId: await this.#compact(conversation,
|
|
6720
|
+
const { instructions: instructions2 } = args;
|
|
6721
|
+
return { taskId: await this.#compact(conversation, instructions2) };
|
|
5410
6722
|
}
|
|
5411
6723
|
case "listCommands":
|
|
5412
6724
|
return listCommands();
|
|
5413
6725
|
case "runCommand": {
|
|
5414
6726
|
const { name, args: input } = args;
|
|
5415
|
-
await runCommand({ compact: (
|
|
6727
|
+
await runCommand({ compact: (instructions2) => this.#compact(conversation, instructions2) }, name, input);
|
|
5416
6728
|
return null;
|
|
5417
6729
|
}
|
|
5418
6730
|
case "setModel": {
|
|
@@ -5488,7 +6800,7 @@ class GatewayClient {
|
|
|
5488
6800
|
const token = doc.token;
|
|
5489
6801
|
return await this.#options.harness.commit(async (tx) => {
|
|
5490
6802
|
const value = await tx.doc(token, conversation.id);
|
|
5491
|
-
if (!
|
|
6803
|
+
if (!Value.Check(QuestionSchema, value))
|
|
5492
6804
|
throw new Error(`Document ${kind} does not hold question requests`);
|
|
5493
6805
|
const request = value.requests.find((candidate) => candidate.id === requestId);
|
|
5494
6806
|
if (request === undefined)
|
|
@@ -5537,6 +6849,11 @@ class GatewayClient {
|
|
|
5537
6849
|
throw new Error("Terminals are unavailable on this host");
|
|
5538
6850
|
return this.#hooks.terminals;
|
|
5539
6851
|
}
|
|
6852
|
+
#processes() {
|
|
6853
|
+
if (this.#hooks.processes === undefined)
|
|
6854
|
+
throw new Error("Background processes are unavailable on this host");
|
|
6855
|
+
return this.#hooks.processes;
|
|
6856
|
+
}
|
|
5540
6857
|
async#terminalOwner(conversation) {
|
|
5541
6858
|
const cwd = (await conversation.agent(context3)).cwd;
|
|
5542
6859
|
if (cwd === undefined)
|
|
@@ -5576,7 +6893,7 @@ class GatewayClient {
|
|
|
5576
6893
|
async#dismissPendingQuestions(conversationId, context4) {
|
|
5577
6894
|
const questionDocs = this.#questionDocs();
|
|
5578
6895
|
const snapshots = await Promise.all(questionDocs.map((doc) => this.#options.harness.snapshot(doc.token, conversationId, context4)));
|
|
5579
|
-
const pending = snapshots.some((value) =>
|
|
6896
|
+
const pending = snapshots.some((value) => Value.Check(QuestionSchema, value) && value.requests.some((request) => request.resolution === undefined));
|
|
5580
6897
|
if (!pending)
|
|
5581
6898
|
return false;
|
|
5582
6899
|
return await this.#options.harness.commit((tx) => dismissPendingQuestions(tx, questionDocs, conversationId), context4);
|
|
@@ -5585,7 +6902,7 @@ class GatewayClient {
|
|
|
5585
6902
|
return (this.#options.docs ?? []).filter((doc) => doc.presentation === "pinomad.question");
|
|
5586
6903
|
}
|
|
5587
6904
|
get #chatsPrefix() {
|
|
5588
|
-
return
|
|
6905
|
+
return join8(this.#options.dataDir, "chats") + sep6;
|
|
5589
6906
|
}
|
|
5590
6907
|
async#changes(conversation, context4) {
|
|
5591
6908
|
const cwd = (await conversation.agent(context4)).cwd;
|
|
@@ -5620,8 +6937,8 @@ class GatewayClient {
|
|
|
5620
6937
|
// apps/host/src/host.ts
|
|
5621
6938
|
import { randomBytes as randomBytes4 } from "node:crypto";
|
|
5622
6939
|
import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile3 } from "node:fs/promises";
|
|
5623
|
-
import { basename as basename3, join as
|
|
5624
|
-
import { BACKGROUND_CONTEXT as
|
|
6940
|
+
import { basename as basename3, join as join11, sep as sep7 } from "node:path";
|
|
6941
|
+
import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT6 } from "@earendil-works/chord/context";
|
|
5625
6942
|
import { createRegistry, Harness } from "@earendil-works/pi-durable";
|
|
5626
6943
|
import { NodeExecutionEnv } from "@earendil-works/pi-durable/env/node";
|
|
5627
6944
|
|
|
@@ -5637,10 +6954,10 @@ import {
|
|
|
5637
6954
|
StreamableHttpTransport,
|
|
5638
6955
|
toLlmContent
|
|
5639
6956
|
} from "@earendil-works/pi-mcp";
|
|
5640
|
-
import { defineExtension as
|
|
6957
|
+
import { defineExtension as defineExtension7, defineTool as defineTool5 } from "@earendil-works/pi-durable";
|
|
5641
6958
|
import { Type as Type5 } from "typebox";
|
|
5642
6959
|
var ENV_VAR = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
5643
|
-
var
|
|
6960
|
+
var isObject3 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
5644
6961
|
function expand(value) {
|
|
5645
6962
|
let missing;
|
|
5646
6963
|
const expanded = value.replace(ENV_VAR, (_all, name) => {
|
|
@@ -5654,7 +6971,7 @@ function expand(value) {
|
|
|
5654
6971
|
function expandAll(values, field, error) {
|
|
5655
6972
|
if (values === undefined)
|
|
5656
6973
|
return;
|
|
5657
|
-
if (!
|
|
6974
|
+
if (!isObject3(values)) {
|
|
5658
6975
|
error(`"${field}" must be an object of strings`);
|
|
5659
6976
|
return;
|
|
5660
6977
|
}
|
|
@@ -5664,21 +6981,21 @@ function expandAll(values, field, error) {
|
|
|
5664
6981
|
error(`"${field}.${key}" must be a string`);
|
|
5665
6982
|
return;
|
|
5666
6983
|
}
|
|
5667
|
-
const
|
|
5668
|
-
if (typeof
|
|
5669
|
-
error(`"${field}.${key}" references unset environment variable ${
|
|
6984
|
+
const result3 = expand(value);
|
|
6985
|
+
if (typeof result3 !== "string") {
|
|
6986
|
+
error(`"${field}.${key}" references unset environment variable ${result3.missing}`);
|
|
5670
6987
|
return;
|
|
5671
6988
|
}
|
|
5672
|
-
expanded[key] =
|
|
6989
|
+
expanded[key] = result3;
|
|
5673
6990
|
}
|
|
5674
6991
|
return expanded;
|
|
5675
6992
|
}
|
|
5676
6993
|
function parseServer(name, raw, error) {
|
|
5677
|
-
const fail = (
|
|
5678
|
-
error(`${name}: ${
|
|
6994
|
+
const fail = (message2) => {
|
|
6995
|
+
error(`${name}: ${message2}`);
|
|
5679
6996
|
return;
|
|
5680
6997
|
};
|
|
5681
|
-
if (!
|
|
6998
|
+
if (!isObject3(raw))
|
|
5682
6999
|
return fail("server entry must be an object");
|
|
5683
7000
|
if (raw.enabled === false)
|
|
5684
7001
|
return { name, disabled: true };
|
|
@@ -5700,7 +7017,7 @@ function parseServer(name, raw, error) {
|
|
|
5700
7017
|
}
|
|
5701
7018
|
if (raw.cwd !== undefined && typeof raw.cwd !== "string")
|
|
5702
7019
|
return fail(`"cwd" must be a string`);
|
|
5703
|
-
const env = expandAll(raw.env, "env", (
|
|
7020
|
+
const env = expandAll(raw.env, "env", (message2) => error(`${name}: ${message2}`));
|
|
5704
7021
|
if (raw.env !== undefined && env === undefined)
|
|
5705
7022
|
return;
|
|
5706
7023
|
const cwd = raw.cwd === undefined ? homedir2() : isAbsolute2(raw.cwd) ? raw.cwd : resolve5(homedir2(), raw.cwd);
|
|
@@ -5715,7 +7032,7 @@ function parseServer(name, raw, error) {
|
|
|
5715
7032
|
if (raw.url !== undefined || raw.type === "http" || raw.type === "sse") {
|
|
5716
7033
|
if (typeof raw.url !== "string" || raw.url === "")
|
|
5717
7034
|
return fail(`HTTP server needs a "url"`);
|
|
5718
|
-
const headers = expandAll(raw.headers, "headers", (
|
|
7035
|
+
const headers = expandAll(raw.headers, "headers", (message2) => error(`${name}: ${message2}`));
|
|
5719
7036
|
if (raw.headers !== undefined && headers === undefined)
|
|
5720
7037
|
return;
|
|
5721
7038
|
return {
|
|
@@ -5737,13 +7054,13 @@ async function loadConfig(configPath) {
|
|
|
5737
7054
|
return { errors: [], entries: [] };
|
|
5738
7055
|
return { errors: [`${configPath}: ${error instanceof Error ? error.message : String(error)}`], entries: [] };
|
|
5739
7056
|
}
|
|
5740
|
-
if (!
|
|
7057
|
+
if (!isObject3(parsed) || parsed.mcpServers !== undefined && !isObject3(parsed.mcpServers)) {
|
|
5741
7058
|
return { errors: [`${configPath}: "mcpServers" must be an object`], entries: [] };
|
|
5742
7059
|
}
|
|
5743
7060
|
const errors = [];
|
|
5744
7061
|
const entries = [];
|
|
5745
7062
|
for (const [name, raw] of Object.entries(parsed.mcpServers ?? {})) {
|
|
5746
|
-
const entry = parseServer(name, raw, (
|
|
7063
|
+
const entry = parseServer(name, raw, (message2) => errors.push(message2));
|
|
5747
7064
|
if (entry !== undefined)
|
|
5748
7065
|
entries.push(entry);
|
|
5749
7066
|
}
|
|
@@ -5761,7 +7078,7 @@ var callToolResultSchema = (tool) => ({
|
|
|
5761
7078
|
required: ["content"]
|
|
5762
7079
|
});
|
|
5763
7080
|
function mcpTool(client, timeoutMs, exposure, name, tool) {
|
|
5764
|
-
return
|
|
7081
|
+
return defineTool5({
|
|
5765
7082
|
name,
|
|
5766
7083
|
description: tool.description ?? tool.title ?? tool.name,
|
|
5767
7084
|
parameters: Type5.Unsafe({
|
|
@@ -5772,11 +7089,11 @@ function mcpTool(client, timeoutMs, exposure, name, tool) {
|
|
|
5772
7089
|
...exposure === "direct" ? {} : { callers: ["tools"] },
|
|
5773
7090
|
structuredOutputSchema: Type5.Unsafe(callToolResultSchema(tool)),
|
|
5774
7091
|
execute: async (args, _api, context4) => {
|
|
5775
|
-
const
|
|
7092
|
+
const result3 = await client.callTool(tool.name, args, {
|
|
5776
7093
|
signal: context4.abortSignal ?? NEVER_ABORT,
|
|
5777
7094
|
timeoutMs
|
|
5778
7095
|
});
|
|
5779
|
-
return { output: toLlmContent(
|
|
7096
|
+
return { output: toLlmContent(result3), structuredOutput: result3, isError: result3.isError === true };
|
|
5780
7097
|
}
|
|
5781
7098
|
});
|
|
5782
7099
|
}
|
|
@@ -5796,7 +7113,7 @@ function startMcp(options) {
|
|
|
5796
7113
|
listener(value);
|
|
5797
7114
|
};
|
|
5798
7115
|
const install = () => {
|
|
5799
|
-
options.registry.install(
|
|
7116
|
+
options.registry.install(defineExtension7({ name: "mcp", tools: [...toolsets.values()].flat() }));
|
|
5800
7117
|
options.onNamespaces?.([...namespaces.values()]);
|
|
5801
7118
|
};
|
|
5802
7119
|
const set = (name, patch) => {
|
|
@@ -5916,7 +7233,7 @@ ${stderr}`;
|
|
|
5916
7233
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
5917
7234
|
import { existsSync as existsSync2, watch } from "node:fs";
|
|
5918
7235
|
import { mkdir as mkdir2, readdir as readdir2, readFile as readFile3, rename, rm as rm2, writeFile } from "node:fs/promises";
|
|
5919
|
-
import { join as
|
|
7236
|
+
import { join as join9 } from "node:path";
|
|
5920
7237
|
import { parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
5921
7238
|
|
|
5922
7239
|
// packages/protocol/src/agents.ts
|
|
@@ -5960,7 +7277,7 @@ async function load(dir) {
|
|
|
5960
7277
|
const problem = (reason2) => void problems.push({ file, reason: reason2 });
|
|
5961
7278
|
let parsed;
|
|
5962
7279
|
try {
|
|
5963
|
-
parsed = parseFrontmatter(await readFile3(
|
|
7280
|
+
parsed = parseFrontmatter(await readFile3(join9(dir, file), "utf8"));
|
|
5964
7281
|
} catch (error) {
|
|
5965
7282
|
problem(`Couldn't parse the file: ${error instanceof Error ? error.message : String(error)}`);
|
|
5966
7283
|
continue;
|
|
@@ -6072,13 +7389,13 @@ async function openAgentRoles(options) {
|
|
|
6072
7389
|
if (problem !== undefined)
|
|
6073
7390
|
throw new Error(problem);
|
|
6074
7391
|
await enqueue(async () => {
|
|
6075
|
-
const target =
|
|
7392
|
+
const target = join9(dir, `${role.name}.md`);
|
|
6076
7393
|
const exists = existsSync2(target);
|
|
6077
7394
|
if (mustExist && !exists)
|
|
6078
7395
|
throw new Error(`No such agent: "${role.name}"`);
|
|
6079
7396
|
if (!mustExist && exists)
|
|
6080
7397
|
throw new Error(`Agent "${role.name}" already exists`);
|
|
6081
|
-
const temp =
|
|
7398
|
+
const temp = join9(dir, `.${role.name}.${randomBytes2(6).toString("hex")}.tmp`);
|
|
6082
7399
|
try {
|
|
6083
7400
|
await writeFile(temp, serialize(role));
|
|
6084
7401
|
await rename(temp, target);
|
|
@@ -6101,7 +7418,7 @@ async function openAgentRoles(options) {
|
|
|
6101
7418
|
if (problem !== undefined)
|
|
6102
7419
|
throw new Error(problem);
|
|
6103
7420
|
await enqueue(async () => {
|
|
6104
|
-
const target =
|
|
7421
|
+
const target = join9(dir, `${name}.md`);
|
|
6105
7422
|
if (!existsSync2(target))
|
|
6106
7423
|
throw new Error(`No such agent: "${name}"`);
|
|
6107
7424
|
await rm2(target);
|
|
@@ -6135,7 +7452,7 @@ import {
|
|
|
6135
7452
|
stat as stat7,
|
|
6136
7453
|
writeFile as writeFile2
|
|
6137
7454
|
} from "node:fs/promises";
|
|
6138
|
-
import { basename as basename2, join as
|
|
7455
|
+
import { basename as basename2, join as join10, resolve as resolve6 } from "node:path";
|
|
6139
7456
|
var META = "meta.json";
|
|
6140
7457
|
var DATA = "file";
|
|
6141
7458
|
var CLEANUP_INTERVAL_MS = 6 * 60 * 60 * 1000;
|
|
@@ -6230,16 +7547,16 @@ class AttachmentStore {
|
|
|
6230
7547
|
return next;
|
|
6231
7548
|
}
|
|
6232
7549
|
#directory(id) {
|
|
6233
|
-
return
|
|
7550
|
+
return join10(this.#root, id);
|
|
6234
7551
|
}
|
|
6235
7552
|
#dataPath(meta) {
|
|
6236
|
-
return
|
|
7553
|
+
return join10(this.#directory(meta.id), DATA, meta.fileName);
|
|
6237
7554
|
}
|
|
6238
7555
|
async#readMeta(id) {
|
|
6239
7556
|
if (!isAttachmentId(id))
|
|
6240
7557
|
return;
|
|
6241
7558
|
try {
|
|
6242
|
-
return metaOf(JSON.parse(await readFile4(
|
|
7559
|
+
return metaOf(JSON.parse(await readFile4(join10(this.#directory(id), META), "utf8")), id);
|
|
6243
7560
|
} catch {
|
|
6244
7561
|
return;
|
|
6245
7562
|
}
|
|
@@ -6247,10 +7564,10 @@ class AttachmentStore {
|
|
|
6247
7564
|
async#writeMeta(meta) {
|
|
6248
7565
|
const { id, ...value } = meta;
|
|
6249
7566
|
const directory = this.#directory(id);
|
|
6250
|
-
const temporary =
|
|
7567
|
+
const temporary = join10(directory, `.meta.${randomBytes3(6).toString("hex")}.tmp`);
|
|
6251
7568
|
await writeFile2(temporary, `${JSON.stringify(value)}
|
|
6252
7569
|
`, { mode: 384 });
|
|
6253
|
-
await rename2(temporary,
|
|
7570
|
+
await rename2(temporary, join10(directory, META));
|
|
6254
7571
|
}
|
|
6255
7572
|
async#rebuildPending() {
|
|
6256
7573
|
this.#pending.clear();
|
|
@@ -6295,7 +7612,7 @@ class AttachmentStore {
|
|
|
6295
7612
|
expiresAt: this.#now() + this.#ttlMs
|
|
6296
7613
|
};
|
|
6297
7614
|
try {
|
|
6298
|
-
await mkdir3(
|
|
7615
|
+
await mkdir3(join10(directory, DATA), { recursive: true });
|
|
6299
7616
|
await writeFile2(this.#dataPath(meta), new Uint8Array(0), { mode: 384 });
|
|
6300
7617
|
if (size === 0)
|
|
6301
7618
|
await chmod(this.#dataPath(meta), 292);
|
|
@@ -6431,16 +7748,16 @@ class AttachmentStore {
|
|
|
6431
7748
|
}
|
|
6432
7749
|
|
|
6433
7750
|
// apps/host/src/answer-timing.ts
|
|
6434
|
-
function withAnswerStart(
|
|
6435
|
-
const pinomad =
|
|
6436
|
-
return { ...
|
|
7751
|
+
function withAnswerStart(message2, answerStartedAt) {
|
|
7752
|
+
const pinomad = message2.pinomad;
|
|
7753
|
+
return { ...message2, pinomad: { ...pinomad, answerStartedAt } };
|
|
6437
7754
|
}
|
|
6438
7755
|
function stripPinomad(context4) {
|
|
6439
7756
|
let changed = false;
|
|
6440
|
-
const messages = context4.messages.map((
|
|
6441
|
-
if (
|
|
6442
|
-
return
|
|
6443
|
-
const { pinomad: _pinomad, ...rest } =
|
|
7757
|
+
const messages = context4.messages.map((message2) => {
|
|
7758
|
+
if (message2.role !== "assistant" || !("pinomad" in message2))
|
|
7759
|
+
return message2;
|
|
7760
|
+
const { pinomad: _pinomad, ...rest } = message2;
|
|
6444
7761
|
changed = true;
|
|
6445
7762
|
return rest;
|
|
6446
7763
|
});
|
|
@@ -6465,7 +7782,7 @@ function trackAnswerStart(stream) {
|
|
|
6465
7782
|
const push = stream.push.bind(stream);
|
|
6466
7783
|
stream.push = (event) => push(annotate(event));
|
|
6467
7784
|
const end = stream.end.bind(stream);
|
|
6468
|
-
stream.end = (
|
|
7785
|
+
stream.end = (result3) => end(result3 === undefined || answerStartedAt === undefined ? result3 : withAnswerStart(result3, answerStartedAt));
|
|
6469
7786
|
return stream;
|
|
6470
7787
|
}
|
|
6471
7788
|
function withAnswerTiming(models) {
|
|
@@ -6480,10 +7797,594 @@ function withAnswerTiming(models) {
|
|
|
6480
7797
|
});
|
|
6481
7798
|
}
|
|
6482
7799
|
|
|
7800
|
+
// apps/host/src/processes.ts
|
|
7801
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
7802
|
+
import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT5 } from "@earendil-works/chord/context";
|
|
7803
|
+
var PROCESS_LIMIT = 8;
|
|
7804
|
+
var ENDED_PROCESS_LIMIT = 20;
|
|
7805
|
+
var ENDED_OUTPUT_BUDGET = 64 * 1024 * 1024;
|
|
7806
|
+
var STOP_GRACE_MS = 5000;
|
|
7807
|
+
var GROUP_WAIT_MS = 2000;
|
|
7808
|
+
var OUTPUT_CHUNK = 16 * 1024;
|
|
7809
|
+
var REPORT_LIMIT = 4 * 1024;
|
|
7810
|
+
var REPORT_LINES = 40;
|
|
7811
|
+
var REPORT_TAIL = `
|
|
7812
|
+
|
|
7813
|
+
Last output:
|
|
7814
|
+
`;
|
|
7815
|
+
|
|
7816
|
+
class Processes {
|
|
7817
|
+
#options;
|
|
7818
|
+
#limit;
|
|
7819
|
+
#endedLimit;
|
|
7820
|
+
#outputBudget;
|
|
7821
|
+
#processes = new Map;
|
|
7822
|
+
#roots = new Map;
|
|
7823
|
+
#sealed = new Set;
|
|
7824
|
+
#busy;
|
|
7825
|
+
#closing = false;
|
|
7826
|
+
constructor(options) {
|
|
7827
|
+
this.#options = options;
|
|
7828
|
+
this.#limit = options.limit ?? PROCESS_LIMIT;
|
|
7829
|
+
this.#endedLimit = options.endedLimit ?? ENDED_PROCESS_LIMIT;
|
|
7830
|
+
this.#outputBudget = options.outputBudget ?? ENDED_OUTPUT_BUDGET;
|
|
7831
|
+
this.#busy = new Published(false, (a, b) => a === b);
|
|
7832
|
+
}
|
|
7833
|
+
get limit() {
|
|
7834
|
+
return this.#limit;
|
|
7835
|
+
}
|
|
7836
|
+
get hello() {
|
|
7837
|
+
return { limit: this.#limit };
|
|
7838
|
+
}
|
|
7839
|
+
get busy() {
|
|
7840
|
+
return this.#busy;
|
|
7841
|
+
}
|
|
7842
|
+
async open(context4) {
|
|
7843
|
+
await settleLostProcesses(this.#options.harness(), context4);
|
|
7844
|
+
}
|
|
7845
|
+
async start(input, context4) {
|
|
7846
|
+
const harness = this.#options.harness();
|
|
7847
|
+
const root = await this.#root(input.conversationId);
|
|
7848
|
+
if (this.#closing)
|
|
7849
|
+
throw new Error("The host is shutting down");
|
|
7850
|
+
if (this.#options.installing?.() === true)
|
|
7851
|
+
throw new Error("An upgrade is installing; try again after it finishes");
|
|
7852
|
+
if (this.#sealed.has(input.conversationId) || await this.#archivedInFamily(harness, input.conversationId, context4)) {
|
|
7853
|
+
throw new Error("This conversation is archived");
|
|
7854
|
+
}
|
|
7855
|
+
if (this.#closing)
|
|
7856
|
+
throw new Error("The host is shutting down");
|
|
7857
|
+
if (this.#options.installing?.() === true)
|
|
7858
|
+
throw new Error("An upgrade is installing; try again after it finishes");
|
|
7859
|
+
if (this.#sealed.has(input.conversationId))
|
|
7860
|
+
throw new Error("This conversation is archived");
|
|
7861
|
+
const existing = input.id === undefined ? undefined : this.#processes.get(input.id);
|
|
7862
|
+
if (existing !== undefined) {
|
|
7863
|
+
if (!isProcessActive(existing.info.state))
|
|
7864
|
+
throw new Error(`Process ${existing.info.id} has already ended`);
|
|
7865
|
+
return info2(existing);
|
|
7866
|
+
}
|
|
7867
|
+
const active = [...this.#processes.values()].filter((entry2) => entry2.root === root && isProcessActive(entry2.info.state)).length;
|
|
7868
|
+
if (active >= this.#limit)
|
|
7869
|
+
throw new Error(`Process limit reached (${this.#limit})`);
|
|
7870
|
+
const id = input.id ?? randomUUID4();
|
|
7871
|
+
let resolveDone;
|
|
7872
|
+
const done = new Promise((resolve7) => {
|
|
7873
|
+
resolveDone = resolve7;
|
|
7874
|
+
});
|
|
7875
|
+
const entry = {
|
|
7876
|
+
info: {
|
|
7877
|
+
id,
|
|
7878
|
+
owner: input.conversationId,
|
|
7879
|
+
name: input.name,
|
|
7880
|
+
command: input.command,
|
|
7881
|
+
startedAt: this.#now(),
|
|
7882
|
+
state: "starting"
|
|
7883
|
+
},
|
|
7884
|
+
root,
|
|
7885
|
+
output: new OutputBuffer({
|
|
7886
|
+
...this.#options.bufferSize === undefined ? {} : { bufferSize: this.#options.bufferSize },
|
|
7887
|
+
...this.#options.flushMs === undefined ? {} : { flushMs: this.#options.flushMs },
|
|
7888
|
+
onFlush: () => this.#options.onOutput?.()
|
|
7889
|
+
}),
|
|
7890
|
+
done,
|
|
7891
|
+
resolveDone,
|
|
7892
|
+
pty: undefined,
|
|
7893
|
+
stopReason: undefined,
|
|
7894
|
+
killTimer: undefined,
|
|
7895
|
+
groupTimer: undefined,
|
|
7896
|
+
killed: false,
|
|
7897
|
+
killedAt: undefined,
|
|
7898
|
+
killWaitTimer: undefined,
|
|
7899
|
+
finishing: false,
|
|
7900
|
+
generation: 0,
|
|
7901
|
+
released: false
|
|
7902
|
+
};
|
|
7903
|
+
this.#processes.set(id, entry);
|
|
7904
|
+
this.#refreshBusy();
|
|
7905
|
+
try {
|
|
7906
|
+
await harness.commit((tx) => this.#applyDoc(tx, entry), context4);
|
|
7907
|
+
} catch (error) {
|
|
7908
|
+
this.#processes.delete(id);
|
|
7909
|
+
this.#refreshBusy();
|
|
7910
|
+
throw error;
|
|
7911
|
+
}
|
|
7912
|
+
try {
|
|
7913
|
+
this.#spawn(entry, input.cwd);
|
|
7914
|
+
} catch (error) {
|
|
7915
|
+
await this.#fail(entry, message2(error), context4);
|
|
7916
|
+
throw error;
|
|
7917
|
+
}
|
|
7918
|
+
await this.#write(entry, context4);
|
|
7919
|
+
return info2(entry);
|
|
7920
|
+
}
|
|
7921
|
+
async output(input, context4) {
|
|
7922
|
+
const root = await this.#root(input.conversationId);
|
|
7923
|
+
const entry = this.#processes.get(input.id);
|
|
7924
|
+
if (entry !== undefined && entry.root === root && !entry.released) {
|
|
7925
|
+
entry.output.flush();
|
|
7926
|
+
const slice = entry.output.snapshot(input.since, OUTPUT_CHUNK);
|
|
7927
|
+
const state = entry.info.state;
|
|
7928
|
+
return {
|
|
7929
|
+
text: stripAnsi(slice.text),
|
|
7930
|
+
next: slice.next,
|
|
7931
|
+
truncated: slice.truncated,
|
|
7932
|
+
status: state,
|
|
7933
|
+
...typeof state === "object" && "exited" in state ? { exitCode: state.exited } : {}
|
|
7934
|
+
};
|
|
7935
|
+
}
|
|
7936
|
+
const record = await this.#docEntry(root, input.id, context4);
|
|
7937
|
+
if (record === undefined)
|
|
7938
|
+
throw new Error("Unknown process");
|
|
7939
|
+
return {
|
|
7940
|
+
text: "",
|
|
7941
|
+
next: input.since ?? 0,
|
|
7942
|
+
truncated: true,
|
|
7943
|
+
status: record.state,
|
|
7944
|
+
...typeof record.state === "object" && "exited" in record.state ? { exitCode: record.state.exited } : {},
|
|
7945
|
+
released: true
|
|
7946
|
+
};
|
|
7947
|
+
}
|
|
7948
|
+
async stop(input, context4) {
|
|
7949
|
+
const root = await this.#root(input.conversationId);
|
|
7950
|
+
const entry = this.#processes.get(input.id);
|
|
7951
|
+
if (entry !== undefined && entry.root === root) {
|
|
7952
|
+
return await this.#stopEntry(entry, input.reason ?? "user", context4);
|
|
7953
|
+
}
|
|
7954
|
+
if (entry !== undefined)
|
|
7955
|
+
throw new Error("This process does not belong to the conversation's family");
|
|
7956
|
+
const record = await this.#docEntry(root, input.id, context4);
|
|
7957
|
+
if (record === undefined)
|
|
7958
|
+
throw new Error("Unknown process");
|
|
7959
|
+
return record;
|
|
7960
|
+
}
|
|
7961
|
+
async list(conversationId, context4) {
|
|
7962
|
+
const root = await this.#root(conversationId);
|
|
7963
|
+
const doc = await this.#options.harness().snapshot(ProcessDoc, root, context4);
|
|
7964
|
+
return doc?.processes ?? [];
|
|
7965
|
+
}
|
|
7966
|
+
async#docEntry(root, id, context4) {
|
|
7967
|
+
const doc = await this.#options.harness().snapshot(ProcessDoc, root, context4);
|
|
7968
|
+
return doc?.processes.find((process2) => process2.id === id);
|
|
7969
|
+
}
|
|
7970
|
+
attach(id, since, sink) {
|
|
7971
|
+
const entry = this.#processes.get(id);
|
|
7972
|
+
if (entry === undefined)
|
|
7973
|
+
throw new Error("Unknown process");
|
|
7974
|
+
if (entry.released)
|
|
7975
|
+
throw new Error("The output of this process is no longer available");
|
|
7976
|
+
return entry.output.attach(since, sink);
|
|
7977
|
+
}
|
|
7978
|
+
seal(owners) {
|
|
7979
|
+
for (const owner of owners)
|
|
7980
|
+
this.#sealed.add(owner);
|
|
7981
|
+
}
|
|
7982
|
+
unseal(owners) {
|
|
7983
|
+
for (const owner of owners)
|
|
7984
|
+
this.#sealed.delete(owner);
|
|
7985
|
+
}
|
|
7986
|
+
async closeOwners(owners, reason, context4) {
|
|
7987
|
+
const set = new Set(owners);
|
|
7988
|
+
const entries = [...this.#processes.values()].filter((entry) => set.has(entry.info.owner));
|
|
7989
|
+
await Promise.all(entries.map((entry) => this.#stopEntry(entry, reason, context4)));
|
|
7990
|
+
for (const entry of entries) {
|
|
7991
|
+
this.#release(entry, "Conversation archived");
|
|
7992
|
+
this.#processes.delete(entry.info.id);
|
|
7993
|
+
entry.resolveDone();
|
|
7994
|
+
}
|
|
7995
|
+
this.#refreshBusy();
|
|
7996
|
+
}
|
|
7997
|
+
async drain(context4) {
|
|
7998
|
+
const entries = [...this.#processes.values()].filter((entry) => isProcessActive(entry.info.state));
|
|
7999
|
+
await Promise.all(entries.map((entry) => this.#stopEntry(entry, "shutdown", context4)));
|
|
8000
|
+
}
|
|
8001
|
+
async close(context4) {
|
|
8002
|
+
this.#closing = true;
|
|
8003
|
+
const entries = [...this.#processes.values()];
|
|
8004
|
+
await Promise.all(entries.filter((entry) => isProcessActive(entry.info.state)).map((entry) => this.#stopEntry(entry, "shutdown", context4)));
|
|
8005
|
+
for (const entry of entries) {
|
|
8006
|
+
this.#release(entry, "The host shut down");
|
|
8007
|
+
this.#processes.delete(entry.info.id);
|
|
8008
|
+
entry.resolveDone();
|
|
8009
|
+
}
|
|
8010
|
+
this.#sealed.clear();
|
|
8011
|
+
this.#refreshBusy();
|
|
8012
|
+
}
|
|
8013
|
+
async#archivedInFamily(harness, conversationId, context4) {
|
|
8014
|
+
let current = conversationId;
|
|
8015
|
+
const seen = new Set;
|
|
8016
|
+
while (current !== undefined && !seen.has(current)) {
|
|
8017
|
+
seen.add(current);
|
|
8018
|
+
const index = await harness.snapshot(IndexDoc, context4);
|
|
8019
|
+
if (index?.conversations.some((entry) => entry.id === current && entry.archived === true) === true)
|
|
8020
|
+
return true;
|
|
8021
|
+
const record = await harness.commit((tx) => tx.conversation(current), BACKGROUND_CONTEXT5);
|
|
8022
|
+
current = record?.parent?.conversationId ?? record?.owner?.conversationId;
|
|
8023
|
+
}
|
|
8024
|
+
return false;
|
|
8025
|
+
}
|
|
8026
|
+
async#root(conversationId) {
|
|
8027
|
+
const cached = this.#roots.get(conversationId);
|
|
8028
|
+
if (cached !== undefined)
|
|
8029
|
+
return cached;
|
|
8030
|
+
const root = await familyRoot(this.#options.harness(), conversationId, BACKGROUND_CONTEXT5);
|
|
8031
|
+
this.#roots.set(conversationId, root);
|
|
8032
|
+
return root;
|
|
8033
|
+
}
|
|
8034
|
+
#spawn(entry, cwd) {
|
|
8035
|
+
const generation = ++entry.generation;
|
|
8036
|
+
const { shell, env } = this.#options;
|
|
8037
|
+
const pty = this.#options.spawn(shell.file, [...shell.args, "-c", entry.info.command], {
|
|
8038
|
+
name: "xterm-256color",
|
|
8039
|
+
cols: 120,
|
|
8040
|
+
rows: 40,
|
|
8041
|
+
cwd,
|
|
8042
|
+
env: { ...env, TERM: "xterm-256color", COLORTERM: "truecolor" }
|
|
8043
|
+
});
|
|
8044
|
+
entry.pty = pty;
|
|
8045
|
+
if (pty.pid !== undefined)
|
|
8046
|
+
entry.info.pid = pty.pid;
|
|
8047
|
+
entry.info.state = "running";
|
|
8048
|
+
pty.onData((data) => {
|
|
8049
|
+
if (entry.generation !== generation || entry.released)
|
|
8050
|
+
return;
|
|
8051
|
+
entry.output.push(data);
|
|
8052
|
+
});
|
|
8053
|
+
pty.onExit(({ exitCode, signal }) => {
|
|
8054
|
+
if (entry.generation !== generation || entry.released || !this.#processes.has(entry.info.id))
|
|
8055
|
+
return;
|
|
8056
|
+
this.#exit(entry, exitCode, signal);
|
|
8057
|
+
});
|
|
8058
|
+
}
|
|
8059
|
+
async#exit(entry, exitCode, signal) {
|
|
8060
|
+
entry.pty = undefined;
|
|
8061
|
+
entry.output.flush();
|
|
8062
|
+
if (entry.stopReason === undefined && this.#closing)
|
|
8063
|
+
entry.stopReason = "shutdown";
|
|
8064
|
+
if (entry.stopReason !== undefined) {
|
|
8065
|
+
if (this.#groupAlive(entry)) {
|
|
8066
|
+
this.#ensureKillTimer(entry);
|
|
8067
|
+
this.#pollGroup(entry);
|
|
8068
|
+
return;
|
|
8069
|
+
}
|
|
8070
|
+
await this.#finishStopped(entry);
|
|
8071
|
+
return;
|
|
8072
|
+
}
|
|
8073
|
+
this.#clearStopTimers(entry);
|
|
8074
|
+
entry.info.endedAt = this.#now();
|
|
8075
|
+
entry.info.state = signal === undefined || signal === 0 ? { exited: exitCode } : { exited: null, signal: signalName(signal) };
|
|
8076
|
+
const report = this.#options.installing?.() !== true;
|
|
8077
|
+
try {
|
|
8078
|
+
if (report)
|
|
8079
|
+
await this.#reportAndWrite(entry);
|
|
8080
|
+
else
|
|
8081
|
+
await this.#write(entry, BACKGROUND_CONTEXT5);
|
|
8082
|
+
} catch (error) {
|
|
8083
|
+
this.#diagnose(error);
|
|
8084
|
+
}
|
|
8085
|
+
entry.resolveDone();
|
|
8086
|
+
this.#refreshBusy();
|
|
8087
|
+
this.#evictOutputs();
|
|
8088
|
+
}
|
|
8089
|
+
async#finishStopped(entry) {
|
|
8090
|
+
if (entry.finishing)
|
|
8091
|
+
return;
|
|
8092
|
+
entry.finishing = true;
|
|
8093
|
+
this.#clearStopTimers(entry);
|
|
8094
|
+
entry.info.endedAt = this.#now();
|
|
8095
|
+
entry.info.state = { stopped: entry.stopReason ?? "shutdown" };
|
|
8096
|
+
try {
|
|
8097
|
+
await this.#write(entry, BACKGROUND_CONTEXT5);
|
|
8098
|
+
} catch (error) {
|
|
8099
|
+
this.#diagnose(error);
|
|
8100
|
+
}
|
|
8101
|
+
entry.resolveDone();
|
|
8102
|
+
this.#refreshBusy();
|
|
8103
|
+
this.#evictOutputs();
|
|
8104
|
+
}
|
|
8105
|
+
#ensureKillTimer(entry) {
|
|
8106
|
+
if (entry.killTimer !== undefined || entry.killed)
|
|
8107
|
+
return;
|
|
8108
|
+
const grace = this.#options.stopGraceMs ?? STOP_GRACE_MS;
|
|
8109
|
+
entry.killTimer = setTimeout(() => {
|
|
8110
|
+
entry.killTimer = undefined;
|
|
8111
|
+
entry.killed = true;
|
|
8112
|
+
entry.killedAt = this.#now();
|
|
8113
|
+
this.#signal(entry, "SIGKILL");
|
|
8114
|
+
entry.killWaitTimer ??= setTimeout(() => this.#finishAfterKillWait(entry), this.#options.groupWaitMs ?? GROUP_WAIT_MS);
|
|
8115
|
+
}, grace);
|
|
8116
|
+
}
|
|
8117
|
+
#pollGroup(entry) {
|
|
8118
|
+
if (entry.groupTimer !== undefined || entry.finishing)
|
|
8119
|
+
return;
|
|
8120
|
+
const check = () => {
|
|
8121
|
+
entry.groupTimer = undefined;
|
|
8122
|
+
if (entry.finishing || entry.released || !this.#processes.has(entry.info.id))
|
|
8123
|
+
return;
|
|
8124
|
+
if (!this.#groupAlive(entry)) {
|
|
8125
|
+
this.#finishStopped(entry);
|
|
8126
|
+
return;
|
|
8127
|
+
}
|
|
8128
|
+
const wait = this.#options.groupWaitMs ?? GROUP_WAIT_MS;
|
|
8129
|
+
if (entry.killedAt !== undefined && this.#now() - entry.killedAt >= wait) {
|
|
8130
|
+
this.#finishAfterKillWait(entry);
|
|
8131
|
+
return;
|
|
8132
|
+
}
|
|
8133
|
+
entry.groupTimer = setTimeout(check, 25);
|
|
8134
|
+
};
|
|
8135
|
+
check();
|
|
8136
|
+
}
|
|
8137
|
+
#finishAfterKillWait(entry) {
|
|
8138
|
+
entry.killWaitTimer = undefined;
|
|
8139
|
+
if (entry.finishing || entry.released || !this.#processes.has(entry.info.id))
|
|
8140
|
+
return;
|
|
8141
|
+
if (this.#groupAlive(entry)) {
|
|
8142
|
+
this.#diagnose(`Process group ${entry.info.pid ?? "unknown"} still appears alive after SIGKILL; marking it stopped`);
|
|
8143
|
+
}
|
|
8144
|
+
this.#finishStopped(entry);
|
|
8145
|
+
}
|
|
8146
|
+
#groupAlive(entry) {
|
|
8147
|
+
const pid = entry.info.pid;
|
|
8148
|
+
if (pid === undefined)
|
|
8149
|
+
return false;
|
|
8150
|
+
const alive = this.#options.groupAlive ?? ((group) => {
|
|
8151
|
+
try {
|
|
8152
|
+
process.kill(-group, 0);
|
|
8153
|
+
return true;
|
|
8154
|
+
} catch {
|
|
8155
|
+
return false;
|
|
8156
|
+
}
|
|
8157
|
+
});
|
|
8158
|
+
try {
|
|
8159
|
+
return alive(pid);
|
|
8160
|
+
} catch {
|
|
8161
|
+
return false;
|
|
8162
|
+
}
|
|
8163
|
+
}
|
|
8164
|
+
#clearStopTimers(entry) {
|
|
8165
|
+
if (entry.killTimer !== undefined)
|
|
8166
|
+
clearTimeout(entry.killTimer);
|
|
8167
|
+
if (entry.groupTimer !== undefined)
|
|
8168
|
+
clearTimeout(entry.groupTimer);
|
|
8169
|
+
if (entry.killWaitTimer !== undefined)
|
|
8170
|
+
clearTimeout(entry.killWaitTimer);
|
|
8171
|
+
entry.killTimer = undefined;
|
|
8172
|
+
entry.groupTimer = undefined;
|
|
8173
|
+
entry.killWaitTimer = undefined;
|
|
8174
|
+
}
|
|
8175
|
+
async#fail(entry, reason, context4) {
|
|
8176
|
+
entry.generation++;
|
|
8177
|
+
entry.pty = undefined;
|
|
8178
|
+
entry.info.state = { failed: reason };
|
|
8179
|
+
entry.info.endedAt = this.#now();
|
|
8180
|
+
try {
|
|
8181
|
+
await this.#write(entry, context4);
|
|
8182
|
+
} catch (error) {
|
|
8183
|
+
this.#diagnose(error);
|
|
8184
|
+
}
|
|
8185
|
+
entry.resolveDone();
|
|
8186
|
+
this.#refreshBusy();
|
|
8187
|
+
this.#evictOutputs();
|
|
8188
|
+
}
|
|
8189
|
+
async#stopEntry(entry, reason, context4) {
|
|
8190
|
+
if (!isProcessActive(entry.info.state))
|
|
8191
|
+
return info2(entry);
|
|
8192
|
+
entry.stopReason ??= reason;
|
|
8193
|
+
this.#signal(entry, "SIGTERM");
|
|
8194
|
+
this.#ensureKillTimer(entry);
|
|
8195
|
+
await entry.done;
|
|
8196
|
+
return info2(entry);
|
|
8197
|
+
}
|
|
8198
|
+
#signal(entry, signal) {
|
|
8199
|
+
const pid = entry.info.pid;
|
|
8200
|
+
try {
|
|
8201
|
+
if (pid !== undefined) {
|
|
8202
|
+
const kill = this.#options.kill ?? ((target, value) => process.kill(target, value));
|
|
8203
|
+
kill(-pid, signal);
|
|
8204
|
+
} else {
|
|
8205
|
+
entry.pty?.kill(signal);
|
|
8206
|
+
}
|
|
8207
|
+
} catch {}
|
|
8208
|
+
}
|
|
8209
|
+
async#write(entry, context4) {
|
|
8210
|
+
const kept = await this.#options.harness().commit((tx) => this.#applyDoc(tx, entry), context4);
|
|
8211
|
+
this.#retireMissing(kept, entry.root);
|
|
8212
|
+
}
|
|
8213
|
+
async#reportAndWrite(entry) {
|
|
8214
|
+
const harness = this.#options.harness();
|
|
8215
|
+
const conversation = await harness.conversation(entry.info.owner, BACKGROUND_CONTEXT5);
|
|
8216
|
+
const report = reportText(entry);
|
|
8217
|
+
if (conversation === undefined) {
|
|
8218
|
+
await this.#write(entry, BACKGROUND_CONTEXT5);
|
|
8219
|
+
return;
|
|
8220
|
+
}
|
|
8221
|
+
const kept = await conversation.commit(async (tx) => {
|
|
8222
|
+
await conversation.submit(tx, {
|
|
8223
|
+
type: "input",
|
|
8224
|
+
content: report,
|
|
8225
|
+
whenBusy: "steer",
|
|
8226
|
+
requestId: `process:${entry.info.id}:exit`
|
|
8227
|
+
});
|
|
8228
|
+
return this.#applyDoc(tx, entry);
|
|
8229
|
+
}, BACKGROUND_CONTEXT5);
|
|
8230
|
+
this.#retireMissing(kept, entry.root);
|
|
8231
|
+
}
|
|
8232
|
+
async#applyDoc(tx, entry) {
|
|
8233
|
+
const doc = await tx.doc(ProcessDoc, entry.root);
|
|
8234
|
+
const next = docEntry(entry.info);
|
|
8235
|
+
const index = doc.processes.findIndex((process2) => process2.id === entry.info.id);
|
|
8236
|
+
if (index === -1)
|
|
8237
|
+
doc.processes.push(next);
|
|
8238
|
+
else
|
|
8239
|
+
doc.processes[index] = next;
|
|
8240
|
+
const ended = doc.processes.filter((process2) => !isProcessActive(process2.state)).sort((a, b) => (b.endedAt ?? b.startedAt) - (a.endedAt ?? a.startedAt)).slice(0, this.#endedLimit);
|
|
8241
|
+
const keep = new Set(ended.map((process2) => process2.id));
|
|
8242
|
+
doc.processes = doc.processes.filter((process2) => isProcessActive(process2.state) || keep.has(process2.id));
|
|
8243
|
+
return new Set(doc.processes.map((process2) => process2.id));
|
|
8244
|
+
}
|
|
8245
|
+
#retireMissing(kept, root) {
|
|
8246
|
+
for (const [id, entry] of this.#processes) {
|
|
8247
|
+
if (entry.root !== root || kept.has(id) || isProcessActive(entry.info.state))
|
|
8248
|
+
continue;
|
|
8249
|
+
this.#release(entry, "Output released");
|
|
8250
|
+
this.#processes.delete(id);
|
|
8251
|
+
entry.resolveDone();
|
|
8252
|
+
}
|
|
8253
|
+
this.#refreshBusy();
|
|
8254
|
+
}
|
|
8255
|
+
#release(entry, reason) {
|
|
8256
|
+
if (entry.released)
|
|
8257
|
+
return;
|
|
8258
|
+
entry.released = true;
|
|
8259
|
+
this.#clearStopTimers(entry);
|
|
8260
|
+
entry.output.close(reason);
|
|
8261
|
+
entry.output.discard();
|
|
8262
|
+
}
|
|
8263
|
+
#evictOutputs() {
|
|
8264
|
+
let total = 0;
|
|
8265
|
+
const ended = [];
|
|
8266
|
+
for (const entry of this.#processes.values()) {
|
|
8267
|
+
if (isProcessActive(entry.info.state) || entry.released)
|
|
8268
|
+
continue;
|
|
8269
|
+
total += entry.output.length;
|
|
8270
|
+
ended.push(entry);
|
|
8271
|
+
}
|
|
8272
|
+
if (total <= this.#outputBudget)
|
|
8273
|
+
return;
|
|
8274
|
+
ended.sort((a, b) => (a.info.endedAt ?? a.info.startedAt) - (b.info.endedAt ?? b.info.startedAt));
|
|
8275
|
+
for (const entry of ended) {
|
|
8276
|
+
if (total <= this.#outputBudget)
|
|
8277
|
+
return;
|
|
8278
|
+
total -= entry.output.length;
|
|
8279
|
+
this.#release(entry, "Output released");
|
|
8280
|
+
}
|
|
8281
|
+
}
|
|
8282
|
+
#refreshBusy() {
|
|
8283
|
+
this.#busy.set([...this.#processes.values()].some((entry) => isProcessActive(entry.info.state)));
|
|
8284
|
+
}
|
|
8285
|
+
#now() {
|
|
8286
|
+
return (this.#options.now ?? Date.now)();
|
|
8287
|
+
}
|
|
8288
|
+
#diagnose(error) {
|
|
8289
|
+
(this.#options.diagnostic ?? console.error)(`Background process update failed: ${message2(error)}`);
|
|
8290
|
+
}
|
|
8291
|
+
}
|
|
8292
|
+
async function startProcesses(options) {
|
|
8293
|
+
let spawn4 = options.spawn;
|
|
8294
|
+
if (spawn4 === undefined) {
|
|
8295
|
+
try {
|
|
8296
|
+
const loaded = options.load === undefined ? await import("@lydell/node-pty") : await options.load();
|
|
8297
|
+
if (typeof loaded.spawn !== "function")
|
|
8298
|
+
throw new Error("the module has no spawn()");
|
|
8299
|
+
spawn4 = loaded.spawn;
|
|
8300
|
+
} catch (error) {
|
|
8301
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
8302
|
+
(options.diagnostic ?? console.error)(`Background processes unavailable: cannot load @lydell/node-pty on ${process.platform} (${process.arch}); missing @lydell/node-pty-${process.platform}-${process.arch}. Install without --omit=optional; the host continues without hello.processes. (${detail})`);
|
|
8303
|
+
return;
|
|
8304
|
+
}
|
|
8305
|
+
}
|
|
8306
|
+
return new Processes({
|
|
8307
|
+
...options,
|
|
8308
|
+
spawn: spawn4,
|
|
8309
|
+
shell: options.shell ?? loginShell(),
|
|
8310
|
+
env: options.env ?? shellEnv(process.env)
|
|
8311
|
+
});
|
|
8312
|
+
}
|
|
8313
|
+
async function settleLostProcesses(harness, context4) {
|
|
8314
|
+
let cursor;
|
|
8315
|
+
do {
|
|
8316
|
+
const page = await harness.commit((tx) => tx.scanConversations({}, 256, cursor), context4);
|
|
8317
|
+
for (const record of page.items) {
|
|
8318
|
+
if (record.parent !== undefined || record.owner !== undefined)
|
|
8319
|
+
continue;
|
|
8320
|
+
const doc = await harness.snapshot(ProcessDoc, record.id, context4);
|
|
8321
|
+
if (doc === undefined || !doc.processes.some((process2) => process2.state === "starting" || process2.state === "running")) {
|
|
8322
|
+
continue;
|
|
8323
|
+
}
|
|
8324
|
+
await harness.commit(async (tx) => {
|
|
8325
|
+
const value = await tx.doc(ProcessDoc, record.id);
|
|
8326
|
+
for (const process2 of value.processes) {
|
|
8327
|
+
if (process2.state === "starting" || process2.state === "running")
|
|
8328
|
+
process2.state = "lost";
|
|
8329
|
+
}
|
|
8330
|
+
}, context4);
|
|
8331
|
+
}
|
|
8332
|
+
cursor = page.next;
|
|
8333
|
+
} while (cursor !== undefined);
|
|
8334
|
+
}
|
|
8335
|
+
function info2(entry) {
|
|
8336
|
+
return { ...entry.info, ...entry.info.pid === undefined ? {} : { pid: entry.info.pid } };
|
|
8337
|
+
}
|
|
8338
|
+
function docEntry(entry) {
|
|
8339
|
+
return {
|
|
8340
|
+
id: entry.id,
|
|
8341
|
+
owner: entry.owner,
|
|
8342
|
+
name: entry.name,
|
|
8343
|
+
command: entry.command,
|
|
8344
|
+
...entry.pid === undefined ? {} : { pid: entry.pid },
|
|
8345
|
+
startedAt: entry.startedAt,
|
|
8346
|
+
...entry.endedAt === undefined ? {} : { endedAt: entry.endedAt },
|
|
8347
|
+
state: typeof entry.state === "object" ? { ...entry.state } : entry.state
|
|
8348
|
+
};
|
|
8349
|
+
}
|
|
8350
|
+
function reportText(entry) {
|
|
8351
|
+
const state = entry.info.state;
|
|
8352
|
+
const outcome = typeof state === "object" && "exited" in state ? state.signal === undefined ? `exited with code ${state.exited ?? "unknown"}` : `exited after signal ${state.signal}` : "exited";
|
|
8353
|
+
const raw = entry.output.snapshot(undefined, Number.MAX_SAFE_INTEGER).text;
|
|
8354
|
+
const clean = stripAnsi(raw).replace(/\r\n/g, `
|
|
8355
|
+
`).replace(/\r/g, `
|
|
8356
|
+
`);
|
|
8357
|
+
const tail = clean.split(`
|
|
8358
|
+
`).slice(-REPORT_LINES).join(`
|
|
8359
|
+
`).trimEnd();
|
|
8360
|
+
const header = `Background process "${entry.info.name}" (${entry.info.id}) ${outcome}.`;
|
|
8361
|
+
const available = Math.max(0, REPORT_LIMIT - header.length - REPORT_TAIL.length);
|
|
8362
|
+
const body = truncateTail(tail, available);
|
|
8363
|
+
return body === "" ? header : `${header}${REPORT_TAIL}${body}`;
|
|
8364
|
+
}
|
|
8365
|
+
function stripAnsi(text) {
|
|
8366
|
+
return text.replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, "").replace(/\x1b[PX^_][^\x1b]*(?:\x1b\\)/g, "").replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "").replace(/\x1b[@-_]/g, "");
|
|
8367
|
+
}
|
|
8368
|
+
function truncateTail(text, max) {
|
|
8369
|
+
if (text.length <= max)
|
|
8370
|
+
return text;
|
|
8371
|
+
let from = text.length - max;
|
|
8372
|
+
const code = text.charCodeAt(from);
|
|
8373
|
+
if (code >= 56320 && code <= 57343)
|
|
8374
|
+
from++;
|
|
8375
|
+
return text.slice(from);
|
|
8376
|
+
}
|
|
8377
|
+
function signalName(signal) {
|
|
8378
|
+
return signal === 1 ? "SIGHUP" : signal === 2 ? "SIGINT" : signal === 9 ? "SIGKILL" : signal === 15 ? "SIGTERM" : String(signal);
|
|
8379
|
+
}
|
|
8380
|
+
function message2(error) {
|
|
8381
|
+
return error instanceof Error ? error.message : String(error);
|
|
8382
|
+
}
|
|
8383
|
+
|
|
6483
8384
|
// apps/host/src/host.ts
|
|
6484
|
-
var context4 =
|
|
8385
|
+
var context4 = BACKGROUND_CONTEXT6;
|
|
6485
8386
|
async function hostToken(dataDir) {
|
|
6486
|
-
const path =
|
|
8387
|
+
const path = join11(dataDir, "token");
|
|
6487
8388
|
try {
|
|
6488
8389
|
return (await readFile5(path, "utf8")).trim();
|
|
6489
8390
|
} catch {
|
|
@@ -6498,7 +8399,7 @@ async function openHost(options) {
|
|
|
6498
8399
|
validatePublicUrl(options.remote.publicUrl);
|
|
6499
8400
|
await mkdir4(options.dataDir, { recursive: true });
|
|
6500
8401
|
const stale = Math.max(2000, options.lockStaleMs ?? 1e4);
|
|
6501
|
-
const
|
|
8402
|
+
const release2 = await lockfile.lock(options.dataDir, {
|
|
6502
8403
|
realpath: false,
|
|
6503
8404
|
stale,
|
|
6504
8405
|
retries: { retries: Math.ceil(stale / 1000) + 2, minTimeout: 1000, maxTimeout: 1000 }
|
|
@@ -6506,26 +8407,38 @@ async function openHost(options) {
|
|
|
6506
8407
|
throw new Error(`Session is already open in another process: ${options.dataDir}`, { cause: error });
|
|
6507
8408
|
});
|
|
6508
8409
|
let harness;
|
|
8410
|
+
let processes;
|
|
6509
8411
|
let mcp;
|
|
6510
8412
|
let agentRoles;
|
|
6511
8413
|
let attachments;
|
|
6512
8414
|
let extensions = [];
|
|
8415
|
+
let upgrader;
|
|
6513
8416
|
try {
|
|
6514
8417
|
const models = withAnswerTiming(options.models);
|
|
6515
8418
|
const token = await hostToken(options.dataDir);
|
|
6516
|
-
attachments = new AttachmentStore({ dir:
|
|
8419
|
+
attachments = new AttachmentStore({ dir: join11(options.dataDir, "attachments") });
|
|
6517
8420
|
await attachments.open();
|
|
6518
8421
|
const reports = [];
|
|
6519
8422
|
let report = (error) => void reports.push(error);
|
|
8423
|
+
processes = await startProcesses({
|
|
8424
|
+
...options.processes,
|
|
8425
|
+
installing: () => upgrader?.installing === true,
|
|
8426
|
+
harness: () => {
|
|
8427
|
+
if (harness === undefined)
|
|
8428
|
+
throw new Error("harness is not open yet");
|
|
8429
|
+
return harness;
|
|
8430
|
+
}
|
|
8431
|
+
});
|
|
6520
8432
|
const registry = createRegistry();
|
|
6521
|
-
agentRoles = await openAgentRoles({ dir:
|
|
8433
|
+
agentRoles = await openAgentRoles({ dir: join11(options.dataDir, "agents") });
|
|
6522
8434
|
extensions = typeof options.extensions === "function" ? options.extensions({
|
|
6523
8435
|
harness: () => {
|
|
6524
8436
|
if (harness === undefined)
|
|
6525
8437
|
throw new Error("harness is not open yet");
|
|
6526
8438
|
return harness;
|
|
6527
8439
|
},
|
|
6528
|
-
agentRoles
|
|
8440
|
+
agentRoles,
|
|
8441
|
+
...processes === undefined ? {} : { processes }
|
|
6529
8442
|
}) : options.extensions ?? [];
|
|
6530
8443
|
for (const { extension } of extensions)
|
|
6531
8444
|
registry.install(extension);
|
|
@@ -6541,13 +8454,13 @@ async function openHost(options) {
|
|
|
6541
8454
|
}
|
|
6542
8455
|
});
|
|
6543
8456
|
const hostSettings = await openHostSettings({
|
|
6544
|
-
path:
|
|
8457
|
+
path: join11(options.dataDir, "settings.json"),
|
|
6545
8458
|
fallback: options.settingsFallback,
|
|
6546
8459
|
models: options.modelAuth?.models ?? constantValue(options.modelSummaries())
|
|
6547
8460
|
});
|
|
6548
8461
|
const environments = new Map;
|
|
6549
|
-
const chatsDir =
|
|
6550
|
-
harness = await Harness.open(await (options.openStorage ?? openNodeSqliteStorage)(
|
|
8462
|
+
const chatsDir = join11(options.dataDir, "chats");
|
|
8463
|
+
harness = await Harness.open(await (options.openStorage ?? openNodeSqliteStorage)(join11(options.dataDir, "session.sqlite")), {
|
|
6551
8464
|
models,
|
|
6552
8465
|
registry,
|
|
6553
8466
|
env: async (target) => {
|
|
@@ -6555,7 +8468,7 @@ async function openHost(options) {
|
|
|
6555
8468
|
if (cwd === undefined)
|
|
6556
8469
|
return;
|
|
6557
8470
|
const worktrees = worktreeRoot(options.dataDir);
|
|
6558
|
-
if (cwd === worktrees || cwd.startsWith(worktrees +
|
|
8471
|
+
if (cwd === worktrees || cwd.startsWith(worktrees + sep7)) {
|
|
6559
8472
|
const index = await target.read.snapshot(IndexDoc, context4);
|
|
6560
8473
|
const record = checkoutAt(index?.checkouts, cwd);
|
|
6561
8474
|
if (record !== undefined) {
|
|
@@ -6577,6 +8490,10 @@ async function openHost(options) {
|
|
|
6577
8490
|
},
|
|
6578
8491
|
onReport: (error) => report(error)
|
|
6579
8492
|
}, context4);
|
|
8493
|
+
if (processes === undefined)
|
|
8494
|
+
await settleLostProcesses(harness, context4);
|
|
8495
|
+
else
|
|
8496
|
+
await processes.open(context4);
|
|
6580
8497
|
await ensureIndex(harness, context4);
|
|
6581
8498
|
await ensureDevices(harness, context4);
|
|
6582
8499
|
for (const path of options.projects ?? [])
|
|
@@ -6584,6 +8501,18 @@ async function openHost(options) {
|
|
|
6584
8501
|
const hostKey = await loadHostKey(options.dataDir);
|
|
6585
8502
|
const offers = pairingOffers(options.remote?.pairingTtlMs);
|
|
6586
8503
|
const relayKey = options.relay === undefined ? undefined : await loadRelayKey(options.dataDir);
|
|
8504
|
+
upgrader = options.upgrade === undefined ? undefined : await startHostUpgrader({
|
|
8505
|
+
current: options.upgrade.current,
|
|
8506
|
+
dataDir: options.dataDir,
|
|
8507
|
+
harness,
|
|
8508
|
+
settings: hostSettings,
|
|
8509
|
+
context: context4,
|
|
8510
|
+
seams: options.upgrade,
|
|
8511
|
+
...processes === undefined ? {} : {
|
|
8512
|
+
processesBusy: processes.busy,
|
|
8513
|
+
drain: () => processes.drain(context4)
|
|
8514
|
+
}
|
|
8515
|
+
});
|
|
6587
8516
|
const gateway = await startGateway({
|
|
6588
8517
|
harness,
|
|
6589
8518
|
models,
|
|
@@ -6613,6 +8542,8 @@ async function openHost(options) {
|
|
|
6613
8542
|
},
|
|
6614
8543
|
...options.rtc === undefined ? {} : { rtc: options.rtc },
|
|
6615
8544
|
...options.terminals === undefined ? {} : { terminals: options.terminals },
|
|
8545
|
+
...processes === undefined ? {} : { processes },
|
|
8546
|
+
...upgrader === undefined ? {} : { upgrade: upgrader },
|
|
6616
8547
|
secure: {
|
|
6617
8548
|
hostKey,
|
|
6618
8549
|
offers,
|
|
@@ -6645,6 +8576,7 @@ async function openHost(options) {
|
|
|
6645
8576
|
harness,
|
|
6646
8577
|
closed: opened.closed,
|
|
6647
8578
|
settings: hostSettings,
|
|
8579
|
+
...processes === undefined ? {} : { processes },
|
|
6648
8580
|
createAccountOffer: async () => {
|
|
6649
8581
|
const binding = await readServerBinding(options.dataDir).catch(() => {
|
|
6650
8582
|
return;
|
|
@@ -6664,6 +8596,7 @@ async function openHost(options) {
|
|
|
6664
8596
|
close() {
|
|
6665
8597
|
closing ??= (async () => {
|
|
6666
8598
|
try {
|
|
8599
|
+
upgrader?.close();
|
|
6667
8600
|
await gateway.close();
|
|
6668
8601
|
await opened.close(context4);
|
|
6669
8602
|
await mcp?.close();
|
|
@@ -6674,13 +8607,16 @@ async function openHost(options) {
|
|
|
6674
8607
|
options.modelAuth?.close();
|
|
6675
8608
|
await Promise.all([...environments.values()].map((env) => env.cleanup(context4)));
|
|
6676
8609
|
} finally {
|
|
6677
|
-
await
|
|
8610
|
+
await release2();
|
|
6678
8611
|
}
|
|
6679
8612
|
})();
|
|
6680
8613
|
return closing;
|
|
6681
8614
|
}
|
|
6682
8615
|
};
|
|
6683
8616
|
} catch (error) {
|
|
8617
|
+
upgrader?.close();
|
|
8618
|
+
if (processes !== undefined)
|
|
8619
|
+
await processes.close(context4).catch(() => {});
|
|
6684
8620
|
options.modelAuth?.close();
|
|
6685
8621
|
attachments?.close();
|
|
6686
8622
|
agentRoles?.close();
|
|
@@ -6688,18 +8624,18 @@ async function openHost(options) {
|
|
|
6688
8624
|
await each.close?.().catch(() => {});
|
|
6689
8625
|
await mcp?.close().catch(() => {});
|
|
6690
8626
|
await harness?.close(context4).catch(() => {});
|
|
6691
|
-
await
|
|
8627
|
+
await release2().catch(() => {});
|
|
6692
8628
|
throw error;
|
|
6693
8629
|
}
|
|
6694
8630
|
}
|
|
6695
8631
|
|
|
6696
8632
|
// apps/host/src/service/log-rotation.ts
|
|
6697
8633
|
import { copyFile, rename as rename3, rm as rm4, stat as stat8, truncate } from "node:fs/promises";
|
|
6698
|
-
import { join as
|
|
8634
|
+
import { join as join12 } from "node:path";
|
|
6699
8635
|
var HOST_LOG_MAX_BYTES = 10 * 1024 * 1024;
|
|
6700
8636
|
var HOST_LOG_HISTORY = 3;
|
|
6701
8637
|
var HOST_LOG_CHECK_INTERVAL_MS = 10 * 60 * 1000;
|
|
6702
|
-
var hostLogPath = (dataDir) =>
|
|
8638
|
+
var hostLogPath = (dataDir) => join12(dataDir, "logs", "host.log");
|
|
6703
8639
|
var rotationTempPath = (log) => `${log}.rotating`;
|
|
6704
8640
|
var isMissing = (error) => error.code === "ENOENT";
|
|
6705
8641
|
async function rotateHostLog(dataDir, copy = copyFile) {
|
|
@@ -6742,8 +8678,8 @@ function startHostLogRotation(dataDir) {
|
|
|
6742
8678
|
return;
|
|
6743
8679
|
running = true;
|
|
6744
8680
|
rotateHostLog(dataDir).catch((error) => {
|
|
6745
|
-
const
|
|
6746
|
-
process.stderr.write(`${JSON.stringify({ event: "log-rotation-failed", error:
|
|
8681
|
+
const message3 = error instanceof Error ? error.message : String(error);
|
|
8682
|
+
process.stderr.write(`${JSON.stringify({ event: "log-rotation-failed", error: message3 })}
|
|
6747
8683
|
`);
|
|
6748
8684
|
}).finally(() => {
|
|
6749
8685
|
running = false;
|
|
@@ -6756,10 +8692,10 @@ function startHostLogRotation(dataDir) {
|
|
|
6756
8692
|
}
|
|
6757
8693
|
|
|
6758
8694
|
// apps/host/src/model-auth.ts
|
|
6759
|
-
import { randomUUID as
|
|
8695
|
+
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
6760
8696
|
import { mkdir as mkdir5, readFile as readFile6, rename as rename4, writeFile as writeFile4 } from "node:fs/promises";
|
|
6761
8697
|
import { dirname as dirname3 } from "node:path";
|
|
6762
|
-
import { getBuiltinProviders } from "@earendil-works/pi-ai/providers/all";
|
|
8698
|
+
import { getBuiltinModels, getBuiltinProviders } from "@earendil-works/pi-ai/providers/all";
|
|
6763
8699
|
|
|
6764
8700
|
// apps/host/src/pi-setup.ts
|
|
6765
8701
|
import { getSupportedThinkingLevels as getSupportedThinkingLevels2 } from "@earendil-works/pi-ai";
|
|
@@ -6874,11 +8810,22 @@ function modelSummaries(modelRuntime) {
|
|
|
6874
8810
|
}
|
|
6875
8811
|
|
|
6876
8812
|
// apps/host/src/model-auth.ts
|
|
6877
|
-
var
|
|
8813
|
+
var BUILTIN_PROVIDERS = getBuiltinProviders();
|
|
8814
|
+
var BUILTIN = new Set(BUILTIN_PROVIDERS);
|
|
6878
8815
|
var CUSTOM_ID = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
6879
8816
|
var LOGIN_TIMEOUT_MS = 15 * 60000;
|
|
6880
8817
|
var FINISHED_LOGIN_TTL_MS = 60000;
|
|
6881
8818
|
var ENDPOINT_TIMEOUT_MS = 1e4;
|
|
8819
|
+
var CATALOG_THINKING_LEVELS = new Map;
|
|
8820
|
+
for (const provider of BUILTIN_PROVIDERS) {
|
|
8821
|
+
for (const model of getBuiltinModels(provider)) {
|
|
8822
|
+
if (!model.reasoning || model.thinkingLevelMap === undefined)
|
|
8823
|
+
continue;
|
|
8824
|
+
const maps = CATALOG_THINKING_LEVELS.get(model.id) ?? [];
|
|
8825
|
+
maps.push(model.thinkingLevelMap);
|
|
8826
|
+
CATALOG_THINKING_LEVELS.set(model.id, maps);
|
|
8827
|
+
}
|
|
8828
|
+
}
|
|
6882
8829
|
var sameModel = (a, b) => a.provider === b.provider && a.modelId === b.modelId && a.name === b.name && a.contextWindow === b.contextWindow && a.images === b.images && sameList(a.thinkingLevels, b.thinkingLevels, (x, y) => x === y);
|
|
6883
8830
|
var sameLogin = (a, b) => a.providerId === b.providerId && a.name === b.name && a.kind === b.kind && a.subscription === b.subscription;
|
|
6884
8831
|
async function readModelsFile(path) {
|
|
@@ -7099,7 +9046,7 @@ async function openModelAuth(runtime, options) {
|
|
|
7099
9046
|
if ([...logins.values()].some((login2) => login2.flow.providerId === providerId && login2.flow.state === "running")) {
|
|
7100
9047
|
throw new Error(`A login for ${provider.name} is already in progress`);
|
|
7101
9048
|
}
|
|
7102
|
-
const id =
|
|
9049
|
+
const id = randomUUID5();
|
|
7103
9050
|
const login = {
|
|
7104
9051
|
flow: { id, providerId, method, events: [], state: "running" },
|
|
7105
9052
|
controller: new AbortController,
|
|
@@ -7264,21 +9211,35 @@ async function fetchEndpointModels(api, baseUrl, key) {
|
|
|
7264
9211
|
if (!response.ok)
|
|
7265
9212
|
throw new Error(`${url.host} answered ${response.status} for /models`);
|
|
7266
9213
|
const body = await response.json();
|
|
7267
|
-
const models = api === "google-generative-ai" ? (body.models ?? []).flatMap((model) =>
|
|
7268
|
-
|
|
7269
|
-
|
|
7270
|
-
|
|
7271
|
-
|
|
7272
|
-
|
|
7273
|
-
|
|
7274
|
-
|
|
7275
|
-
|
|
7276
|
-
|
|
7277
|
-
|
|
7278
|
-
|
|
7279
|
-
|
|
7280
|
-
|
|
7281
|
-
|
|
9214
|
+
const models = api === "google-generative-ai" ? (body.models ?? []).flatMap((model) => {
|
|
9215
|
+
if (typeof model.name !== "string")
|
|
9216
|
+
return [];
|
|
9217
|
+
const id = model.name.replace(/^models\//, "");
|
|
9218
|
+
const reasoning = model.thinking === true;
|
|
9219
|
+
return [
|
|
9220
|
+
endpointModel(id, {
|
|
9221
|
+
contextWindow: positive(model.inputTokenLimit),
|
|
9222
|
+
maxTokens: positive(model.outputTokenLimit),
|
|
9223
|
+
reasoning,
|
|
9224
|
+
image: false,
|
|
9225
|
+
thinkingLevelMap: reasoning ? thinkingLevelMapFrom(reportedThinkingLevels(model)) ?? catalogThinkingLevelMap(id) : undefined
|
|
9226
|
+
})
|
|
9227
|
+
];
|
|
9228
|
+
}) : (body.data ?? []).flatMap((model) => {
|
|
9229
|
+
if (typeof model.id !== "string")
|
|
9230
|
+
return [];
|
|
9231
|
+
const id = model.id;
|
|
9232
|
+
const reasoning = model.reasoning === true;
|
|
9233
|
+
return [
|
|
9234
|
+
endpointModel(id, {
|
|
9235
|
+
contextWindow: positive(model.context_window) ?? positive(model.context_length) ?? positive(model.max_input_tokens),
|
|
9236
|
+
maxTokens: positive(model.max_output_tokens),
|
|
9237
|
+
reasoning,
|
|
9238
|
+
image: Array.isArray(model.modalities?.input) && model.modalities.input.includes("image"),
|
|
9239
|
+
thinkingLevelMap: reasoning ? thinkingLevelMapFrom(reportedThinkingLevels(model)) ?? catalogThinkingLevelMap(id) : undefined
|
|
9240
|
+
})
|
|
9241
|
+
];
|
|
9242
|
+
});
|
|
7282
9243
|
if (models.length === 0)
|
|
7283
9244
|
throw new Error(`${url.host} listed no models`);
|
|
7284
9245
|
return models;
|
|
@@ -7286,12 +9247,62 @@ async function fetchEndpointModels(api, baseUrl, key) {
|
|
|
7286
9247
|
var LIMITS = ["contextWindow", "maxTokens"];
|
|
7287
9248
|
var lacksLimits = (model) => typeof model === "object" && model !== null && LIMITS.some((field) => !(field in model));
|
|
7288
9249
|
var positive = (value) => typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined;
|
|
9250
|
+
var THINKING_LEVELS3 = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
9251
|
+
function reportedThinkingLevels(model) {
|
|
9252
|
+
const levels = new Map;
|
|
9253
|
+
const add = (value) => {
|
|
9254
|
+
if (typeof value !== "string")
|
|
9255
|
+
return;
|
|
9256
|
+
const level = value.trim();
|
|
9257
|
+
const piLevel = level.toLowerCase() === "none" ? "off" : level.toLowerCase();
|
|
9258
|
+
if (!THINKING_LEVELS3.includes(piLevel) || levels.has(piLevel))
|
|
9259
|
+
return;
|
|
9260
|
+
levels.set(piLevel, level);
|
|
9261
|
+
};
|
|
9262
|
+
if (Array.isArray(model.reasoning_levels)) {
|
|
9263
|
+
for (const value of model.reasoning_levels)
|
|
9264
|
+
add(value);
|
|
9265
|
+
}
|
|
9266
|
+
if (Array.isArray(model.supported_reasoning_levels)) {
|
|
9267
|
+
for (const value of model.supported_reasoning_levels) {
|
|
9268
|
+
if (typeof value === "string")
|
|
9269
|
+
add(value);
|
|
9270
|
+
else if (typeof value === "object" && value !== null && "effort" in value)
|
|
9271
|
+
add(value.effort);
|
|
9272
|
+
}
|
|
9273
|
+
}
|
|
9274
|
+
return levels;
|
|
9275
|
+
}
|
|
9276
|
+
function thinkingLevelMapFrom(levels) {
|
|
9277
|
+
if (levels.size === 0)
|
|
9278
|
+
return;
|
|
9279
|
+
return {
|
|
9280
|
+
off: levels.get("off") ?? null,
|
|
9281
|
+
minimal: levels.get("minimal") ?? null,
|
|
9282
|
+
low: levels.get("low") ?? null,
|
|
9283
|
+
medium: levels.get("medium") ?? null,
|
|
9284
|
+
high: levels.get("high") ?? null,
|
|
9285
|
+
xhigh: levels.get("xhigh") ?? null,
|
|
9286
|
+
max: levels.get("max") ?? null
|
|
9287
|
+
};
|
|
9288
|
+
}
|
|
9289
|
+
function catalogThinkingLevelMap(id) {
|
|
9290
|
+
let matches = CATALOG_THINKING_LEVELS.get(id);
|
|
9291
|
+
if (matches === undefined) {
|
|
9292
|
+
const slash = id.indexOf("/");
|
|
9293
|
+
if (slash === -1)
|
|
9294
|
+
return;
|
|
9295
|
+
matches = CATALOG_THINKING_LEVELS.get(id.slice(slash + 1));
|
|
9296
|
+
}
|
|
9297
|
+
return matches?.length === 1 ? matches[0] : undefined;
|
|
9298
|
+
}
|
|
7289
9299
|
function endpointModel(id, reported) {
|
|
7290
9300
|
return {
|
|
7291
9301
|
id,
|
|
7292
9302
|
...reported.contextWindow === undefined ? {} : { contextWindow: reported.contextWindow },
|
|
7293
9303
|
...reported.maxTokens === undefined ? {} : { maxTokens: reported.maxTokens },
|
|
7294
9304
|
...reported.reasoning ? { reasoning: true } : {},
|
|
9305
|
+
...reported.thinkingLevelMap === undefined ? {} : { thinkingLevelMap: reported.thinkingLevelMap },
|
|
7295
9306
|
...reported.image ? { input: ["text", "image"] } : {}
|
|
7296
9307
|
};
|
|
7297
9308
|
}
|
|
@@ -7326,29 +9337,40 @@ async function main(argv) {
|
|
|
7326
9337
|
}
|
|
7327
9338
|
if (relayOrigin !== undefined)
|
|
7328
9339
|
validateRelayOrigin(relayOrigin);
|
|
7329
|
-
const agentsHome =
|
|
7330
|
-
const extensionsFor = (models, summaries, options2) => (host2) =>
|
|
7331
|
-
|
|
7332
|
-
|
|
7333
|
-
|
|
7334
|
-
|
|
7335
|
-
|
|
7336
|
-
|
|
7337
|
-
|
|
7338
|
-
|
|
7339
|
-
|
|
7340
|
-
|
|
7341
|
-
|
|
7342
|
-
|
|
7343
|
-
|
|
7344
|
-
|
|
7345
|
-
|
|
9340
|
+
const agentsHome = join13(homedir3(), ".agents");
|
|
9341
|
+
const extensionsFor = (models, summaries, options2) => (host2) => {
|
|
9342
|
+
const processes = createProcesses({
|
|
9343
|
+
...host2.processes === undefined ? {} : { runtime: host2.processes }
|
|
9344
|
+
});
|
|
9345
|
+
return [
|
|
9346
|
+
createContext({
|
|
9347
|
+
agentsHome,
|
|
9348
|
+
checkout: (input, context5) => checkoutInfo(host2.harness(), input, context5)
|
|
9349
|
+
}),
|
|
9350
|
+
createCoding(options2),
|
|
9351
|
+
todo,
|
|
9352
|
+
question,
|
|
9353
|
+
processes,
|
|
9354
|
+
createSubagent({
|
|
9355
|
+
models,
|
|
9356
|
+
modelSummaries: summaries,
|
|
9357
|
+
exclude: [question.extension, processes.extension],
|
|
9358
|
+
roles: () => host2.agentRoles.value.roles,
|
|
9359
|
+
worktree: {
|
|
9360
|
+
base: (dispatcher, context5) => subagentWorktreeBase(host2.harness(), dataDir, dispatcher, context5),
|
|
9361
|
+
path: (child) => worktreePath(dataDir, child)
|
|
9362
|
+
}
|
|
9363
|
+
}),
|
|
9364
|
+
createCodemode(),
|
|
9365
|
+
createContextBreakdown({ harness: host2.harness })
|
|
9366
|
+
];
|
|
9367
|
+
};
|
|
7346
9368
|
const common = {
|
|
7347
9369
|
dataDir,
|
|
7348
9370
|
agentsHome,
|
|
7349
9371
|
projects: values.project ?? [],
|
|
7350
9372
|
port: Number(values.port),
|
|
7351
|
-
mcpConfig:
|
|
9373
|
+
mcpConfig: join13(agentsHome, "mcp.json"),
|
|
7352
9374
|
browserOrigins: values["browser-origin"] ?? ["http://127.0.0.1:5199"],
|
|
7353
9375
|
webRoot,
|
|
7354
9376
|
...values["lock-stale-ms"] === undefined ? {} : { lockStaleMs: Number(values["lock-stale-ms"]) },
|
|
@@ -7391,10 +9413,10 @@ async function main(argv) {
|
|
|
7391
9413
|
};
|
|
7392
9414
|
}
|
|
7393
9415
|
async function piOptions() {
|
|
7394
|
-
const modelsPath =
|
|
7395
|
-
const modelRuntime = await ModelRuntime.create({ authPath:
|
|
9416
|
+
const modelsPath = join13(dataDir, "models.json");
|
|
9417
|
+
const modelRuntime = await ModelRuntime.create({ authPath: join13(dataDir, "auth.json"), modelsPath });
|
|
7396
9418
|
const modelAuth = await openModelAuth(modelRuntime, { modelsPath, installId: await installId(dataDir) });
|
|
7397
|
-
const settingsManager = SettingsManager.inMemory(await piSettings(
|
|
9419
|
+
const settingsManager = SettingsManager.inMemory(await piSettings(join13(dataDir, "settings.json")));
|
|
7398
9420
|
configureHarnessHttp(settingsManager);
|
|
7399
9421
|
const summaries = () => modelAuth.models.value;
|
|
7400
9422
|
const images = await createNodePhotonImages();
|
|
@@ -7419,7 +9441,11 @@ async function main(argv) {
|
|
|
7419
9441
|
};
|
|
7420
9442
|
}
|
|
7421
9443
|
const options = values.faux !== undefined ? fauxOptions(() => () => fauxAssistantMessage(values.faux)) : await piOptions();
|
|
7422
|
-
|
|
9444
|
+
let stop = () => {};
|
|
9445
|
+
const host = await openHost({
|
|
9446
|
+
...options,
|
|
9447
|
+
...packagedVersion === undefined ? {} : { upgrade: { current: packagedVersion, restart: () => stop(UPGRADE_EXIT_CODE) } }
|
|
9448
|
+
});
|
|
7423
9449
|
const stopLogRotation = startHostLogRotation(dataDir);
|
|
7424
9450
|
const { defaultModel: effectiveModel } = host.settings.value;
|
|
7425
9451
|
const model = effectiveModel === null ? null : `${effectiveModel.provider}/${effectiveModel.modelId}`;
|
|
@@ -7427,7 +9453,7 @@ async function main(argv) {
|
|
|
7427
9453
|
console.log(JSON.stringify({
|
|
7428
9454
|
event: "ready",
|
|
7429
9455
|
url: host.url,
|
|
7430
|
-
tokenFile:
|
|
9456
|
+
tokenFile: join13(dataDir, "token"),
|
|
7431
9457
|
dataDir,
|
|
7432
9458
|
model,
|
|
7433
9459
|
web,
|
|
@@ -7435,15 +9461,19 @@ async function main(argv) {
|
|
|
7435
9461
|
...host.relay === undefined ? {} : { relay: { origin: host.relay.origin, hostId: host.relay.hostId } }
|
|
7436
9462
|
}));
|
|
7437
9463
|
let stopping = false;
|
|
7438
|
-
|
|
9464
|
+
stop = (code) => {
|
|
7439
9465
|
if (stopping)
|
|
7440
9466
|
return;
|
|
7441
9467
|
stopping = true;
|
|
7442
9468
|
stopLogRotation();
|
|
7443
|
-
|
|
9469
|
+
if (code === UPGRADE_EXIT_CODE) {
|
|
9470
|
+
console.log(JSON.stringify({ event: "upgraded", exitCode: code }));
|
|
9471
|
+
setTimeout(() => process.exit(code), 15000).unref();
|
|
9472
|
+
}
|
|
9473
|
+
host.close().finally(() => process.exit(code));
|
|
7444
9474
|
};
|
|
7445
|
-
process.on("SIGINT", stop);
|
|
7446
|
-
process.on("SIGTERM", stop);
|
|
9475
|
+
process.on("SIGINT", () => stop(0));
|
|
9476
|
+
process.on("SIGTERM", () => stop(0));
|
|
7447
9477
|
host.closed.then((end) => {
|
|
7448
9478
|
if (end.kind !== "failed" || stopping)
|
|
7449
9479
|
return;
|
|
@@ -7473,10 +9503,10 @@ async function piSettings(path) {
|
|
|
7473
9503
|
return typeof block === "object" && block !== null && !Array.isArray(block) ? block : {};
|
|
7474
9504
|
}
|
|
7475
9505
|
async function installId(dataDir) {
|
|
7476
|
-
const path =
|
|
9506
|
+
const path = join13(dataDir, "install-id");
|
|
7477
9507
|
let id = (await readFile7(path, "utf8").catch(() => "")).trim();
|
|
7478
9508
|
if (id === "") {
|
|
7479
|
-
id =
|
|
9509
|
+
id = randomUUID6();
|
|
7480
9510
|
await mkdir6(dataDir, { recursive: true });
|
|
7481
9511
|
await writeFile5(path, `${id}
|
|
7482
9512
|
`, { mode: 384 });
|