pinomad 0.12.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-47ff1d1t.js → chunk-1mjbt2z2.js} +191 -11
- package/dist/{chunk-7wj0jh3t.js → chunk-1rdbnvy6.js} +1 -1
- package/dist/{chunk-ksfxqd8s.js → chunk-35szq73k.js} +2682 -459
- package/dist/{chunk-mnmqyq7d.js → chunk-5j7ybhjj.js} +1984 -333
- package/dist/{chunk-agq4tn9r.js → chunk-5pv0g1nj.js} +12 -10
- package/dist/{chunk-7mn38n4f.js → chunk-63ymn5c9.js} +2 -2
- package/dist/{chunk-nppgxh6c.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-gg2dcqbg.js → chunk-b0nwq1dt.js} +29 -12
- package/dist/chunk-dsr19q5h.js +23 -0
- package/dist/{chunk-41q0saz0.js → chunk-e53c0s8z.js} +9 -7
- package/dist/{chunk-np4jd070.js → chunk-e59k3bmz.js} +307 -56
- package/dist/chunk-eerpgvy9.js +315 -0
- package/dist/chunk-eewxy4cp.js +240 -0
- package/dist/{chunk-vqkb70d7.js → chunk-evbv1fkn.js} +3 -3
- package/dist/{chunk-vx7dy07y.js → chunk-ex5yr09h.js} +11 -6
- package/dist/{chunk-t91zt5f0.js → chunk-hg9gs396.js} +1 -1
- package/dist/{chunk-aqj9wfmb.js → chunk-mtkvft88.js} +1 -1
- package/dist/{chunk-t5abqsva.js → chunk-mxwcnnkg.js} +15 -305
- package/dist/{chunk-5qf1grkk.js → chunk-w927q9m1.js} +52 -16
- package/dist/pinomad.js +12 -12
- package/package.json +16 -11
- package/web/assets/{core-DnpzcQzs.js → core-BWHGsFpV.js} +1 -1
- package/web/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
- package/web/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
- package/web/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
- package/web/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
- package/web/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
- package/web/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
- package/web/assets/{highlighted-diff-Dn5ifsvK.js → highlighted-diff-DJ7KZCpS.js} +2 -2
- package/web/assets/{highlights-DnHv6NBM.js → highlights-CyEmo6OI.js} +1 -1
- package/web/assets/{highlights-themes-CUdrlZk3.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-BuPP5HCg.js → shiki-BzCmRG-v.js} +2 -2
- package/web/assets/{shiki-langs-FJk0v0Ga.js → shiki-langs-DOkjyV4d.js} +1 -1
- package/web/assets/{stream-tokenizer-Bl6bisIa.js → stream-tokenizer-CBPqFe4Z.js} +1 -1
- package/web/assets/{theming-themes-BL_CC-Mm.js → theming-themes-wKVca0aB.js} +1 -1
- package/web/assets/{xterm-view-ZsuzJb2C.js → xterm-view-CKa1-QCM.js} +12 -12
- package/web/index.html +3 -2
- package/web/theme-boot.js +11 -0
- package/dist/chunk-42nqtx5r.js +0 -22
- package/dist/chunk-jeqsdcj1.js +0 -96
- package/web/assets/index-BV0ecolo.css +0 -1
- package/web/assets/index-DNsTTgPE.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,18 +30,34 @@ 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,
|
|
39
51
|
MAX_PENDING_ATTACHMENTS,
|
|
40
52
|
MAX_PENDING_ATTACHMENT_BYTES,
|
|
53
|
+
MAX_TRANSCRIPT_PAGE,
|
|
41
54
|
PROTOCOL,
|
|
55
|
+
QuestionItemSchema,
|
|
56
|
+
QuestionSchema,
|
|
42
57
|
RTC_DATA_CHANNEL_LABEL,
|
|
43
58
|
RTC_DEFAULT_STUN,
|
|
44
59
|
RTC_MAX_RECORD_BYTES,
|
|
60
|
+
TodoItemSchema,
|
|
45
61
|
UNAUTHORIZED_CLOSE_CODE,
|
|
46
62
|
attachmentBeginProblem,
|
|
47
63
|
attachmentBlock,
|
|
@@ -49,6 +65,7 @@ import {
|
|
|
49
65
|
base64DecodedLength,
|
|
50
66
|
isAttachmentId,
|
|
51
67
|
isCustomProviderApi,
|
|
68
|
+
isProcessActive,
|
|
52
69
|
organize,
|
|
53
70
|
parseAttachmentBlock,
|
|
54
71
|
parseClientFrame,
|
|
@@ -56,7 +73,7 @@ import {
|
|
|
56
73
|
rtcCandidateType,
|
|
57
74
|
sdpCandidates,
|
|
58
75
|
withRtcCandidates
|
|
59
|
-
} from "./chunk-
|
|
76
|
+
} from "./chunk-1mjbt2z2.js";
|
|
60
77
|
import {
|
|
61
78
|
ENDPOINT_SET_SCHEMA_VERSION,
|
|
62
79
|
MAX_HOST_ENDPOINT_URL_LENGTH,
|
|
@@ -72,29 +89,348 @@ import {
|
|
|
72
89
|
parseEndpointSet,
|
|
73
90
|
respondIK,
|
|
74
91
|
toBase64Url
|
|
75
|
-
} from "./chunk-
|
|
92
|
+
} from "./chunk-6vbp1vva.js";
|
|
76
93
|
import {
|
|
77
94
|
DEFAULT_DATA_DIR
|
|
78
95
|
} from "./chunk-ngf1zatm.js";
|
|
79
96
|
import {
|
|
80
97
|
packagedVersion,
|
|
81
98
|
webRoot
|
|
82
|
-
} from "./chunk-
|
|
99
|
+
} from "./chunk-1rdbnvy6.js";
|
|
83
100
|
import {
|
|
84
101
|
__require
|
|
85
102
|
} from "./chunk-df32avw6.js";
|
|
86
103
|
|
|
87
104
|
// apps/host/src/main.ts
|
|
88
|
-
import { randomUUID as
|
|
105
|
+
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
89
106
|
import { mkdir as mkdir6, readFile as readFile7, writeFile as writeFile5 } from "node:fs/promises";
|
|
90
107
|
import { homedir as homedir3 } from "node:os";
|
|
91
|
-
import { join as
|
|
108
|
+
import { join as join13, resolve as resolve7 } from "node:path";
|
|
92
109
|
import { parseArgs } from "node:util";
|
|
93
110
|
import { createModels } from "@earendil-works/pi-ai/models";
|
|
94
111
|
import { fauxAssistantMessage, fauxProvider } from "@earendil-works/pi-ai/providers/faux";
|
|
95
112
|
import { ModelRuntime, SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
96
113
|
import { createNodePhotonImages } from "@earendil-works/pi-durable/images/node";
|
|
97
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
|
+
|
|
98
434
|
// apps/host/src/extensions/question.ts
|
|
99
435
|
import {
|
|
100
436
|
defineDoc,
|
|
@@ -102,55 +438,7 @@ import {
|
|
|
102
438
|
defineTask,
|
|
103
439
|
defineTool
|
|
104
440
|
} from "@earendil-works/pi-durable";
|
|
105
|
-
import { Type as Type2 } from "typebox";
|
|
106
|
-
|
|
107
|
-
// packages/protocol/src/presentation.ts
|
|
108
441
|
import { Type } from "typebox";
|
|
109
|
-
import { Value } from "typebox/value";
|
|
110
|
-
var TodoItemSchema = Type.Object({
|
|
111
|
-
text: Type.String({ minLength: 1 }),
|
|
112
|
-
status: Type.Union([Type.Literal("pending"), Type.Literal("in_progress"), Type.Literal("completed")])
|
|
113
|
-
});
|
|
114
|
-
var TodoSchema = Type.Object({ items: Type.Array(TodoItemSchema) });
|
|
115
|
-
var QuestionOptionSchema = Type.Object({
|
|
116
|
-
label: Type.String({ minLength: 1 }),
|
|
117
|
-
description: Type.Optional(Type.String())
|
|
118
|
-
});
|
|
119
|
-
var QuestionItemSchema = Type.Object({
|
|
120
|
-
question: Type.String({ minLength: 1 }),
|
|
121
|
-
header: Type.String({ minLength: 1, maxLength: 16 }),
|
|
122
|
-
options: Type.Array(QuestionOptionSchema, { minItems: 2, maxItems: 4 }),
|
|
123
|
-
multiSelect: Type.Optional(Type.Boolean())
|
|
124
|
-
});
|
|
125
|
-
var QuestionAnswerSchema = Type.Object({
|
|
126
|
-
selected: Type.Array(Type.String()),
|
|
127
|
-
other: Type.Optional(Type.String())
|
|
128
|
-
});
|
|
129
|
-
var QuestionResolutionSchema = Type.Union([
|
|
130
|
-
Type.Object({ outcome: Type.Literal("answered"), answers: Type.Array(QuestionAnswerSchema), at: Type.Number() }),
|
|
131
|
-
Type.Object({ outcome: Type.Literal("dismissed"), at: Type.Number() }),
|
|
132
|
-
Type.Object({ outcome: Type.Literal("cancelled"), at: Type.Number() })
|
|
133
|
-
]);
|
|
134
|
-
var QuestionRequestSchema = Type.Object({
|
|
135
|
-
id: Type.String({ minLength: 1 }),
|
|
136
|
-
questions: Type.Array(QuestionItemSchema, { minItems: 1, maxItems: 4 }),
|
|
137
|
-
askedAt: Type.Number(),
|
|
138
|
-
resolution: Type.Optional(QuestionResolutionSchema)
|
|
139
|
-
});
|
|
140
|
-
var QuestionSchema = Type.Object({ requests: Type.Array(QuestionRequestSchema) });
|
|
141
|
-
var DiffSchema = Type.Object({ patch: Type.String() });
|
|
142
|
-
var CodemodeCallSchema = Type.Object({
|
|
143
|
-
id: Type.Optional(Type.Number()),
|
|
144
|
-
name: Type.String(),
|
|
145
|
-
args: Type.String(),
|
|
146
|
-
status: Type.Union([Type.Literal("running"), Type.Literal("ok"), Type.Literal("error"), Type.Literal("cancelled")]),
|
|
147
|
-
durationMs: Type.Optional(Type.Number()),
|
|
148
|
-
error: Type.Optional(Type.String()),
|
|
149
|
-
details: Type.Optional(Type.Unknown())
|
|
150
|
-
});
|
|
151
|
-
var CodemodeSchema = Type.Object({ code: Type.Optional(Type.String()), calls: Type.Array(CodemodeCallSchema) });
|
|
152
|
-
|
|
153
|
-
// apps/host/src/extensions/question.ts
|
|
154
442
|
var QuestionDoc = defineDoc({
|
|
155
443
|
kind: "question.requests",
|
|
156
444
|
version: 1,
|
|
@@ -178,8 +466,10 @@ var QuestionWait = defineTask({
|
|
|
178
466
|
resolve(initial);
|
|
179
467
|
return;
|
|
180
468
|
}
|
|
181
|
-
watch.start(async (
|
|
182
|
-
|
|
469
|
+
watch.start(async (call) => {
|
|
470
|
+
if (call.kind !== "value")
|
|
471
|
+
return;
|
|
472
|
+
const found = resolutionOf(call.value, task.input.requestId);
|
|
183
473
|
if (found !== undefined)
|
|
184
474
|
resolve(found);
|
|
185
475
|
});
|
|
@@ -224,8 +514,8 @@ var askUserQuestion = defineTool({
|
|
|
224
514
|
name: "ask_user_question",
|
|
225
515
|
callers: ["model"],
|
|
226
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.",
|
|
227
|
-
parameters:
|
|
228
|
-
questions:
|
|
517
|
+
parameters: Type.Object({
|
|
518
|
+
questions: Type.Array(QuestionItemSchema, { minItems: 1, maxItems: 4 })
|
|
229
519
|
}),
|
|
230
520
|
replay: "safe",
|
|
231
521
|
execute: async (args, api, context) => {
|
|
@@ -294,31 +584,31 @@ function createCodemode() {
|
|
|
294
584
|
}
|
|
295
585
|
|
|
296
586
|
// apps/host/src/extensions/context.ts
|
|
297
|
-
import { dirname as dirname2, sep as
|
|
587
|
+
import { dirname as dirname2, sep as sep5 } from "node:path";
|
|
298
588
|
import { defineExtension as defineExtension2, section } from "@earendil-works/pi-durable";
|
|
299
589
|
import { formatSkillsForPrompt, loadProjectContextFiles } from "@earendil-works/pi-coding-agent";
|
|
300
590
|
|
|
301
591
|
// apps/host/src/skills.ts
|
|
302
|
-
import { execFile as
|
|
592
|
+
import { execFile as execFile3 } from "node:child_process";
|
|
303
593
|
import { existsSync } from "node:fs";
|
|
304
594
|
import { realpath as realpath4, stat as stat4 } from "node:fs/promises";
|
|
305
|
-
import { dirname, join as join4, relative as relative2, resolve as resolve3, sep as
|
|
306
|
-
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";
|
|
307
597
|
import { AgentDoc as AgentDoc3 } from "@earendil-works/pi-durable";
|
|
308
598
|
import { loadSkills } from "@earendil-works/pi-coding-agent";
|
|
309
599
|
|
|
310
600
|
// apps/host/src/checkout.ts
|
|
311
|
-
import { execFile, spawn } from "node:child_process";
|
|
601
|
+
import { execFile as execFile2, spawn as spawn2 } from "node:child_process";
|
|
312
602
|
import { randomBytes } from "node:crypto";
|
|
313
603
|
import { realpath, rm, stat } from "node:fs/promises";
|
|
314
604
|
import { tmpdir } from "node:os";
|
|
315
605
|
import { join, relative, sep } from "node:path";
|
|
316
|
-
import { promisify } from "node:util";
|
|
317
|
-
var run =
|
|
606
|
+
import { promisify as promisify2 } from "node:util";
|
|
607
|
+
var run = promisify2(execFile2);
|
|
318
608
|
var git = (dir, args, env) => run("git", ["-C", dir, ...args], env === undefined ? {} : { env: { ...process.env, ...env } });
|
|
319
609
|
function gitCapped(dir, args, maxBytes, env) {
|
|
320
610
|
return new Promise((resolve, reject) => {
|
|
321
|
-
const child =
|
|
611
|
+
const child = spawn2("git", ["-C", dir, ...args], env === undefined ? {} : { env: { ...process.env, ...env } });
|
|
322
612
|
const chunks = [];
|
|
323
613
|
let size = 0;
|
|
324
614
|
let killed = false;
|
|
@@ -443,14 +733,14 @@ async function changesOf(dir, base, maxBytes = 1024 * 1024) {
|
|
|
443
733
|
}
|
|
444
734
|
|
|
445
735
|
// apps/host/src/files.ts
|
|
446
|
-
import { spawn as
|
|
736
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
447
737
|
import { opendir, realpath as realpath3, stat as stat3 } from "node:fs/promises";
|
|
448
|
-
import { join as join3, resolve as resolve2, sep as
|
|
738
|
+
import { join as join3, resolve as resolve2, sep as sep3 } from "node:path";
|
|
449
739
|
import { AgentDoc as AgentDoc2 } from "@earendil-works/pi-durable";
|
|
450
740
|
|
|
451
741
|
// apps/host/src/organization.ts
|
|
452
742
|
import { mkdir, realpath as realpath2, stat as stat2 } from "node:fs/promises";
|
|
453
|
-
import { basename, join as join2, resolve } from "node:path";
|
|
743
|
+
import { basename, join as join2, resolve, sep as sep2 } from "node:path";
|
|
454
744
|
import { clampThinkingLevel } from "@earendil-works/pi-ai";
|
|
455
745
|
import {
|
|
456
746
|
AgentDoc,
|
|
@@ -756,6 +1046,17 @@ async function familyRoot(harness, id, context) {
|
|
|
756
1046
|
}
|
|
757
1047
|
return current;
|
|
758
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
|
+
}
|
|
759
1060
|
async function cleanupArchivedWorktrees(harness, rootId, context) {
|
|
760
1061
|
const index = await harness.snapshot(IndexDoc, context);
|
|
761
1062
|
const checkouts = index?.checkouts ?? [];
|
|
@@ -827,7 +1128,7 @@ function createCandidateLister(enumerate, options = {}) {
|
|
|
827
1128
|
}
|
|
828
1129
|
function gitLines(cwd, args, limits) {
|
|
829
1130
|
return new Promise((resolvePromise, rejectPromise) => {
|
|
830
|
-
const child =
|
|
1131
|
+
const child = spawn3("git", ["-C", cwd, ...args], { stdio: ["ignore", "pipe", "pipe"] });
|
|
831
1132
|
const chunks = [];
|
|
832
1133
|
let bytes = 0;
|
|
833
1134
|
let truncated = false;
|
|
@@ -875,7 +1176,7 @@ function gitLines(cwd, args, limits) {
|
|
|
875
1176
|
}
|
|
876
1177
|
async function inWorkTree(root) {
|
|
877
1178
|
return await new Promise((resolvePromise) => {
|
|
878
|
-
const child =
|
|
1179
|
+
const child = spawn3("git", ["-C", root, "rev-parse", "--is-inside-work-tree"], { stdio: ["ignore", "pipe", "ignore"] });
|
|
879
1180
|
let out = "";
|
|
880
1181
|
child.stdout.on("data", (chunk) => out += chunk.toString("utf8"));
|
|
881
1182
|
child.on("error", () => resolvePromise(false));
|
|
@@ -884,7 +1185,7 @@ async function inWorkTree(root) {
|
|
|
884
1185
|
}
|
|
885
1186
|
async function hasHead(root) {
|
|
886
1187
|
return await new Promise((resolvePromise) => {
|
|
887
|
-
const child =
|
|
1188
|
+
const child = spawn3("git", ["-C", root, "rev-parse", "--verify", "-q", "HEAD"], { stdio: ["ignore", "ignore", "ignore"] });
|
|
888
1189
|
child.on("error", () => resolvePromise(false));
|
|
889
1190
|
child.on("close", (code) => resolvePromise(code === 0));
|
|
890
1191
|
});
|
|
@@ -1005,7 +1306,7 @@ async function listFiles(harness, dataDir, enumerate, scope, query, context) {
|
|
|
1005
1306
|
const cwd = agent.cwd;
|
|
1006
1307
|
if (cwd === undefined)
|
|
1007
1308
|
throw new Error(`Conversation ${scope.conversationId} has no checkout`);
|
|
1008
|
-
if (cwd.startsWith(join3(dataDir, "chats") +
|
|
1309
|
+
if (cwd.startsWith(join3(dataDir, "chats") + sep3))
|
|
1009
1310
|
return { files: [], truncated: false };
|
|
1010
1311
|
const record = checkoutAt((await harness.snapshot(IndexDoc, context))?.checkouts, cwd);
|
|
1011
1312
|
const ready = record === undefined || await worktreeSettled(record);
|
|
@@ -1037,7 +1338,7 @@ async function listFiles(harness, dataDir, enumerate, scope, query, context) {
|
|
|
1037
1338
|
}
|
|
1038
1339
|
|
|
1039
1340
|
// apps/host/src/skills.ts
|
|
1040
|
-
var
|
|
1341
|
+
var exec2 = promisify3(execFile3);
|
|
1041
1342
|
function invocableSkillName(name) {
|
|
1042
1343
|
return name.length <= 64 && /^[a-z0-9-]+$/.test(name) && !name.startsWith("-") && !name.endsWith("-") && !name.includes("--");
|
|
1043
1344
|
}
|
|
@@ -1101,10 +1402,10 @@ async function committedSkillFiles(gitRoot, layers) {
|
|
|
1101
1402
|
if (!await inWorkTree(gitRoot) || !await hasHead(gitRoot))
|
|
1102
1403
|
return null;
|
|
1103
1404
|
const pathspecs = layers.map((dir) => {
|
|
1104
|
-
const rel = relative2(gitRoot, dir).split(
|
|
1405
|
+
const rel = relative2(gitRoot, dir).split(sep4).join("/");
|
|
1105
1406
|
return rel === "" ? ".agents/skills" : `${rel}/.agents/skills`;
|
|
1106
1407
|
});
|
|
1107
|
-
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 });
|
|
1108
1409
|
return new Set(stdout.split("\x00").filter((path) => path !== ""));
|
|
1109
1410
|
}
|
|
1110
1411
|
async function enumerateSkills(root, mode, agentsHome) {
|
|
@@ -1116,7 +1417,7 @@ async function enumerateSkills(root, mode, agentsHome) {
|
|
|
1116
1417
|
if (mode === "head" && gitRoot !== undefined) {
|
|
1117
1418
|
const committed = await committedSkillFiles(gitRoot, layers.dirs);
|
|
1118
1419
|
if (committed !== null) {
|
|
1119
|
-
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("/")));
|
|
1120
1421
|
}
|
|
1121
1422
|
}
|
|
1122
1423
|
}
|
|
@@ -1250,7 +1551,7 @@ function createContext(options) {
|
|
|
1250
1551
|
const seen = new Set;
|
|
1251
1552
|
files = [
|
|
1252
1553
|
...loadProjectContextFiles({ cwd: dirname2(checkout.repo), agentDir: agentsHome }),
|
|
1253
|
-
...loadProjectContextFiles({ cwd, agentDir: agentsHome }).filter((file) => file.path.startsWith(checkout.worktreeRoot +
|
|
1554
|
+
...loadProjectContextFiles({ cwd, agentDir: agentsHome }).filter((file) => file.path.startsWith(checkout.worktreeRoot + sep5))
|
|
1254
1555
|
].filter((file) => {
|
|
1255
1556
|
if (seen.has(file.path))
|
|
1256
1557
|
return false;
|
|
@@ -1277,21 +1578,336 @@ ${file.content}`).join(`
|
|
|
1277
1578
|
};
|
|
1278
1579
|
}
|
|
1279
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
|
+
|
|
1280
1896
|
// apps/host/src/extensions/subagent.ts
|
|
1897
|
+
import { join as join6 } from "node:path";
|
|
1281
1898
|
import { clampThinkingLevel as clampThinkingLevel2 } from "@earendil-works/pi-ai";
|
|
1282
1899
|
import {
|
|
1283
1900
|
AgentDoc as AgentDoc4,
|
|
1284
|
-
AssistantEntry,
|
|
1285
1901
|
configure as configure2,
|
|
1286
|
-
defineExtension as
|
|
1287
|
-
defineTool as
|
|
1902
|
+
defineExtension as defineExtension5,
|
|
1903
|
+
defineTool as defineTool3,
|
|
1288
1904
|
section as section2
|
|
1289
1905
|
} from "@earendil-works/pi-durable";
|
|
1290
1906
|
import { Type as Type3 } from "typebox";
|
|
1291
1907
|
|
|
1292
1908
|
// apps/host/src/subagent-doc.ts
|
|
1293
|
-
import { defineDoc as
|
|
1294
|
-
var SubagentDoc =
|
|
1909
|
+
import { defineDoc as defineDoc6 } from "@earendil-works/pi-durable";
|
|
1910
|
+
var SubagentDoc = defineDoc6({
|
|
1295
1911
|
kind: "pinomad.subagent",
|
|
1296
1912
|
version: 1,
|
|
1297
1913
|
scope: "conversation",
|
|
@@ -1301,8 +1917,273 @@ var SubagentDoc = defineDoc4({
|
|
|
1301
1917
|
checkpointWhen: () => false
|
|
1302
1918
|
});
|
|
1303
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
|
+
|
|
1304
2181
|
// apps/host/src/extensions/subagent.ts
|
|
1305
|
-
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
|
+
}
|
|
1306
2187
|
var THINKING_LEVELS2 = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
1307
2188
|
function createSubagent(options) {
|
|
1308
2189
|
const available = () => options.modelSummaries().map((model) => `${model.provider}/${model.modelId}`).join(", ");
|
|
@@ -1314,15 +2195,22 @@ function createSubagent(options) {
|
|
|
1314
2195
|
const defined = roles();
|
|
1315
2196
|
throw new Error(defined.length === 0 ? "No named agents are defined" : `Unknown agent "${name}". Available agents: ${defined.map((each) => each.name).join(", ")}`);
|
|
1316
2197
|
};
|
|
1317
|
-
const tool =
|
|
2198
|
+
const tool = defineTool3({
|
|
1318
2199
|
name: "subagent",
|
|
1319
2200
|
callers: ["model"],
|
|
1320
|
-
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()}.`,
|
|
1321
2202
|
parameters: Type3.Object({
|
|
1322
2203
|
task: Type3.String({
|
|
1323
2204
|
minLength: 1,
|
|
1324
2205
|
description: "Complete, self-contained instructions. The subagent sees none of this conversation."
|
|
1325
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." })),
|
|
1326
2214
|
role: Type3.Optional(Type3.String({
|
|
1327
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."
|
|
1328
2216
|
})),
|
|
@@ -1332,16 +2220,39 @@ function createSubagent(options) {
|
|
|
1332
2220
|
}),
|
|
1333
2221
|
replay: "safe",
|
|
1334
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
|
+
}
|
|
1335
2230
|
const child = await api.commit(async (tx) => {
|
|
1336
|
-
const
|
|
1337
|
-
|
|
1338
|
-
|
|
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);
|
|
1339
2237
|
return {
|
|
1340
|
-
id:
|
|
2238
|
+
id: existingId,
|
|
2239
|
+
name: recorded?.[0] ?? doc2?.name,
|
|
2240
|
+
...recorded?.[1].branch === undefined ? {} : { branch: recorded[1].branch, base: recorded[1].base },
|
|
1341
2241
|
...doc2?.model === undefined ? {} : { model: doc2.model },
|
|
1342
2242
|
...doc2?.startedAt === undefined ? {} : { startedAt: doc2.startedAt }
|
|
1343
2243
|
};
|
|
1344
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
|
+
}
|
|
1345
2256
|
const role = args.role === undefined ? undefined : roleNamed(args.role);
|
|
1346
2257
|
if (role?.model !== undefined && args.model !== undefined) {
|
|
1347
2258
|
throw new Error(`Agent "${role.name}" pins model ${role.model}; omit \`model\``);
|
|
@@ -1374,21 +2285,38 @@ function createSubagent(options) {
|
|
|
1374
2285
|
thinkingLevel = clampThinkingLevel2(override, parent.thinkingLevel);
|
|
1375
2286
|
}
|
|
1376
2287
|
}
|
|
2288
|
+
await assertCapacity(tx, root);
|
|
1377
2289
|
const change = {
|
|
1378
2290
|
extensions: { remove: [extension, ...options.exclude] },
|
|
1379
|
-
instructions: role === undefined || role.prompt === "" ?
|
|
2291
|
+
instructions: role === undefined || role.prompt === "" ? instructions(name2) : `${instructions(name2)}
|
|
1380
2292
|
|
|
1381
2293
|
${role.prompt}`,
|
|
1382
2294
|
...model === undefined ? {} : { model },
|
|
1383
2295
|
...thinkingLevel === undefined ? {} : { thinkingLevel }
|
|
1384
2296
|
};
|
|
1385
|
-
const
|
|
1386
|
-
|
|
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 });
|
|
1387
2313
|
const childAgent = await tx.doc(AgentDoc4, created.id);
|
|
1388
2314
|
const ref = childAgent.model ?? model ?? (await tx.doc(AgentDoc4, api.conversationId)).model;
|
|
1389
2315
|
const startedAt = Date.now();
|
|
1390
2316
|
const doc = await tx.doc(SubagentDoc, created.id);
|
|
1391
2317
|
doc.startedAt = startedAt;
|
|
2318
|
+
doc.name = name2;
|
|
2319
|
+
doc.lifecycle = background ? "background" : "foreground";
|
|
1392
2320
|
if (ref !== undefined)
|
|
1393
2321
|
doc.model = `${ref.provider}/${ref.modelId}`;
|
|
1394
2322
|
if (role !== undefined)
|
|
@@ -1396,33 +2324,183 @@ ${role.prompt}`,
|
|
|
1396
2324
|
const label = args.description?.trim();
|
|
1397
2325
|
if (label !== undefined && label !== "")
|
|
1398
2326
|
doc.label = label;
|
|
1399
|
-
|
|
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
|
+
};
|
|
1400
2349
|
}, context);
|
|
1401
|
-
|
|
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);
|
|
1402
2371
|
const handle = await api.conversation(child.id, context);
|
|
1403
2372
|
if (handle === undefined)
|
|
1404
2373
|
throw new Error(`Subagent conversation ${child.id} does not exist`);
|
|
1405
2374
|
const request = { type: "input", content: args.task, requestId: `subagent:${api.taskId}` };
|
|
1406
2375
|
const settled = await (await handle.submit(request, context)).wait(context);
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
...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";
|
|
1418
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
|
|
2396
|
+
};
|
|
2397
|
+
}
|
|
2398
|
+
});
|
|
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 }
|
|
1419
2418
|
};
|
|
1420
|
-
return result2;
|
|
1421
2419
|
}
|
|
1422
2420
|
});
|
|
1423
|
-
const
|
|
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({
|
|
1424
2501
|
name: "subagent",
|
|
1425
|
-
tools: [tool],
|
|
2502
|
+
tools: [tool, send, stop, read, list],
|
|
2503
|
+
tasks: [SubagentAnchor, SubagentReporter],
|
|
1426
2504
|
sections: [
|
|
1427
2505
|
section2("agent_roles", () => {
|
|
1428
2506
|
const defined = roles();
|
|
@@ -1440,9 +2518,9 @@ ${role.prompt}`,
|
|
|
1440
2518
|
}
|
|
1441
2519
|
|
|
1442
2520
|
// apps/host/src/extensions/todo.ts
|
|
1443
|
-
import { defineDoc as
|
|
2521
|
+
import { defineDoc as defineDoc8, defineExtension as defineExtension6, defineTool as defineTool4 } from "@earendil-works/pi-durable";
|
|
1444
2522
|
import { Type as Type4 } from "typebox";
|
|
1445
|
-
var TodoDoc =
|
|
2523
|
+
var TodoDoc = defineDoc8({
|
|
1446
2524
|
kind: "todo.list",
|
|
1447
2525
|
version: 1,
|
|
1448
2526
|
scope: "conversation",
|
|
@@ -1451,7 +2529,7 @@ var TodoDoc = defineDoc5({
|
|
|
1451
2529
|
initial: () => ({ items: [] }),
|
|
1452
2530
|
checkpointWhen: () => false
|
|
1453
2531
|
});
|
|
1454
|
-
var todoWrite =
|
|
2532
|
+
var todoWrite = defineTool4({
|
|
1455
2533
|
name: "todo_write",
|
|
1456
2534
|
description: "Replace the todo list shown to the user with the given items.",
|
|
1457
2535
|
parameters: Type4.Object({ items: Type4.Array(TodoItemSchema) }),
|
|
@@ -1465,7 +2543,7 @@ var todoWrite = defineTool3({
|
|
|
1465
2543
|
}
|
|
1466
2544
|
});
|
|
1467
2545
|
var todo = {
|
|
1468
|
-
extension:
|
|
2546
|
+
extension: defineExtension6({ name: "todo", tools: [todoWrite] }),
|
|
1469
2547
|
docs: [{ token: TodoDoc, presentation: "pinomad.todo" }]
|
|
1470
2548
|
};
|
|
1471
2549
|
|
|
@@ -1473,8 +2551,8 @@ var todo = {
|
|
|
1473
2551
|
import { stat as stat6 } from "node:fs/promises";
|
|
1474
2552
|
import { createServer } from "node:http";
|
|
1475
2553
|
import { homedir, networkInterfaces } from "node:os";
|
|
1476
|
-
import { join as
|
|
1477
|
-
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";
|
|
1478
2556
|
import { clampThinkingLevel as clampThinkingLevel3, getSupportedThinkingLevels } from "@earendil-works/pi-ai";
|
|
1479
2557
|
import {
|
|
1480
2558
|
AgentDoc as AgentDoc5,
|
|
@@ -1483,7 +2561,7 @@ import {
|
|
|
1483
2561
|
LiveDoc
|
|
1484
2562
|
} from "@earendil-works/pi-durable";
|
|
1485
2563
|
import { WebSocket as WebSocket2, WebSocketServer } from "ws";
|
|
1486
|
-
import { Value
|
|
2564
|
+
import { Value } from "typebox/value";
|
|
1487
2565
|
|
|
1488
2566
|
// apps/host/src/secure-carrier.ts
|
|
1489
2567
|
import { WebSocket } from "ws";
|
|
@@ -1519,7 +2597,7 @@ function webSocketCarrier(socket) {
|
|
|
1519
2597
|
}
|
|
1520
2598
|
|
|
1521
2599
|
// apps/host/src/rtc.ts
|
|
1522
|
-
import { randomUUID } from "node:crypto";
|
|
2600
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
1523
2601
|
var BUFFER_HIGH_WATER = 256 * 1024;
|
|
1524
2602
|
var BUFFER_LOW_WATER = 64 * 1024;
|
|
1525
2603
|
var PUMP_STALL_MS = 1e4;
|
|
@@ -1778,7 +2856,7 @@ function nodeRtcFactory(loaded) {
|
|
|
1778
2856
|
const max = parseRtcMaxMessageSize(input.offerSdp);
|
|
1779
2857
|
if (max.kind === "invalid")
|
|
1780
2858
|
throw new Error(max.reason);
|
|
1781
|
-
const pc = new ndc.PeerConnection(`pinomad-${
|
|
2859
|
+
const pc = new ndc.PeerConnection(`pinomad-${randomUUID2()}`, { iceServers: [...input.stun] });
|
|
1782
2860
|
const timers = input.timers;
|
|
1783
2861
|
const candidates = new Set;
|
|
1784
2862
|
let localSdp;
|
|
@@ -2037,17 +3115,17 @@ function dataChannelCarrier(channel, timers, log, onClosed) {
|
|
|
2037
3115
|
stallTimer = timers.setTimeout(stall, 1000);
|
|
2038
3116
|
};
|
|
2039
3117
|
channel.onBufferedAmountLow(() => pump());
|
|
2040
|
-
channel.onMessage((
|
|
2041
|
-
if (typeof
|
|
3118
|
+
channel.onMessage((message2) => {
|
|
3119
|
+
if (typeof message2 === "string")
|
|
2042
3120
|
handlers?.text();
|
|
2043
|
-
else if (
|
|
2044
|
-
handlers?.binary(new Uint8Array(
|
|
3121
|
+
else if (message2 instanceof ArrayBuffer)
|
|
3122
|
+
handlers?.binary(new Uint8Array(message2));
|
|
2045
3123
|
else
|
|
2046
|
-
handlers?.binary(
|
|
3124
|
+
handlers?.binary(message2);
|
|
2047
3125
|
});
|
|
2048
3126
|
channel.onClosed(finish);
|
|
2049
|
-
channel.onError((
|
|
2050
|
-
log("rtc-datachannel-error", { message });
|
|
3127
|
+
channel.onError((message2) => {
|
|
3128
|
+
log("rtc-datachannel-error", { message: message2 });
|
|
2051
3129
|
finish();
|
|
2052
3130
|
});
|
|
2053
3131
|
return {
|
|
@@ -2077,12 +3155,218 @@ function dataChannelCarrier(channel, timers, log, onClosed) {
|
|
|
2077
3155
|
}
|
|
2078
3156
|
|
|
2079
3157
|
// apps/host/src/terminals.ts
|
|
2080
|
-
import { randomUUID as
|
|
3158
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
2081
3159
|
import { userInfo } from "node:os";
|
|
2082
|
-
|
|
3160
|
+
|
|
3161
|
+
// apps/host/src/output-buffer.ts
|
|
2083
3162
|
var BUFFER_SIZE = 512 * 1024;
|
|
2084
3163
|
var FLUSH_MS = 8;
|
|
2085
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;
|
|
2086
3370
|
var MAX_TITLE = 1024;
|
|
2087
3371
|
var TITLE = /\x1b\][02];([^\x07\x1b]*)(?:\x07|\x1b\\)/g;
|
|
2088
3372
|
|
|
@@ -2122,7 +3406,7 @@ class Terminals {
|
|
|
2122
3406
|
const number = (this.#numbers.get(owner) ?? 0) + 1;
|
|
2123
3407
|
this.#numbers.set(owner, number);
|
|
2124
3408
|
const terminal = {
|
|
2125
|
-
id:
|
|
3409
|
+
id: randomUUID3(),
|
|
2126
3410
|
owner,
|
|
2127
3411
|
cwd,
|
|
2128
3412
|
number,
|
|
@@ -2133,12 +3417,16 @@ class Terminals {
|
|
|
2133
3417
|
title: undefined,
|
|
2134
3418
|
rename: undefined,
|
|
2135
3419
|
exit: undefined,
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
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: ""
|
|
2142
3430
|
};
|
|
2143
3431
|
this.#terminals.set(terminal.id, terminal);
|
|
2144
3432
|
try {
|
|
@@ -2168,16 +3456,12 @@ class Terminals {
|
|
|
2168
3456
|
restart(id, size) {
|
|
2169
3457
|
const terminal = this.#get(id);
|
|
2170
3458
|
this.#stop(terminal);
|
|
2171
|
-
terminal.
|
|
2172
|
-
terminal.buffer = "";
|
|
2173
|
-
terminal.pending = "";
|
|
3459
|
+
terminal.output.discard();
|
|
2174
3460
|
terminal.titleCarry = "";
|
|
2175
3461
|
terminal.title = undefined;
|
|
2176
3462
|
terminal.exit = undefined;
|
|
2177
3463
|
terminal.cols = size.cols;
|
|
2178
3464
|
terminal.rows = size.rows;
|
|
2179
|
-
for (const viewer of terminal.viewers)
|
|
2180
|
-
viewer.position = undefined;
|
|
2181
3465
|
try {
|
|
2182
3466
|
this.#start(terminal);
|
|
2183
3467
|
} catch (error) {
|
|
@@ -2206,15 +3490,7 @@ class Terminals {
|
|
|
2206
3490
|
this.closeOwners([...this.#terminals.values()].map((terminal) => terminal.owner));
|
|
2207
3491
|
}
|
|
2208
3492
|
attach(id, since, sink) {
|
|
2209
|
-
|
|
2210
|
-
const end = terminal.start + terminal.buffer.length;
|
|
2211
|
-
const position = since !== undefined && since >= terminal.start && since <= end ? since : undefined;
|
|
2212
|
-
const viewer = { sink, position, sending: false };
|
|
2213
|
-
terminal.viewers.add(viewer);
|
|
2214
|
-
this.#pump(terminal, viewer);
|
|
2215
|
-
return () => {
|
|
2216
|
-
terminal.viewers.delete(viewer);
|
|
2217
|
-
};
|
|
3493
|
+
return this.#get(id).output.attach(since, sink);
|
|
2218
3494
|
}
|
|
2219
3495
|
#get(id) {
|
|
2220
3496
|
const terminal = this.#terminals.get(id);
|
|
@@ -2236,25 +3512,19 @@ class Terminals {
|
|
|
2236
3512
|
pty.onData((data) => {
|
|
2237
3513
|
if (terminal.generation !== generation)
|
|
2238
3514
|
return;
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
this.#flush(terminal);
|
|
2242
|
-
else
|
|
2243
|
-
terminal.flushTimer ??= setTimeout(() => this.#flush(terminal), this.#options.flushMs ?? FLUSH_MS);
|
|
3515
|
+
this.#options.onOutput?.();
|
|
3516
|
+
terminal.output.push(data);
|
|
2244
3517
|
});
|
|
2245
3518
|
pty.onExit(({ exitCode, signal }) => {
|
|
2246
3519
|
if (terminal.generation !== generation || !this.#terminals.has(terminal.id))
|
|
2247
3520
|
return;
|
|
2248
|
-
|
|
3521
|
+
terminal.output.flush();
|
|
2249
3522
|
terminal.pty = undefined;
|
|
2250
3523
|
terminal.exit = signal === undefined || signal === 0 ? { code: exitCode } : { code: exitCode, signal };
|
|
2251
3524
|
this.#changed(terminal.owner);
|
|
2252
3525
|
});
|
|
2253
3526
|
}
|
|
2254
3527
|
#stop(terminal) {
|
|
2255
|
-
if (terminal.flushTimer !== undefined)
|
|
2256
|
-
clearTimeout(terminal.flushTimer);
|
|
2257
|
-
terminal.flushTimer = undefined;
|
|
2258
3528
|
terminal.generation++;
|
|
2259
3529
|
const pty = terminal.pty;
|
|
2260
3530
|
terminal.pty = undefined;
|
|
@@ -2264,10 +3534,8 @@ class Terminals {
|
|
|
2264
3534
|
}
|
|
2265
3535
|
#remove(terminal) {
|
|
2266
3536
|
this.#stop(terminal);
|
|
3537
|
+
terminal.output.close("Terminal closed");
|
|
2267
3538
|
this.#terminals.delete(terminal.id);
|
|
2268
|
-
for (const viewer of terminal.viewers)
|
|
2269
|
-
viewer.sink.ended("Terminal closed");
|
|
2270
|
-
terminal.viewers.clear();
|
|
2271
3539
|
}
|
|
2272
3540
|
#resize(terminal, size) {
|
|
2273
3541
|
if (terminal.cols === size.cols && terminal.rows === size.rows)
|
|
@@ -2279,30 +3547,6 @@ class Terminals {
|
|
|
2279
3547
|
} catch {}
|
|
2280
3548
|
this.#changed(terminal.owner);
|
|
2281
3549
|
}
|
|
2282
|
-
#flush(terminal) {
|
|
2283
|
-
if (terminal.flushTimer !== undefined)
|
|
2284
|
-
clearTimeout(terminal.flushTimer);
|
|
2285
|
-
terminal.flushTimer = undefined;
|
|
2286
|
-
const data = terminal.pending;
|
|
2287
|
-
if (data === "")
|
|
2288
|
-
return;
|
|
2289
|
-
terminal.pending = "";
|
|
2290
|
-
terminal.buffer += data;
|
|
2291
|
-
const bufferSize = this.#options.bufferSize ?? BUFFER_SIZE;
|
|
2292
|
-
if (terminal.buffer.length > bufferSize) {
|
|
2293
|
-
let cut = terminal.buffer.length - bufferSize;
|
|
2294
|
-
const line = terminal.buffer.indexOf(`
|
|
2295
|
-
`, cut);
|
|
2296
|
-
if (line !== -1 && line - cut < 1024)
|
|
2297
|
-
cut = line + 1;
|
|
2298
|
-
terminal.buffer = terminal.buffer.slice(cut);
|
|
2299
|
-
terminal.start += cut;
|
|
2300
|
-
}
|
|
2301
|
-
if (this.#readTitle(terminal, data))
|
|
2302
|
-
this.#changed(terminal.owner);
|
|
2303
|
-
for (const viewer of terminal.viewers)
|
|
2304
|
-
this.#pump(terminal, viewer);
|
|
2305
|
-
}
|
|
2306
3550
|
#readTitle(terminal, data) {
|
|
2307
3551
|
const text = terminal.titleCarry + data;
|
|
2308
3552
|
let title;
|
|
@@ -2318,29 +3562,6 @@ class Terminals {
|
|
|
2318
3562
|
terminal.title = title;
|
|
2319
3563
|
return terminal.rename === undefined;
|
|
2320
3564
|
}
|
|
2321
|
-
async#pump(terminal, viewer) {
|
|
2322
|
-
if (viewer.sending)
|
|
2323
|
-
return;
|
|
2324
|
-
viewer.sending = true;
|
|
2325
|
-
try {
|
|
2326
|
-
const maxChunk = this.#options.maxChunk ?? MAX_CHUNK;
|
|
2327
|
-
while (terminal.viewers.has(viewer)) {
|
|
2328
|
-
const end = terminal.start + terminal.buffer.length;
|
|
2329
|
-
const reset = viewer.position === undefined || viewer.position < terminal.start;
|
|
2330
|
-
const from = reset ? terminal.start : viewer.position;
|
|
2331
|
-
if (!reset && from >= end)
|
|
2332
|
-
return;
|
|
2333
|
-
let to = reset ? end : Math.min(end, from + maxChunk);
|
|
2334
|
-
if (to < end && isHighSurrogate(terminal.buffer.charCodeAt(to - terminal.start - 1)))
|
|
2335
|
-
to--;
|
|
2336
|
-
const data = terminal.buffer.slice(from - terminal.start, to - terminal.start);
|
|
2337
|
-
viewer.position = to;
|
|
2338
|
-
await viewer.sink.output(reset ? { seq: to, data, reset: true } : { seq: to, data });
|
|
2339
|
-
}
|
|
2340
|
-
} finally {
|
|
2341
|
-
viewer.sending = false;
|
|
2342
|
-
}
|
|
2343
|
-
}
|
|
2344
3565
|
#changed(owner) {
|
|
2345
3566
|
const watchers = this.#watchers.get(owner);
|
|
2346
3567
|
if (watchers === undefined)
|
|
@@ -2351,20 +3572,20 @@ class Terminals {
|
|
|
2351
3572
|
}
|
|
2352
3573
|
}
|
|
2353
3574
|
async function startTerminals(options = {}) {
|
|
2354
|
-
let
|
|
2355
|
-
if (
|
|
3575
|
+
let spawn4 = options.spawn;
|
|
3576
|
+
if (spawn4 === undefined) {
|
|
2356
3577
|
try {
|
|
2357
3578
|
const loaded = options.load === undefined ? await import("@lydell/node-pty") : await options.load();
|
|
2358
3579
|
if (typeof loaded.spawn !== "function")
|
|
2359
3580
|
throw new Error("the module has no spawn()");
|
|
2360
|
-
|
|
3581
|
+
spawn4 = loaded.spawn;
|
|
2361
3582
|
} catch (error) {
|
|
2362
3583
|
const detail = error instanceof Error ? error.message : String(error);
|
|
2363
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})`);
|
|
2364
3585
|
return;
|
|
2365
3586
|
}
|
|
2366
3587
|
}
|
|
2367
|
-
return new Terminals({ ...options, spawn:
|
|
3588
|
+
return new Terminals({ ...options, spawn: spawn4, shell: options.shell ?? loginShell(), env: options.env ?? shellEnv(process.env) });
|
|
2368
3589
|
}
|
|
2369
3590
|
function loginShell() {
|
|
2370
3591
|
let file = process.env.SHELL;
|
|
@@ -2378,12 +3599,12 @@ function loginShell() {
|
|
|
2378
3599
|
return { file: file === undefined || file === "" ? "/bin/sh" : file, args: ["-l"] };
|
|
2379
3600
|
}
|
|
2380
3601
|
function shellEnv(env) {
|
|
2381
|
-
const
|
|
3602
|
+
const result3 = {};
|
|
2382
3603
|
for (const [key, value] of Object.entries(env)) {
|
|
2383
3604
|
if (value !== undefined && !key.startsWith("PINOMAD_"))
|
|
2384
|
-
|
|
3605
|
+
result3[key] = value;
|
|
2385
3606
|
}
|
|
2386
|
-
return
|
|
3607
|
+
return result3;
|
|
2387
3608
|
}
|
|
2388
3609
|
function info(terminal) {
|
|
2389
3610
|
return {
|
|
@@ -2395,13 +3616,10 @@ function info(terminal) {
|
|
|
2395
3616
|
...terminal.exit === undefined ? {} : { exit: terminal.exit }
|
|
2396
3617
|
};
|
|
2397
3618
|
}
|
|
2398
|
-
function isHighSurrogate(code) {
|
|
2399
|
-
return code >= 55296 && code <= 56319;
|
|
2400
|
-
}
|
|
2401
3619
|
|
|
2402
3620
|
// apps/host/src/folders.ts
|
|
2403
3621
|
import { readdir, realpath as realpath5, stat as stat5 } from "node:fs/promises";
|
|
2404
|
-
import { isAbsolute, join as
|
|
3622
|
+
import { isAbsolute, join as join7 } from "node:path";
|
|
2405
3623
|
var denied = (error) => {
|
|
2406
3624
|
const code = error?.code;
|
|
2407
3625
|
return code === "EACCES" || code === "EPERM";
|
|
@@ -2431,10 +3649,10 @@ async function listFolders(path, home) {
|
|
|
2431
3649
|
continue;
|
|
2432
3650
|
let isDirectory = entry.isDirectory();
|
|
2433
3651
|
if (!isDirectory && entry.isSymbolicLink()) {
|
|
2434
|
-
isDirectory = await stat5(
|
|
3652
|
+
isDirectory = await stat5(join7(dir, entry.name)).then((target) => target.isDirectory(), () => false);
|
|
2435
3653
|
}
|
|
2436
3654
|
if (isDirectory)
|
|
2437
|
-
folders.push({ name: entry.name, path:
|
|
3655
|
+
folders.push({ name: entry.name, path: join7(dir, entry.name) });
|
|
2438
3656
|
}
|
|
2439
3657
|
folders.sort((a, b) => a.name.localeCompare(b.name, "en", { numeric: true, sensitivity: "base" }));
|
|
2440
3658
|
return { path: dir, home: await realpath5(home).catch(() => home), folders };
|
|
@@ -2620,7 +3838,7 @@ function imageProblem(images) {
|
|
|
2620
3838
|
// apps/host/src/image-refs.ts
|
|
2621
3839
|
import { createHash } from "node:crypto";
|
|
2622
3840
|
import { applyImmutable } from "@earendil-works/chord/delta";
|
|
2623
|
-
var
|
|
3841
|
+
var isObject2 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2624
3842
|
var isImageBlock = (value) => value.type === "image" && typeof value.data === "string" && typeof value.mimeType === "string";
|
|
2625
3843
|
var hashed = new WeakMap;
|
|
2626
3844
|
function refOf(block) {
|
|
@@ -2712,7 +3930,7 @@ function rewriteEntries(value) {
|
|
|
2712
3930
|
return out ?? value;
|
|
2713
3931
|
}
|
|
2714
3932
|
function rewriteEntry(value) {
|
|
2715
|
-
if (!
|
|
3933
|
+
if (!isObject2(value) || value.model === undefined)
|
|
2716
3934
|
return value;
|
|
2717
3935
|
const model = rewriteMessages(value.model);
|
|
2718
3936
|
return model === value.model ? value : { ...value, model };
|
|
@@ -2729,7 +3947,7 @@ function rewriteMessages(value) {
|
|
|
2729
3947
|
return out ?? value;
|
|
2730
3948
|
}
|
|
2731
3949
|
function rewriteMessage(value) {
|
|
2732
|
-
if (!
|
|
3950
|
+
if (!isObject2(value) || value.content === undefined)
|
|
2733
3951
|
return value;
|
|
2734
3952
|
const content = rewriteContent(value.content);
|
|
2735
3953
|
return content === value.content ? value : { ...value, content };
|
|
@@ -2740,7 +3958,7 @@ function rewriteContent(value) {
|
|
|
2740
3958
|
let out;
|
|
2741
3959
|
for (let i = 0;i < value.length; i++) {
|
|
2742
3960
|
const block = value[i];
|
|
2743
|
-
const next =
|
|
3961
|
+
const next = isObject2(block) && isImageBlock(block) ? withRef(block) : block;
|
|
2744
3962
|
if (next !== block)
|
|
2745
3963
|
(out ??= value.slice())[i] = next;
|
|
2746
3964
|
}
|
|
@@ -2758,31 +3976,31 @@ function rewriteItems(value) {
|
|
|
2758
3976
|
return out ?? value;
|
|
2759
3977
|
}
|
|
2760
3978
|
function rewriteItem(value) {
|
|
2761
|
-
if (!
|
|
3979
|
+
if (!isObject2(value) || value.content === undefined)
|
|
2762
3980
|
return value;
|
|
2763
3981
|
const content = rewriteContent(value.content);
|
|
2764
3982
|
return content === value.content ? value : { ...value, content };
|
|
2765
3983
|
}
|
|
2766
3984
|
function rewriteInbox(value) {
|
|
2767
|
-
if (!
|
|
3985
|
+
if (!isObject2(value) || value.items === undefined)
|
|
2768
3986
|
return value;
|
|
2769
3987
|
const items = rewriteItems(value.items);
|
|
2770
3988
|
return items === value.items ? value : { ...value, items };
|
|
2771
3989
|
}
|
|
2772
3990
|
function rewriteGeneration(value) {
|
|
2773
|
-
if (!
|
|
3991
|
+
if (!isObject2(value) || value.message === undefined)
|
|
2774
3992
|
return value;
|
|
2775
|
-
const
|
|
2776
|
-
return
|
|
3993
|
+
const message2 = rewriteMessage(value.message);
|
|
3994
|
+
return message2 === value.message ? value : { ...value, message: message2 };
|
|
2777
3995
|
}
|
|
2778
3996
|
function rewriteLive(value) {
|
|
2779
|
-
if (!
|
|
3997
|
+
if (!isObject2(value) || value.generation === undefined)
|
|
2780
3998
|
return value;
|
|
2781
3999
|
const generation = rewriteGeneration(value.generation);
|
|
2782
4000
|
return generation === value.generation ? value : { ...value, generation };
|
|
2783
4001
|
}
|
|
2784
4002
|
function rewriteDocs(value) {
|
|
2785
|
-
if (!
|
|
4003
|
+
if (!isObject2(value))
|
|
2786
4004
|
return value;
|
|
2787
4005
|
let out;
|
|
2788
4006
|
for (const key of ["pi.inbox", "pi.live"]) {
|
|
@@ -2796,7 +4014,7 @@ function rewriteDocs(value) {
|
|
|
2796
4014
|
return out ?? value;
|
|
2797
4015
|
}
|
|
2798
4016
|
function rewriteView(value) {
|
|
2799
|
-
if (!
|
|
4017
|
+
if (!isObject2(value))
|
|
2800
4018
|
return value;
|
|
2801
4019
|
let out;
|
|
2802
4020
|
if (value.entries !== undefined) {
|
|
@@ -2824,7 +4042,7 @@ function rewriteSlot(slot, value) {
|
|
|
2824
4042
|
case "content":
|
|
2825
4043
|
return rewriteContent(value);
|
|
2826
4044
|
case "block":
|
|
2827
|
-
return
|
|
4045
|
+
return isObject2(value) && isImageBlock(value) ? withRef(value) : value;
|
|
2828
4046
|
case "docs":
|
|
2829
4047
|
return rewriteDocs(value);
|
|
2830
4048
|
case "inbox":
|
|
@@ -2889,35 +4107,35 @@ function reachesIntoImage(op, root) {
|
|
|
2889
4107
|
if (slotAt(path.slice(0, length)) !== "block")
|
|
2890
4108
|
continue;
|
|
2891
4109
|
const node = resolve4(root, path, length);
|
|
2892
|
-
if (
|
|
4110
|
+
if (isObject2(node) && isImageBlock(node))
|
|
2893
4111
|
return true;
|
|
2894
4112
|
}
|
|
2895
4113
|
return false;
|
|
2896
4114
|
}
|
|
2897
4115
|
function* contentArrays(view) {
|
|
2898
|
-
if (!
|
|
4116
|
+
if (!isObject2(view))
|
|
2899
4117
|
return;
|
|
2900
4118
|
if (Array.isArray(view.entries)) {
|
|
2901
4119
|
for (const entry of view.entries) {
|
|
2902
|
-
if (!
|
|
4120
|
+
if (!isObject2(entry) || !Array.isArray(entry.model))
|
|
2903
4121
|
continue;
|
|
2904
|
-
for (const
|
|
2905
|
-
if (
|
|
2906
|
-
yield
|
|
4122
|
+
for (const message2 of entry.model) {
|
|
4123
|
+
if (isObject2(message2) && Array.isArray(message2.content))
|
|
4124
|
+
yield message2.content;
|
|
2907
4125
|
}
|
|
2908
4126
|
}
|
|
2909
4127
|
}
|
|
2910
|
-
if (!
|
|
4128
|
+
if (!isObject2(view.docs))
|
|
2911
4129
|
return;
|
|
2912
4130
|
const inbox = view.docs["pi.inbox"];
|
|
2913
|
-
if (
|
|
4131
|
+
if (isObject2(inbox) && Array.isArray(inbox.items)) {
|
|
2914
4132
|
for (const item of inbox.items) {
|
|
2915
|
-
if (
|
|
4133
|
+
if (isObject2(item) && Array.isArray(item.content))
|
|
2916
4134
|
yield item.content;
|
|
2917
4135
|
}
|
|
2918
4136
|
}
|
|
2919
4137
|
const live = view.docs["pi.live"];
|
|
2920
|
-
if (
|
|
4138
|
+
if (isObject2(live) && isObject2(live.generation) && isObject2(live.generation.message) && Array.isArray(live.generation.message.content)) {
|
|
2921
4139
|
yield live.generation.message.content;
|
|
2922
4140
|
}
|
|
2923
4141
|
}
|
|
@@ -2925,7 +4143,7 @@ function imageBlocks(view) {
|
|
|
2925
4143
|
const blocks = [];
|
|
2926
4144
|
for (const content of contentArrays(view)) {
|
|
2927
4145
|
for (const block of content) {
|
|
2928
|
-
if (
|
|
4146
|
+
if (isObject2(block) && isImageBlock(block))
|
|
2929
4147
|
blocks.push(block);
|
|
2930
4148
|
}
|
|
2931
4149
|
}
|
|
@@ -3013,8 +4231,8 @@ class ImageVariants {
|
|
|
3013
4231
|
if (job === undefined)
|
|
3014
4232
|
return;
|
|
3015
4233
|
this.#active += 1;
|
|
3016
|
-
this.#derive(job.key, job.variant, job.original).then((
|
|
3017
|
-
job.resolve(
|
|
4234
|
+
this.#derive(job.key, job.variant, job.original).then((result3) => {
|
|
4235
|
+
job.resolve(result3);
|
|
3018
4236
|
this.#finish();
|
|
3019
4237
|
}, (error) => {
|
|
3020
4238
|
job.reject(error instanceof Error ? error : new Error(String(error)));
|
|
@@ -3035,13 +4253,13 @@ class ImageVariants {
|
|
|
3035
4253
|
if (prepared === undefined || prepared.data === original.data && prepared.mimeType === original.mimeType) {
|
|
3036
4254
|
return { ...original, variant: "original" };
|
|
3037
4255
|
}
|
|
3038
|
-
const
|
|
4256
|
+
const result3 = {
|
|
3039
4257
|
data: prepared.data,
|
|
3040
4258
|
mimeType: prepared.mimeType,
|
|
3041
4259
|
variant
|
|
3042
4260
|
};
|
|
3043
|
-
this.#write(key,
|
|
3044
|
-
return
|
|
4261
|
+
this.#write(key, result3);
|
|
4262
|
+
return result3;
|
|
3045
4263
|
} catch {
|
|
3046
4264
|
return { ...original, variant: "original" };
|
|
3047
4265
|
}
|
|
@@ -3054,8 +4272,8 @@ class ImageVariants {
|
|
|
3054
4272
|
this.#cache.set(key, cached);
|
|
3055
4273
|
return cached;
|
|
3056
4274
|
}
|
|
3057
|
-
#write(key,
|
|
3058
|
-
const bytes =
|
|
4275
|
+
#write(key, result3) {
|
|
4276
|
+
const bytes = result3.data.length;
|
|
3059
4277
|
if (bytes > this.#maxCacheBytes)
|
|
3060
4278
|
return;
|
|
3061
4279
|
while (this.#cacheBytes + bytes > this.#maxCacheBytes) {
|
|
@@ -3066,17 +4284,17 @@ class ImageVariants {
|
|
|
3066
4284
|
this.#cache.delete(oldest);
|
|
3067
4285
|
this.#cacheBytes -= previous.data.length;
|
|
3068
4286
|
}
|
|
3069
|
-
this.#cache.set(key,
|
|
4287
|
+
this.#cache.set(key, result3);
|
|
3070
4288
|
this.#cacheBytes += bytes;
|
|
3071
4289
|
}
|
|
3072
4290
|
}
|
|
3073
4291
|
|
|
3074
4292
|
// apps/host/src/auto-title.ts
|
|
3075
|
-
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
|
4293
|
+
import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT2 } from "@earendil-works/chord/context";
|
|
3076
4294
|
|
|
3077
4295
|
// apps/host/src/title-doc.ts
|
|
3078
|
-
import { defineDoc as
|
|
3079
|
-
var TitleDoc =
|
|
4296
|
+
import { defineDoc as defineDoc9 } from "@earendil-works/pi-durable";
|
|
4297
|
+
var TitleDoc = defineDoc9({
|
|
3080
4298
|
kind: "pinomad.title",
|
|
3081
4299
|
version: 1,
|
|
3082
4300
|
scope: "conversation",
|
|
@@ -3087,11 +4305,11 @@ var TitleDoc = defineDoc6({
|
|
|
3087
4305
|
});
|
|
3088
4306
|
|
|
3089
4307
|
// apps/host/src/auto-title.ts
|
|
3090
|
-
var context =
|
|
4308
|
+
var context = BACKGROUND_CONTEXT2;
|
|
3091
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.";
|
|
3092
4310
|
var TITLE_INPUT_LIMIT = 2000;
|
|
3093
4311
|
var TITLE_MAX_LENGTH = 80;
|
|
3094
|
-
var
|
|
4312
|
+
var textOf2 = (content) => {
|
|
3095
4313
|
if (typeof content === "string")
|
|
3096
4314
|
return content;
|
|
3097
4315
|
if (!Array.isArray(content))
|
|
@@ -3148,7 +4366,7 @@ function attachAutoTitle(runs, deps) {
|
|
|
3148
4366
|
return;
|
|
3149
4367
|
const firstMessage = first.model?.[0];
|
|
3150
4368
|
const firstText = (stripUserBlocks(firstMessage?.content ?? "").text || "(images or files only)").slice(0, TITLE_INPUT_LIMIT);
|
|
3151
|
-
const answer =
|
|
4369
|
+
const answer = textOf2(last.content).slice(0, TITLE_INPUT_LIMIT);
|
|
3152
4370
|
const request = {
|
|
3153
4371
|
systemPrompt: TITLE_PROMPT,
|
|
3154
4372
|
messages: [
|
|
@@ -3195,8 +4413,8 @@ ${answer}`}`,
|
|
|
3195
4413
|
|
|
3196
4414
|
// apps/host/src/push.ts
|
|
3197
4415
|
import { createHash as createHash2 } from "node:crypto";
|
|
3198
|
-
import { BACKGROUND_CONTEXT as
|
|
3199
|
-
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";
|
|
3200
4418
|
|
|
3201
4419
|
// packages/client/src/family.ts
|
|
3202
4420
|
var SUBAGENT_TASK_PREFIX = /^\s*in the working directory\s+\S+,\s*/i;
|
|
@@ -3253,8 +4471,8 @@ function diffRunNotices(previous, organized) {
|
|
|
3253
4471
|
}
|
|
3254
4472
|
|
|
3255
4473
|
// apps/host/src/push.ts
|
|
3256
|
-
var context2 =
|
|
3257
|
-
var PushDoc =
|
|
4474
|
+
var context2 = BACKGROUND_CONTEXT3;
|
|
4475
|
+
var PushDoc = defineDoc10({
|
|
3258
4476
|
kind: "pinomad.push",
|
|
3259
4477
|
version: 2,
|
|
3260
4478
|
scope: "session",
|
|
@@ -3391,18 +4609,18 @@ async function attachPushSender(options) {
|
|
|
3391
4609
|
return await new Promise((resolve5) => {
|
|
3392
4610
|
const waiters = titleWaiters.get(notice.id) ?? new Set;
|
|
3393
4611
|
titleWaiters.set(notice.id, waiters);
|
|
3394
|
-
const
|
|
3395
|
-
waiters.delete(
|
|
4612
|
+
const settle2 = (title) => {
|
|
4613
|
+
waiters.delete(settle2);
|
|
3396
4614
|
if (waiters.size === 0)
|
|
3397
4615
|
titleWaiters.delete(notice.id);
|
|
3398
4616
|
timers.clearTimeout(timer);
|
|
3399
4617
|
resolve5(title);
|
|
3400
4618
|
};
|
|
3401
|
-
const timer = timers.setTimeout(() =>
|
|
3402
|
-
waiters.add(
|
|
4619
|
+
const timer = timers.setTimeout(() => settle2(FALLBACK_TITLE), TITLE_WAIT_MS);
|
|
4620
|
+
waiters.add(settle2);
|
|
3403
4621
|
namedTitle(notice.id).then((title) => {
|
|
3404
4622
|
if (title !== undefined)
|
|
3405
|
-
|
|
4623
|
+
settle2(title);
|
|
3406
4624
|
});
|
|
3407
4625
|
});
|
|
3408
4626
|
};
|
|
@@ -3529,24 +4747,29 @@ async function attachPushSender(options) {
|
|
|
3529
4747
|
}
|
|
3530
4748
|
};
|
|
3531
4749
|
const unsubscribeConversations = conversations.subscribe(() => update());
|
|
3532
|
-
const
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
4750
|
+
const commits = harness.observeCommits({
|
|
4751
|
+
advance: (publication) => {
|
|
4752
|
+
for (const change of publication.changes) {
|
|
4753
|
+
if (change.type !== "document" || change.kind !== "change" || change.record.kind !== TitleDoc.definition.kind)
|
|
4754
|
+
continue;
|
|
4755
|
+
const id = change.conversationId;
|
|
4756
|
+
if (id === undefined)
|
|
4757
|
+
continue;
|
|
4758
|
+
const title = change.value.title;
|
|
4759
|
+
if (typeof title !== "string" || title === "")
|
|
4760
|
+
continue;
|
|
4761
|
+
const waiters = titleWaiters.get(id);
|
|
4762
|
+
titleWaiters.delete(id);
|
|
4763
|
+
for (const settle2 of waiters ?? [])
|
|
4764
|
+
settle2(title);
|
|
4765
|
+
}
|
|
4766
|
+
},
|
|
4767
|
+
end: () => {}
|
|
3547
4768
|
});
|
|
3548
|
-
indexWatch?.start(async (
|
|
3549
|
-
|
|
4769
|
+
indexWatch?.start(async (call) => {
|
|
4770
|
+
if (call.kind !== "value")
|
|
4771
|
+
return;
|
|
4772
|
+
index = call.value;
|
|
3550
4773
|
update();
|
|
3551
4774
|
});
|
|
3552
4775
|
update();
|
|
@@ -3555,7 +4778,7 @@ async function attachPushSender(options) {
|
|
|
3555
4778
|
async close() {
|
|
3556
4779
|
disposed = true;
|
|
3557
4780
|
unsubscribeConversations();
|
|
3558
|
-
|
|
4781
|
+
commits.detach();
|
|
3559
4782
|
await indexWatch?.stop();
|
|
3560
4783
|
for (const item of pending)
|
|
3561
4784
|
drop(item);
|
|
@@ -3594,7 +4817,7 @@ function toolTarget(args) {
|
|
|
3594
4817
|
}
|
|
3595
4818
|
|
|
3596
4819
|
// apps/host/src/gateway.ts
|
|
3597
|
-
var context3 =
|
|
4820
|
+
var context3 = BACKGROUND_CONTEXT4;
|
|
3598
4821
|
var forkTails = new WeakMap;
|
|
3599
4822
|
function assertQueuedInput(record, conversationId) {
|
|
3600
4823
|
if (record.conversationId !== conversationId) {
|
|
@@ -3607,7 +4830,7 @@ async function dismissPendingQuestions(tx, docs, conversationId) {
|
|
|
3607
4830
|
let dismissed = false;
|
|
3608
4831
|
for (const doc of docs) {
|
|
3609
4832
|
const value = await tx.doc(doc.token, conversationId);
|
|
3610
|
-
if (!
|
|
4833
|
+
if (!Value.Check(QuestionSchema, value))
|
|
3611
4834
|
continue;
|
|
3612
4835
|
for (const request of value.requests) {
|
|
3613
4836
|
if (request.resolution === undefined) {
|
|
@@ -3659,6 +4882,19 @@ function submitResult(record) {
|
|
|
3659
4882
|
...record.entry === undefined ? {} : { entry: record.entry }
|
|
3660
4883
|
};
|
|
3661
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
|
+
});
|
|
3662
4898
|
var LOOPBACK_SECURE_PATH = "/secure";
|
|
3663
4899
|
async function startGateway(options) {
|
|
3664
4900
|
if (options.secure?.lan?.publicUrl !== undefined)
|
|
@@ -3694,10 +4930,16 @@ async function startGateway(options) {
|
|
|
3694
4930
|
const clients = new Set;
|
|
3695
4931
|
let rtc;
|
|
3696
4932
|
const rtcHello = () => rtc?.hello();
|
|
3697
|
-
const terminals = await startTerminals(
|
|
3698
|
-
|
|
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) => {
|
|
3699
4941
|
for (const client of clients)
|
|
3700
|
-
client.send({ type: "notice", level, message });
|
|
4942
|
+
client.send({ type: "notice", level, message: message2 });
|
|
3701
4943
|
};
|
|
3702
4944
|
let remote;
|
|
3703
4945
|
let relay;
|
|
@@ -3732,6 +4974,7 @@ async function startGateway(options) {
|
|
|
3732
4974
|
devicePublicKey,
|
|
3733
4975
|
rtcHello,
|
|
3734
4976
|
terminals,
|
|
4977
|
+
...options.processes === undefined ? {} : { processes: options.processes },
|
|
3735
4978
|
closed: (self) => {
|
|
3736
4979
|
clients.delete(self);
|
|
3737
4980
|
connections.delete(self);
|
|
@@ -3969,6 +5212,7 @@ async function startGateway(options) {
|
|
|
3969
5212
|
await detachPush?.close();
|
|
3970
5213
|
conversations.dispose();
|
|
3971
5214
|
terminals?.closeAll();
|
|
5215
|
+
await options.processes?.close(context3);
|
|
3972
5216
|
await rtc?.close();
|
|
3973
5217
|
await Promise.all([...connections].map((connection) => connection.dispose()));
|
|
3974
5218
|
for (const socket of server.clients)
|
|
@@ -4141,6 +5385,8 @@ function secureHandshake(carrier, secure, policy, handlers) {
|
|
|
4141
5385
|
session = new SecureSession(admitted.result);
|
|
4142
5386
|
stage = "settled";
|
|
4143
5387
|
await carrier.send(admitted.message);
|
|
5388
|
+
if (closed)
|
|
5389
|
+
return;
|
|
4144
5390
|
if (admitted.purpose === "signal")
|
|
4145
5391
|
handlers.signal?.(connection, admitted.deviceKey);
|
|
4146
5392
|
else
|
|
@@ -4201,10 +5447,10 @@ function secureHandshake(carrier, secure, policy, handlers) {
|
|
|
4201
5447
|
if (decision.outcome === "admitted" && decision.requestId !== undefined) {
|
|
4202
5448
|
policy.accountAdmitted(decision.requestId, deviceKey);
|
|
4203
5449
|
}
|
|
4204
|
-
const { message, result:
|
|
5450
|
+
const { message: message2, result: result3 } = responder.writeMessage2(encodeHostHello({ v: 1 }));
|
|
4205
5451
|
return {
|
|
4206
|
-
result:
|
|
4207
|
-
message,
|
|
5452
|
+
result: result3,
|
|
5453
|
+
message: message2,
|
|
4208
5454
|
deviceKey,
|
|
4209
5455
|
purpose,
|
|
4210
5456
|
...decision.outcome === "admitted" ? { joined: decision.entry } : {}
|
|
@@ -4271,12 +5517,16 @@ class ConversationList {
|
|
|
4271
5517
|
#list = [];
|
|
4272
5518
|
#unsubscribe = () => {};
|
|
4273
5519
|
#notifying = false;
|
|
5520
|
+
#harness;
|
|
5521
|
+
#live = new Map;
|
|
5522
|
+
#disposed = false;
|
|
4274
5523
|
constructor(docs) {
|
|
4275
5524
|
this.#questionTokens = (docs ?? []).filter((doc) => doc.presentation === "pinomad.question").map((doc) => doc.token);
|
|
4276
5525
|
this.#questionKinds = new Set(this.#questionTokens.map((token) => token.definition.kind));
|
|
4277
5526
|
}
|
|
4278
5527
|
static async open(harness, docs) {
|
|
4279
5528
|
const list = new ConversationList(docs);
|
|
5529
|
+
list.#harness = harness;
|
|
4280
5530
|
const summaries = [];
|
|
4281
5531
|
let cursor;
|
|
4282
5532
|
const records = [];
|
|
@@ -4295,10 +5545,13 @@ class ConversationList {
|
|
|
4295
5545
|
summaries.push(list.#summaryOf(record));
|
|
4296
5546
|
}
|
|
4297
5547
|
list.#list = summaries;
|
|
4298
|
-
|
|
5548
|
+
const observation = harness.observeCommits({ advance: (publication) => list.#observe(publication), end: () => {} });
|
|
5549
|
+
list.#unsubscribe = () => observation.detach();
|
|
4299
5550
|
const touched = new Set;
|
|
4300
5551
|
list.#reindex(await harness.snapshot(IndexDoc, context3), touched);
|
|
4301
5552
|
list.#refreshSummaries(touched);
|
|
5553
|
+
for (const [id, facts] of list.#facts)
|
|
5554
|
+
list.#followLive(id, facts.running === true);
|
|
4302
5555
|
return list;
|
|
4303
5556
|
}
|
|
4304
5557
|
async#load(harness, record) {
|
|
@@ -4309,7 +5562,7 @@ class ConversationList {
|
|
|
4309
5562
|
facts.tools = live?.tools;
|
|
4310
5563
|
for (const token of this.#questionTokens) {
|
|
4311
5564
|
const value = await harness.snapshot(token, record.id, context3);
|
|
4312
|
-
if (
|
|
5565
|
+
if (Value.Check(QuestionSchema, value) && value.requests.some((request) => request.resolution === undefined)) {
|
|
4313
5566
|
facts.pending.add(token.definition.kind);
|
|
4314
5567
|
}
|
|
4315
5568
|
}
|
|
@@ -4320,6 +5573,9 @@ class ConversationList {
|
|
|
4320
5573
|
if (typeof subagent?.role === "string" && subagent.role !== "")
|
|
4321
5574
|
facts.role = subagent.role;
|
|
4322
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";
|
|
4323
5579
|
}
|
|
4324
5580
|
facts.agent = roleNameOf(await harness.snapshot(RoleDoc, record.id, context3));
|
|
4325
5581
|
const named = (await harness.snapshot(TitleDoc, record.id, context3))?.title;
|
|
@@ -4348,29 +5604,30 @@ class ConversationList {
|
|
|
4348
5604
|
continue;
|
|
4349
5605
|
touched.add(entry.conversationId);
|
|
4350
5606
|
facts.newest = entry.id;
|
|
4351
|
-
const
|
|
4352
|
-
facts.updatedAt = typeof
|
|
4353
|
-
facts.failed = entry.kind === "pi.assistant" &&
|
|
4354
|
-
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;
|
|
4355
5611
|
} else if (change.type === "document") {
|
|
4356
5612
|
const id = change.conversationId;
|
|
5613
|
+
const value = change.kind === "change" ? change.value : null;
|
|
4357
5614
|
if (id === undefined) {
|
|
4358
5615
|
if (change.record.kind === IndexDoc.definition.kind)
|
|
4359
|
-
this.#reindex(
|
|
5616
|
+
this.#reindex(value, touched);
|
|
4360
5617
|
continue;
|
|
4361
5618
|
}
|
|
4362
5619
|
const facts = this.#fact(id);
|
|
4363
5620
|
if (change.record.kind === "pi.live") {
|
|
4364
|
-
const live =
|
|
5621
|
+
const live = value;
|
|
4365
5622
|
const wasRunning = facts.running === true;
|
|
4366
5623
|
facts.running = live?.run !== undefined;
|
|
4367
5624
|
facts.tools = live?.tools;
|
|
4368
5625
|
touched.add(id);
|
|
5626
|
+
this.#followLive(id, facts.running);
|
|
4369
5627
|
if (wasRunning && !facts.running && this.#records.get(id)?.owner === undefined)
|
|
4370
5628
|
ended.add(id);
|
|
4371
5629
|
} else if (this.#questionKinds.has(change.record.kind)) {
|
|
4372
|
-
const
|
|
4373
|
-
const pending = Value2.Check(QuestionSchema, value) && value.requests.some((request) => request.resolution === undefined);
|
|
5630
|
+
const pending = Value.Check(QuestionSchema, value) && value.requests.some((request) => request.resolution === undefined);
|
|
4374
5631
|
const next = new Set(facts.pending);
|
|
4375
5632
|
if (pending)
|
|
4376
5633
|
next.add(change.record.kind);
|
|
@@ -4379,16 +5636,18 @@ class ConversationList {
|
|
|
4379
5636
|
facts.pending = next;
|
|
4380
5637
|
touched.add(id);
|
|
4381
5638
|
} else if (change.record.kind === RoleDoc.definition.kind) {
|
|
4382
|
-
facts.agent = roleNameOf(
|
|
5639
|
+
facts.agent = roleNameOf(value);
|
|
4383
5640
|
touched.add(id);
|
|
4384
5641
|
} else if (change.record.kind === SubagentDoc.definition.kind) {
|
|
4385
|
-
const
|
|
4386
|
-
facts.label = typeof
|
|
4387
|
-
facts.role = typeof
|
|
4388
|
-
facts.model = modelRefOf(
|
|
5642
|
+
const subagent = value;
|
|
5643
|
+
facts.label = typeof subagent?.label === "string" && subagent.label !== "" ? subagent.label : undefined;
|
|
5644
|
+
facts.role = typeof subagent?.role === "string" && subagent.role !== "" ? subagent.role : undefined;
|
|
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";
|
|
4389
5648
|
touched.add(id);
|
|
4390
5649
|
} else if (change.record.kind === TitleDoc.definition.kind) {
|
|
4391
|
-
const named =
|
|
5650
|
+
const named = value?.title;
|
|
4392
5651
|
facts.named = typeof named === "string" && named !== "" ? named : undefined;
|
|
4393
5652
|
touched.add(id);
|
|
4394
5653
|
}
|
|
@@ -4463,9 +5722,51 @@ class ConversationList {
|
|
|
4463
5722
|
listener(this.#list);
|
|
4464
5723
|
});
|
|
4465
5724
|
}
|
|
5725
|
+
#followLive(id, running) {
|
|
5726
|
+
if (!running || this.#disposed) {
|
|
5727
|
+
const watch = this.#live.get(id);
|
|
5728
|
+
this.#live.delete(id);
|
|
5729
|
+
watch?.stop();
|
|
5730
|
+
return;
|
|
5731
|
+
}
|
|
5732
|
+
if (this.#live.has(id))
|
|
5733
|
+
return;
|
|
5734
|
+
this.#live.set(id, undefined);
|
|
5735
|
+
setImmediate(() => {
|
|
5736
|
+
this.#harness.watchDoc(LiveDoc, id, context3).then(async (watch) => {
|
|
5737
|
+
if (watch === undefined)
|
|
5738
|
+
return;
|
|
5739
|
+
if (this.#live.get(id) !== undefined || !this.#live.has(id)) {
|
|
5740
|
+
await watch.stop();
|
|
5741
|
+
return;
|
|
5742
|
+
}
|
|
5743
|
+
this.#live.set(id, watch);
|
|
5744
|
+
const apply = (live) => {
|
|
5745
|
+
const facts = this.#fact(id);
|
|
5746
|
+
if (live.run === undefined)
|
|
5747
|
+
return;
|
|
5748
|
+
facts.tools = live.tools;
|
|
5749
|
+
this.#refreshSummaries(new Set([id]));
|
|
5750
|
+
};
|
|
5751
|
+
apply(watch.value);
|
|
5752
|
+
watch.start(async (call) => {
|
|
5753
|
+
if (call.kind === "value")
|
|
5754
|
+
apply(call.value);
|
|
5755
|
+
});
|
|
5756
|
+
}, (error) => {
|
|
5757
|
+
if (this.#live.get(id) === undefined)
|
|
5758
|
+
this.#live.delete(id);
|
|
5759
|
+
console.error(`conversation ${id}: could not follow pi.live: ${error instanceof Error ? error.message : String(error)}`);
|
|
5760
|
+
});
|
|
5761
|
+
});
|
|
5762
|
+
}
|
|
4466
5763
|
dispose() {
|
|
5764
|
+
this.#disposed = true;
|
|
4467
5765
|
this.#unsubscribe();
|
|
4468
5766
|
this.#listeners.clear();
|
|
5767
|
+
for (const watch of this.#live.values())
|
|
5768
|
+
watch?.stop();
|
|
5769
|
+
this.#live.clear();
|
|
4469
5770
|
}
|
|
4470
5771
|
}
|
|
4471
5772
|
function roleNameOf(value) {
|
|
@@ -4511,7 +5812,8 @@ function summaryOf(record, facts, role, currentAgent) {
|
|
|
4511
5812
|
return {
|
|
4512
5813
|
id: record.id,
|
|
4513
5814
|
kind: record.owner !== undefined ? "subagent" : record.parent !== undefined ? "fork" : "conversation",
|
|
4514
|
-
...record.owner === undefined ? {} : { lifecycle: "foreground" },
|
|
5815
|
+
...record.owner === undefined ? {} : { lifecycle: facts.lifecycle ?? "foreground" },
|
|
5816
|
+
...facts.name === undefined ? {} : { name: facts.name },
|
|
4515
5817
|
...facts.model === undefined ? {} : { model: facts.model },
|
|
4516
5818
|
...parent === undefined ? {} : { parent },
|
|
4517
5819
|
...record.parent === undefined ? {} : { forkedAt: String(record.parent.at) },
|
|
@@ -4525,15 +5827,47 @@ function summaryOf(record, facts, role, currentAgent) {
|
|
|
4525
5827
|
};
|
|
4526
5828
|
}
|
|
4527
5829
|
function sameSummary(a, b) {
|
|
4528
|
-
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;
|
|
4529
5831
|
}
|
|
4530
5832
|
function titleOf(entry) {
|
|
4531
|
-
const
|
|
4532
|
-
if (
|
|
5833
|
+
const message2 = entry?.model?.[0];
|
|
5834
|
+
if (message2?.role !== "user")
|
|
4533
5835
|
return {};
|
|
4534
|
-
const title = stripUserBlocks(
|
|
5836
|
+
const title = stripUserBlocks(message2.content).text.replace(/\s+/g, " ").trim();
|
|
4535
5837
|
return title === "" ? {} : { title };
|
|
4536
5838
|
}
|
|
5839
|
+
var WHOLE_TRANSCRIPT = { min: Number.MAX_SAFE_INTEGER, max: Number.MAX_SAFE_INTEGER };
|
|
5840
|
+
var TURN_STARTS = new Set(["pi.user", "pi.compaction", "pi.reset"]);
|
|
5841
|
+
async function turnPage(conversation, before, limit) {
|
|
5842
|
+
const want = Math.min(limit, MAX_TRANSCRIPT_PAGE);
|
|
5843
|
+
const done = (taken2) => taken2.length >= MAX_TRANSCRIPT_PAGE || taken2.length >= want && TURN_STARTS.has(taken2.at(-1).kind);
|
|
5844
|
+
const query = { order: "descending", ...before === undefined ? {} : { maxEntryId: before } };
|
|
5845
|
+
const taken = [];
|
|
5846
|
+
let cursor;
|
|
5847
|
+
for (;; ) {
|
|
5848
|
+
const page = await conversation.entries(query, Math.max(want + 1 - taken.length, 64), cursor, context3);
|
|
5849
|
+
for (const entry of page.items) {
|
|
5850
|
+
if (done(taken))
|
|
5851
|
+
return { entries: taken.reverse(), before: entry.id };
|
|
5852
|
+
taken.push(entry);
|
|
5853
|
+
}
|
|
5854
|
+
if (page.next === undefined)
|
|
5855
|
+
return { entries: taken.reverse() };
|
|
5856
|
+
cursor = page.next;
|
|
5857
|
+
}
|
|
5858
|
+
}
|
|
5859
|
+
async function viewWindow(conversation, window) {
|
|
5860
|
+
if (window === undefined)
|
|
5861
|
+
return WHOLE_TRANSCRIPT;
|
|
5862
|
+
const from = window.from ?? await startOf(conversation, window.limit);
|
|
5863
|
+
return from === undefined ? WHOLE_TRANSCRIPT : { ...WHOLE_TRANSCRIPT, from };
|
|
5864
|
+
}
|
|
5865
|
+
async function startOf(conversation, limit) {
|
|
5866
|
+
const page = await turnPage(conversation, undefined, limit);
|
|
5867
|
+
return page.before === undefined ? undefined : page.entries[0].id;
|
|
5868
|
+
}
|
|
5869
|
+
var shownOps = (call) => isGone(call) ? [["r", null]] : call.ops;
|
|
5870
|
+
var isGone = (call) => call.retired && !call.ops.some((op) => op[0] === "r");
|
|
4537
5871
|
async function historyFacts(harness, id) {
|
|
4538
5872
|
const conversation = await harness.conversation(id, context3);
|
|
4539
5873
|
let first;
|
|
@@ -4545,21 +5879,21 @@ async function historyFacts(harness, id) {
|
|
|
4545
5879
|
newest ??= page.items.find((entry) => entry.conversationId === id && (entry.kind === "pi.user" || entry.kind === "pi.assistant"));
|
|
4546
5880
|
cursor = page.next;
|
|
4547
5881
|
} while (cursor !== undefined);
|
|
4548
|
-
const
|
|
5882
|
+
const message2 = newest?.model?.[0];
|
|
4549
5883
|
return {
|
|
4550
5884
|
...titleOf(first),
|
|
4551
5885
|
...newest === undefined ? {} : {
|
|
4552
5886
|
newest: newest.id,
|
|
4553
|
-
...typeof
|
|
4554
|
-
failed: newest.kind === "pi.assistant" &&
|
|
4555
|
-
...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) } : {}
|
|
4556
5890
|
}
|
|
4557
5891
|
};
|
|
4558
5892
|
}
|
|
4559
|
-
function callsOf(
|
|
4560
|
-
if (
|
|
5893
|
+
function callsOf(message2) {
|
|
5894
|
+
if (message2?.role !== "assistant")
|
|
4561
5895
|
return;
|
|
4562
|
-
const content =
|
|
5896
|
+
const content = message2.content;
|
|
4563
5897
|
if (!Array.isArray(content))
|
|
4564
5898
|
return;
|
|
4565
5899
|
const calls = new Map;
|
|
@@ -4666,6 +6000,12 @@ class GatewayClient {
|
|
|
4666
6000
|
throw new Error("Provider setup is not available on this host");
|
|
4667
6001
|
return modelAuth;
|
|
4668
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
|
+
}
|
|
4669
6009
|
#hostValues;
|
|
4670
6010
|
#subscriptions = new Map;
|
|
4671
6011
|
#terminalSizes = new Map;
|
|
@@ -4683,7 +6023,8 @@ class GatewayClient {
|
|
|
4683
6023
|
auth: options.modelAuth?.auth ?? constantValue({ logins: [] }),
|
|
4684
6024
|
providers: options.modelAuth?.providers ?? constantValue({ providers: [], logins: [] }),
|
|
4685
6025
|
settings: options.settings,
|
|
4686
|
-
agents: options.agents ?? constantValue({ roles: [], problems: [] })
|
|
6026
|
+
agents: options.agents ?? constantValue({ roles: [], problems: [] }),
|
|
6027
|
+
upgrade: options.upgrade ?? NO_UPGRADES
|
|
4687
6028
|
};
|
|
4688
6029
|
this.#hooks = hooks;
|
|
4689
6030
|
this.devicePublicKey = hooks.devicePublicKey;
|
|
@@ -4732,7 +6073,8 @@ class GatewayClient {
|
|
|
4732
6073
|
toolPresentations: options.toolPresentations ?? {},
|
|
4733
6074
|
endpointSet: this.#hooks.endpointSet(),
|
|
4734
6075
|
...rtc === undefined ? {} : { rtc },
|
|
4735
|
-
...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 }
|
|
4736
6078
|
});
|
|
4737
6079
|
}
|
|
4738
6080
|
send(frame) {
|
|
@@ -4745,15 +6087,19 @@ class GatewayClient {
|
|
|
4745
6087
|
}
|
|
4746
6088
|
async#receive(frame) {
|
|
4747
6089
|
if (frame.type === "subscribe")
|
|
4748
|
-
await this.#subscribe(frame.stream, frame.images === "ref", frame.since);
|
|
6090
|
+
await this.#subscribe(frame.stream, frame.images === "ref", frame.since, frame.window);
|
|
4749
6091
|
else if (frame.type === "unsubscribe")
|
|
4750
6092
|
await this.#unsubscribe(frame.stream);
|
|
4751
6093
|
else
|
|
4752
6094
|
await this.#answer(frame.id, frame.method, frame.args);
|
|
4753
6095
|
}
|
|
4754
|
-
async#subscribe(stream, imageRefs, since) {
|
|
6096
|
+
async#subscribe(stream, imageRefs, since, window) {
|
|
4755
6097
|
const previous = this.#subscriptions.get(stream);
|
|
4756
|
-
const state =
|
|
6098
|
+
const state = {
|
|
6099
|
+
...imageRefs && stream.startsWith("conversation:") ? { images: new ImageRefs } : {},
|
|
6100
|
+
...since === undefined ? {} : { since },
|
|
6101
|
+
...window === undefined ? {} : { window }
|
|
6102
|
+
};
|
|
4757
6103
|
this.#subscriptions.set(stream, state);
|
|
4758
6104
|
const current = () => !this.#disposed && this.#subscriptions.get(stream) === state;
|
|
4759
6105
|
const send = (frame) => current() ? this.send(frame) : Promise.resolve();
|
|
@@ -4815,12 +6161,24 @@ class GatewayClient {
|
|
|
4815
6161
|
});
|
|
4816
6162
|
return { stop: detach };
|
|
4817
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
|
+
}
|
|
4818
6176
|
if (stream.startsWith("doc:")) {
|
|
4819
6177
|
const at = stream.lastIndexOf(":");
|
|
4820
6178
|
return this.#openDoc(stream, stream.slice("doc:".length, at), Number(stream.slice(at + 1)), send);
|
|
4821
6179
|
}
|
|
4822
6180
|
const conversation = await this.#conversation(Number(stream.slice("conversation:".length)));
|
|
4823
|
-
return this.#forward(stream, await conversation.watch(context3), send, state);
|
|
6181
|
+
return this.#forward(stream, await conversation.watch(context3, await viewWindow(conversation, state?.window)), send, state);
|
|
4824
6182
|
}
|
|
4825
6183
|
#hostValue(stream) {
|
|
4826
6184
|
return Object.hasOwn(this.#hostValues, stream) ? this.#hostValues[stream] : undefined;
|
|
@@ -4838,13 +6196,17 @@ class GatewayClient {
|
|
|
4838
6196
|
});
|
|
4839
6197
|
let inner;
|
|
4840
6198
|
let stopped = false;
|
|
4841
|
-
const
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
|
|
6199
|
+
const observation = harness.observeCommits({
|
|
6200
|
+
advance: (publication) => {
|
|
6201
|
+
const matches = publication.changes.some((change) => (change.type === "document" || change.type === "document.copy") && change.record.kind === kind && change.conversationId === id);
|
|
6202
|
+
if (!matches)
|
|
6203
|
+
return;
|
|
6204
|
+
unsubscribe();
|
|
6205
|
+
setImmediate(() => signalCreation());
|
|
6206
|
+
},
|
|
6207
|
+
end: () => {}
|
|
4847
6208
|
});
|
|
6209
|
+
const unsubscribe = () => observation.detach();
|
|
4848
6210
|
try {
|
|
4849
6211
|
const existing = await harness.watchDoc(token, id, context3);
|
|
4850
6212
|
if (existing !== undefined) {
|
|
@@ -4885,16 +6247,19 @@ class GatewayClient {
|
|
|
4885
6247
|
async#forward(stream, watch, send, state) {
|
|
4886
6248
|
if (state?.images === undefined) {
|
|
4887
6249
|
await send({ type: "snapshot", stream, value: watch.value });
|
|
4888
|
-
watch.start((
|
|
6250
|
+
watch.start((call) => call.kind === "value" ? send({ type: "ops", stream, ops: shownOps(call) }) : Promise.resolve());
|
|
4889
6251
|
return watch;
|
|
4890
6252
|
}
|
|
4891
6253
|
const images = state.images;
|
|
4892
6254
|
let before = watch.value;
|
|
4893
6255
|
state.view = before;
|
|
4894
6256
|
await send({ type: "snapshot", stream, value: await images.value(before) });
|
|
4895
|
-
watch.start(async (
|
|
6257
|
+
watch.start(async (call) => {
|
|
6258
|
+
if (call.kind !== "value")
|
|
6259
|
+
return;
|
|
6260
|
+
const ops = shownOps(call);
|
|
4896
6261
|
const sent = await images.ops(ops, before);
|
|
4897
|
-
before = value;
|
|
6262
|
+
before = isGone(call) ? null : call.value;
|
|
4898
6263
|
state.view = before;
|
|
4899
6264
|
return send({ type: "ops", stream, ops: sent });
|
|
4900
6265
|
});
|
|
@@ -4904,7 +6269,7 @@ class GatewayClient {
|
|
|
4904
6269
|
for (const state of this.#subscriptions.values()) {
|
|
4905
6270
|
if (state.images === undefined || state.view === undefined)
|
|
4906
6271
|
continue;
|
|
4907
|
-
const image = state.images.image(state.view, ref);
|
|
6272
|
+
const image = state.images.image(state.view, ref) ?? (state.earlier === undefined ? undefined : state.images.image({ entries: state.earlier }, ref));
|
|
4908
6273
|
if (image !== undefined)
|
|
4909
6274
|
return image;
|
|
4910
6275
|
}
|
|
@@ -4953,8 +6318,11 @@ class GatewayClient {
|
|
|
4953
6318
|
#submissionByRequest(conversationId, requestId) {
|
|
4954
6319
|
return this.#options.harness.commit((tx) => tx.submissionByRequest(conversationId, requestId), context3);
|
|
4955
6320
|
}
|
|
4956
|
-
async#
|
|
4957
|
-
|
|
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);
|
|
4958
6326
|
this.#options.harness.waitForTask(taskId, context3).then((receipt) => void this.send({ type: "notice", level: "info", message: `Compaction ${receipt.state.outcome.status}.` }), () => {});
|
|
4959
6327
|
return taskId;
|
|
4960
6328
|
}
|
|
@@ -4972,6 +6340,17 @@ class GatewayClient {
|
|
|
4972
6340
|
}
|
|
4973
6341
|
}
|
|
4974
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) {
|
|
4975
6354
|
switch (method) {
|
|
4976
6355
|
case "terminalInput": {
|
|
4977
6356
|
const input = args;
|
|
@@ -5001,6 +6380,11 @@ class GatewayClient {
|
|
|
5001
6380
|
this.#terminals().close(args.terminalId);
|
|
5002
6381
|
this.#terminalSizes.delete(args.terminalId);
|
|
5003
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
|
+
}
|
|
5004
6388
|
case "addProject":
|
|
5005
6389
|
return addProject(this.#options.harness, args.path, context3);
|
|
5006
6390
|
case "removeProject":
|
|
@@ -5013,6 +6397,9 @@ class GatewayClient {
|
|
|
5013
6397
|
const existing = await this.#submissionByRequest(input.conversationId, input.requestId);
|
|
5014
6398
|
if (existing !== undefined)
|
|
5015
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
|
+
}
|
|
5016
6403
|
const resolved = images();
|
|
5017
6404
|
const conversation2 = await this.#conversation(input.conversationId);
|
|
5018
6405
|
const attachments = await this.#attachmentText(input.attachments);
|
|
@@ -5062,13 +6449,27 @@ class GatewayClient {
|
|
|
5062
6449
|
}
|
|
5063
6450
|
case "archive": {
|
|
5064
6451
|
const input = args;
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
this.#hooks.
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
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}` });
|
|
5072
6473
|
}
|
|
5073
6474
|
return null;
|
|
5074
6475
|
}
|
|
@@ -5137,6 +6538,15 @@ class GatewayClient {
|
|
|
5137
6538
|
await this.#options.settings.update(args.patch);
|
|
5138
6539
|
return null;
|
|
5139
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;
|
|
5140
6550
|
case "createAgent": {
|
|
5141
6551
|
const store = this.#options.agents;
|
|
5142
6552
|
if (store === undefined)
|
|
@@ -5168,6 +6578,26 @@ class GatewayClient {
|
|
|
5168
6578
|
const input = args;
|
|
5169
6579
|
return listSkills(this.#options.harness, this.#skills, input.scope, context3);
|
|
5170
6580
|
}
|
|
6581
|
+
case "earlierEntries": {
|
|
6582
|
+
const { conversationId, before, limit } = args;
|
|
6583
|
+
const stream = `conversation:${conversationId}`;
|
|
6584
|
+
const state = this.#subscriptions.get(stream);
|
|
6585
|
+
if (state === undefined)
|
|
6586
|
+
throw new Error(`Not subscribed to conversation ${conversationId}`);
|
|
6587
|
+
const replaced = () => this.#subscriptions.get(stream) !== state;
|
|
6588
|
+
const resubscribed = () => new Error(`Conversation ${conversationId} was resubscribed while its page loaded`);
|
|
6589
|
+
const page = await turnPage(await this.#conversation(conversationId), before, limit);
|
|
6590
|
+
if (replaced())
|
|
6591
|
+
throw resubscribed();
|
|
6592
|
+
if (state.images === undefined)
|
|
6593
|
+
return page;
|
|
6594
|
+
const raw = page.entries;
|
|
6595
|
+
const { entries } = await state.images.value({ entries: raw });
|
|
6596
|
+
if (replaced())
|
|
6597
|
+
throw resubscribed();
|
|
6598
|
+
state.earlier = [...state.earlier ?? [], ...raw];
|
|
6599
|
+
return { ...page, entries };
|
|
6600
|
+
}
|
|
5171
6601
|
case "image": {
|
|
5172
6602
|
const input = args;
|
|
5173
6603
|
const image = this.#shownImage(input.ref);
|
|
@@ -5229,6 +6659,37 @@ class GatewayClient {
|
|
|
5229
6659
|
return { outcome: "steered" };
|
|
5230
6660
|
}, context3);
|
|
5231
6661
|
}
|
|
6662
|
+
case "reorderQueued": {
|
|
6663
|
+
const { conversationId, submissionId, before } = args;
|
|
6664
|
+
return await this.#options.harness.commit(async (tx) => {
|
|
6665
|
+
const queuedState = async (id) => {
|
|
6666
|
+
const record = await tx.submission(id);
|
|
6667
|
+
if (record === undefined)
|
|
6668
|
+
throw new Error(`Submission ${id} does not exist`);
|
|
6669
|
+
assertQueuedInput(record, conversationId);
|
|
6670
|
+
if (record.entry !== undefined)
|
|
6671
|
+
return "sent";
|
|
6672
|
+
return record.status === "queued" ? "queued" : "unavailable";
|
|
6673
|
+
};
|
|
6674
|
+
const moved = await queuedState(submissionId);
|
|
6675
|
+
if (moved !== "queued")
|
|
6676
|
+
return { outcome: moved };
|
|
6677
|
+
if (before !== undefined && await queuedState(before) !== "queued") {
|
|
6678
|
+
return { outcome: "unavailable" };
|
|
6679
|
+
}
|
|
6680
|
+
if (before === submissionId)
|
|
6681
|
+
return { outcome: "moved" };
|
|
6682
|
+
const items = (await tx.doc(InboxDoc, conversationId)).items;
|
|
6683
|
+
const from = items.findIndex((candidate) => candidate.id === submissionId);
|
|
6684
|
+
if (from < 0 || before !== undefined && !items.some((candidate) => candidate.id === before)) {
|
|
6685
|
+
return { outcome: "unavailable" };
|
|
6686
|
+
}
|
|
6687
|
+
const [item] = items.splice(from, 1);
|
|
6688
|
+
const to = before === undefined ? items.findLastIndex((candidate) => candidate.mode !== "write") + 1 : items.findIndex((candidate) => candidate.id === before);
|
|
6689
|
+
items.splice(to, 0, item);
|
|
6690
|
+
return { outcome: "moved" };
|
|
6691
|
+
}, context3);
|
|
6692
|
+
}
|
|
5232
6693
|
case "renameConversation": {
|
|
5233
6694
|
const { title } = args;
|
|
5234
6695
|
const trimmed = title.trim();
|
|
@@ -5250,17 +6711,20 @@ class GatewayClient {
|
|
|
5250
6711
|
return null;
|
|
5251
6712
|
}
|
|
5252
6713
|
case "abort":
|
|
6714
|
+
if (await this.#isSubagent(conversation.id)) {
|
|
6715
|
+
await this.#options.harness.commit((tx) => markStop(tx, conversation.id, "user"), context3);
|
|
6716
|
+
}
|
|
5253
6717
|
await conversation.abort(context3);
|
|
5254
6718
|
return null;
|
|
5255
6719
|
case "compact": {
|
|
5256
|
-
const { instructions } = args;
|
|
5257
|
-
return { taskId: await this.#compact(conversation,
|
|
6720
|
+
const { instructions: instructions2 } = args;
|
|
6721
|
+
return { taskId: await this.#compact(conversation, instructions2) };
|
|
5258
6722
|
}
|
|
5259
6723
|
case "listCommands":
|
|
5260
6724
|
return listCommands();
|
|
5261
6725
|
case "runCommand": {
|
|
5262
6726
|
const { name, args: input } = args;
|
|
5263
|
-
await runCommand({ compact: (
|
|
6727
|
+
await runCommand({ compact: (instructions2) => this.#compact(conversation, instructions2) }, name, input);
|
|
5264
6728
|
return null;
|
|
5265
6729
|
}
|
|
5266
6730
|
case "setModel": {
|
|
@@ -5336,7 +6800,7 @@ class GatewayClient {
|
|
|
5336
6800
|
const token = doc.token;
|
|
5337
6801
|
return await this.#options.harness.commit(async (tx) => {
|
|
5338
6802
|
const value = await tx.doc(token, conversation.id);
|
|
5339
|
-
if (!
|
|
6803
|
+
if (!Value.Check(QuestionSchema, value))
|
|
5340
6804
|
throw new Error(`Document ${kind} does not hold question requests`);
|
|
5341
6805
|
const request = value.requests.find((candidate) => candidate.id === requestId);
|
|
5342
6806
|
if (request === undefined)
|
|
@@ -5385,6 +6849,11 @@ class GatewayClient {
|
|
|
5385
6849
|
throw new Error("Terminals are unavailable on this host");
|
|
5386
6850
|
return this.#hooks.terminals;
|
|
5387
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
|
+
}
|
|
5388
6857
|
async#terminalOwner(conversation) {
|
|
5389
6858
|
const cwd = (await conversation.agent(context3)).cwd;
|
|
5390
6859
|
if (cwd === undefined)
|
|
@@ -5424,7 +6893,7 @@ class GatewayClient {
|
|
|
5424
6893
|
async#dismissPendingQuestions(conversationId, context4) {
|
|
5425
6894
|
const questionDocs = this.#questionDocs();
|
|
5426
6895
|
const snapshots = await Promise.all(questionDocs.map((doc) => this.#options.harness.snapshot(doc.token, conversationId, context4)));
|
|
5427
|
-
const pending = snapshots.some((value) =>
|
|
6896
|
+
const pending = snapshots.some((value) => Value.Check(QuestionSchema, value) && value.requests.some((request) => request.resolution === undefined));
|
|
5428
6897
|
if (!pending)
|
|
5429
6898
|
return false;
|
|
5430
6899
|
return await this.#options.harness.commit((tx) => dismissPendingQuestions(tx, questionDocs, conversationId), context4);
|
|
@@ -5433,7 +6902,7 @@ class GatewayClient {
|
|
|
5433
6902
|
return (this.#options.docs ?? []).filter((doc) => doc.presentation === "pinomad.question");
|
|
5434
6903
|
}
|
|
5435
6904
|
get #chatsPrefix() {
|
|
5436
|
-
return
|
|
6905
|
+
return join8(this.#options.dataDir, "chats") + sep6;
|
|
5437
6906
|
}
|
|
5438
6907
|
async#changes(conversation, context4) {
|
|
5439
6908
|
const cwd = (await conversation.agent(context4)).cwd;
|
|
@@ -5468,8 +6937,8 @@ class GatewayClient {
|
|
|
5468
6937
|
// apps/host/src/host.ts
|
|
5469
6938
|
import { randomBytes as randomBytes4 } from "node:crypto";
|
|
5470
6939
|
import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile3 } from "node:fs/promises";
|
|
5471
|
-
import { basename as basename3, join as
|
|
5472
|
-
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";
|
|
5473
6942
|
import { createRegistry, Harness } from "@earendil-works/pi-durable";
|
|
5474
6943
|
import { NodeExecutionEnv } from "@earendil-works/pi-durable/env/node";
|
|
5475
6944
|
|
|
@@ -5485,10 +6954,10 @@ import {
|
|
|
5485
6954
|
StreamableHttpTransport,
|
|
5486
6955
|
toLlmContent
|
|
5487
6956
|
} from "@earendil-works/pi-mcp";
|
|
5488
|
-
import { defineExtension as
|
|
6957
|
+
import { defineExtension as defineExtension7, defineTool as defineTool5 } from "@earendil-works/pi-durable";
|
|
5489
6958
|
import { Type as Type5 } from "typebox";
|
|
5490
6959
|
var ENV_VAR = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
5491
|
-
var
|
|
6960
|
+
var isObject3 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
5492
6961
|
function expand(value) {
|
|
5493
6962
|
let missing;
|
|
5494
6963
|
const expanded = value.replace(ENV_VAR, (_all, name) => {
|
|
@@ -5502,7 +6971,7 @@ function expand(value) {
|
|
|
5502
6971
|
function expandAll(values, field, error) {
|
|
5503
6972
|
if (values === undefined)
|
|
5504
6973
|
return;
|
|
5505
|
-
if (!
|
|
6974
|
+
if (!isObject3(values)) {
|
|
5506
6975
|
error(`"${field}" must be an object of strings`);
|
|
5507
6976
|
return;
|
|
5508
6977
|
}
|
|
@@ -5512,21 +6981,21 @@ function expandAll(values, field, error) {
|
|
|
5512
6981
|
error(`"${field}.${key}" must be a string`);
|
|
5513
6982
|
return;
|
|
5514
6983
|
}
|
|
5515
|
-
const
|
|
5516
|
-
if (typeof
|
|
5517
|
-
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}`);
|
|
5518
6987
|
return;
|
|
5519
6988
|
}
|
|
5520
|
-
expanded[key] =
|
|
6989
|
+
expanded[key] = result3;
|
|
5521
6990
|
}
|
|
5522
6991
|
return expanded;
|
|
5523
6992
|
}
|
|
5524
6993
|
function parseServer(name, raw, error) {
|
|
5525
|
-
const fail = (
|
|
5526
|
-
error(`${name}: ${
|
|
6994
|
+
const fail = (message2) => {
|
|
6995
|
+
error(`${name}: ${message2}`);
|
|
5527
6996
|
return;
|
|
5528
6997
|
};
|
|
5529
|
-
if (!
|
|
6998
|
+
if (!isObject3(raw))
|
|
5530
6999
|
return fail("server entry must be an object");
|
|
5531
7000
|
if (raw.enabled === false)
|
|
5532
7001
|
return { name, disabled: true };
|
|
@@ -5548,7 +7017,7 @@ function parseServer(name, raw, error) {
|
|
|
5548
7017
|
}
|
|
5549
7018
|
if (raw.cwd !== undefined && typeof raw.cwd !== "string")
|
|
5550
7019
|
return fail(`"cwd" must be a string`);
|
|
5551
|
-
const env = expandAll(raw.env, "env", (
|
|
7020
|
+
const env = expandAll(raw.env, "env", (message2) => error(`${name}: ${message2}`));
|
|
5552
7021
|
if (raw.env !== undefined && env === undefined)
|
|
5553
7022
|
return;
|
|
5554
7023
|
const cwd = raw.cwd === undefined ? homedir2() : isAbsolute2(raw.cwd) ? raw.cwd : resolve5(homedir2(), raw.cwd);
|
|
@@ -5563,7 +7032,7 @@ function parseServer(name, raw, error) {
|
|
|
5563
7032
|
if (raw.url !== undefined || raw.type === "http" || raw.type === "sse") {
|
|
5564
7033
|
if (typeof raw.url !== "string" || raw.url === "")
|
|
5565
7034
|
return fail(`HTTP server needs a "url"`);
|
|
5566
|
-
const headers = expandAll(raw.headers, "headers", (
|
|
7035
|
+
const headers = expandAll(raw.headers, "headers", (message2) => error(`${name}: ${message2}`));
|
|
5567
7036
|
if (raw.headers !== undefined && headers === undefined)
|
|
5568
7037
|
return;
|
|
5569
7038
|
return {
|
|
@@ -5585,13 +7054,13 @@ async function loadConfig(configPath) {
|
|
|
5585
7054
|
return { errors: [], entries: [] };
|
|
5586
7055
|
return { errors: [`${configPath}: ${error instanceof Error ? error.message : String(error)}`], entries: [] };
|
|
5587
7056
|
}
|
|
5588
|
-
if (!
|
|
7057
|
+
if (!isObject3(parsed) || parsed.mcpServers !== undefined && !isObject3(parsed.mcpServers)) {
|
|
5589
7058
|
return { errors: [`${configPath}: "mcpServers" must be an object`], entries: [] };
|
|
5590
7059
|
}
|
|
5591
7060
|
const errors = [];
|
|
5592
7061
|
const entries = [];
|
|
5593
7062
|
for (const [name, raw] of Object.entries(parsed.mcpServers ?? {})) {
|
|
5594
|
-
const entry = parseServer(name, raw, (
|
|
7063
|
+
const entry = parseServer(name, raw, (message2) => errors.push(message2));
|
|
5595
7064
|
if (entry !== undefined)
|
|
5596
7065
|
entries.push(entry);
|
|
5597
7066
|
}
|
|
@@ -5609,7 +7078,7 @@ var callToolResultSchema = (tool) => ({
|
|
|
5609
7078
|
required: ["content"]
|
|
5610
7079
|
});
|
|
5611
7080
|
function mcpTool(client, timeoutMs, exposure, name, tool) {
|
|
5612
|
-
return
|
|
7081
|
+
return defineTool5({
|
|
5613
7082
|
name,
|
|
5614
7083
|
description: tool.description ?? tool.title ?? tool.name,
|
|
5615
7084
|
parameters: Type5.Unsafe({
|
|
@@ -5620,11 +7089,11 @@ function mcpTool(client, timeoutMs, exposure, name, tool) {
|
|
|
5620
7089
|
...exposure === "direct" ? {} : { callers: ["tools"] },
|
|
5621
7090
|
structuredOutputSchema: Type5.Unsafe(callToolResultSchema(tool)),
|
|
5622
7091
|
execute: async (args, _api, context4) => {
|
|
5623
|
-
const
|
|
7092
|
+
const result3 = await client.callTool(tool.name, args, {
|
|
5624
7093
|
signal: context4.abortSignal ?? NEVER_ABORT,
|
|
5625
7094
|
timeoutMs
|
|
5626
7095
|
});
|
|
5627
|
-
return { output: toLlmContent(
|
|
7096
|
+
return { output: toLlmContent(result3), structuredOutput: result3, isError: result3.isError === true };
|
|
5628
7097
|
}
|
|
5629
7098
|
});
|
|
5630
7099
|
}
|
|
@@ -5644,7 +7113,7 @@ function startMcp(options) {
|
|
|
5644
7113
|
listener(value);
|
|
5645
7114
|
};
|
|
5646
7115
|
const install = () => {
|
|
5647
|
-
options.registry.install(
|
|
7116
|
+
options.registry.install(defineExtension7({ name: "mcp", tools: [...toolsets.values()].flat() }));
|
|
5648
7117
|
options.onNamespaces?.([...namespaces.values()]);
|
|
5649
7118
|
};
|
|
5650
7119
|
const set = (name, patch) => {
|
|
@@ -5764,7 +7233,7 @@ ${stderr}`;
|
|
|
5764
7233
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
5765
7234
|
import { existsSync as existsSync2, watch } from "node:fs";
|
|
5766
7235
|
import { mkdir as mkdir2, readdir as readdir2, readFile as readFile3, rename, rm as rm2, writeFile } from "node:fs/promises";
|
|
5767
|
-
import { join as
|
|
7236
|
+
import { join as join9 } from "node:path";
|
|
5768
7237
|
import { parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
5769
7238
|
|
|
5770
7239
|
// packages/protocol/src/agents.ts
|
|
@@ -5808,7 +7277,7 @@ async function load(dir) {
|
|
|
5808
7277
|
const problem = (reason2) => void problems.push({ file, reason: reason2 });
|
|
5809
7278
|
let parsed;
|
|
5810
7279
|
try {
|
|
5811
|
-
parsed = parseFrontmatter(await readFile3(
|
|
7280
|
+
parsed = parseFrontmatter(await readFile3(join9(dir, file), "utf8"));
|
|
5812
7281
|
} catch (error) {
|
|
5813
7282
|
problem(`Couldn't parse the file: ${error instanceof Error ? error.message : String(error)}`);
|
|
5814
7283
|
continue;
|
|
@@ -5920,13 +7389,13 @@ async function openAgentRoles(options) {
|
|
|
5920
7389
|
if (problem !== undefined)
|
|
5921
7390
|
throw new Error(problem);
|
|
5922
7391
|
await enqueue(async () => {
|
|
5923
|
-
const target =
|
|
7392
|
+
const target = join9(dir, `${role.name}.md`);
|
|
5924
7393
|
const exists = existsSync2(target);
|
|
5925
7394
|
if (mustExist && !exists)
|
|
5926
7395
|
throw new Error(`No such agent: "${role.name}"`);
|
|
5927
7396
|
if (!mustExist && exists)
|
|
5928
7397
|
throw new Error(`Agent "${role.name}" already exists`);
|
|
5929
|
-
const temp =
|
|
7398
|
+
const temp = join9(dir, `.${role.name}.${randomBytes2(6).toString("hex")}.tmp`);
|
|
5930
7399
|
try {
|
|
5931
7400
|
await writeFile(temp, serialize(role));
|
|
5932
7401
|
await rename(temp, target);
|
|
@@ -5949,7 +7418,7 @@ async function openAgentRoles(options) {
|
|
|
5949
7418
|
if (problem !== undefined)
|
|
5950
7419
|
throw new Error(problem);
|
|
5951
7420
|
await enqueue(async () => {
|
|
5952
|
-
const target =
|
|
7421
|
+
const target = join9(dir, `${name}.md`);
|
|
5953
7422
|
if (!existsSync2(target))
|
|
5954
7423
|
throw new Error(`No such agent: "${name}"`);
|
|
5955
7424
|
await rm2(target);
|
|
@@ -5983,7 +7452,7 @@ import {
|
|
|
5983
7452
|
stat as stat7,
|
|
5984
7453
|
writeFile as writeFile2
|
|
5985
7454
|
} from "node:fs/promises";
|
|
5986
|
-
import { basename as basename2, join as
|
|
7455
|
+
import { basename as basename2, join as join10, resolve as resolve6 } from "node:path";
|
|
5987
7456
|
var META = "meta.json";
|
|
5988
7457
|
var DATA = "file";
|
|
5989
7458
|
var CLEANUP_INTERVAL_MS = 6 * 60 * 60 * 1000;
|
|
@@ -6078,16 +7547,16 @@ class AttachmentStore {
|
|
|
6078
7547
|
return next;
|
|
6079
7548
|
}
|
|
6080
7549
|
#directory(id) {
|
|
6081
|
-
return
|
|
7550
|
+
return join10(this.#root, id);
|
|
6082
7551
|
}
|
|
6083
7552
|
#dataPath(meta) {
|
|
6084
|
-
return
|
|
7553
|
+
return join10(this.#directory(meta.id), DATA, meta.fileName);
|
|
6085
7554
|
}
|
|
6086
7555
|
async#readMeta(id) {
|
|
6087
7556
|
if (!isAttachmentId(id))
|
|
6088
7557
|
return;
|
|
6089
7558
|
try {
|
|
6090
|
-
return metaOf(JSON.parse(await readFile4(
|
|
7559
|
+
return metaOf(JSON.parse(await readFile4(join10(this.#directory(id), META), "utf8")), id);
|
|
6091
7560
|
} catch {
|
|
6092
7561
|
return;
|
|
6093
7562
|
}
|
|
@@ -6095,10 +7564,10 @@ class AttachmentStore {
|
|
|
6095
7564
|
async#writeMeta(meta) {
|
|
6096
7565
|
const { id, ...value } = meta;
|
|
6097
7566
|
const directory = this.#directory(id);
|
|
6098
|
-
const temporary =
|
|
7567
|
+
const temporary = join10(directory, `.meta.${randomBytes3(6).toString("hex")}.tmp`);
|
|
6099
7568
|
await writeFile2(temporary, `${JSON.stringify(value)}
|
|
6100
7569
|
`, { mode: 384 });
|
|
6101
|
-
await rename2(temporary,
|
|
7570
|
+
await rename2(temporary, join10(directory, META));
|
|
6102
7571
|
}
|
|
6103
7572
|
async#rebuildPending() {
|
|
6104
7573
|
this.#pending.clear();
|
|
@@ -6143,7 +7612,7 @@ class AttachmentStore {
|
|
|
6143
7612
|
expiresAt: this.#now() + this.#ttlMs
|
|
6144
7613
|
};
|
|
6145
7614
|
try {
|
|
6146
|
-
await mkdir3(
|
|
7615
|
+
await mkdir3(join10(directory, DATA), { recursive: true });
|
|
6147
7616
|
await writeFile2(this.#dataPath(meta), new Uint8Array(0), { mode: 384 });
|
|
6148
7617
|
if (size === 0)
|
|
6149
7618
|
await chmod(this.#dataPath(meta), 292);
|
|
@@ -6279,16 +7748,16 @@ class AttachmentStore {
|
|
|
6279
7748
|
}
|
|
6280
7749
|
|
|
6281
7750
|
// apps/host/src/answer-timing.ts
|
|
6282
|
-
function withAnswerStart(
|
|
6283
|
-
const pinomad =
|
|
6284
|
-
return { ...
|
|
7751
|
+
function withAnswerStart(message2, answerStartedAt) {
|
|
7752
|
+
const pinomad = message2.pinomad;
|
|
7753
|
+
return { ...message2, pinomad: { ...pinomad, answerStartedAt } };
|
|
6285
7754
|
}
|
|
6286
7755
|
function stripPinomad(context4) {
|
|
6287
7756
|
let changed = false;
|
|
6288
|
-
const messages = context4.messages.map((
|
|
6289
|
-
if (
|
|
6290
|
-
return
|
|
6291
|
-
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;
|
|
6292
7761
|
changed = true;
|
|
6293
7762
|
return rest;
|
|
6294
7763
|
});
|
|
@@ -6313,7 +7782,7 @@ function trackAnswerStart(stream) {
|
|
|
6313
7782
|
const push = stream.push.bind(stream);
|
|
6314
7783
|
stream.push = (event) => push(annotate(event));
|
|
6315
7784
|
const end = stream.end.bind(stream);
|
|
6316
|
-
stream.end = (
|
|
7785
|
+
stream.end = (result3) => end(result3 === undefined || answerStartedAt === undefined ? result3 : withAnswerStart(result3, answerStartedAt));
|
|
6317
7786
|
return stream;
|
|
6318
7787
|
}
|
|
6319
7788
|
function withAnswerTiming(models) {
|
|
@@ -6328,10 +7797,594 @@ function withAnswerTiming(models) {
|
|
|
6328
7797
|
});
|
|
6329
7798
|
}
|
|
6330
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
|
+
|
|
6331
8384
|
// apps/host/src/host.ts
|
|
6332
|
-
var context4 =
|
|
8385
|
+
var context4 = BACKGROUND_CONTEXT6;
|
|
6333
8386
|
async function hostToken(dataDir) {
|
|
6334
|
-
const path =
|
|
8387
|
+
const path = join11(dataDir, "token");
|
|
6335
8388
|
try {
|
|
6336
8389
|
return (await readFile5(path, "utf8")).trim();
|
|
6337
8390
|
} catch {
|
|
@@ -6346,7 +8399,7 @@ async function openHost(options) {
|
|
|
6346
8399
|
validatePublicUrl(options.remote.publicUrl);
|
|
6347
8400
|
await mkdir4(options.dataDir, { recursive: true });
|
|
6348
8401
|
const stale = Math.max(2000, options.lockStaleMs ?? 1e4);
|
|
6349
|
-
const
|
|
8402
|
+
const release2 = await lockfile.lock(options.dataDir, {
|
|
6350
8403
|
realpath: false,
|
|
6351
8404
|
stale,
|
|
6352
8405
|
retries: { retries: Math.ceil(stale / 1000) + 2, minTimeout: 1000, maxTimeout: 1000 }
|
|
@@ -6354,26 +8407,38 @@ async function openHost(options) {
|
|
|
6354
8407
|
throw new Error(`Session is already open in another process: ${options.dataDir}`, { cause: error });
|
|
6355
8408
|
});
|
|
6356
8409
|
let harness;
|
|
8410
|
+
let processes;
|
|
6357
8411
|
let mcp;
|
|
6358
8412
|
let agentRoles;
|
|
6359
8413
|
let attachments;
|
|
6360
8414
|
let extensions = [];
|
|
8415
|
+
let upgrader;
|
|
6361
8416
|
try {
|
|
6362
8417
|
const models = withAnswerTiming(options.models);
|
|
6363
8418
|
const token = await hostToken(options.dataDir);
|
|
6364
|
-
attachments = new AttachmentStore({ dir:
|
|
8419
|
+
attachments = new AttachmentStore({ dir: join11(options.dataDir, "attachments") });
|
|
6365
8420
|
await attachments.open();
|
|
6366
8421
|
const reports = [];
|
|
6367
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
|
+
});
|
|
6368
8432
|
const registry = createRegistry();
|
|
6369
|
-
agentRoles = await openAgentRoles({ dir:
|
|
8433
|
+
agentRoles = await openAgentRoles({ dir: join11(options.dataDir, "agents") });
|
|
6370
8434
|
extensions = typeof options.extensions === "function" ? options.extensions({
|
|
6371
8435
|
harness: () => {
|
|
6372
8436
|
if (harness === undefined)
|
|
6373
8437
|
throw new Error("harness is not open yet");
|
|
6374
8438
|
return harness;
|
|
6375
8439
|
},
|
|
6376
|
-
agentRoles
|
|
8440
|
+
agentRoles,
|
|
8441
|
+
...processes === undefined ? {} : { processes }
|
|
6377
8442
|
}) : options.extensions ?? [];
|
|
6378
8443
|
for (const { extension } of extensions)
|
|
6379
8444
|
registry.install(extension);
|
|
@@ -6389,13 +8454,13 @@ async function openHost(options) {
|
|
|
6389
8454
|
}
|
|
6390
8455
|
});
|
|
6391
8456
|
const hostSettings = await openHostSettings({
|
|
6392
|
-
path:
|
|
8457
|
+
path: join11(options.dataDir, "settings.json"),
|
|
6393
8458
|
fallback: options.settingsFallback,
|
|
6394
8459
|
models: options.modelAuth?.models ?? constantValue(options.modelSummaries())
|
|
6395
8460
|
});
|
|
6396
8461
|
const environments = new Map;
|
|
6397
|
-
const chatsDir =
|
|
6398
|
-
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")), {
|
|
6399
8464
|
models,
|
|
6400
8465
|
registry,
|
|
6401
8466
|
env: async (target) => {
|
|
@@ -6403,7 +8468,7 @@ async function openHost(options) {
|
|
|
6403
8468
|
if (cwd === undefined)
|
|
6404
8469
|
return;
|
|
6405
8470
|
const worktrees = worktreeRoot(options.dataDir);
|
|
6406
|
-
if (cwd === worktrees || cwd.startsWith(worktrees +
|
|
8471
|
+
if (cwd === worktrees || cwd.startsWith(worktrees + sep7)) {
|
|
6407
8472
|
const index = await target.read.snapshot(IndexDoc, context4);
|
|
6408
8473
|
const record = checkoutAt(index?.checkouts, cwd);
|
|
6409
8474
|
if (record !== undefined) {
|
|
@@ -6425,6 +8490,10 @@ async function openHost(options) {
|
|
|
6425
8490
|
},
|
|
6426
8491
|
onReport: (error) => report(error)
|
|
6427
8492
|
}, context4);
|
|
8493
|
+
if (processes === undefined)
|
|
8494
|
+
await settleLostProcesses(harness, context4);
|
|
8495
|
+
else
|
|
8496
|
+
await processes.open(context4);
|
|
6428
8497
|
await ensureIndex(harness, context4);
|
|
6429
8498
|
await ensureDevices(harness, context4);
|
|
6430
8499
|
for (const path of options.projects ?? [])
|
|
@@ -6432,6 +8501,18 @@ async function openHost(options) {
|
|
|
6432
8501
|
const hostKey = await loadHostKey(options.dataDir);
|
|
6433
8502
|
const offers = pairingOffers(options.remote?.pairingTtlMs);
|
|
6434
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
|
+
});
|
|
6435
8516
|
const gateway = await startGateway({
|
|
6436
8517
|
harness,
|
|
6437
8518
|
models,
|
|
@@ -6461,6 +8542,8 @@ async function openHost(options) {
|
|
|
6461
8542
|
},
|
|
6462
8543
|
...options.rtc === undefined ? {} : { rtc: options.rtc },
|
|
6463
8544
|
...options.terminals === undefined ? {} : { terminals: options.terminals },
|
|
8545
|
+
...processes === undefined ? {} : { processes },
|
|
8546
|
+
...upgrader === undefined ? {} : { upgrade: upgrader },
|
|
6464
8547
|
secure: {
|
|
6465
8548
|
hostKey,
|
|
6466
8549
|
offers,
|
|
@@ -6493,6 +8576,7 @@ async function openHost(options) {
|
|
|
6493
8576
|
harness,
|
|
6494
8577
|
closed: opened.closed,
|
|
6495
8578
|
settings: hostSettings,
|
|
8579
|
+
...processes === undefined ? {} : { processes },
|
|
6496
8580
|
createAccountOffer: async () => {
|
|
6497
8581
|
const binding = await readServerBinding(options.dataDir).catch(() => {
|
|
6498
8582
|
return;
|
|
@@ -6512,6 +8596,7 @@ async function openHost(options) {
|
|
|
6512
8596
|
close() {
|
|
6513
8597
|
closing ??= (async () => {
|
|
6514
8598
|
try {
|
|
8599
|
+
upgrader?.close();
|
|
6515
8600
|
await gateway.close();
|
|
6516
8601
|
await opened.close(context4);
|
|
6517
8602
|
await mcp?.close();
|
|
@@ -6522,13 +8607,16 @@ async function openHost(options) {
|
|
|
6522
8607
|
options.modelAuth?.close();
|
|
6523
8608
|
await Promise.all([...environments.values()].map((env) => env.cleanup(context4)));
|
|
6524
8609
|
} finally {
|
|
6525
|
-
await
|
|
8610
|
+
await release2();
|
|
6526
8611
|
}
|
|
6527
8612
|
})();
|
|
6528
8613
|
return closing;
|
|
6529
8614
|
}
|
|
6530
8615
|
};
|
|
6531
8616
|
} catch (error) {
|
|
8617
|
+
upgrader?.close();
|
|
8618
|
+
if (processes !== undefined)
|
|
8619
|
+
await processes.close(context4).catch(() => {});
|
|
6532
8620
|
options.modelAuth?.close();
|
|
6533
8621
|
attachments?.close();
|
|
6534
8622
|
agentRoles?.close();
|
|
@@ -6536,18 +8624,18 @@ async function openHost(options) {
|
|
|
6536
8624
|
await each.close?.().catch(() => {});
|
|
6537
8625
|
await mcp?.close().catch(() => {});
|
|
6538
8626
|
await harness?.close(context4).catch(() => {});
|
|
6539
|
-
await
|
|
8627
|
+
await release2().catch(() => {});
|
|
6540
8628
|
throw error;
|
|
6541
8629
|
}
|
|
6542
8630
|
}
|
|
6543
8631
|
|
|
6544
8632
|
// apps/host/src/service/log-rotation.ts
|
|
6545
8633
|
import { copyFile, rename as rename3, rm as rm4, stat as stat8, truncate } from "node:fs/promises";
|
|
6546
|
-
import { join as
|
|
8634
|
+
import { join as join12 } from "node:path";
|
|
6547
8635
|
var HOST_LOG_MAX_BYTES = 10 * 1024 * 1024;
|
|
6548
8636
|
var HOST_LOG_HISTORY = 3;
|
|
6549
8637
|
var HOST_LOG_CHECK_INTERVAL_MS = 10 * 60 * 1000;
|
|
6550
|
-
var hostLogPath = (dataDir) =>
|
|
8638
|
+
var hostLogPath = (dataDir) => join12(dataDir, "logs", "host.log");
|
|
6551
8639
|
var rotationTempPath = (log) => `${log}.rotating`;
|
|
6552
8640
|
var isMissing = (error) => error.code === "ENOENT";
|
|
6553
8641
|
async function rotateHostLog(dataDir, copy = copyFile) {
|
|
@@ -6590,8 +8678,8 @@ function startHostLogRotation(dataDir) {
|
|
|
6590
8678
|
return;
|
|
6591
8679
|
running = true;
|
|
6592
8680
|
rotateHostLog(dataDir).catch((error) => {
|
|
6593
|
-
const
|
|
6594
|
-
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 })}
|
|
6595
8683
|
`);
|
|
6596
8684
|
}).finally(() => {
|
|
6597
8685
|
running = false;
|
|
@@ -6604,10 +8692,10 @@ function startHostLogRotation(dataDir) {
|
|
|
6604
8692
|
}
|
|
6605
8693
|
|
|
6606
8694
|
// apps/host/src/model-auth.ts
|
|
6607
|
-
import { randomUUID as
|
|
8695
|
+
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
6608
8696
|
import { mkdir as mkdir5, readFile as readFile6, rename as rename4, writeFile as writeFile4 } from "node:fs/promises";
|
|
6609
8697
|
import { dirname as dirname3 } from "node:path";
|
|
6610
|
-
import { getBuiltinProviders } from "@earendil-works/pi-ai/providers/all";
|
|
8698
|
+
import { getBuiltinModels, getBuiltinProviders } from "@earendil-works/pi-ai/providers/all";
|
|
6611
8699
|
|
|
6612
8700
|
// apps/host/src/pi-setup.ts
|
|
6613
8701
|
import { getSupportedThinkingLevels as getSupportedThinkingLevels2 } from "@earendil-works/pi-ai";
|
|
@@ -6722,11 +8810,22 @@ function modelSummaries(modelRuntime) {
|
|
|
6722
8810
|
}
|
|
6723
8811
|
|
|
6724
8812
|
// apps/host/src/model-auth.ts
|
|
6725
|
-
var
|
|
8813
|
+
var BUILTIN_PROVIDERS = getBuiltinProviders();
|
|
8814
|
+
var BUILTIN = new Set(BUILTIN_PROVIDERS);
|
|
6726
8815
|
var CUSTOM_ID = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
6727
8816
|
var LOGIN_TIMEOUT_MS = 15 * 60000;
|
|
6728
8817
|
var FINISHED_LOGIN_TTL_MS = 60000;
|
|
6729
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
|
+
}
|
|
6730
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);
|
|
6731
8830
|
var sameLogin = (a, b) => a.providerId === b.providerId && a.name === b.name && a.kind === b.kind && a.subscription === b.subscription;
|
|
6732
8831
|
async function readModelsFile(path) {
|
|
@@ -6887,6 +8986,45 @@ async function openModelAuth(runtime, options) {
|
|
|
6887
8986
|
tail = next.catch(() => {});
|
|
6888
8987
|
return next;
|
|
6889
8988
|
};
|
|
8989
|
+
const opened = await readModelsFile(options.modelsPath);
|
|
8990
|
+
Promise.all([...custom].flatMap(([id, endpoint]) => {
|
|
8991
|
+
const entries = opened.providers[id]?.models;
|
|
8992
|
+
if (!Array.isArray(entries) || !entries.some(lacksLimits))
|
|
8993
|
+
return [];
|
|
8994
|
+
return [
|
|
8995
|
+
(async () => {
|
|
8996
|
+
const key = (await runtime.getAuth(id))?.auth.apiKey;
|
|
8997
|
+
const listed = await fetchEndpointModels(endpoint.api, endpoint.baseUrl, key).catch(() => []);
|
|
8998
|
+
return { id, endpoint, listed: new Map(listed.map((model) => [model.id, model])) };
|
|
8999
|
+
})()
|
|
9000
|
+
];
|
|
9001
|
+
})).then((answers) => serial(async () => {
|
|
9002
|
+
if (closed)
|
|
9003
|
+
return;
|
|
9004
|
+
const filled = new Set;
|
|
9005
|
+
const fill = (target) => {
|
|
9006
|
+
for (const { id, endpoint, listed } of answers) {
|
|
9007
|
+
const entry = target.providers[id];
|
|
9008
|
+
if (entry?.api !== endpoint.api || entry.baseUrl !== endpoint.baseUrl || !Array.isArray(entry.models))
|
|
9009
|
+
continue;
|
|
9010
|
+
for (const model of entry.models) {
|
|
9011
|
+
const limits = listed.get(model.id);
|
|
9012
|
+
for (const field of LIMITS) {
|
|
9013
|
+
if (field in model || limits?.[field] === undefined)
|
|
9014
|
+
continue;
|
|
9015
|
+
model[field] = limits[field];
|
|
9016
|
+
filled.add(id);
|
|
9017
|
+
}
|
|
9018
|
+
}
|
|
9019
|
+
}
|
|
9020
|
+
};
|
|
9021
|
+
fill(await readModelsFile(options.modelsPath));
|
|
9022
|
+
if (filled.size === 0)
|
|
9023
|
+
return;
|
|
9024
|
+
await updateModelsFile(fill);
|
|
9025
|
+
await runtime.refresh({ providers: [...filled], allowNetwork: false });
|
|
9026
|
+
await republish();
|
|
9027
|
+
}));
|
|
6890
9028
|
return {
|
|
6891
9029
|
models,
|
|
6892
9030
|
auth,
|
|
@@ -6908,7 +9046,7 @@ async function openModelAuth(runtime, options) {
|
|
|
6908
9046
|
if ([...logins.values()].some((login2) => login2.flow.providerId === providerId && login2.flow.state === "running")) {
|
|
6909
9047
|
throw new Error(`A login for ${provider.name} is already in progress`);
|
|
6910
9048
|
}
|
|
6911
|
-
const id =
|
|
9049
|
+
const id = randomUUID5();
|
|
6912
9050
|
const login = {
|
|
6913
9051
|
flow: { id, providerId, method, events: [], state: "running" },
|
|
6914
9052
|
controller: new AbortController,
|
|
@@ -7073,32 +9211,98 @@ async function fetchEndpointModels(api, baseUrl, key) {
|
|
|
7073
9211
|
if (!response.ok)
|
|
7074
9212
|
throw new Error(`${url.host} answered ${response.status} for /models`);
|
|
7075
9213
|
const body = await response.json();
|
|
7076
|
-
const models = api === "google-generative-ai" ? (body.models ?? []).flatMap((model) =>
|
|
7077
|
-
|
|
7078
|
-
|
|
7079
|
-
|
|
7080
|
-
|
|
7081
|
-
|
|
7082
|
-
|
|
7083
|
-
|
|
7084
|
-
|
|
7085
|
-
|
|
7086
|
-
|
|
7087
|
-
|
|
7088
|
-
|
|
7089
|
-
|
|
7090
|
-
|
|
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
|
+
});
|
|
7091
9243
|
if (models.length === 0)
|
|
7092
9244
|
throw new Error(`${url.host} listed no models`);
|
|
7093
9245
|
return models;
|
|
7094
9246
|
}
|
|
9247
|
+
var LIMITS = ["contextWindow", "maxTokens"];
|
|
9248
|
+
var lacksLimits = (model) => typeof model === "object" && model !== null && LIMITS.some((field) => !(field in model));
|
|
7095
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
|
+
}
|
|
7096
9299
|
function endpointModel(id, reported) {
|
|
7097
9300
|
return {
|
|
7098
9301
|
id,
|
|
7099
9302
|
...reported.contextWindow === undefined ? {} : { contextWindow: reported.contextWindow },
|
|
7100
9303
|
...reported.maxTokens === undefined ? {} : { maxTokens: reported.maxTokens },
|
|
7101
9304
|
...reported.reasoning ? { reasoning: true } : {},
|
|
9305
|
+
...reported.thinkingLevelMap === undefined ? {} : { thinkingLevelMap: reported.thinkingLevelMap },
|
|
7102
9306
|
...reported.image ? { input: ["text", "image"] } : {}
|
|
7103
9307
|
};
|
|
7104
9308
|
}
|
|
@@ -7133,29 +9337,40 @@ async function main(argv) {
|
|
|
7133
9337
|
}
|
|
7134
9338
|
if (relayOrigin !== undefined)
|
|
7135
9339
|
validateRelayOrigin(relayOrigin);
|
|
7136
|
-
const agentsHome =
|
|
7137
|
-
const extensionsFor = (models, summaries, options2) => (host2) =>
|
|
7138
|
-
|
|
7139
|
-
|
|
7140
|
-
|
|
7141
|
-
|
|
7142
|
-
|
|
7143
|
-
|
|
7144
|
-
|
|
7145
|
-
|
|
7146
|
-
|
|
7147
|
-
|
|
7148
|
-
|
|
7149
|
-
|
|
7150
|
-
|
|
7151
|
-
|
|
7152
|
-
|
|
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
|
+
};
|
|
7153
9368
|
const common = {
|
|
7154
9369
|
dataDir,
|
|
7155
9370
|
agentsHome,
|
|
7156
9371
|
projects: values.project ?? [],
|
|
7157
9372
|
port: Number(values.port),
|
|
7158
|
-
mcpConfig:
|
|
9373
|
+
mcpConfig: join13(agentsHome, "mcp.json"),
|
|
7159
9374
|
browserOrigins: values["browser-origin"] ?? ["http://127.0.0.1:5199"],
|
|
7160
9375
|
webRoot,
|
|
7161
9376
|
...values["lock-stale-ms"] === undefined ? {} : { lockStaleMs: Number(values["lock-stale-ms"]) },
|
|
@@ -7198,10 +9413,10 @@ async function main(argv) {
|
|
|
7198
9413
|
};
|
|
7199
9414
|
}
|
|
7200
9415
|
async function piOptions() {
|
|
7201
|
-
const modelsPath =
|
|
7202
|
-
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 });
|
|
7203
9418
|
const modelAuth = await openModelAuth(modelRuntime, { modelsPath, installId: await installId(dataDir) });
|
|
7204
|
-
const settingsManager = SettingsManager.inMemory(await piSettings(
|
|
9419
|
+
const settingsManager = SettingsManager.inMemory(await piSettings(join13(dataDir, "settings.json")));
|
|
7205
9420
|
configureHarnessHttp(settingsManager);
|
|
7206
9421
|
const summaries = () => modelAuth.models.value;
|
|
7207
9422
|
const images = await createNodePhotonImages();
|
|
@@ -7226,7 +9441,11 @@ async function main(argv) {
|
|
|
7226
9441
|
};
|
|
7227
9442
|
}
|
|
7228
9443
|
const options = values.faux !== undefined ? fauxOptions(() => () => fauxAssistantMessage(values.faux)) : await piOptions();
|
|
7229
|
-
|
|
9444
|
+
let stop = () => {};
|
|
9445
|
+
const host = await openHost({
|
|
9446
|
+
...options,
|
|
9447
|
+
...packagedVersion === undefined ? {} : { upgrade: { current: packagedVersion, restart: () => stop(UPGRADE_EXIT_CODE) } }
|
|
9448
|
+
});
|
|
7230
9449
|
const stopLogRotation = startHostLogRotation(dataDir);
|
|
7231
9450
|
const { defaultModel: effectiveModel } = host.settings.value;
|
|
7232
9451
|
const model = effectiveModel === null ? null : `${effectiveModel.provider}/${effectiveModel.modelId}`;
|
|
@@ -7234,7 +9453,7 @@ async function main(argv) {
|
|
|
7234
9453
|
console.log(JSON.stringify({
|
|
7235
9454
|
event: "ready",
|
|
7236
9455
|
url: host.url,
|
|
7237
|
-
tokenFile:
|
|
9456
|
+
tokenFile: join13(dataDir, "token"),
|
|
7238
9457
|
dataDir,
|
|
7239
9458
|
model,
|
|
7240
9459
|
web,
|
|
@@ -7242,17 +9461,21 @@ async function main(argv) {
|
|
|
7242
9461
|
...host.relay === undefined ? {} : { relay: { origin: host.relay.origin, hostId: host.relay.hostId } }
|
|
7243
9462
|
}));
|
|
7244
9463
|
let stopping = false;
|
|
7245
|
-
|
|
9464
|
+
stop = (code) => {
|
|
7246
9465
|
if (stopping)
|
|
7247
9466
|
return;
|
|
7248
9467
|
stopping = true;
|
|
7249
9468
|
stopLogRotation();
|
|
7250
|
-
|
|
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));
|
|
7251
9474
|
};
|
|
7252
|
-
process.on("SIGINT", stop);
|
|
7253
|
-
process.on("SIGTERM", stop);
|
|
9475
|
+
process.on("SIGINT", () => stop(0));
|
|
9476
|
+
process.on("SIGTERM", () => stop(0));
|
|
7254
9477
|
host.closed.then((end) => {
|
|
7255
|
-
if (end.
|
|
9478
|
+
if (end.kind !== "failed" || stopping)
|
|
7256
9479
|
return;
|
|
7257
9480
|
stopping = true;
|
|
7258
9481
|
stopLogRotation();
|
|
@@ -7280,10 +9503,10 @@ async function piSettings(path) {
|
|
|
7280
9503
|
return typeof block === "object" && block !== null && !Array.isArray(block) ? block : {};
|
|
7281
9504
|
}
|
|
7282
9505
|
async function installId(dataDir) {
|
|
7283
|
-
const path =
|
|
9506
|
+
const path = join13(dataDir, "install-id");
|
|
7284
9507
|
let id = (await readFile7(path, "utf8").catch(() => "")).trim();
|
|
7285
9508
|
if (id === "") {
|
|
7286
|
-
id =
|
|
9509
|
+
id = randomUUID6();
|
|
7287
9510
|
await mkdir6(dataDir, { recursive: true });
|
|
7288
9511
|
await writeFile5(path, `${id}
|
|
7289
9512
|
`, { mode: 384 });
|