pinomad 0.1.1 → 0.3.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/README.md +60 -180
- package/dist/{chunk-tpd3trvd.js → chunk-3qn8gbjb.js} +1 -1
- package/dist/{chunk-fpb6f9n5.js → chunk-3tf23vjq.js} +1 -1
- package/dist/{chunk-ne6a5q2j.js → chunk-4dj2vb8t.js} +1 -1
- package/dist/{chunk-y73t2jmc.js → chunk-bggesvnj.js} +253 -69
- package/dist/chunk-c155p0t0.js +154 -0
- package/dist/{chunk-9z7echn7.js → chunk-kxws8b83.js} +4 -4
- package/dist/{chunk-8h3wrn84.js → chunk-n60kkf1m.js} +4 -4
- package/dist/{chunk-hsdzb73r.js → chunk-syf0z68f.js} +10 -4
- package/dist/{chunk-zx77w1nm.js → chunk-t9y2wfk5.js} +2 -2
- package/dist/{chunk-mqbg2cbw.js → chunk-tfqc1djy.js} +1390 -749
- package/dist/pinomad.js +7 -7
- package/package.json +7 -7
- package/web/assets/index-CBXdQLua.js +383 -0
- package/web/assets/index-CG33GBxM.css +1 -0
- package/web/index.html +2 -2
- package/dist/chunk-wgn3cx5q.js +0 -56
- package/web/assets/index-B9yvK0AN.js +0 -383
- package/web/assets/index-t5dAiohJ.css +0 -1
|
@@ -5,12 +5,17 @@ import {
|
|
|
5
5
|
import {
|
|
6
6
|
packagedVersion,
|
|
7
7
|
webRoot
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-3tf23vjq.js";
|
|
9
9
|
import {
|
|
10
|
-
|
|
10
|
+
HOST_SETTING_KEYS,
|
|
11
|
+
INVALID_FRAME_CLOSE_CODE,
|
|
12
|
+
PROTOCOL,
|
|
13
|
+
THINKING_LEVELS,
|
|
11
14
|
UNAUTHORIZED_CLOSE_CODE,
|
|
12
|
-
|
|
13
|
-
|
|
15
|
+
isQueueMode,
|
|
16
|
+
isThinkingLevel,
|
|
17
|
+
parseClientFrame
|
|
18
|
+
} from "./chunk-c155p0t0.js";
|
|
14
19
|
import {
|
|
15
20
|
DevicesDoc,
|
|
16
21
|
ensureDevices,
|
|
@@ -40,12 +45,13 @@ import {
|
|
|
40
45
|
import"./chunk-df32avw6.js";
|
|
41
46
|
|
|
42
47
|
// apps/host/src/main.ts
|
|
43
|
-
import { homedir as
|
|
44
|
-
import { join as
|
|
48
|
+
import { homedir as homedir3 } from "node:os";
|
|
49
|
+
import { join as join8, resolve as resolve3 } from "node:path";
|
|
45
50
|
import { parseArgs } from "node:util";
|
|
46
51
|
import { createModels } from "@earendil-works/pi-ai/models";
|
|
47
52
|
import { fauxAssistantMessage, fauxProvider } from "@earendil-works/pi-ai/providers/faux";
|
|
48
|
-
import { ModelRuntime, SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
53
|
+
import { getAgentDir, ModelRuntime, SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
54
|
+
import { createNodePhotonImages } from "@earendil-works/pi-durable/images/node";
|
|
49
55
|
|
|
50
56
|
// apps/host/src/extensions/question.ts
|
|
51
57
|
import {
|
|
@@ -92,6 +98,7 @@ var QuestionRequestSchema = Type.Object({
|
|
|
92
98
|
var QuestionSchema = Type.Object({ requests: Type.Array(QuestionRequestSchema) });
|
|
93
99
|
var DiffSchema = Type.Object({ patch: Type.String() });
|
|
94
100
|
var CodemodeCallSchema = Type.Object({
|
|
101
|
+
id: Type.Optional(Type.Number()),
|
|
95
102
|
name: Type.String(),
|
|
96
103
|
args: Type.String(),
|
|
97
104
|
status: Type.Union([Type.Literal("running"), Type.Literal("ok"), Type.Literal("error"), Type.Literal("cancelled")]),
|
|
@@ -99,7 +106,7 @@ var CodemodeCallSchema = Type.Object({
|
|
|
99
106
|
error: Type.Optional(Type.String()),
|
|
100
107
|
details: Type.Optional(Type.Unknown())
|
|
101
108
|
});
|
|
102
|
-
var CodemodeSchema = Type.Object({ code: Type.String(), calls: Type.Array(CodemodeCallSchema) });
|
|
109
|
+
var CodemodeSchema = Type.Object({ code: Type.Optional(Type.String()), calls: Type.Array(CodemodeCallSchema) });
|
|
103
110
|
|
|
104
111
|
// apps/host/src/extensions/question.ts
|
|
105
112
|
var QuestionDoc = defineDoc({
|
|
@@ -168,10 +175,11 @@ function result(questions, resolution) {
|
|
|
168
175
|
} else {
|
|
169
176
|
text = "Question cancelled.";
|
|
170
177
|
}
|
|
171
|
-
return {
|
|
178
|
+
return { output: [{ type: "text", text }], details: { resolution } };
|
|
172
179
|
}
|
|
173
180
|
var askUserQuestion = defineTool({
|
|
174
181
|
name: "ask_user_question",
|
|
182
|
+
callers: ["model"],
|
|
175
183
|
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 headers short.",
|
|
176
184
|
parameters: Type2.Object({
|
|
177
185
|
questions: Type2.Array(QuestionItemSchema, { minItems: 1, maxItems: 4 })
|
|
@@ -198,13 +206,12 @@ var askUserQuestion = defineTool({
|
|
|
198
206
|
});
|
|
199
207
|
var question = {
|
|
200
208
|
extension: defineExtension({ name: "question", tools: [askUserQuestion], tasks: [QuestionWait] }),
|
|
201
|
-
modelOnly: ["ask_user_question"],
|
|
202
209
|
docs: [{ token: QuestionDoc, presentation: "pinomad.question" }]
|
|
203
210
|
};
|
|
204
211
|
|
|
205
212
|
// apps/host/src/extensions/coding.ts
|
|
206
213
|
import { wrapTool } from "@earendil-works/pi-durable";
|
|
207
|
-
import {
|
|
214
|
+
import { createCodingTools, createWriteTool } from "@earendil-works/pi-durable/tools";
|
|
208
215
|
import { createPatch } from "diff";
|
|
209
216
|
var writeWithDiff = wrapTool(createWriteTool(), (tool) => ({
|
|
210
217
|
...tool,
|
|
@@ -220,433 +227,68 @@ var writeWithDiff = wrapTool(createWriteTool(), (tool) => ({
|
|
|
220
227
|
return { ...result2, details: { patch: createPatch(args.path, oldContent, args.content) } };
|
|
221
228
|
}
|
|
222
229
|
}));
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
}
|
|
230
|
+
function createCoding(options) {
|
|
231
|
+
return {
|
|
232
|
+
extension: { ...createCodingTools(options), wraps: [writeWithDiff] },
|
|
233
|
+
tools: { edit: "pinomad.diff", write: "pinomad.diff" }
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
var coding = createCoding();
|
|
227
237
|
|
|
228
238
|
// apps/host/src/extensions/codemode.ts
|
|
229
|
-
import {
|
|
230
|
-
import {
|
|
231
|
-
CodemodeSandbox,
|
|
232
|
-
CodemodeSourceError,
|
|
233
|
-
loadQuickJSWasm,
|
|
234
|
-
parseCodemodeSource,
|
|
235
|
-
renderToolSignature
|
|
236
|
-
} from "@earendil-works/pi-codemode";
|
|
237
|
-
import { validateToolArguments } from "@earendil-works/pi-ai/utils/validation";
|
|
238
|
-
import {
|
|
239
|
-
defineExtension as defineExtension2,
|
|
240
|
-
defineTool as defineTool2,
|
|
241
|
-
section
|
|
242
|
-
} from "@earendil-works/pi-durable";
|
|
243
|
-
import { Type as Type3 } from "typebox";
|
|
239
|
+
import { CodemodeSandbox, loadQuickJSWasm } from "@earendil-works/pi-codemode";
|
|
240
|
+
import { createCodemodeExtension } from "@earendil-works/pi-durable/codemode";
|
|
244
241
|
var wasm = loadQuickJSWasm();
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
class NestedOutput {
|
|
249
|
-
#head = "";
|
|
250
|
-
#tail = "";
|
|
251
|
-
#dropped = 0;
|
|
252
|
-
#decoder = new TextDecoder;
|
|
253
|
-
push(chunk) {
|
|
254
|
-
const text = typeof chunk === "string" ? chunk : this.#decoder.decode(chunk, { stream: true });
|
|
255
|
-
if (this.#dropped === 0 && this.#head.length + text.length <= OUTPUT_CAP) {
|
|
256
|
-
this.#head += text;
|
|
257
|
-
return;
|
|
258
|
-
}
|
|
259
|
-
if (this.#dropped === 0) {
|
|
260
|
-
const rest = this.#head.slice(OUTPUT_HALF) + text;
|
|
261
|
-
this.#head = this.#head.slice(0, OUTPUT_HALF);
|
|
262
|
-
this.#dropped = Math.max(0, rest.length - OUTPUT_HALF);
|
|
263
|
-
this.#tail = rest.slice(-OUTPUT_HALF);
|
|
264
|
-
return;
|
|
265
|
-
}
|
|
266
|
-
const merged = this.#tail + text;
|
|
267
|
-
this.#dropped += Math.max(0, merged.length - OUTPUT_HALF);
|
|
268
|
-
this.#tail = merged.slice(-OUTPUT_HALF);
|
|
269
|
-
}
|
|
270
|
-
text() {
|
|
271
|
-
const tail = this.#tail + this.#decoder.decode();
|
|
272
|
-
return this.#dropped === 0 ? this.#head + tail : `${this.#head}
|
|
273
|
-
[... ${this.#dropped} characters omitted ...]
|
|
274
|
-
${tail}`;
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
var summarizeArgs = (args) => {
|
|
278
|
-
let text;
|
|
279
|
-
try {
|
|
280
|
-
text = JSON.stringify(args) ?? "null";
|
|
281
|
-
} catch {
|
|
282
|
-
text = String(args);
|
|
283
|
-
}
|
|
284
|
-
return text.length <= 200 ? text : `${text.slice(0, 199)}…`;
|
|
285
|
-
};
|
|
286
|
-
var errorText = (error) => error instanceof Error ? error.message : String(error);
|
|
287
|
-
var textOf = (content) => content.flatMap((block) => block.type === "text" ? [block.text] : []).join("");
|
|
288
|
-
var TOKENIZE = /[^a-z0-9]+/;
|
|
289
|
-
var tokens = (text) => text.replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase().split(TOKENIZE).filter(Boolean);
|
|
290
|
-
function search(callables, query, limit, namespace) {
|
|
291
|
-
const docs = [...callables].filter((tool) => namespace === undefined || tool.namespace?.name === namespace).map((tool) => {
|
|
292
|
-
const text = [tool.name, tool.description, tool.namespace?.name, tool.namespace?.description].filter(Boolean).join(" ");
|
|
293
|
-
return { tool, terms: tokens(text) };
|
|
294
|
-
});
|
|
295
|
-
const avgLength = docs.length === 0 ? 1 : docs.reduce((sum, doc) => sum + doc.terms.length, 0) / docs.length;
|
|
296
|
-
const frequency = new Map;
|
|
297
|
-
for (const doc of docs)
|
|
298
|
-
for (const term of new Set(doc.terms))
|
|
299
|
-
frequency.set(term, (frequency.get(term) ?? 0) + 1);
|
|
300
|
-
const scored = docs.map((doc) => {
|
|
301
|
-
const counts = new Map;
|
|
302
|
-
for (const term of doc.terms)
|
|
303
|
-
counts.set(term, (counts.get(term) ?? 0) + 1);
|
|
304
|
-
let score = 0;
|
|
305
|
-
for (const term of tokens(query)) {
|
|
306
|
-
const df = frequency.get(term);
|
|
307
|
-
if (df === undefined)
|
|
308
|
-
continue;
|
|
309
|
-
const idf = Math.log(1 + (docs.length - df + 0.5) / (df + 0.5));
|
|
310
|
-
const tf = counts.get(term) ?? 0;
|
|
311
|
-
score += idf * tf * 2.2 / (tf + 1.2 * (0.25 + 0.75 * doc.terms.length / avgLength));
|
|
312
|
-
}
|
|
313
|
-
return { tool: doc.tool, score };
|
|
314
|
-
});
|
|
315
|
-
return scored.filter((entry) => entry.score > 0).sort((a, b) => b.score - a.score).slice(0, limit).map((entry) => ({
|
|
316
|
-
name: entry.tool.name,
|
|
317
|
-
description: entry.tool.description.length <= 300 ? entry.tool.description : `${entry.tool.description.slice(0, 299)}…`
|
|
318
|
-
}));
|
|
319
|
-
}
|
|
320
|
-
function createCodemode(options) {
|
|
321
|
-
const excluded = new Set([...options.modelOnly, "codemode"]);
|
|
322
|
-
const modelOnlyList = [...excluded].join(", ");
|
|
323
|
-
const tool = defineTool2({
|
|
324
|
-
name: "codemode",
|
|
325
|
-
description: DESCRIPTION.replace("{MODEL_ONLY}", modelOnlyList),
|
|
326
|
-
parameters: Type3.Object({
|
|
327
|
-
code: Type3.String({ minLength: 1, description: "Raw JavaScript source; see the tool description." })
|
|
328
|
-
}),
|
|
329
|
-
execute: async (args, api, context) => {
|
|
330
|
-
let parsed;
|
|
331
|
-
try {
|
|
332
|
-
parsed = parseCodemodeSource(args.code);
|
|
333
|
-
} catch (error) {
|
|
334
|
-
if (error instanceof CodemodeSourceError) {
|
|
335
|
-
return { content: [{ type: "text", text: `Script failed: ${error.message}` }], isError: true };
|
|
336
|
-
}
|
|
337
|
-
throw error;
|
|
338
|
-
}
|
|
339
|
-
const agent = await api.agent(context);
|
|
340
|
-
const callables = new Map;
|
|
341
|
-
for (const offered of agent.tools) {
|
|
342
|
-
if (!excluded.has(offered.name))
|
|
343
|
-
callables.set(offered.name, { kind: "agent", tool: offered });
|
|
344
|
-
}
|
|
345
|
-
for (const scriptTool of options.scriptTools())
|
|
346
|
-
callables.set(scriptTool.name, { kind: "script", tool: scriptTool });
|
|
347
|
-
const calls = [];
|
|
348
|
-
let detailsBudget = 512 * 1024;
|
|
349
|
-
const snapshot = () => ({
|
|
350
|
-
code: parsed.code,
|
|
351
|
-
calls: calls.map((call) => ({ ...call }))
|
|
352
|
-
});
|
|
353
|
-
const publish = () => {
|
|
354
|
-
api.details(snapshot(), context).catch(() => {});
|
|
355
|
-
};
|
|
356
|
-
const executeNested = async (name, callArgs, signal) => {
|
|
357
|
-
const record = { name, args: summarizeArgs(callArgs), status: "running" };
|
|
358
|
-
calls.push(record);
|
|
359
|
-
publish();
|
|
360
|
-
const started2 = Date.now();
|
|
361
|
-
try {
|
|
362
|
-
const callable = callables.get(name);
|
|
363
|
-
if (callable === undefined)
|
|
364
|
-
throw new Error(`Unknown tool "${name}"`);
|
|
365
|
-
const value = callable.kind === "script" ? await callable.tool.call(callArgs, signal) : await runAgentTool(callable.tool, callArgs, signal, record, calls.length - 1);
|
|
366
|
-
record.status = signal.aborted ? "cancelled" : "ok";
|
|
367
|
-
return value;
|
|
368
|
-
} catch (error) {
|
|
369
|
-
record.status = signal.aborted ? "cancelled" : "error";
|
|
370
|
-
record.error = errorText(error).slice(0, 500);
|
|
371
|
-
throw error;
|
|
372
|
-
} finally {
|
|
373
|
-
record.durationMs = Date.now() - started2;
|
|
374
|
-
publish();
|
|
375
|
-
}
|
|
376
|
-
};
|
|
377
|
-
const runAgentTool = async (offered, callArgs, signal, record, index) => {
|
|
378
|
-
const prepared = offered.prepareArguments === undefined ? callArgs : offered.prepareArguments(callArgs);
|
|
379
|
-
const validated = validateToolArguments(offered, {
|
|
380
|
-
type: "toolCall",
|
|
381
|
-
id: `${api.callId}/${index}`,
|
|
382
|
-
name: offered.name,
|
|
383
|
-
arguments: prepared
|
|
384
|
-
});
|
|
385
|
-
const output = new NestedOutput;
|
|
386
|
-
const diagnostics = [];
|
|
387
|
-
let reported;
|
|
388
|
-
const derived = {
|
|
389
|
-
...api,
|
|
390
|
-
callId: `${api.callId}/${index}`,
|
|
391
|
-
output: (chunk) => output.push(chunk),
|
|
392
|
-
outputWindow: undefined,
|
|
393
|
-
diagnostic: (diagnostic) => diagnostics.push(`[${diagnostic.severity}] ${diagnostic.message}`),
|
|
394
|
-
details: async (value) => {
|
|
395
|
-
reported = value;
|
|
396
|
-
}
|
|
397
|
-
};
|
|
398
|
-
let result2;
|
|
399
|
-
try {
|
|
400
|
-
result2 = await offered.execute(validated, derived, withAbortSignal(signal, context));
|
|
401
|
-
} catch (error) {
|
|
402
|
-
if (signal.aborted)
|
|
403
|
-
throw error;
|
|
404
|
-
throw new Error(`${[output.text(), errorText(error)].filter(Boolean).join(`
|
|
405
|
-
`)}`.trim());
|
|
406
|
-
}
|
|
407
|
-
const captured = result2.details ?? reported;
|
|
408
|
-
if (captured !== undefined && options.presentations[offered.name] !== undefined) {
|
|
409
|
-
try {
|
|
410
|
-
const size = JSON.stringify(captured).length;
|
|
411
|
-
if (size <= 64 * 1024 && detailsBudget >= size) {
|
|
412
|
-
detailsBudget -= size;
|
|
413
|
-
record.details = captured;
|
|
414
|
-
}
|
|
415
|
-
} catch {}
|
|
416
|
-
}
|
|
417
|
-
const collected = [output.text(), ...diagnostics].filter(Boolean).join(`
|
|
418
|
-
`);
|
|
419
|
-
const content = result2.content ?? [{ type: "text", text: collected }];
|
|
420
|
-
if (result2.isError === true)
|
|
421
|
-
throw new Error(textOf(content) || collected);
|
|
422
|
-
const texts = content.filter((block) => block.type === "text");
|
|
423
|
-
return texts.length === content.length ? texts.map((block) => block.text).join("") : content;
|
|
424
|
-
};
|
|
425
|
-
const globals = [
|
|
426
|
-
{
|
|
427
|
-
name: "searchTools",
|
|
428
|
-
spread: true,
|
|
429
|
-
description: "Search callable tools by relevance.",
|
|
430
|
-
execute: (callArgs) => {
|
|
431
|
-
const [query, opts] = callArgs;
|
|
432
|
-
return search([...callables.values()].map((callable) => callable.kind === "script" ? { name: callable.tool.name, description: callable.tool.description, namespace: callable.tool.namespace } : { name: callable.tool.name, description: callable.tool.description ?? "", namespace: undefined }), String(query ?? ""), typeof opts?.limit === "number" && opts.limit > 0 ? Math.floor(opts.limit) : 8, opts?.namespace);
|
|
433
|
-
}
|
|
434
|
-
},
|
|
435
|
-
{
|
|
436
|
-
name: "describeTool",
|
|
437
|
-
description: "The declaration of one callable tool, or undefined.",
|
|
438
|
-
execute: (callArgs) => {
|
|
439
|
-
const callable = callables.get(String(callArgs));
|
|
440
|
-
if (callable === undefined)
|
|
441
|
-
return;
|
|
442
|
-
return {
|
|
443
|
-
name: callable.tool.name,
|
|
444
|
-
description: callable.tool.description,
|
|
445
|
-
declaration: renderToolSignature(callable.kind === "script" ? { name: callable.tool.name, inputSchema: callable.tool.inputSchema, outputSchema: callable.tool.outputSchema } : { name: callable.tool.name, inputSchema: callable.tool.parameters })
|
|
446
|
-
};
|
|
447
|
-
}
|
|
448
|
-
},
|
|
449
|
-
{
|
|
450
|
-
name: "describeNamespace",
|
|
451
|
-
description: "The tools of one script-only namespace, or undefined.",
|
|
452
|
-
execute: (callArgs) => {
|
|
453
|
-
const wanted = String(callArgs);
|
|
454
|
-
const members = options.scriptTools().filter((tool2) => tool2.namespace?.name === wanted);
|
|
455
|
-
if (members.length === 0)
|
|
456
|
-
return;
|
|
457
|
-
const first = members[0].namespace;
|
|
458
|
-
return {
|
|
459
|
-
name: wanted,
|
|
460
|
-
...first.description === undefined ? {} : { description: first.description },
|
|
461
|
-
...first.instructions === undefined ? {} : { instructions: first.instructions },
|
|
462
|
-
tools: members.map((tool2) => ({ name: tool2.name, description: tool2.description }))
|
|
463
|
-
};
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
];
|
|
467
|
-
const sandbox = new CodemodeSandbox({
|
|
468
|
-
tools: [...callables.values()].map((callable) => ({
|
|
469
|
-
name: callable.tool.name,
|
|
470
|
-
...callable.tool.description === undefined ? {} : { description: callable.tool.description },
|
|
471
|
-
inputSchema: callable.kind === "script" ? callable.tool.inputSchema : callable.tool.parameters,
|
|
472
|
-
...callable.kind === "script" && callable.tool.outputSchema !== undefined ? { outputSchema: callable.tool.outputSchema } : {},
|
|
473
|
-
execute: (callArgs, { signal }) => executeNested(callable.tool.name, callArgs, signal)
|
|
474
|
-
})),
|
|
475
|
-
globals,
|
|
476
|
-
memoryLimitBytes: 256 * 1024 * 1024,
|
|
477
|
-
timeoutMs: parsed.options.timeoutMs ?? Infinity,
|
|
478
|
-
wasm
|
|
479
|
-
});
|
|
480
|
-
const started = Date.now();
|
|
481
|
-
try {
|
|
482
|
-
const result2 = await sandbox.execute(parsed.code, { signal: context.abortSignal });
|
|
483
|
-
context.abortSignal?.throwIfAborted();
|
|
484
|
-
return {
|
|
485
|
-
content: resultContent(result2, started, parsed.options.maxOutputTokens),
|
|
486
|
-
isError: !result2.ok,
|
|
487
|
-
details: snapshot()
|
|
488
|
-
};
|
|
489
|
-
} finally {
|
|
490
|
-
await sandbox.close();
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
});
|
|
242
|
+
function createCodemode() {
|
|
243
|
+
const sandbox = new CodemodeSandbox({ wasm, memoryLimitBytes: 256 * 1024 * 1024 });
|
|
244
|
+
const namespaced = (namespaces) => createCodemodeExtension({ sandbox, namespaces });
|
|
494
245
|
return {
|
|
495
|
-
extension:
|
|
496
|
-
name: "codemode",
|
|
497
|
-
tools: [tool],
|
|
498
|
-
sections: [
|
|
499
|
-
section("codemode", (input) => {
|
|
500
|
-
const offered = new Set(input.agent.tools.map((each) => each.name));
|
|
501
|
-
const namespaces = new Map;
|
|
502
|
-
for (const tool2 of options.scriptTools()) {
|
|
503
|
-
if (tool2.namespace === undefined)
|
|
504
|
-
continue;
|
|
505
|
-
const entry = namespaces.get(tool2.namespace.name) ?? { scriptOnly: 0 };
|
|
506
|
-
namespaces.set(tool2.namespace.name, {
|
|
507
|
-
description: entry.description ?? tool2.namespace.description,
|
|
508
|
-
scriptOnly: entry.scriptOnly + (offered.has(tool2.name) ? 0 : 1)
|
|
509
|
-
});
|
|
510
|
-
}
|
|
511
|
-
const scriptOnly = [...namespaces.entries()].filter(([, entry]) => entry.scriptOnly > 0).sort(([a], [b]) => a.localeCompare(b));
|
|
512
|
-
let text = `Every tool declared to you except ${modelOnlyList} can also be called from codemode scripts as tools.<name>(args), with the same arguments.`;
|
|
513
|
-
if (scriptOnly.length > 0) {
|
|
514
|
-
const lines = scriptOnly.map(([name, entry]) => {
|
|
515
|
-
const firstLine = entry.description?.split(`
|
|
516
|
-
`)[0];
|
|
517
|
-
const clipped = firstLine === undefined ? "" : `: ${firstLine.slice(0, 200)}`;
|
|
518
|
-
return `- ${name} (${entry.scriptOnly} tools)${clipped}`;
|
|
519
|
-
});
|
|
520
|
-
text += `
|
|
521
|
-
|
|
522
|
-
These namespaces are reachable only from codemode scripts; find their tools with describeNamespace(name) or searchTools(query):
|
|
523
|
-
${lines.join(`
|
|
524
|
-
`)}`;
|
|
525
|
-
}
|
|
526
|
-
return text;
|
|
527
|
-
})
|
|
528
|
-
]
|
|
529
|
-
}),
|
|
246
|
+
extension: namespaced([]),
|
|
530
247
|
tools: { codemode: "pinomad.codemode" },
|
|
531
|
-
|
|
248
|
+
namespaced,
|
|
249
|
+
close: () => sandbox.close()
|
|
532
250
|
};
|
|
533
251
|
}
|
|
534
|
-
var DESCRIPTION = `Run a JavaScript script that calls other tools; only what the script outputs reaches you. Use it to run several tool calls in parallel (Promise.all / Promise.allSettled), to filter or aggregate large results before you see them, and to call tools that are only reachable from scripts, such as most MCP tools.
|
|
535
|
-
|
|
536
|
-
\`code\` is raw JavaScript, not JSON and not a markdown fence. It runs as the body of an async function in a QuickJS sandbox, so top-level await and return work. There are no Node APIs, file system, network, timers, or modules: scripts reach the outside world only through tools. The script may start with one options line: // @options: {"max_output_tokens": 2000, "timeout_ms": 60000}. max_output_tokens defaults to 10000; longer output keeps its start and end. timeout_ms is unset by default.
|
|
537
|
-
|
|
538
|
-
Globals:
|
|
539
|
-
- tools.<name>(args): call a tool with one arguments object. Characters that are invalid in identifiers become _, so mcp__my-server__search is tools.mcp__my_server__search.
|
|
540
|
-
- text(value): add to the output; strings as is, other values as JSON. console.log(...) and a top-level \`return value\` do the same.
|
|
541
|
-
- image(block): add an image from an image block { type: "image", data, mimeType } or a base64 data: URL.
|
|
542
|
-
- exit(): end the script successfully.
|
|
543
|
-
- ALL_TOOLS: every callable tool as { name, description }.
|
|
544
|
-
- searchTools(query, { limit?, namespace? }): resolves to the callable tools ranked by relevance (default limit 8), as { name, description }[].
|
|
545
|
-
- describeTool(name): resolves to { name, description, declaration } with the tool's TypeScript declaration, or undefined.
|
|
546
|
-
- describeNamespace(name): resolves to { name, description?, instructions?, tools } for a namespace such as an MCP server, or undefined.
|
|
547
|
-
These three return promises: await them.
|
|
548
|
-
|
|
549
|
-
What a call resolves to: MCP tools resolve to their CallToolResult { content, isError?, structuredContent? }, also when isError is true. Other tools resolve to their text output, or to an array of text and image blocks when the output has images. A call that fails or gets invalid arguments rejects with an Error carrying the tool's error text; use Promise.allSettled to keep the calls that succeed. Tool calls are real: calls made before a failure are not undone, and calls still running when the script ends are cancelled.
|
|
550
|
-
|
|
551
|
-
Call describeTool before using a tool whose arguments you have not seen. Scripts cannot call {MODEL_ONLY}.`;
|
|
552
|
-
function resultContent(result2, started, maxOutputTokens) {
|
|
553
|
-
const seconds = ((Date.now() - started) / 1000).toFixed(1);
|
|
554
|
-
const items = [
|
|
555
|
-
{ type: "text", text: result2.ok ? `Script completed in ${seconds}s` : `Script failed in ${seconds}s` }
|
|
556
|
-
];
|
|
557
|
-
for (const item of result2.output) {
|
|
558
|
-
items.push(item.type === "text" ? { type: "text", text: item.text } : { type: "image", data: item.data, mimeType: item.mimeType });
|
|
559
|
-
}
|
|
560
|
-
if (result2.ok && result2.value !== undefined) {
|
|
561
|
-
items.push({ type: "text", text: typeof result2.value === "string" ? result2.value : JSON.stringify(result2.value) });
|
|
562
|
-
}
|
|
563
|
-
if (!result2.ok) {
|
|
564
|
-
const error = result2.error;
|
|
565
|
-
items.push({
|
|
566
|
-
type: "text",
|
|
567
|
-
text: error.kind === "timeout" ? "Script error: timed out" : error.kind === "aborted" ? "Script error: aborted" : `Script error: ${error.name ?? "Error"}: ${error.message}`
|
|
568
|
-
});
|
|
569
|
-
}
|
|
570
|
-
return truncateText(mergeText(items), (maxOutputTokens ?? 1e4) * 4);
|
|
571
|
-
}
|
|
572
|
-
function mergeText(items) {
|
|
573
|
-
const merged = [];
|
|
574
|
-
for (const item of items) {
|
|
575
|
-
const last = merged.at(-1);
|
|
576
|
-
if (item.type === "text" && last?.type === "text")
|
|
577
|
-
merged[merged.length - 1] = { type: "text", text: `${last.text}
|
|
578
|
-
${item.text}` };
|
|
579
|
-
else
|
|
580
|
-
merged.push(item);
|
|
581
|
-
}
|
|
582
|
-
return merged;
|
|
583
|
-
}
|
|
584
|
-
function truncateText(items, budget) {
|
|
585
|
-
const total = items.reduce((sum, item) => sum + (item.type === "text" ? item.text.length : 0), 0);
|
|
586
|
-
if (total <= budget)
|
|
587
|
-
return items;
|
|
588
|
-
const headBudget = Math.floor(budget / 2);
|
|
589
|
-
const tailStart = total - (budget - headBudget);
|
|
590
|
-
const omitted = total - headBudget - (total - tailStart);
|
|
591
|
-
const out = [];
|
|
592
|
-
let offset = 0;
|
|
593
|
-
let marked = false;
|
|
594
|
-
for (const item of items) {
|
|
595
|
-
if (item.type !== "text") {
|
|
596
|
-
out.push(item);
|
|
597
|
-
continue;
|
|
598
|
-
}
|
|
599
|
-
const end = offset + item.text.length;
|
|
600
|
-
const head = item.text.slice(0, Math.max(headBudget - offset, 0));
|
|
601
|
-
const tail = end > tailStart ? item.text.slice(Math.max(tailStart - offset, 0)) : "";
|
|
602
|
-
if (head !== "")
|
|
603
|
-
out.push({ type: "text", text: head });
|
|
604
|
-
if (!marked && end >= headBudget) {
|
|
605
|
-
out.push({ type: "text", text: `
|
|
606
|
-
[... ${omitted} characters omitted ...]
|
|
607
|
-
` });
|
|
608
|
-
marked = true;
|
|
609
|
-
}
|
|
610
|
-
if (tail !== "")
|
|
611
|
-
out.push({ type: "text", text: tail });
|
|
612
|
-
offset = end;
|
|
613
|
-
}
|
|
614
|
-
return mergeText(out);
|
|
615
|
-
}
|
|
616
252
|
|
|
617
253
|
// apps/host/src/extensions/context.ts
|
|
618
254
|
import { dirname, join, sep } from "node:path";
|
|
619
|
-
import { defineExtension as
|
|
255
|
+
import { defineExtension as defineExtension2, section } from "@earendil-works/pi-durable";
|
|
620
256
|
import { formatSkillsForPrompt, loadProjectContextFiles, loadSkills } from "@earendil-works/pi-coding-agent";
|
|
621
257
|
var PREAMBLE = "You are PiNomad, a coding agent running on the user's own machine. You work in the project directory below with the read, write, edit, and bash tools: read files before changing them, prefer edit over rewriting whole files, and run commands to check your work. Keep answers short and concrete. When it is unclear what the user wants, ask instead of guessing.";
|
|
258
|
+
var FORK_SHARED_DIR = "This conversation is a fork: it shares this working directory with the conversation it was forked from, which may be editing the same files. If your task needs isolation, create your own git worktree (for example `git worktree add <dir> -b <branch>`) and use absolute paths inside it for every read, write, edit and bash command.";
|
|
622
259
|
function createContext(options) {
|
|
623
260
|
const { agentsHome } = options;
|
|
624
261
|
const resolveCheckout = (input, context) => options.checkout?.(input, context);
|
|
625
262
|
return {
|
|
626
|
-
extension:
|
|
263
|
+
extension: defineExtension2({
|
|
627
264
|
name: "context",
|
|
628
265
|
sections: [
|
|
629
|
-
|
|
630
|
-
|
|
266
|
+
section("preamble", () => PREAMBLE, { tag: false }),
|
|
267
|
+
section("environment", async (input, context) => {
|
|
631
268
|
const cwd = input.agent.cwd;
|
|
632
269
|
if (cwd === undefined)
|
|
633
270
|
return;
|
|
634
|
-
const
|
|
635
|
-
|
|
636
|
-
|
|
271
|
+
const resolved = await resolveCheckout({ conversationId: input.conversationId, read: input.read }, context);
|
|
272
|
+
const checkout = resolved?.checkout;
|
|
273
|
+
const forkLine = resolved?.fork === true && checkout?.own !== true ? [FORK_SHARED_DIR] : [];
|
|
274
|
+
if (checkout === undefined) {
|
|
275
|
+
return [`Working directory: ${cwd}`, ...forkLine].join(`
|
|
276
|
+
`);
|
|
277
|
+
}
|
|
637
278
|
return [
|
|
638
279
|
`Working directory: ${cwd}`,
|
|
639
|
-
`This directory is a git worktree PiNomad created for this conversation, on branch ${checkout.branch} starting from commit ${checkout.base.slice(0, 12)}.`,
|
|
280
|
+
checkout.own ? `This directory is a git worktree PiNomad created for this conversation, on branch ${checkout.branch} starting from commit ${checkout.base.slice(0, 12)}.` : `This directory is a git worktree PiNomad created for the conversation this one belongs to, on branch ${checkout.branch} starting from commit ${checkout.base.slice(0, 12)}.`,
|
|
640
281
|
`The user's project directory is ${checkout.projectPath} and must not be modified.`,
|
|
641
|
-
"This is a fresh checkout: ignored files (dependencies such as node_modules, or .env) are absent — install dependencies per AGENTS.md when needed."
|
|
282
|
+
"This is a fresh checkout: ignored files (dependencies such as node_modules, or .env) are absent — install dependencies per AGENTS.md when needed.",
|
|
283
|
+
...forkLine
|
|
642
284
|
].join(`
|
|
643
285
|
`);
|
|
644
286
|
}),
|
|
645
|
-
|
|
287
|
+
section("project_context", async (input, context) => {
|
|
646
288
|
const cwd = input.agent.cwd;
|
|
647
289
|
if (cwd === undefined)
|
|
648
290
|
return;
|
|
649
|
-
const checkout = await resolveCheckout({ conversationId: input.conversationId, read: input.read }, context);
|
|
291
|
+
const checkout = (await resolveCheckout({ conversationId: input.conversationId, read: input.read }, context))?.checkout;
|
|
650
292
|
let files;
|
|
651
293
|
if (checkout === undefined) {
|
|
652
294
|
files = loadProjectContextFiles({ cwd, agentDir: agentsHome });
|
|
@@ -668,7 +310,7 @@ ${file.content}`).join(`
|
|
|
668
310
|
|
|
669
311
|
`);
|
|
670
312
|
}),
|
|
671
|
-
|
|
313
|
+
section("skills", (input) => {
|
|
672
314
|
const cwd = input.agent.cwd;
|
|
673
315
|
if (cwd === undefined)
|
|
674
316
|
return;
|
|
@@ -692,68 +334,124 @@ import {
|
|
|
692
334
|
AgentDoc,
|
|
693
335
|
AssistantEntry,
|
|
694
336
|
configure,
|
|
695
|
-
defineExtension as
|
|
696
|
-
defineTool as
|
|
337
|
+
defineExtension as defineExtension3,
|
|
338
|
+
defineTool as defineTool2,
|
|
339
|
+
section as section2
|
|
697
340
|
} from "@earendil-works/pi-durable";
|
|
698
|
-
import { Type as
|
|
341
|
+
import { Type as Type3 } from "typebox";
|
|
342
|
+
|
|
343
|
+
// apps/host/src/subagent-doc.ts
|
|
344
|
+
import { defineDoc as defineDoc2 } from "@earendil-works/pi-durable";
|
|
345
|
+
var SubagentDoc = defineDoc2({
|
|
346
|
+
kind: "pinomad.subagent",
|
|
347
|
+
version: 1,
|
|
348
|
+
scope: "conversation",
|
|
349
|
+
history: "latest",
|
|
350
|
+
fork: "initial",
|
|
351
|
+
initial: () => ({})
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// apps/host/src/extensions/subagent.ts
|
|
699
355
|
var INSTRUCTIONS = "You are a subagent working on one delegated task. You cannot ask the user questions; if you need a decision or clarification, stop and state exactly what you need in your final answer. Your final message is returned to the delegating agent as the result, so make it a complete, self-contained report.";
|
|
700
|
-
var
|
|
356
|
+
var THINKING_LEVELS2 = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
701
357
|
function createSubagent(options) {
|
|
702
358
|
const available = () => options.modelSummaries().map((model) => `${model.provider}/${model.modelId}`).join(", ");
|
|
703
|
-
const
|
|
359
|
+
const roles = options.roles ?? (() => []);
|
|
360
|
+
const roleNamed = (name) => {
|
|
361
|
+
const found = roles().find((each) => each.name === name);
|
|
362
|
+
if (found !== undefined)
|
|
363
|
+
return found;
|
|
364
|
+
const defined = roles();
|
|
365
|
+
throw new Error(defined.length === 0 ? "No named agents are defined" : `Unknown agent "${name}". Available agents: ${defined.map((each) => each.name).join(", ")}`);
|
|
366
|
+
};
|
|
367
|
+
const tool = defineTool2({
|
|
704
368
|
name: "subagent",
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
369
|
+
callers: ["model"],
|
|
370
|
+
description: "Delegate a self-contained task to a subagent that runs in its own conversation and returns its final answer. " + "The subagent starts with none of this conversation's context, so put everything it needs in `task`. " + "It works in the same files as you: subagents called in the same turn run concurrently, so give parallel subagents read-only work or non-overlapping 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; `description` only labels the call. " + `Available models: ${available()}.`,
|
|
371
|
+
parameters: Type3.Object({
|
|
372
|
+
task: Type3.String({
|
|
708
373
|
minLength: 1,
|
|
709
374
|
description: "Complete, self-contained instructions. The subagent sees none of this conversation."
|
|
710
375
|
}),
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
376
|
+
role: Type3.Optional(Type3.String({
|
|
377
|
+
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."
|
|
378
|
+
})),
|
|
379
|
+
description: Type3.Optional(Type3.String({ description: 'A short 3-6 word label for the user, e.g. "Audit auth middleware".' })),
|
|
380
|
+
model: Type3.Optional(Type3.String({ description: "Optional `provider/modelId` to use instead of yours." })),
|
|
381
|
+
thinkingLevel: Type3.Optional(Type3.Union(THINKING_LEVELS2.map((level) => Type3.Literal(level)), { description: "Optional thinking level instead of yours." }))
|
|
714
382
|
}),
|
|
715
383
|
replay: "safe",
|
|
716
384
|
execute: async (args, api, context) => {
|
|
717
385
|
const child = await api.commit(async (tx) => {
|
|
718
386
|
const existing = (await tx.scanConversations({ ownerTaskId: api.taskId }, 1)).items[0];
|
|
719
|
-
if (existing !== undefined)
|
|
720
|
-
|
|
387
|
+
if (existing !== undefined) {
|
|
388
|
+
const doc2 = await tx.doc(SubagentDoc, existing.id);
|
|
389
|
+
return {
|
|
390
|
+
id: existing.id,
|
|
391
|
+
...doc2?.model === undefined ? {} : { model: doc2.model },
|
|
392
|
+
...doc2?.startedAt === undefined ? {} : { startedAt: doc2.startedAt }
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
const role = args.role === undefined ? undefined : roleNamed(args.role);
|
|
396
|
+
if (role?.model !== undefined && args.model !== undefined) {
|
|
397
|
+
throw new Error(`Agent "${role.name}" pins model ${role.model}; omit \`model\``);
|
|
398
|
+
}
|
|
399
|
+
if (role?.thinkingLevel !== undefined && args.thinkingLevel !== undefined) {
|
|
400
|
+
throw new Error(`Agent "${role.name}" pins thinking level ${role.thinkingLevel}; omit \`thinkingLevel\``);
|
|
401
|
+
}
|
|
402
|
+
const modelString = role?.model ?? args.model;
|
|
403
|
+
const levelString = role?.thinkingLevel ?? args.thinkingLevel;
|
|
721
404
|
let model;
|
|
722
405
|
let thinkingLevel;
|
|
723
406
|
let override;
|
|
724
|
-
if (
|
|
725
|
-
const slash =
|
|
726
|
-
const provider = slash === -1 ? "" :
|
|
727
|
-
const modelId = slash === -1 ? "" :
|
|
407
|
+
if (modelString !== undefined) {
|
|
408
|
+
const slash = modelString.indexOf("/");
|
|
409
|
+
const provider = slash === -1 ? "" : modelString.slice(0, slash);
|
|
410
|
+
const modelId = slash === -1 ? "" : modelString.slice(slash + 1);
|
|
728
411
|
override = provider === "" || modelId === "" ? undefined : options.models.getModel(provider, modelId);
|
|
729
412
|
const listed = options.modelSummaries().some((each) => each.provider === provider && each.modelId === modelId);
|
|
730
|
-
if (override === undefined || !listed)
|
|
731
|
-
throw new Error(`Unknown model "${
|
|
413
|
+
if (override === undefined || !listed) {
|
|
414
|
+
throw new Error(role === undefined ? `Unknown model "${modelString}". Available models: ${available()}` : `Agent "${role.name}" uses model "${modelString}", which is not available. Available models: ${available()}`);
|
|
415
|
+
}
|
|
732
416
|
model = { provider, modelId };
|
|
733
417
|
}
|
|
734
|
-
if (
|
|
418
|
+
if (levelString !== undefined || override !== undefined) {
|
|
735
419
|
const parent = await tx.doc(AgentDoc, api.conversationId);
|
|
736
420
|
const effective = override ?? (parent.model === undefined ? undefined : options.models.getModel(parent.model.provider, parent.model.modelId));
|
|
737
|
-
if (
|
|
738
|
-
thinkingLevel = effective === undefined ?
|
|
421
|
+
if (levelString !== undefined) {
|
|
422
|
+
thinkingLevel = effective === undefined ? levelString : clampThinkingLevel(effective, levelString);
|
|
739
423
|
} else if (parent.thinkingLevel !== undefined) {
|
|
740
424
|
thinkingLevel = clampThinkingLevel(override, parent.thinkingLevel);
|
|
741
425
|
}
|
|
742
426
|
}
|
|
743
427
|
const change = {
|
|
744
428
|
extensions: { remove: [extension, ...options.exclude] },
|
|
745
|
-
instructions: INSTRUCTIONS
|
|
429
|
+
instructions: role === undefined || role.prompt === "" ? INSTRUCTIONS : `${INSTRUCTIONS}
|
|
430
|
+
|
|
431
|
+
${role.prompt}`,
|
|
746
432
|
...model === undefined ? {} : { model },
|
|
747
433
|
...thinkingLevel === undefined ? {} : { thinkingLevel }
|
|
748
434
|
};
|
|
749
435
|
const created = await tx.createConversation({ ownership: { kind: "task", taskId: api.taskId } });
|
|
750
436
|
await configure(tx, created.id, change);
|
|
751
|
-
|
|
437
|
+
const childAgent = await tx.doc(AgentDoc, created.id);
|
|
438
|
+
const ref = childAgent.model ?? model ?? (await tx.doc(AgentDoc, api.conversationId)).model;
|
|
439
|
+
const startedAt = Date.now();
|
|
440
|
+
const doc = await tx.doc(SubagentDoc, created.id);
|
|
441
|
+
doc.startedAt = startedAt;
|
|
442
|
+
if (ref !== undefined)
|
|
443
|
+
doc.model = `${ref.provider}/${ref.modelId}`;
|
|
444
|
+
if (role !== undefined)
|
|
445
|
+
doc.role = role.name;
|
|
446
|
+
const label = args.description?.trim();
|
|
447
|
+
if (label !== undefined && label !== "")
|
|
448
|
+
doc.label = label;
|
|
449
|
+
return { id: created.id, ...ref === undefined ? {} : { model: doc.model }, startedAt };
|
|
752
450
|
}, context);
|
|
753
|
-
await api.details({ conversationId: child }, context);
|
|
754
|
-
const handle = await api.conversation(child, context);
|
|
451
|
+
await api.details({ conversationId: child.id, ...child.model === undefined ? {} : { model: child.model }, ...child.startedAt === undefined ? {} : { startedAt: child.startedAt } }, context);
|
|
452
|
+
const handle = await api.conversation(child.id, context);
|
|
755
453
|
if (handle === undefined)
|
|
756
|
-
throw new Error(`Subagent conversation ${child} does not exist`);
|
|
454
|
+
throw new Error(`Subagent conversation ${child.id} does not exist`);
|
|
757
455
|
const request = { type: "input", content: args.task, requestId: `subagent:${api.taskId}` };
|
|
758
456
|
const settled = await (await handle.submit(request, context)).wait(context);
|
|
759
457
|
if (settled.status !== "done" || settled.type !== "input")
|
|
@@ -762,20 +460,39 @@ function createSubagent(options) {
|
|
|
762
460
|
const message = answer?.model?.[0];
|
|
763
461
|
const text = message?.role === "assistant" ? message.content.flatMap((block) => block.type === "text" ? [block.text] : []).join("") : "";
|
|
764
462
|
const result2 = {
|
|
765
|
-
|
|
766
|
-
details: {
|
|
463
|
+
output: [{ type: "text", text: text === "" ? "(The subagent returned no text.)" : text }],
|
|
464
|
+
details: {
|
|
465
|
+
conversationId: child.id,
|
|
466
|
+
...child.model === undefined ? {} : { model: child.model },
|
|
467
|
+
...child.startedAt === undefined ? {} : { startedAt: child.startedAt }
|
|
468
|
+
}
|
|
767
469
|
};
|
|
768
470
|
return result2;
|
|
769
471
|
}
|
|
770
472
|
});
|
|
771
|
-
const extension =
|
|
772
|
-
|
|
473
|
+
const extension = defineExtension3({
|
|
474
|
+
name: "subagent",
|
|
475
|
+
tools: [tool],
|
|
476
|
+
sections: [
|
|
477
|
+
section2("agent_roles", () => {
|
|
478
|
+
const defined = roles();
|
|
479
|
+
if (defined.length === 0)
|
|
480
|
+
return;
|
|
481
|
+
return [
|
|
482
|
+
"Named agents: pass one as `role` to `subagent` to delegate to it. When the user writes @name for one of these agents, delegate the relevant work to that agent with `subagent`, putting everything it needs in `task`.",
|
|
483
|
+
...defined.map((each) => `- ${each.name}: ${each.description.replace(/\s+/g, " ")}`)
|
|
484
|
+
].join(`
|
|
485
|
+
`);
|
|
486
|
+
})
|
|
487
|
+
]
|
|
488
|
+
});
|
|
489
|
+
return { extension };
|
|
773
490
|
}
|
|
774
491
|
|
|
775
492
|
// apps/host/src/extensions/todo.ts
|
|
776
|
-
import { defineDoc as
|
|
777
|
-
import { Type as
|
|
778
|
-
var TodoDoc =
|
|
493
|
+
import { defineDoc as defineDoc3, defineExtension as defineExtension4, defineTool as defineTool3 } from "@earendil-works/pi-durable";
|
|
494
|
+
import { Type as Type4 } from "typebox";
|
|
495
|
+
var TodoDoc = defineDoc3({
|
|
779
496
|
kind: "todo.list",
|
|
780
497
|
version: 1,
|
|
781
498
|
scope: "conversation",
|
|
@@ -783,33 +500,30 @@ var TodoDoc = defineDoc2({
|
|
|
783
500
|
fork: "asOf",
|
|
784
501
|
initial: () => ({ items: [] })
|
|
785
502
|
});
|
|
786
|
-
var todoWrite =
|
|
503
|
+
var todoWrite = defineTool3({
|
|
787
504
|
name: "todo_write",
|
|
788
505
|
description: "Replace the todo list shown to the user with the given items.",
|
|
789
|
-
parameters:
|
|
506
|
+
parameters: Type4.Object({ items: Type4.Array(TodoItemSchema) }),
|
|
790
507
|
replay: "safe",
|
|
791
508
|
execute: async (args, api, context) => {
|
|
792
509
|
await api.commit(async (tx) => {
|
|
793
510
|
const doc = await tx.doc(TodoDoc, api.conversationId);
|
|
794
511
|
doc.items = args.items.map((item) => ({ ...item }));
|
|
795
512
|
}, context);
|
|
796
|
-
return {
|
|
513
|
+
return { output: [{ type: "text", text: `Todo list updated: ${args.items.length} item(s).` }] };
|
|
797
514
|
}
|
|
798
515
|
});
|
|
799
516
|
var todo = {
|
|
800
|
-
extension:
|
|
517
|
+
extension: defineExtension4({ name: "todo", tools: [todoWrite] }),
|
|
801
518
|
docs: [{ token: TodoDoc, presentation: "pinomad.todo" }]
|
|
802
519
|
};
|
|
803
520
|
|
|
804
521
|
// apps/host/src/host.ts
|
|
805
|
-
import { randomBytes as
|
|
806
|
-
import { mkdir as
|
|
807
|
-
import { basename as basename2, join as
|
|
808
|
-
import { BACKGROUND_CONTEXT as
|
|
809
|
-
import {
|
|
810
|
-
createRegistry,
|
|
811
|
-
Harness
|
|
812
|
-
} from "@earendil-works/pi-durable";
|
|
522
|
+
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
523
|
+
import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
|
|
524
|
+
import { basename as basename2, join as join7, sep as sep4 } from "node:path";
|
|
525
|
+
import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT3 } from "@earendil-works/chord/context";
|
|
526
|
+
import { createRegistry, Harness } from "@earendil-works/pi-durable";
|
|
813
527
|
import { NodeExecutionEnv } from "@earendil-works/pi-durable/env/node";
|
|
814
528
|
|
|
815
529
|
// apps/host/src/organization.ts
|
|
@@ -818,7 +532,7 @@ import { basename, join as join3, resolve } from "node:path";
|
|
|
818
532
|
import { clampThinkingLevel as clampThinkingLevel2 } from "@earendil-works/pi-ai";
|
|
819
533
|
import {
|
|
820
534
|
AgentDoc as AgentDoc2,
|
|
821
|
-
defineDoc as
|
|
535
|
+
defineDoc as defineDoc4
|
|
822
536
|
} from "@earendil-works/pi-durable";
|
|
823
537
|
|
|
824
538
|
// apps/host/src/checkout.ts
|
|
@@ -849,27 +563,6 @@ async function gitBase(projectPath) {
|
|
|
849
563
|
return;
|
|
850
564
|
return { repo, subdir: relative(repo, projectPath), base };
|
|
851
565
|
}
|
|
852
|
-
var SNAPSHOT_IDENTITY = {
|
|
853
|
-
GIT_AUTHOR_NAME: "PiNomad",
|
|
854
|
-
GIT_AUTHOR_EMAIL: "pinomad@localhost",
|
|
855
|
-
GIT_COMMITTER_NAME: "PiNomad",
|
|
856
|
-
GIT_COMMITTER_EMAIL: "pinomad@localhost"
|
|
857
|
-
};
|
|
858
|
-
async function snapshotOf(sourceDir) {
|
|
859
|
-
const indexFile = join2(tmpdir(), `pinomad-index-${randomBytes(8).toString("hex")}`);
|
|
860
|
-
const env = { GIT_INDEX_FILE: indexFile };
|
|
861
|
-
try {
|
|
862
|
-
const { stdout: head } = await git(sourceDir, ["rev-parse", "--verify", "HEAD"], env);
|
|
863
|
-
const base = head.trim();
|
|
864
|
-
await git(sourceDir, ["read-tree", base], env);
|
|
865
|
-
await git(sourceDir, ["add", "-A"], env);
|
|
866
|
-
const { stdout: tree } = await git(sourceDir, ["write-tree"], env);
|
|
867
|
-
const { stdout: commit } = await git(sourceDir, ["commit-tree", tree.trim(), "-p", base, "-m", "pinomad fork snapshot"], { ...env, ...SNAPSHOT_IDENTITY });
|
|
868
|
-
return { base, snapshot: commit.trim() };
|
|
869
|
-
} finally {
|
|
870
|
-
await rm(indexFile, { force: true });
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
566
|
var repoLocks = new Map;
|
|
874
567
|
function locked(repo, work) {
|
|
875
568
|
const next = (repoLocks.get(repo) ?? Promise.resolve()).then(work, work);
|
|
@@ -920,7 +613,7 @@ async function changesOf(dir, base, maxBytes = 1024 * 1024) {
|
|
|
920
613
|
try {
|
|
921
614
|
await git(dir, ["read-tree", "HEAD"], env);
|
|
922
615
|
await git(dir, ["add", "-A"], env);
|
|
923
|
-
const { stdout } = await git(dir, ["diff", "--cached", "--no-color", "--find-renames", base], env);
|
|
616
|
+
const { stdout } = await git(dir, ["diff", "--cached", "--no-color", "--find-renames", "--src-prefix=a/", "--dst-prefix=b/", base], env);
|
|
924
617
|
if (Buffer.byteLength(stdout) <= maxBytes)
|
|
925
618
|
return { base, patch: stdout, truncated: false };
|
|
926
619
|
const cut = stdout.slice(0, maxBytes);
|
|
@@ -932,8 +625,18 @@ async function changesOf(dir, base, maxBytes = 1024 * 1024) {
|
|
|
932
625
|
}
|
|
933
626
|
}
|
|
934
627
|
|
|
628
|
+
// apps/host/src/message-content.ts
|
|
629
|
+
function messageContent(text, images) {
|
|
630
|
+
if (images === undefined || images.length === 0)
|
|
631
|
+
return text;
|
|
632
|
+
return [
|
|
633
|
+
...text === "" ? [] : [{ type: "text", text }],
|
|
634
|
+
...images.map(({ data, mimeType }) => ({ type: "image", data, mimeType }))
|
|
635
|
+
];
|
|
636
|
+
}
|
|
637
|
+
|
|
935
638
|
// apps/host/src/organization.ts
|
|
936
|
-
var IndexDoc =
|
|
639
|
+
var IndexDoc = defineDoc4({
|
|
937
640
|
kind: "pinomad.index",
|
|
938
641
|
version: 1,
|
|
939
642
|
scope: "session",
|
|
@@ -984,7 +687,7 @@ async function createConversation(harness, dataDir, input, defaults, models, con
|
|
|
984
687
|
if (prior !== undefined) {
|
|
985
688
|
const existing = await harness.conversation(prior.id, context);
|
|
986
689
|
if (existing !== undefined) {
|
|
987
|
-
await existing.submit({ type: "input", content: input.text, whenBusy: "followUp", requestId: input.requestId }, context);
|
|
690
|
+
await existing.submit({ type: "input", content: messageContent(input.text, input.images), whenBusy: "followUp", requestId: input.requestId }, context);
|
|
988
691
|
}
|
|
989
692
|
return prior.id;
|
|
990
693
|
}
|
|
@@ -1036,14 +739,14 @@ async function createConversation(harness, dataDir, input, defaults, models, con
|
|
|
1036
739
|
}, context);
|
|
1037
740
|
if (home.kind === "chat")
|
|
1038
741
|
await mkdir(join3(dataDir, "chats", String(conversation.id)), { recursive: true });
|
|
1039
|
-
await conversation.submit({ type: "input", content: input.text, whenBusy: "followUp", requestId: input.requestId }, context);
|
|
742
|
+
await conversation.submit({ type: "input", content: messageContent(input.text, input.images), whenBusy: "followUp", requestId: input.requestId }, context);
|
|
1040
743
|
return conversation.id;
|
|
1041
744
|
} catch (error) {
|
|
1042
745
|
if (!(error instanceof DuplicateRequest))
|
|
1043
746
|
throw error;
|
|
1044
747
|
const existing = await harness.conversation(error.conversationId, context);
|
|
1045
748
|
if (existing !== undefined) {
|
|
1046
|
-
await existing.submit({ type: "input", content: input.text, whenBusy: "followUp", requestId: input.requestId }, context);
|
|
749
|
+
await existing.submit({ type: "input", content: messageContent(input.text, input.images), whenBusy: "followUp", requestId: input.requestId }, context);
|
|
1047
750
|
}
|
|
1048
751
|
return error.conversationId;
|
|
1049
752
|
}
|
|
@@ -1057,21 +760,42 @@ async function archive(harness, id, archived, context) {
|
|
|
1057
760
|
entry.archived = archived;
|
|
1058
761
|
}, context);
|
|
1059
762
|
}
|
|
1060
|
-
|
|
763
|
+
var forkFlags = new WeakMap;
|
|
764
|
+
async function forkFlagOf(harness, id, context) {
|
|
765
|
+
const cached = forkFlags.get(harness)?.get(id);
|
|
766
|
+
if (cached !== undefined)
|
|
767
|
+
return cached;
|
|
768
|
+
const record = await harness.commit((tx) => tx.conversation(id), context);
|
|
769
|
+
if (record === undefined)
|
|
770
|
+
return false;
|
|
771
|
+
let flags = forkFlags.get(harness);
|
|
772
|
+
if (flags === undefined)
|
|
773
|
+
forkFlags.set(harness, flags = new Map);
|
|
774
|
+
flags.set(id, record.parent !== undefined);
|
|
775
|
+
return record.parent !== undefined;
|
|
776
|
+
}
|
|
777
|
+
async function checkoutInfo(harness, input, context) {
|
|
1061
778
|
const agent = await input.read.snapshot(AgentDoc2, input.conversationId, context);
|
|
1062
779
|
const cwd = agent?.cwd;
|
|
1063
780
|
if (cwd === undefined)
|
|
1064
781
|
return;
|
|
1065
|
-
const index = await
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
782
|
+
const [index, fork] = await Promise.all([
|
|
783
|
+
input.read.snapshot(IndexDoc, context),
|
|
784
|
+
forkFlagOf(harness, input.conversationId, context)
|
|
785
|
+
]);
|
|
786
|
+
const checkout = checkoutAt(index?.checkouts, cwd);
|
|
1069
787
|
return {
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
788
|
+
fork,
|
|
789
|
+
...checkout === undefined ? {} : {
|
|
790
|
+
checkout: {
|
|
791
|
+
branch: checkout.branch,
|
|
792
|
+
base: checkout.base,
|
|
793
|
+
projectPath: join3(checkout.repo, checkout.subdir),
|
|
794
|
+
repo: checkout.repo,
|
|
795
|
+
worktreeRoot: checkout.path,
|
|
796
|
+
own: checkout.conversationId === input.conversationId
|
|
797
|
+
}
|
|
798
|
+
}
|
|
1075
799
|
};
|
|
1076
800
|
}
|
|
1077
801
|
async function cleanupArchivedWorktrees(harness, rootId, context) {
|
|
@@ -1126,8 +850,8 @@ import {
|
|
|
1126
850
|
StreamableHttpTransport,
|
|
1127
851
|
toLlmContent
|
|
1128
852
|
} from "@earendil-works/pi-mcp";
|
|
1129
|
-
import { defineExtension as
|
|
1130
|
-
import { Type as
|
|
853
|
+
import { defineExtension as defineExtension5, defineTool as defineTool4 } from "@earendil-works/pi-durable";
|
|
854
|
+
import { Type as Type5 } from "typebox";
|
|
1131
855
|
var ENV_VAR = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
1132
856
|
var isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1133
857
|
function expand(value) {
|
|
@@ -1249,32 +973,23 @@ var callToolResultSchema = (tool) => ({
|
|
|
1249
973
|
},
|
|
1250
974
|
required: ["content"]
|
|
1251
975
|
});
|
|
1252
|
-
function
|
|
1253
|
-
return {
|
|
976
|
+
function mcpTool(client, timeoutMs, exposure, name, tool) {
|
|
977
|
+
return defineTool4({
|
|
1254
978
|
name,
|
|
1255
979
|
description: tool.description ?? tool.title ?? tool.name,
|
|
1256
|
-
|
|
1257
|
-
outputSchema: callToolResultSchema(tool),
|
|
1258
|
-
namespace: {
|
|
1259
|
-
name: server,
|
|
1260
|
-
...description === undefined ? {} : { description },
|
|
1261
|
-
...client.instructions === undefined ? {} : { instructions: client.instructions }
|
|
1262
|
-
},
|
|
1263
|
-
call: (args, signal) => client.callTool(tool.name, args, { signal, timeoutMs })
|
|
1264
|
-
};
|
|
1265
|
-
}
|
|
1266
|
-
function asModelTool(tool) {
|
|
1267
|
-
return defineTool5({
|
|
1268
|
-
name: tool.name,
|
|
1269
|
-
description: tool.description,
|
|
1270
|
-
parameters: Type6.Unsafe({
|
|
980
|
+
parameters: Type5.Unsafe({
|
|
1271
981
|
...tool.inputSchema,
|
|
1272
982
|
type: "object",
|
|
1273
983
|
properties: tool.inputSchema.properties ?? {}
|
|
1274
984
|
}),
|
|
985
|
+
...exposure === "direct" ? {} : { callers: ["tools"] },
|
|
986
|
+
structuredOutputSchema: Type5.Unsafe(callToolResultSchema(tool)),
|
|
1275
987
|
execute: async (args, _api, context) => {
|
|
1276
|
-
const result2 = await tool.
|
|
1277
|
-
|
|
988
|
+
const result2 = await client.callTool(tool.name, args, {
|
|
989
|
+
signal: context.abortSignal ?? NEVER_ABORT,
|
|
990
|
+
timeoutMs
|
|
991
|
+
});
|
|
992
|
+
return { output: toLlmContent(result2), structuredOutput: result2, isError: result2.isError === true };
|
|
1278
993
|
}
|
|
1279
994
|
});
|
|
1280
995
|
}
|
|
@@ -1283,6 +998,7 @@ function startMcp(options) {
|
|
|
1283
998
|
const listeners = new Set;
|
|
1284
999
|
const servers = new Map;
|
|
1285
1000
|
const toolsets = new Map;
|
|
1001
|
+
const namespaces = new Map;
|
|
1286
1002
|
const exposures = new Map;
|
|
1287
1003
|
const live = new Set;
|
|
1288
1004
|
let errors = [];
|
|
@@ -1293,8 +1009,8 @@ function startMcp(options) {
|
|
|
1293
1009
|
listener(value);
|
|
1294
1010
|
};
|
|
1295
1011
|
const install = () => {
|
|
1296
|
-
|
|
1297
|
-
options.
|
|
1012
|
+
options.registry.install(defineExtension5({ name: "mcp", tools: [...toolsets.values()].flat() }));
|
|
1013
|
+
options.onNamespaces?.([...namespaces.values()]);
|
|
1298
1014
|
};
|
|
1299
1015
|
const set = (name, patch) => {
|
|
1300
1016
|
servers.set(name, { name, tools: 0, ...servers.get(name), ...patch });
|
|
@@ -1311,16 +1027,23 @@ function startMcp(options) {
|
|
|
1311
1027
|
continue;
|
|
1312
1028
|
}
|
|
1313
1029
|
taken.add(wire);
|
|
1314
|
-
tools.push(
|
|
1030
|
+
tools.push(mcpTool(client, timeoutMs, exposures.get(name) ?? "codemode", wire, tool));
|
|
1315
1031
|
}
|
|
1316
1032
|
if (closed)
|
|
1317
1033
|
return;
|
|
1318
1034
|
toolsets.set(name, tools);
|
|
1035
|
+
namespaces.set(name, {
|
|
1036
|
+
name,
|
|
1037
|
+
...description === undefined ? {} : { description },
|
|
1038
|
+
...client.instructions === undefined ? {} : { instructions: client.instructions },
|
|
1039
|
+
prefix: callName(name, "")
|
|
1040
|
+
});
|
|
1319
1041
|
set(name, { state: "connected", tools: tools.length, error: collisions.length === 0 ? undefined : collisions.join("; ") });
|
|
1320
1042
|
install();
|
|
1321
1043
|
};
|
|
1322
1044
|
const fail = (name, error) => {
|
|
1323
1045
|
toolsets.delete(name);
|
|
1046
|
+
namespaces.delete(name);
|
|
1324
1047
|
set(name, { state: "failed", error, tools: 0 });
|
|
1325
1048
|
install();
|
|
1326
1049
|
};
|
|
@@ -1395,7 +1118,6 @@ ${stderr}`;
|
|
|
1395
1118
|
});
|
|
1396
1119
|
return {
|
|
1397
1120
|
status,
|
|
1398
|
-
scriptTools: () => [...toolsets.values()].flat(),
|
|
1399
1121
|
async close() {
|
|
1400
1122
|
closed = true;
|
|
1401
1123
|
await Promise.all([...live].map((client) => client.close().catch(() => {})));
|
|
@@ -1403,22 +1125,559 @@ ${stderr}`;
|
|
|
1403
1125
|
};
|
|
1404
1126
|
}
|
|
1405
1127
|
|
|
1128
|
+
// apps/host/src/agent-roles.ts
|
|
1129
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
1130
|
+
import { existsSync, watch } from "node:fs";
|
|
1131
|
+
import { mkdir as mkdir2, readdir, readFile as readFile2, rename, rm as rm2, writeFile } from "node:fs/promises";
|
|
1132
|
+
import { join as join4 } from "node:path";
|
|
1133
|
+
import { parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
1134
|
+
|
|
1135
|
+
// packages/protocol/src/agents.ts
|
|
1136
|
+
var MAX_AGENT_NAME = 32;
|
|
1137
|
+
var MAX_AGENT_DESCRIPTION = 1024;
|
|
1138
|
+
var NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
1139
|
+
function agentNameProblem(name) {
|
|
1140
|
+
return name.length > MAX_AGENT_NAME || !NAME_PATTERN.test(name) ? `Name must be lowercase letters, digits and hyphens (max ${MAX_AGENT_NAME})` : undefined;
|
|
1141
|
+
}
|
|
1142
|
+
function agentRoleProblem(role) {
|
|
1143
|
+
const name = agentNameProblem(role.name);
|
|
1144
|
+
if (name !== undefined)
|
|
1145
|
+
return name;
|
|
1146
|
+
const description = role.description.trim();
|
|
1147
|
+
if (description === "")
|
|
1148
|
+
return "description is required";
|
|
1149
|
+
if (description.length > MAX_AGENT_DESCRIPTION)
|
|
1150
|
+
return `description is too long (max ${MAX_AGENT_DESCRIPTION})`;
|
|
1151
|
+
if (role.model !== undefined) {
|
|
1152
|
+
const slash = role.model.indexOf("/");
|
|
1153
|
+
if (slash <= 0 || slash === role.model.length - 1)
|
|
1154
|
+
return "model must be provider/modelId";
|
|
1155
|
+
}
|
|
1156
|
+
if (role.thinkingLevel !== undefined && !isThinkingLevel(role.thinkingLevel)) {
|
|
1157
|
+
return `thinkingLevel must be one of ${THINKING_LEVELS.join(", ")}`;
|
|
1158
|
+
}
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
// apps/host/src/host-value.ts
|
|
1163
|
+
function constantValue(value) {
|
|
1164
|
+
return {
|
|
1165
|
+
value,
|
|
1166
|
+
subscribe: () => () => {}
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
class Published {
|
|
1171
|
+
#listeners = new Set;
|
|
1172
|
+
#equal;
|
|
1173
|
+
#value;
|
|
1174
|
+
constructor(value, equal) {
|
|
1175
|
+
this.#value = value;
|
|
1176
|
+
this.#equal = equal;
|
|
1177
|
+
}
|
|
1178
|
+
get value() {
|
|
1179
|
+
return this.#value;
|
|
1180
|
+
}
|
|
1181
|
+
subscribe(listener) {
|
|
1182
|
+
this.#listeners.add(listener);
|
|
1183
|
+
return () => this.#listeners.delete(listener);
|
|
1184
|
+
}
|
|
1185
|
+
set(next) {
|
|
1186
|
+
if (this.#equal(this.#value, next))
|
|
1187
|
+
return;
|
|
1188
|
+
this.#value = next;
|
|
1189
|
+
for (const listener of this.#listeners)
|
|
1190
|
+
listener(next);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
var sameList = (a, b, same) => a.length === b.length && a.every((entry, index) => same(entry, b[index]));
|
|
1194
|
+
|
|
1195
|
+
// apps/host/src/agent-roles.ts
|
|
1196
|
+
var KNOWN_FIELDS = new Set(["description", "model", "thinkingLevel"]);
|
|
1197
|
+
async function load(dir) {
|
|
1198
|
+
const roles = [];
|
|
1199
|
+
const problems = [];
|
|
1200
|
+
const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
1201
|
+
for (const entry of entries) {
|
|
1202
|
+
if (!entry.isFile() || !entry.name.endsWith(".md"))
|
|
1203
|
+
continue;
|
|
1204
|
+
const file = entry.name;
|
|
1205
|
+
const name = file.slice(0, -".md".length);
|
|
1206
|
+
const problem = (reason2) => void problems.push({ file, reason: reason2 });
|
|
1207
|
+
let parsed;
|
|
1208
|
+
try {
|
|
1209
|
+
parsed = parseFrontmatter(await readFile2(join4(dir, file), "utf8"));
|
|
1210
|
+
} catch (error) {
|
|
1211
|
+
problem(`Couldn't parse the file: ${error instanceof Error ? error.message : String(error)}`);
|
|
1212
|
+
continue;
|
|
1213
|
+
}
|
|
1214
|
+
if (typeof parsed.frontmatter !== "object" || parsed.frontmatter === null || Array.isArray(parsed.frontmatter)) {
|
|
1215
|
+
problem("Frontmatter must be a record of fields");
|
|
1216
|
+
continue;
|
|
1217
|
+
}
|
|
1218
|
+
const unknown = Object.keys(parsed.frontmatter).find((key) => !KNOWN_FIELDS.has(key));
|
|
1219
|
+
if (unknown !== undefined) {
|
|
1220
|
+
problem(`Unknown field "${unknown}"`);
|
|
1221
|
+
continue;
|
|
1222
|
+
}
|
|
1223
|
+
const bad = Object.entries(parsed.frontmatter).find(([, value]) => typeof value !== "string");
|
|
1224
|
+
if (bad !== undefined) {
|
|
1225
|
+
problem(`Field "${bad[0]}" must be a string`);
|
|
1226
|
+
continue;
|
|
1227
|
+
}
|
|
1228
|
+
const frontmatter = parsed.frontmatter;
|
|
1229
|
+
const role = {
|
|
1230
|
+
name,
|
|
1231
|
+
description: frontmatter.description ?? "",
|
|
1232
|
+
...frontmatter.model === undefined ? {} : { model: frontmatter.model },
|
|
1233
|
+
...frontmatter.thinkingLevel === undefined ? {} : { thinkingLevel: frontmatter.thinkingLevel },
|
|
1234
|
+
prompt: parsed.body.trim()
|
|
1235
|
+
};
|
|
1236
|
+
const reason = agentRoleProblem(role);
|
|
1237
|
+
if (reason !== undefined) {
|
|
1238
|
+
problem(reason);
|
|
1239
|
+
continue;
|
|
1240
|
+
}
|
|
1241
|
+
roles.push(role);
|
|
1242
|
+
}
|
|
1243
|
+
roles.sort((a, b) => a.name.localeCompare(b.name));
|
|
1244
|
+
problems.sort((a, b) => a.file.localeCompare(b.file));
|
|
1245
|
+
return { roles, problems };
|
|
1246
|
+
}
|
|
1247
|
+
function serialize(role) {
|
|
1248
|
+
const fields = [
|
|
1249
|
+
`description: ${JSON.stringify(role.description)}`,
|
|
1250
|
+
...role.model === undefined ? [] : [`model: ${JSON.stringify(role.model)}`],
|
|
1251
|
+
...role.thinkingLevel === undefined ? [] : [`thinkingLevel: ${JSON.stringify(role.thinkingLevel)}`]
|
|
1252
|
+
];
|
|
1253
|
+
return `---
|
|
1254
|
+
${fields.join(`
|
|
1255
|
+
`)}
|
|
1256
|
+
---
|
|
1257
|
+
|
|
1258
|
+
${role.prompt}
|
|
1259
|
+
`;
|
|
1260
|
+
}
|
|
1261
|
+
var sameRole = (a, b) => a.name === b.name && a.description === b.description && a.model === b.model && a.thinkingLevel === b.thinkingLevel && a.prompt === b.prompt;
|
|
1262
|
+
var sameProblem = (a, b) => a.file === b.file && a.reason === b.reason;
|
|
1263
|
+
async function openAgentRoles(options) {
|
|
1264
|
+
const dir = options.dir;
|
|
1265
|
+
await mkdir2(dir, { recursive: true });
|
|
1266
|
+
const debounceMs = options.debounceMs ?? 150;
|
|
1267
|
+
const published = new Published({ roles: [], problems: [] }, (a, b) => sameList(a.roles, b.roles, sameRole) && sameList(a.problems, b.problems, sameProblem));
|
|
1268
|
+
let closed = false;
|
|
1269
|
+
let reported = false;
|
|
1270
|
+
let timer;
|
|
1271
|
+
const reload = async () => {
|
|
1272
|
+
try {
|
|
1273
|
+
published.set(await load(dir));
|
|
1274
|
+
reported = false;
|
|
1275
|
+
} catch (error) {
|
|
1276
|
+
if (!reported)
|
|
1277
|
+
console.error(`agent-roles reload failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1278
|
+
reported = true;
|
|
1279
|
+
}
|
|
1280
|
+
};
|
|
1281
|
+
await reload();
|
|
1282
|
+
const watcher = watch(dir, () => {
|
|
1283
|
+
if (closed)
|
|
1284
|
+
return;
|
|
1285
|
+
clearTimeout(timer);
|
|
1286
|
+
timer = setTimeout(() => void reload(), debounceMs);
|
|
1287
|
+
});
|
|
1288
|
+
watcher.on("error", (error) => {
|
|
1289
|
+
if (!reported)
|
|
1290
|
+
console.error(`agent-roles watch failed: ${error.message}`);
|
|
1291
|
+
reported = true;
|
|
1292
|
+
});
|
|
1293
|
+
let pending = Promise.resolve();
|
|
1294
|
+
const enqueue = (operation) => {
|
|
1295
|
+
const run2 = pending.then(operation, operation);
|
|
1296
|
+
pending = run2.then(() => {}, () => {});
|
|
1297
|
+
return run2;
|
|
1298
|
+
};
|
|
1299
|
+
const write = async (role, mustExist) => {
|
|
1300
|
+
const problem = agentRoleProblem(role);
|
|
1301
|
+
if (problem !== undefined)
|
|
1302
|
+
throw new Error(problem);
|
|
1303
|
+
await enqueue(async () => {
|
|
1304
|
+
const target = join4(dir, `${role.name}.md`);
|
|
1305
|
+
const exists = existsSync(target);
|
|
1306
|
+
if (mustExist && !exists)
|
|
1307
|
+
throw new Error(`No such agent: "${role.name}"`);
|
|
1308
|
+
if (!mustExist && exists)
|
|
1309
|
+
throw new Error(`Agent "${role.name}" already exists`);
|
|
1310
|
+
const temp = join4(dir, `.${role.name}.${randomBytes2(6).toString("hex")}.tmp`);
|
|
1311
|
+
try {
|
|
1312
|
+
await writeFile(temp, serialize(role));
|
|
1313
|
+
await rename(temp, target);
|
|
1314
|
+
} catch (error) {
|
|
1315
|
+
await rm2(temp, { force: true }).catch(() => {});
|
|
1316
|
+
throw error;
|
|
1317
|
+
}
|
|
1318
|
+
await reload();
|
|
1319
|
+
});
|
|
1320
|
+
};
|
|
1321
|
+
return {
|
|
1322
|
+
get value() {
|
|
1323
|
+
return published.value;
|
|
1324
|
+
},
|
|
1325
|
+
subscribe: (listener) => published.subscribe(listener),
|
|
1326
|
+
create: (role) => write(role, false),
|
|
1327
|
+
update: (role) => write(role, true),
|
|
1328
|
+
async remove(name) {
|
|
1329
|
+
const problem = agentNameProblem(name);
|
|
1330
|
+
if (problem !== undefined)
|
|
1331
|
+
throw new Error(problem);
|
|
1332
|
+
await enqueue(async () => {
|
|
1333
|
+
const target = join4(dir, `${name}.md`);
|
|
1334
|
+
if (!existsSync(target))
|
|
1335
|
+
throw new Error(`No such agent: "${name}"`);
|
|
1336
|
+
await rm2(target);
|
|
1337
|
+
await reload();
|
|
1338
|
+
});
|
|
1339
|
+
},
|
|
1340
|
+
close() {
|
|
1341
|
+
closed = true;
|
|
1342
|
+
clearTimeout(timer);
|
|
1343
|
+
watcher.close();
|
|
1344
|
+
}
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
// apps/host/src/host-settings.ts
|
|
1349
|
+
import { mkdir as mkdir3, readFile as readFile3, rename as rename2, writeFile as writeFile2 } from "node:fs/promises";
|
|
1350
|
+
import { dirname as dirname2 } from "node:path";
|
|
1351
|
+
function settingsDefaults(settings) {
|
|
1352
|
+
return {
|
|
1353
|
+
...settings.defaultModel === null ? {} : { model: settings.defaultModel },
|
|
1354
|
+
...settings.defaultThinkingLevel === null ? {} : { thinkingLevel: settings.defaultThinkingLevel }
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1357
|
+
var isModelRef = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && typeof value.provider === "string" && value.provider !== "" && typeof value.modelId === "string" && value.modelId !== "";
|
|
1358
|
+
var fileValueError = (key, value, models) => {
|
|
1359
|
+
switch (key) {
|
|
1360
|
+
case "defaultModel":
|
|
1361
|
+
if (!isModelRef(value))
|
|
1362
|
+
return "malformed";
|
|
1363
|
+
return models.some((each) => each.provider === value.provider && each.modelId === value.modelId) ? undefined : "unavailable";
|
|
1364
|
+
case "defaultThinkingLevel":
|
|
1365
|
+
return isThinkingLevel(value) ? undefined : "malformed";
|
|
1366
|
+
case "steeringMode":
|
|
1367
|
+
case "followUpMode":
|
|
1368
|
+
return isQueueMode(value) ? undefined : "malformed";
|
|
1369
|
+
}
|
|
1370
|
+
};
|
|
1371
|
+
var sameModel = (a, b) => a === b || a !== null && b !== null && a.provider === b.provider && a.modelId === b.modelId;
|
|
1372
|
+
var sameSettings = (a, b) => a.steeringMode === b.steeringMode && a.followUpMode === b.followUpMode && a.defaultThinkingLevel === b.defaultThinkingLevel && sameModel(a.defaultModel, b.defaultModel) && sameList(a.custom, b.custom, (x, y) => x === y);
|
|
1373
|
+
var parse = (path, text) => {
|
|
1374
|
+
let parsed;
|
|
1375
|
+
try {
|
|
1376
|
+
parsed = JSON.parse(text);
|
|
1377
|
+
} catch (error) {
|
|
1378
|
+
throw new Error(`Invalid settings file ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1379
|
+
}
|
|
1380
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
1381
|
+
throw new Error(`Invalid settings file ${path}: expected a JSON object`);
|
|
1382
|
+
}
|
|
1383
|
+
return parsed;
|
|
1384
|
+
};
|
|
1385
|
+
async function openHostSettings(options) {
|
|
1386
|
+
let file = await readFile3(options.path, "utf8").then((text) => parse(options.path, text), (error) => {
|
|
1387
|
+
if (error.code === "ENOENT")
|
|
1388
|
+
return {};
|
|
1389
|
+
throw new Error(`Cannot read settings file ${options.path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1390
|
+
});
|
|
1391
|
+
const warned = new Set;
|
|
1392
|
+
const compute = () => {
|
|
1393
|
+
const fileValues = {};
|
|
1394
|
+
const custom = [];
|
|
1395
|
+
for (const key of HOST_SETTING_KEYS) {
|
|
1396
|
+
if (!Object.hasOwn(file, key))
|
|
1397
|
+
continue;
|
|
1398
|
+
const value = file[key];
|
|
1399
|
+
const invalid = fileValueError(key, value, options.models.value);
|
|
1400
|
+
if (invalid === "malformed" && !warned.has(key)) {
|
|
1401
|
+
warned.add(key);
|
|
1402
|
+
console.error(`${options.path}: ignoring invalid value for settings key "${key}": ${JSON.stringify(value)}`);
|
|
1403
|
+
}
|
|
1404
|
+
if (invalid !== undefined)
|
|
1405
|
+
continue;
|
|
1406
|
+
fileValues[key] = value;
|
|
1407
|
+
custom.push(key);
|
|
1408
|
+
}
|
|
1409
|
+
const { fallback } = options;
|
|
1410
|
+
return {
|
|
1411
|
+
defaultModel: fileValues.defaultModel ?? fallback.defaultModel(),
|
|
1412
|
+
defaultThinkingLevel: fileValues.defaultThinkingLevel ?? fallback.defaultThinkingLevel(),
|
|
1413
|
+
steeringMode: fileValues.steeringMode ?? fallback.steeringMode(),
|
|
1414
|
+
followUpMode: fileValues.followUpMode ?? fallback.followUpMode(),
|
|
1415
|
+
custom
|
|
1416
|
+
};
|
|
1417
|
+
};
|
|
1418
|
+
const published = new Published(compute(), sameSettings);
|
|
1419
|
+
options.models.subscribe(() => published.set(compute()));
|
|
1420
|
+
let tail = Promise.resolve();
|
|
1421
|
+
const write = async (patch) => {
|
|
1422
|
+
const model = patch.defaultModel;
|
|
1423
|
+
if (model !== undefined && model !== null && !options.models.value.some((each) => each.provider === model.provider && each.modelId === model.modelId)) {
|
|
1424
|
+
throw new Error(`Unknown model: ${model.provider}/${model.modelId}`);
|
|
1425
|
+
}
|
|
1426
|
+
const next = { ...file };
|
|
1427
|
+
for (const key of HOST_SETTING_KEYS) {
|
|
1428
|
+
const value = patch[key];
|
|
1429
|
+
if (value === undefined)
|
|
1430
|
+
continue;
|
|
1431
|
+
if (value === null)
|
|
1432
|
+
delete next[key];
|
|
1433
|
+
else
|
|
1434
|
+
next[key] = value;
|
|
1435
|
+
}
|
|
1436
|
+
const tmp = `${options.path}.tmp`;
|
|
1437
|
+
await mkdir3(dirname2(options.path), { recursive: true });
|
|
1438
|
+
await writeFile2(tmp, `${JSON.stringify(next, null, 2)}
|
|
1439
|
+
`, { mode: 384 });
|
|
1440
|
+
await rename2(tmp, options.path);
|
|
1441
|
+
file = next;
|
|
1442
|
+
published.set(compute());
|
|
1443
|
+
};
|
|
1444
|
+
return {
|
|
1445
|
+
get value() {
|
|
1446
|
+
return published.value;
|
|
1447
|
+
},
|
|
1448
|
+
subscribe: (listener) => published.subscribe(listener),
|
|
1449
|
+
update: (patch) => tail = tail.then(() => write(patch), () => write(patch))
|
|
1450
|
+
};
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1406
1453
|
// apps/host/src/host.ts
|
|
1407
1454
|
import { openNodeSqliteStorage } from "@earendil-works/pi-durable/storage/sqlite/node";
|
|
1408
1455
|
import lockfile from "proper-lockfile";
|
|
1409
1456
|
|
|
1410
1457
|
// apps/host/src/gateway.ts
|
|
1411
1458
|
import { createServer } from "node:http";
|
|
1412
|
-
import { networkInterfaces } from "node:os";
|
|
1413
|
-
import { join as
|
|
1414
|
-
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
|
1459
|
+
import { homedir as homedir2, networkInterfaces } from "node:os";
|
|
1460
|
+
import { join as join6, sep as sep3 } from "node:path";
|
|
1461
|
+
import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT2 } from "@earendil-works/chord/context";
|
|
1415
1462
|
import { clampThinkingLevel as clampThinkingLevel3, getSupportedThinkingLevels } from "@earendil-works/pi-ai";
|
|
1416
1463
|
import {
|
|
1417
|
-
AgentDoc as AgentDoc3
|
|
1464
|
+
AgentDoc as AgentDoc3,
|
|
1465
|
+
LiveDoc
|
|
1418
1466
|
} from "@earendil-works/pi-durable";
|
|
1419
1467
|
import { WebSocket as WebSocket2, WebSocketServer } from "ws";
|
|
1420
1468
|
import { Value as Value2 } from "typebox/value";
|
|
1421
1469
|
|
|
1470
|
+
// apps/host/src/folders.ts
|
|
1471
|
+
import { readdir as readdir2, realpath as realpath3, stat as stat3 } from "node:fs/promises";
|
|
1472
|
+
import { isAbsolute as isAbsolute2, join as join5 } from "node:path";
|
|
1473
|
+
var denied = (error) => {
|
|
1474
|
+
const code = error?.code;
|
|
1475
|
+
return code === "EACCES" || code === "EPERM";
|
|
1476
|
+
};
|
|
1477
|
+
async function listFolders(path, home) {
|
|
1478
|
+
if (path !== undefined && !isAbsolute2(path))
|
|
1479
|
+
throw new Error(`Not an absolute path: ${path}`);
|
|
1480
|
+
const given = path ?? home;
|
|
1481
|
+
const dir = await realpath3(given).catch((error) => {
|
|
1482
|
+
if (denied(error))
|
|
1483
|
+
throw new Error(`Permission denied: ${given}`);
|
|
1484
|
+
throw new Error(`No such directory: ${given}`);
|
|
1485
|
+
});
|
|
1486
|
+
if (!(await stat3(dir)).isDirectory())
|
|
1487
|
+
throw new Error(`Not a directory: ${given}`);
|
|
1488
|
+
let entries;
|
|
1489
|
+
try {
|
|
1490
|
+
entries = await readdir2(dir, { withFileTypes: true });
|
|
1491
|
+
} catch (error) {
|
|
1492
|
+
if (denied(error))
|
|
1493
|
+
throw new Error(`Permission denied: ${given}`);
|
|
1494
|
+
throw error;
|
|
1495
|
+
}
|
|
1496
|
+
const folders = [];
|
|
1497
|
+
for (const entry of entries) {
|
|
1498
|
+
if (entry.name.startsWith("."))
|
|
1499
|
+
continue;
|
|
1500
|
+
let isDirectory = entry.isDirectory();
|
|
1501
|
+
if (!isDirectory && entry.isSymbolicLink()) {
|
|
1502
|
+
isDirectory = await stat3(join5(dir, entry.name)).then((target) => target.isDirectory(), () => false);
|
|
1503
|
+
}
|
|
1504
|
+
if (isDirectory)
|
|
1505
|
+
folders.push({ name: entry.name, path: join5(dir, entry.name) });
|
|
1506
|
+
}
|
|
1507
|
+
folders.sort((a, b) => a.name.localeCompare(b.name, "en", { numeric: true, sensitivity: "base" }));
|
|
1508
|
+
return { path: dir, home: await realpath3(home).catch(() => home), folders };
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
// packages/protocol/src/images.ts
|
|
1512
|
+
var IMAGE_MIME_TYPES = ["image/jpeg", "image/png", "image/gif", "image/webp"];
|
|
1513
|
+
var MAX_IMAGES_PER_MESSAGE = 4;
|
|
1514
|
+
var MAX_IMAGE_BYTES = 5 * 1024 * 1024;
|
|
1515
|
+
var BASE64 = /^[A-Za-z0-9+/]*={0,2}$/;
|
|
1516
|
+
function imageProblem(images) {
|
|
1517
|
+
if (images.length > MAX_IMAGES_PER_MESSAGE)
|
|
1518
|
+
return `At most ${MAX_IMAGES_PER_MESSAGE} images per message`;
|
|
1519
|
+
for (let i = 0;i < images.length; i++) {
|
|
1520
|
+
const image = images[i];
|
|
1521
|
+
if (!IMAGE_MIME_TYPES.includes(image.mimeType)) {
|
|
1522
|
+
return `Unsupported image type: ${image.mimeType} (use JPEG, PNG, GIF or WebP)`;
|
|
1523
|
+
}
|
|
1524
|
+
if (image.data === "" || image.data.length % 4 !== 0 || !BASE64.test(image.data)) {
|
|
1525
|
+
return `Image ${i + 1} is not valid base64`;
|
|
1526
|
+
}
|
|
1527
|
+
const padding = image.data.endsWith("==") ? 2 : image.data.endsWith("=") ? 1 : 0;
|
|
1528
|
+
if (image.data.length * 3 / 4 - padding > MAX_IMAGE_BYTES)
|
|
1529
|
+
return `Image ${i + 1} is larger than 5 MB`;
|
|
1530
|
+
}
|
|
1531
|
+
return;
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
// apps/host/src/auto-title.ts
|
|
1535
|
+
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
|
1536
|
+
|
|
1537
|
+
// apps/host/src/title-doc.ts
|
|
1538
|
+
import { defineDoc as defineDoc5 } from "@earendil-works/pi-durable";
|
|
1539
|
+
var TitleDoc = defineDoc5({
|
|
1540
|
+
kind: "pinomad.title",
|
|
1541
|
+
version: 1,
|
|
1542
|
+
scope: "conversation",
|
|
1543
|
+
history: "latest",
|
|
1544
|
+
fork: "initial",
|
|
1545
|
+
initial: () => ({})
|
|
1546
|
+
});
|
|
1547
|
+
|
|
1548
|
+
// apps/host/src/auto-title.ts
|
|
1549
|
+
var context = BACKGROUND_CONTEXT;
|
|
1550
|
+
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.";
|
|
1551
|
+
var TITLE_INPUT_LIMIT = 2000;
|
|
1552
|
+
var TITLE_MAX_LENGTH = 80;
|
|
1553
|
+
var textOf = (content) => {
|
|
1554
|
+
if (typeof content === "string")
|
|
1555
|
+
return content;
|
|
1556
|
+
if (!Array.isArray(content))
|
|
1557
|
+
return "";
|
|
1558
|
+
return content.flatMap((block) => typeof block === "object" && block !== null && block.type === "text" ? [block.text ?? ""] : []).join("");
|
|
1559
|
+
};
|
|
1560
|
+
function cleanTitle(raw) {
|
|
1561
|
+
const line = raw.split(`
|
|
1562
|
+
`).map((each) => each.trim()).find((each) => each !== "");
|
|
1563
|
+
if (line === undefined)
|
|
1564
|
+
return;
|
|
1565
|
+
let text = line.replace(/^\s*title\s*:\s*/i, "");
|
|
1566
|
+
let before = "";
|
|
1567
|
+
while (text !== before) {
|
|
1568
|
+
before = text;
|
|
1569
|
+
text = text.replace(/^["'“”‘’「」`]+/, "").replace(/["'“”‘’「」`]+$/, "").replace(/[.。!!??]+$/, "");
|
|
1570
|
+
}
|
|
1571
|
+
text = text.replace(/\s+/g, " ").trim();
|
|
1572
|
+
if (text === "")
|
|
1573
|
+
return;
|
|
1574
|
+
return text.length > TITLE_MAX_LENGTH ? text.slice(0, TITLE_MAX_LENGTH).trimEnd() : text;
|
|
1575
|
+
}
|
|
1576
|
+
function attachAutoTitle(runs, deps) {
|
|
1577
|
+
const inFlight = new Set;
|
|
1578
|
+
const pending = new Set;
|
|
1579
|
+
const start = (id) => {
|
|
1580
|
+
inFlight.add(id);
|
|
1581
|
+
setImmediate(() => void title(id));
|
|
1582
|
+
};
|
|
1583
|
+
const title = async (id) => {
|
|
1584
|
+
try {
|
|
1585
|
+
const existing = await deps.harness.snapshot(TitleDoc, id, context);
|
|
1586
|
+
if (typeof existing?.title === "string" && existing.title !== "")
|
|
1587
|
+
return;
|
|
1588
|
+
const conversation = await deps.harness.conversation(id, context);
|
|
1589
|
+
if (conversation === undefined)
|
|
1590
|
+
return;
|
|
1591
|
+
let first;
|
|
1592
|
+
let newest;
|
|
1593
|
+
let cursor;
|
|
1594
|
+
do {
|
|
1595
|
+
const page = await conversation.entries({}, 256, cursor, context);
|
|
1596
|
+
first = page.items.findLast((entry) => entry.kind === "pi.user" && entry.conversationId === id) ?? first;
|
|
1597
|
+
newest ??= page.items.find((entry) => entry.conversationId === id && entry.kind === "pi.assistant");
|
|
1598
|
+
cursor = page.next;
|
|
1599
|
+
} while (cursor !== undefined);
|
|
1600
|
+
const last = newest?.model?.[0];
|
|
1601
|
+
if (first === undefined || last?.role !== "assistant" || last.stopReason === "error" || last.stopReason === "aborted") {
|
|
1602
|
+
return;
|
|
1603
|
+
}
|
|
1604
|
+
const ref = deps.settings.value.defaultModel;
|
|
1605
|
+
const model = ref === null ? undefined : deps.models.getModel(ref.provider, ref.modelId);
|
|
1606
|
+
if (model === undefined)
|
|
1607
|
+
return;
|
|
1608
|
+
const firstText = (textOf(first.model?.[0] !== undefined ? first.model[0].content : "") || "(images only)").slice(0, TITLE_INPUT_LIMIT);
|
|
1609
|
+
const answer = textOf(last.content).slice(0, TITLE_INPUT_LIMIT);
|
|
1610
|
+
const request = {
|
|
1611
|
+
systemPrompt: TITLE_PROMPT,
|
|
1612
|
+
messages: [
|
|
1613
|
+
{
|
|
1614
|
+
role: "user",
|
|
1615
|
+
content: `User's first message:
|
|
1616
|
+
${firstText}${answer === "" ? "" : `
|
|
1617
|
+
|
|
1618
|
+
Agent's reply:
|
|
1619
|
+
${answer}`}`,
|
|
1620
|
+
timestamp: Date.now()
|
|
1621
|
+
}
|
|
1622
|
+
]
|
|
1623
|
+
};
|
|
1624
|
+
const generated = await deps.models.completeSimple(model, request, { maxTokens: 256 });
|
|
1625
|
+
const cleaned = cleanTitle(generated.content.flatMap((block) => block.type === "text" ? [block.text] : []).join(""));
|
|
1626
|
+
if (generated.stopReason === "error" || cleaned === undefined) {
|
|
1627
|
+
console.error(`auto-title: conversation ${id}: ${generated.stopReason === "error" ? generated.errorMessage ?? "provider error" : "unusable model output"}`);
|
|
1628
|
+
return;
|
|
1629
|
+
}
|
|
1630
|
+
await deps.harness.commit(async (tx) => {
|
|
1631
|
+
const doc = await tx.doc(TitleDoc, id);
|
|
1632
|
+
if (typeof doc.title === "string" && doc.title !== "")
|
|
1633
|
+
return;
|
|
1634
|
+
doc.title = cleaned;
|
|
1635
|
+
doc.source = "auto";
|
|
1636
|
+
}, context);
|
|
1637
|
+
} catch (error) {
|
|
1638
|
+
console.error(`auto-title: conversation ${id}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1639
|
+
} finally {
|
|
1640
|
+
inFlight.delete(id);
|
|
1641
|
+
if (pending.delete(id))
|
|
1642
|
+
start(id);
|
|
1643
|
+
}
|
|
1644
|
+
};
|
|
1645
|
+
return runs.onRunEnd((id) => {
|
|
1646
|
+
if (inFlight.has(id)) {
|
|
1647
|
+
pending.add(id);
|
|
1648
|
+
return;
|
|
1649
|
+
}
|
|
1650
|
+
start(id);
|
|
1651
|
+
});
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
// packages/protocol/src/tool-target.ts
|
|
1655
|
+
var TARGET_KEYS = [
|
|
1656
|
+
"path",
|
|
1657
|
+
"file_path",
|
|
1658
|
+
"filePath",
|
|
1659
|
+
"command",
|
|
1660
|
+
"cmd",
|
|
1661
|
+
"query",
|
|
1662
|
+
"pattern",
|
|
1663
|
+
"url",
|
|
1664
|
+
"name",
|
|
1665
|
+
"description",
|
|
1666
|
+
"task"
|
|
1667
|
+
];
|
|
1668
|
+
var TARGET_MAX_LENGTH = 120;
|
|
1669
|
+
function toolTarget(args) {
|
|
1670
|
+
if (typeof args !== "object" || args === null)
|
|
1671
|
+
return;
|
|
1672
|
+
for (const key of TARGET_KEYS) {
|
|
1673
|
+
const value = args[key];
|
|
1674
|
+
if (typeof value === "string" && value.length > 0) {
|
|
1675
|
+
return value.length > TARGET_MAX_LENGTH ? `${value.slice(0, TARGET_MAX_LENGTH)}…` : value;
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
return;
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1422
1681
|
// apps/host/src/relay-link.ts
|
|
1423
1682
|
import { WebSocket } from "ws";
|
|
1424
1683
|
function relayWsBase(origin) {
|
|
@@ -1468,7 +1727,7 @@ function startRelayLink(options) {
|
|
|
1468
1727
|
}
|
|
1469
1728
|
}
|
|
1470
1729
|
}, pingIntervalMs);
|
|
1471
|
-
function
|
|
1730
|
+
function watch2(socket) {
|
|
1472
1731
|
watched.add(socket);
|
|
1473
1732
|
ponged.set(socket, true);
|
|
1474
1733
|
socket.on("pong", () => ponged.set(socket, true));
|
|
@@ -1480,7 +1739,7 @@ function startRelayLink(options) {
|
|
|
1480
1739
|
return;
|
|
1481
1740
|
const socket = new WebSocket(`${wsBase}/host`, { handshakeTimeout: pingIntervalMs });
|
|
1482
1741
|
control = socket;
|
|
1483
|
-
|
|
1742
|
+
watch2(socket);
|
|
1484
1743
|
socket.on("message", onControlMessage);
|
|
1485
1744
|
socket.once("close", onControlClosed);
|
|
1486
1745
|
}
|
|
@@ -1519,7 +1778,7 @@ function startRelayLink(options) {
|
|
|
1519
1778
|
break;
|
|
1520
1779
|
pendingAccepts += 1;
|
|
1521
1780
|
const data2 = new WebSocket(`${wsBase}/accept/${frame.connId}`, { handshakeTimeout: pingIntervalMs });
|
|
1522
|
-
|
|
1781
|
+
watch2(data2);
|
|
1523
1782
|
const release = () => {
|
|
1524
1783
|
pendingAccepts -= 1;
|
|
1525
1784
|
clearTimeout(ageOut);
|
|
@@ -1566,9 +1825,26 @@ function startRelayLink(options) {
|
|
|
1566
1825
|
}
|
|
1567
1826
|
|
|
1568
1827
|
// apps/host/src/gateway.ts
|
|
1569
|
-
var
|
|
1828
|
+
var context2 = BACKGROUND_CONTEXT2;
|
|
1829
|
+
var forkTails = new WeakMap;
|
|
1830
|
+
function serializedFork(harness, work) {
|
|
1831
|
+
const next = (forkTails.get(harness) ?? Promise.resolve()).then(work, work);
|
|
1832
|
+
const settled = next.then(() => {}, () => {});
|
|
1833
|
+
forkTails.set(harness, settled);
|
|
1834
|
+
settled.then(() => {
|
|
1835
|
+
if (forkTails.get(harness) === settled)
|
|
1836
|
+
forkTails.delete(harness);
|
|
1837
|
+
});
|
|
1838
|
+
return next;
|
|
1839
|
+
}
|
|
1840
|
+
var LOOPBACK_SECURE_PATH = "/secure";
|
|
1570
1841
|
async function startGateway(options) {
|
|
1571
|
-
const conversations = await ConversationList.open(options.harness);
|
|
1842
|
+
const conversations = await ConversationList.open(options.harness, options.docs);
|
|
1843
|
+
const detachAutoTitle = options.autoTitle === false ? undefined : attachAutoTitle(conversations, {
|
|
1844
|
+
harness: options.harness,
|
|
1845
|
+
models: options.models,
|
|
1846
|
+
settings: options.settings
|
|
1847
|
+
});
|
|
1572
1848
|
const http = createServer((request, response) => {
|
|
1573
1849
|
serveWebClient(request, response, options.webRoot).catch(() => {
|
|
1574
1850
|
if (!response.headersSent)
|
|
@@ -1606,22 +1882,35 @@ async function startGateway(options) {
|
|
|
1606
1882
|
},
|
|
1607
1883
|
createPairing: () => {
|
|
1608
1884
|
const spec = options.secure;
|
|
1609
|
-
if (spec === undefined
|
|
1885
|
+
if (spec === undefined)
|
|
1610
1886
|
return;
|
|
1611
1887
|
const { secret, expiresAt } = spec.offers.create();
|
|
1612
1888
|
const pair = `pair=${toBase64Url(spec.hostKey.publicKey)}.${secret}`;
|
|
1613
1889
|
if (relay !== undefined) {
|
|
1614
|
-
const
|
|
1615
|
-
return { url: `${relay.origin}/#${pair}&url=${encodeURIComponent(
|
|
1890
|
+
const target2 = `${relayWsBase(relay.origin)}/c/${relay.hostId}`;
|
|
1891
|
+
return { url: `${relay.origin}/#${pair}&url=${encodeURIComponent(target2)}`, expiresAt };
|
|
1616
1892
|
}
|
|
1617
|
-
|
|
1893
|
+
if (remote !== undefined)
|
|
1894
|
+
return { url: `${remote.advertiseUrl}/#${pair}`, expiresAt };
|
|
1895
|
+
const target = `ws://127.0.0.1:${port}${LOOPBACK_SECURE_PATH}`;
|
|
1896
|
+
return { url: `http://127.0.0.1:${port}/#${pair}&url=${encodeURIComponent(target)}`, expiresAt };
|
|
1618
1897
|
}
|
|
1619
1898
|
});
|
|
1620
1899
|
clients.add(client);
|
|
1621
1900
|
};
|
|
1622
1901
|
server.on("connection", (socket, request) => {
|
|
1623
1902
|
socket.on("error", () => socket.terminate());
|
|
1624
|
-
const
|
|
1903
|
+
const url = new URL(request.url ?? "/", "ws://127.0.0.1");
|
|
1904
|
+
if (url.pathname === LOOPBACK_SECURE_PATH) {
|
|
1905
|
+
const secure2 = options.secure;
|
|
1906
|
+
if (secure2 === undefined) {
|
|
1907
|
+
socket.close(UNAUTHORIZED_CLOSE_CODE, "unauthorized");
|
|
1908
|
+
return;
|
|
1909
|
+
}
|
|
1910
|
+
secureHandshake(socket, secure2, options.harness, register);
|
|
1911
|
+
return;
|
|
1912
|
+
}
|
|
1913
|
+
const token = url.searchParams.get("token");
|
|
1625
1914
|
const origin = request.headers.origin;
|
|
1626
1915
|
if (token !== options.token || origin !== undefined && !browserOrigins.includes(origin)) {
|
|
1627
1916
|
socket.close(UNAUTHORIZED_CLOSE_CODE, "unauthorized");
|
|
@@ -1659,6 +1948,7 @@ async function startGateway(options) {
|
|
|
1659
1948
|
broadcast,
|
|
1660
1949
|
async close() {
|
|
1661
1950
|
await relay?.close();
|
|
1951
|
+
detachAutoTitle?.();
|
|
1662
1952
|
conversations.dispose();
|
|
1663
1953
|
await Promise.all([...clients].map((client) => client.dispose()));
|
|
1664
1954
|
for (const socket of server.clients)
|
|
@@ -1759,12 +2049,12 @@ function secureHandshake(socket, secure, harness, register) {
|
|
|
1759
2049
|
return;
|
|
1760
2050
|
}
|
|
1761
2051
|
const deviceKey = toBase64Url(remoteStatic);
|
|
1762
|
-
if (!await isRegistered(harness, deviceKey,
|
|
2052
|
+
if (!await isRegistered(harness, deviceKey, context2)) {
|
|
1763
2053
|
if (hello.pair === undefined || !secure.offers.consume(hello.pair.secret)) {
|
|
1764
2054
|
socket.close(UNAUTHORIZED_CLOSE_CODE, "unauthorized");
|
|
1765
2055
|
return;
|
|
1766
2056
|
}
|
|
1767
|
-
await registerDevice(harness, { publicKey: deviceKey, name: hello.pair.name },
|
|
2057
|
+
await registerDevice(harness, { publicKey: deviceKey, name: hello.pair.name }, context2);
|
|
1768
2058
|
}
|
|
1769
2059
|
const { message, result: result2 } = responder.writeMessage2(encodeHostHello({ v: 1 }));
|
|
1770
2060
|
return { result: result2, message, deviceKey };
|
|
@@ -1807,36 +2097,153 @@ async function startRemote(options, register) {
|
|
|
1807
2097
|
|
|
1808
2098
|
class ConversationList {
|
|
1809
2099
|
#listeners = new Set;
|
|
1810
|
-
#
|
|
2100
|
+
#questionTokens;
|
|
2101
|
+
#questionKinds;
|
|
2102
|
+
#records = new Map;
|
|
2103
|
+
#facts = new Map;
|
|
2104
|
+
#index = new Map;
|
|
2105
|
+
#runEnd = new Set;
|
|
2106
|
+
#list = [];
|
|
1811
2107
|
#unsubscribe = () => {};
|
|
1812
2108
|
#notifying = false;
|
|
1813
|
-
constructor(
|
|
1814
|
-
this.#
|
|
2109
|
+
constructor(docs) {
|
|
2110
|
+
this.#questionTokens = (docs ?? []).filter((doc) => doc.presentation === "pinomad.question").map((doc) => doc.token);
|
|
2111
|
+
this.#questionKinds = new Set(this.#questionTokens.map((token) => token.definition.kind));
|
|
1815
2112
|
}
|
|
1816
|
-
static async open(harness) {
|
|
2113
|
+
static async open(harness, docs) {
|
|
2114
|
+
const list = new ConversationList(docs);
|
|
1817
2115
|
const summaries = [];
|
|
1818
2116
|
let cursor;
|
|
1819
2117
|
do {
|
|
1820
|
-
const page = await harness.commit((tx) => tx.scanConversations({}, 256, cursor),
|
|
1821
|
-
for (const record of page.items)
|
|
1822
|
-
|
|
2118
|
+
const page = await harness.commit((tx) => tx.scanConversations({}, 256, cursor), context2);
|
|
2119
|
+
for (const record of page.items) {
|
|
2120
|
+
list.#records.set(record.id, record);
|
|
2121
|
+
const facts = await list.#load(harness, record);
|
|
2122
|
+
list.#facts.set(record.id, facts);
|
|
2123
|
+
list.#index.set(record.id, summaries.length);
|
|
2124
|
+
summaries.push(summaryOf(record, facts));
|
|
2125
|
+
}
|
|
1823
2126
|
cursor = page.next;
|
|
1824
2127
|
} while (cursor !== undefined);
|
|
1825
|
-
|
|
1826
|
-
list.#unsubscribe = harness.subscribeCommits((publication) =>
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
2128
|
+
list.#list = summaries;
|
|
2129
|
+
list.#unsubscribe = harness.subscribeCommits((publication) => list.#observe(publication));
|
|
2130
|
+
return list;
|
|
2131
|
+
}
|
|
2132
|
+
async#load(harness, record) {
|
|
2133
|
+
const history = await historyFacts(harness, record.id);
|
|
2134
|
+
const facts = { pending: new Set, ...history };
|
|
2135
|
+
const live = await harness.snapshot(LiveDoc, record.id, context2);
|
|
2136
|
+
facts.running = live?.run !== undefined;
|
|
2137
|
+
facts.tools = live?.tools;
|
|
2138
|
+
for (const token of this.#questionTokens) {
|
|
2139
|
+
const value = await harness.snapshot(token, record.id, context2);
|
|
2140
|
+
if (Value2.Check(QuestionSchema, value) && value.requests.some((request) => request.resolution === undefined)) {
|
|
2141
|
+
facts.pending.add(token.definition.kind);
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
if (record.owner !== undefined) {
|
|
2145
|
+
const subagent = await harness.snapshot(SubagentDoc, record.id, context2);
|
|
2146
|
+
if (typeof subagent?.label === "string" && subagent.label !== "")
|
|
2147
|
+
facts.label = subagent.label;
|
|
2148
|
+
if (typeof subagent?.role === "string" && subagent.role !== "")
|
|
2149
|
+
facts.role = subagent.role;
|
|
2150
|
+
}
|
|
2151
|
+
const named = (await harness.snapshot(TitleDoc, record.id, context2))?.title;
|
|
2152
|
+
if (typeof named === "string" && named !== "")
|
|
2153
|
+
facts.named = named;
|
|
2154
|
+
return facts;
|
|
2155
|
+
}
|
|
2156
|
+
#observe(publication) {
|
|
2157
|
+
const touched = new Set;
|
|
2158
|
+
const ended = new Set;
|
|
2159
|
+
for (const change of publication.changes) {
|
|
2160
|
+
if (change.type === "conversation") {
|
|
2161
|
+
this.#records.set(change.value.id, change.value);
|
|
2162
|
+
touched.add(change.value.id);
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
for (const change of publication.changes) {
|
|
2166
|
+
if (change.type === "entry") {
|
|
2167
|
+
const entry = change.value;
|
|
2168
|
+
if (entry.kind !== "pi.user" && entry.kind !== "pi.assistant")
|
|
2169
|
+
continue;
|
|
2170
|
+
const facts = this.#fact(entry.conversationId);
|
|
2171
|
+
if (entry.kind === "pi.user" && facts.title === undefined)
|
|
2172
|
+
Object.assign(facts, titleOf(entry));
|
|
2173
|
+
if (facts.newest !== undefined && entry.id <= facts.newest)
|
|
2174
|
+
continue;
|
|
2175
|
+
touched.add(entry.conversationId);
|
|
2176
|
+
facts.newest = entry.id;
|
|
2177
|
+
const message = entry.model?.[0];
|
|
2178
|
+
facts.updatedAt = typeof message?.timestamp === "number" ? message.timestamp : undefined;
|
|
2179
|
+
facts.failed = entry.kind === "pi.assistant" && message?.role === "assistant" && message.stopReason === "error";
|
|
2180
|
+
facts.calls = entry.kind === "pi.assistant" ? callsOf(message) : undefined;
|
|
2181
|
+
} else if (change.type === "document") {
|
|
2182
|
+
const id = change.conversationId;
|
|
2183
|
+
if (id === undefined)
|
|
2184
|
+
continue;
|
|
2185
|
+
const facts = this.#fact(id);
|
|
2186
|
+
if (change.record.kind === "pi.live") {
|
|
2187
|
+
const live = change.value;
|
|
2188
|
+
const wasRunning = facts.running === true;
|
|
2189
|
+
facts.running = live?.run !== undefined;
|
|
2190
|
+
facts.tools = live?.tools;
|
|
2191
|
+
touched.add(id);
|
|
2192
|
+
if (wasRunning && !facts.running && this.#records.get(id)?.owner === undefined)
|
|
2193
|
+
ended.add(id);
|
|
2194
|
+
} else if (this.#questionKinds.has(change.record.kind)) {
|
|
2195
|
+
const value = change.value;
|
|
2196
|
+
const pending = Value2.Check(QuestionSchema, value) && value.requests.some((request) => request.resolution === undefined);
|
|
2197
|
+
const next2 = new Set(facts.pending);
|
|
2198
|
+
if (pending)
|
|
2199
|
+
next2.add(change.record.kind);
|
|
2200
|
+
else
|
|
2201
|
+
next2.delete(change.record.kind);
|
|
2202
|
+
facts.pending = next2;
|
|
2203
|
+
touched.add(id);
|
|
2204
|
+
} else if (change.record.kind === SubagentDoc.definition.kind) {
|
|
2205
|
+
const value = change.value;
|
|
2206
|
+
facts.label = typeof value?.label === "string" && value.label !== "" ? value.label : undefined;
|
|
2207
|
+
facts.role = typeof value?.role === "string" && value.role !== "" ? value.role : undefined;
|
|
2208
|
+
touched.add(id);
|
|
2209
|
+
} else if (change.record.kind === TitleDoc.definition.kind) {
|
|
2210
|
+
const named = change.value?.title;
|
|
2211
|
+
facts.named = typeof named === "string" && named !== "" ? named : undefined;
|
|
2212
|
+
touched.add(id);
|
|
1834
2213
|
}
|
|
1835
2214
|
}
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
2215
|
+
}
|
|
2216
|
+
let next;
|
|
2217
|
+
for (const id of touched) {
|
|
2218
|
+
const record = this.#records.get(id);
|
|
2219
|
+
if (record === undefined)
|
|
2220
|
+
continue;
|
|
2221
|
+
const summary = summaryOf(record, this.#fact(id));
|
|
2222
|
+
const index = this.#index.get(id);
|
|
2223
|
+
if (index === undefined) {
|
|
2224
|
+
next ??= this.#list.slice();
|
|
2225
|
+
this.#index.set(id, next.length);
|
|
2226
|
+
next.push(summary);
|
|
2227
|
+
} else if (!sameSummary(this.#list[index], summary)) {
|
|
2228
|
+
next ??= this.#list.slice();
|
|
2229
|
+
next[index] = summary;
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
if (next !== undefined)
|
|
2233
|
+
this.#set(next);
|
|
2234
|
+
for (const id of ended)
|
|
2235
|
+
for (const listener of this.#runEnd)
|
|
2236
|
+
listener(id);
|
|
2237
|
+
}
|
|
2238
|
+
onRunEnd(listener) {
|
|
2239
|
+
this.#runEnd.add(listener);
|
|
2240
|
+
return () => this.#runEnd.delete(listener);
|
|
2241
|
+
}
|
|
2242
|
+
#fact(id) {
|
|
2243
|
+
let facts = this.#facts.get(id);
|
|
2244
|
+
if (facts === undefined)
|
|
2245
|
+
this.#facts.set(id, facts = { pending: new Set });
|
|
2246
|
+
return facts;
|
|
1840
2247
|
}
|
|
1841
2248
|
get value() {
|
|
1842
2249
|
return this.#list;
|
|
@@ -1861,31 +2268,72 @@ class ConversationList {
|
|
|
1861
2268
|
this.#listeners.clear();
|
|
1862
2269
|
}
|
|
1863
2270
|
}
|
|
1864
|
-
function summaryOf(record) {
|
|
2271
|
+
function summaryOf(record, facts) {
|
|
1865
2272
|
const parent = record.parent?.conversationId ?? record.owner?.conversationId;
|
|
2273
|
+
const status = facts.pending.size > 0 ? "needs-answer" : facts.running === true ? "running" : facts.failed === true ? "failed" : undefined;
|
|
2274
|
+
const runningSlot = facts.running === true ? facts.tools?.find((slot) => slot.status === "running") : undefined;
|
|
2275
|
+
const activity = runningSlot === undefined ? undefined : {
|
|
2276
|
+
tool: runningSlot.name,
|
|
2277
|
+
...facts.calls?.get(runningSlot.callId) === undefined ? {} : { target: facts.calls.get(runningSlot.callId) }
|
|
2278
|
+
};
|
|
1866
2279
|
return {
|
|
1867
2280
|
id: record.id,
|
|
1868
2281
|
kind: record.owner !== undefined ? "subagent" : record.parent !== undefined ? "fork" : "conversation",
|
|
1869
|
-
...parent === undefined ? {} : { parent }
|
|
2282
|
+
...parent === undefined ? {} : { parent },
|
|
2283
|
+
...record.parent === undefined ? {} : { forkedAt: String(record.parent.at) },
|
|
2284
|
+
...(facts.named ?? facts.title) === undefined ? {} : { title: facts.named ?? facts.title },
|
|
2285
|
+
...status === undefined ? {} : { status },
|
|
2286
|
+
...facts.updatedAt === undefined ? {} : { updatedAt: facts.updatedAt },
|
|
2287
|
+
...facts.label === undefined ? {} : { label: facts.label },
|
|
2288
|
+
...facts.role === undefined ? {} : { role: facts.role },
|
|
2289
|
+
...activity === undefined ? {} : { activity }
|
|
1870
2290
|
};
|
|
1871
2291
|
}
|
|
2292
|
+
function sameSummary(a, b) {
|
|
2293
|
+
return a.id === b.id && a.kind === b.kind && 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.activity?.tool === b.activity?.tool && a.activity?.target === b.activity?.target;
|
|
2294
|
+
}
|
|
1872
2295
|
function titleOf(entry) {
|
|
1873
2296
|
const message = entry?.model?.[0];
|
|
1874
2297
|
if (message?.role !== "user")
|
|
1875
2298
|
return {};
|
|
1876
2299
|
const text = typeof message.content === "string" ? message.content : message.content.flatMap((block) => block.type === "text" ? [block.text] : []).join(" ");
|
|
1877
|
-
|
|
2300
|
+
const title = text.replace(/\s+/g, " ").trim();
|
|
2301
|
+
return title === "" ? {} : { title };
|
|
1878
2302
|
}
|
|
1879
|
-
async function
|
|
1880
|
-
const conversation = await harness.conversation(id,
|
|
2303
|
+
async function historyFacts(harness, id) {
|
|
2304
|
+
const conversation = await harness.conversation(id, context2);
|
|
1881
2305
|
let first;
|
|
2306
|
+
let newest;
|
|
1882
2307
|
let cursor;
|
|
1883
2308
|
do {
|
|
1884
|
-
const page = await conversation.entries({}, 256, cursor,
|
|
1885
|
-
first = page.items.findLast((entry) => entry.kind === "pi.user" && entry.conversationId === id) ?? first;
|
|
2309
|
+
const page = await conversation.entries({}, 256, cursor, context2);
|
|
2310
|
+
first = page.items.findLast((entry) => entry.kind === "pi.user" && entry.conversationId === id && titleOf(entry).title !== undefined) ?? first;
|
|
2311
|
+
newest ??= page.items.find((entry) => entry.conversationId === id && (entry.kind === "pi.user" || entry.kind === "pi.assistant"));
|
|
1886
2312
|
cursor = page.next;
|
|
1887
2313
|
} while (cursor !== undefined);
|
|
1888
|
-
|
|
2314
|
+
const message = newest?.model?.[0];
|
|
2315
|
+
return {
|
|
2316
|
+
...titleOf(first),
|
|
2317
|
+
...newest === undefined ? {} : {
|
|
2318
|
+
newest: newest.id,
|
|
2319
|
+
...typeof message?.timestamp === "number" ? { updatedAt: message.timestamp } : {},
|
|
2320
|
+
failed: newest.kind === "pi.assistant" && message?.role === "assistant" && message.stopReason === "error",
|
|
2321
|
+
...newest.kind === "pi.assistant" ? { calls: callsOf(message) } : {}
|
|
2322
|
+
}
|
|
2323
|
+
};
|
|
2324
|
+
}
|
|
2325
|
+
function callsOf(message) {
|
|
2326
|
+
if (message?.role !== "assistant")
|
|
2327
|
+
return;
|
|
2328
|
+
const content = message.content;
|
|
2329
|
+
if (!Array.isArray(content))
|
|
2330
|
+
return;
|
|
2331
|
+
const calls = new Map;
|
|
2332
|
+
for (const block of content) {
|
|
2333
|
+
if (block?.type === "toolCall" && typeof block.id === "string")
|
|
2334
|
+
calls.set(block.id, toolTarget(block.arguments));
|
|
2335
|
+
}
|
|
2336
|
+
return calls.size === 0 ? undefined : calls;
|
|
1889
2337
|
}
|
|
1890
2338
|
|
|
1891
2339
|
class GatewayClient {
|
|
@@ -1893,38 +2341,59 @@ class GatewayClient {
|
|
|
1893
2341
|
#connection;
|
|
1894
2342
|
#hooks;
|
|
1895
2343
|
#options;
|
|
1896
|
-
#
|
|
2344
|
+
#hostValues;
|
|
1897
2345
|
#subscriptions = new Map;
|
|
1898
2346
|
#disposed = false;
|
|
1899
2347
|
constructor(connection, options, conversations, hooks) {
|
|
1900
2348
|
this.#connection = connection;
|
|
1901
2349
|
this.#options = options;
|
|
1902
|
-
this.#
|
|
2350
|
+
this.#hostValues = {
|
|
2351
|
+
conversations,
|
|
2352
|
+
mcp: options.mcp ?? constantValue(null),
|
|
2353
|
+
models: options.modelAuth?.models ?? constantValue(options.modelSummaries()),
|
|
2354
|
+
auth: options.modelAuth?.auth ?? constantValue({ logins: [] }),
|
|
2355
|
+
settings: options.settings,
|
|
2356
|
+
agents: options.agents ?? constantValue({ roles: [], problems: [] })
|
|
2357
|
+
};
|
|
1903
2358
|
this.#hooks = hooks;
|
|
1904
2359
|
this.devicePublicKey = hooks.devicePublicKey;
|
|
1905
2360
|
connection.handle({
|
|
1906
2361
|
message: (text) => {
|
|
1907
|
-
let
|
|
2362
|
+
let parsed;
|
|
1908
2363
|
try {
|
|
1909
|
-
|
|
2364
|
+
parsed = parseClientFrame(JSON.parse(text));
|
|
1910
2365
|
} catch {
|
|
1911
|
-
connection.close(
|
|
2366
|
+
connection.close(INVALID_FRAME_CLOSE_CODE, "Invalid client frame");
|
|
1912
2367
|
return;
|
|
1913
2368
|
}
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
2369
|
+
switch (parsed.kind) {
|
|
2370
|
+
case "frame":
|
|
2371
|
+
this.#receive(parsed.frame).catch(() => connection.close(1011, "Gateway request failed"));
|
|
2372
|
+
return;
|
|
2373
|
+
case "unsupportedCall":
|
|
2374
|
+
this.send({ type: "result", id: parsed.id, ok: false, error: `Unsupported method: ${parsed.method}` });
|
|
2375
|
+
return;
|
|
2376
|
+
case "unsupportedStream":
|
|
2377
|
+
if (parsed.type === "subscribe") {
|
|
2378
|
+
this.send({ type: "ended", stream: parsed.stream, reason: "Unsupported stream" });
|
|
2379
|
+
}
|
|
2380
|
+
return;
|
|
2381
|
+
case "unknownType":
|
|
2382
|
+
return;
|
|
2383
|
+
case "malformed":
|
|
2384
|
+
connection.close(INVALID_FRAME_CLOSE_CODE, "Invalid client frame");
|
|
2385
|
+
return;
|
|
1917
2386
|
}
|
|
1918
|
-
this.#receive(frame).catch(() => connection.close(1011, "Gateway request failed"));
|
|
1919
2387
|
},
|
|
1920
2388
|
closed: () => hooks.closed(this)
|
|
1921
2389
|
});
|
|
1922
2390
|
this.send({
|
|
1923
2391
|
type: "hello",
|
|
1924
|
-
protocol:
|
|
2392
|
+
protocol: PROTOCOL,
|
|
2393
|
+
...packagedVersion === undefined ? {} : { hostVersion: packagedVersion },
|
|
1925
2394
|
session: options.session,
|
|
1926
2395
|
models: options.modelSummaries(),
|
|
1927
|
-
defaults: options.
|
|
2396
|
+
defaults: settingsDefaults(options.settings.value),
|
|
1928
2397
|
docs: (options.docs ?? []).map((doc) => ({
|
|
1929
2398
|
kind: doc.token.definition.kind,
|
|
1930
2399
|
...doc.presentation === undefined ? {} : { presentation: doc.presentation }
|
|
@@ -1975,36 +2444,34 @@ class GatewayClient {
|
|
|
1975
2444
|
state.subscription = subscription;
|
|
1976
2445
|
}
|
|
1977
2446
|
async#open(stream, send) {
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
return { stop:
|
|
2447
|
+
const source = this.#hostValue(stream);
|
|
2448
|
+
if (source !== undefined) {
|
|
2449
|
+
send({ type: "snapshot", stream, value: source.value });
|
|
2450
|
+
return { stop: source.subscribe((value) => void send({ type: "snapshot", stream, value })) };
|
|
1982
2451
|
}
|
|
1983
2452
|
if (stream === "index") {
|
|
1984
|
-
const
|
|
1985
|
-
if (
|
|
2453
|
+
const watch2 = await this.#options.harness.watchDoc(IndexDoc, context2);
|
|
2454
|
+
if (watch2 === undefined)
|
|
1986
2455
|
throw new Error("Index document is missing");
|
|
1987
|
-
return this.#forward(stream,
|
|
2456
|
+
return this.#forward(stream, watch2, send);
|
|
1988
2457
|
}
|
|
1989
2458
|
if (stream === "devices") {
|
|
1990
|
-
const
|
|
1991
|
-
if (
|
|
2459
|
+
const watch2 = await this.#options.harness.watchDoc(DevicesDoc, context2);
|
|
2460
|
+
if (watch2 === undefined)
|
|
1992
2461
|
throw new Error("Devices document is missing");
|
|
1993
|
-
return this.#forward(stream,
|
|
2462
|
+
return this.#forward(stream, watch2, send);
|
|
1994
2463
|
}
|
|
1995
2464
|
if (stream === "tasks")
|
|
1996
|
-
return this.#forward(stream, await this.#options.harness.watchTaskGraph(
|
|
1997
|
-
if (stream === "mcp") {
|
|
1998
|
-
await send({ type: "snapshot", stream, value: this.#options.mcp?.value ?? null });
|
|
1999
|
-
const mcp = this.#options.mcp;
|
|
2000
|
-
return mcp === undefined ? { stop: () => {} } : { stop: mcp.subscribe((value) => void send({ type: "snapshot", stream, value })) };
|
|
2001
|
-
}
|
|
2465
|
+
return this.#forward(stream, await this.#options.harness.watchTaskGraph(context2), send);
|
|
2002
2466
|
if (stream.startsWith("doc:")) {
|
|
2003
2467
|
const at = stream.lastIndexOf(":");
|
|
2004
2468
|
return this.#openDoc(stream, stream.slice("doc:".length, at), Number(stream.slice(at + 1)), send);
|
|
2005
2469
|
}
|
|
2006
2470
|
const conversation = await this.#conversation(Number(stream.slice("conversation:".length)));
|
|
2007
|
-
return this.#forward(stream, await conversation.watch(
|
|
2471
|
+
return this.#forward(stream, await conversation.watch(context2), send);
|
|
2472
|
+
}
|
|
2473
|
+
#hostValue(stream) {
|
|
2474
|
+
return Object.hasOwn(this.#hostValues, stream) ? this.#hostValues[stream] : undefined;
|
|
2008
2475
|
}
|
|
2009
2476
|
async#openDoc(stream, kind, id, send) {
|
|
2010
2477
|
const doc = this.#options.docs?.find((candidate) => candidate.token.definition.kind === kind);
|
|
@@ -2027,7 +2494,7 @@ class GatewayClient {
|
|
|
2027
2494
|
setImmediate(() => signalCreation());
|
|
2028
2495
|
});
|
|
2029
2496
|
try {
|
|
2030
|
-
const existing = await harness.watchDoc(token, id,
|
|
2497
|
+
const existing = await harness.watchDoc(token, id, context2);
|
|
2031
2498
|
if (existing !== undefined) {
|
|
2032
2499
|
unsubscribe();
|
|
2033
2500
|
return this.#forward(stream, existing, send);
|
|
@@ -2040,13 +2507,13 @@ class GatewayClient {
|
|
|
2040
2507
|
created.then(async () => {
|
|
2041
2508
|
if (stopped)
|
|
2042
2509
|
return;
|
|
2043
|
-
const
|
|
2044
|
-
if (
|
|
2510
|
+
const watch2 = await harness.watchDoc(token, id, context2);
|
|
2511
|
+
if (watch2 === undefined)
|
|
2045
2512
|
return;
|
|
2046
2513
|
if (stopped)
|
|
2047
|
-
await
|
|
2514
|
+
await watch2.stop();
|
|
2048
2515
|
else {
|
|
2049
|
-
inner = await this.#forward(stream,
|
|
2516
|
+
inner = await this.#forward(stream, watch2, send);
|
|
2050
2517
|
if (stopped)
|
|
2051
2518
|
await inner.stop();
|
|
2052
2519
|
}
|
|
@@ -2063,10 +2530,10 @@ class GatewayClient {
|
|
|
2063
2530
|
}
|
|
2064
2531
|
};
|
|
2065
2532
|
}
|
|
2066
|
-
async#forward(stream,
|
|
2067
|
-
await send({ type: "snapshot", stream, value:
|
|
2068
|
-
|
|
2069
|
-
return
|
|
2533
|
+
async#forward(stream, watch2, send) {
|
|
2534
|
+
await send({ type: "snapshot", stream, value: watch2.value });
|
|
2535
|
+
watch2.start((_value, ops) => send({ type: "ops", stream, ops }));
|
|
2536
|
+
return watch2;
|
|
2070
2537
|
}
|
|
2071
2538
|
async#unsubscribe(stream) {
|
|
2072
2539
|
const subscription = this.#subscriptions.get(stream);
|
|
@@ -2074,7 +2541,7 @@ class GatewayClient {
|
|
|
2074
2541
|
await subscription?.subscription?.stop();
|
|
2075
2542
|
}
|
|
2076
2543
|
async#conversation(id) {
|
|
2077
|
-
const conversation = await this.#options.harness.conversation(id,
|
|
2544
|
+
const conversation = await this.#options.harness.conversation(id, context2);
|
|
2078
2545
|
if (conversation === undefined)
|
|
2079
2546
|
throw new Error(`Conversation ${id} does not exist`);
|
|
2080
2547
|
return conversation;
|
|
@@ -2090,27 +2557,31 @@ class GatewayClient {
|
|
|
2090
2557
|
async#call(method, args) {
|
|
2091
2558
|
switch (method) {
|
|
2092
2559
|
case "addProject":
|
|
2093
|
-
return addProject(this.#options.harness, args.path,
|
|
2560
|
+
return addProject(this.#options.harness, args.path, context2);
|
|
2094
2561
|
case "removeProject":
|
|
2095
|
-
await removeProject(this.#options.harness, args.path,
|
|
2562
|
+
await removeProject(this.#options.harness, args.path, context2);
|
|
2096
2563
|
return null;
|
|
2097
2564
|
case "createConversation": {
|
|
2098
2565
|
const create = args;
|
|
2566
|
+
const problem = imageProblem(create.images ?? []);
|
|
2567
|
+
if (problem !== undefined)
|
|
2568
|
+
throw new Error(problem);
|
|
2099
2569
|
const conversationId = await createConversation(this.#options.harness, this.#options.dataDir, {
|
|
2100
2570
|
home: create.home,
|
|
2101
2571
|
text: create.text,
|
|
2102
2572
|
requestId: create.requestId,
|
|
2573
|
+
...create.images === undefined ? {} : { images: create.images },
|
|
2103
2574
|
...create.checkout === undefined ? {} : { checkout: create.checkout },
|
|
2104
2575
|
...create.model === undefined ? {} : { model: create.model },
|
|
2105
2576
|
...create.thinkingLevel === undefined ? {} : { thinkingLevel: create.thinkingLevel }
|
|
2106
|
-
}, this.#options.
|
|
2577
|
+
}, settingsDefaults(this.#options.settings.value), this.#options.models, context2);
|
|
2107
2578
|
return { conversationId };
|
|
2108
2579
|
}
|
|
2109
2580
|
case "archive": {
|
|
2110
2581
|
const input = args;
|
|
2111
|
-
await archive(this.#options.harness, input.conversationId, input.archived,
|
|
2582
|
+
await archive(this.#options.harness, input.conversationId, input.archived, context2);
|
|
2112
2583
|
if (input.archived) {
|
|
2113
|
-
const kept = await cleanupArchivedWorktrees(this.#options.harness, input.conversationId,
|
|
2584
|
+
const kept = await cleanupArchivedWorktrees(this.#options.harness, input.conversationId, context2);
|
|
2114
2585
|
for (const { record, reason } of kept) {
|
|
2115
2586
|
await this.send({ type: "notice", level: "warning", message: `Kept worktree ${record.path} (branch ${record.branch}): ${reason}` });
|
|
2116
2587
|
}
|
|
@@ -2125,31 +2596,88 @@ class GatewayClient {
|
|
|
2125
2596
|
}
|
|
2126
2597
|
case "revokeDevice": {
|
|
2127
2598
|
const { publicKey } = args;
|
|
2128
|
-
await revokeDevice(this.#options.harness, publicKey,
|
|
2599
|
+
await revokeDevice(this.#options.harness, publicKey, context2);
|
|
2129
2600
|
this.#hooks.closeDevice(publicKey);
|
|
2130
2601
|
return null;
|
|
2131
2602
|
}
|
|
2603
|
+
case "logout": {
|
|
2604
|
+
const modelAuth = this.#options.modelAuth;
|
|
2605
|
+
if (modelAuth === undefined)
|
|
2606
|
+
throw new Error("Logging out is not available on this host");
|
|
2607
|
+
await modelAuth.logout(args.providerId);
|
|
2608
|
+
return null;
|
|
2609
|
+
}
|
|
2610
|
+
case "updateSettings": {
|
|
2611
|
+
await this.#options.settings.update(args.patch);
|
|
2612
|
+
return null;
|
|
2613
|
+
}
|
|
2614
|
+
case "createAgent": {
|
|
2615
|
+
const store = this.#options.agents;
|
|
2616
|
+
if (store === undefined)
|
|
2617
|
+
throw new Error("Named agents are not available on this host");
|
|
2618
|
+
await store.create(args.role);
|
|
2619
|
+
return null;
|
|
2620
|
+
}
|
|
2621
|
+
case "updateAgent": {
|
|
2622
|
+
const store = this.#options.agents;
|
|
2623
|
+
if (store === undefined)
|
|
2624
|
+
throw new Error("Named agents are not available on this host");
|
|
2625
|
+
await store.update(args.role);
|
|
2626
|
+
return null;
|
|
2627
|
+
}
|
|
2628
|
+
case "deleteAgent": {
|
|
2629
|
+
const store = this.#options.agents;
|
|
2630
|
+
if (store === undefined)
|
|
2631
|
+
throw new Error("Named agents are not available on this host");
|
|
2632
|
+
await store.remove(args.name);
|
|
2633
|
+
return null;
|
|
2634
|
+
}
|
|
2635
|
+
case "listFolders":
|
|
2636
|
+
return listFolders(args.path, homedir2());
|
|
2132
2637
|
}
|
|
2133
2638
|
const conversation = await this.#conversation(args.conversationId);
|
|
2134
2639
|
switch (method) {
|
|
2135
2640
|
case "submit": {
|
|
2136
|
-
const { text, whenBusy, requestId } = args;
|
|
2137
|
-
const
|
|
2138
|
-
|
|
2139
|
-
|
|
2641
|
+
const { text, whenBusy, requestId, images } = args;
|
|
2642
|
+
const problem = imageProblem(images ?? []);
|
|
2643
|
+
if (problem !== undefined)
|
|
2644
|
+
throw new Error(problem);
|
|
2645
|
+
const dismissed = await this.#dismissPendingQuestions(conversation.id, context2);
|
|
2646
|
+
const submission = await conversation.submit({ type: "input", content: messageContent(text, images), whenBusy: dismissed ? "steer" : whenBusy, requestId }, context2);
|
|
2647
|
+
submission.wait(context2).then((settled) => {
|
|
2140
2648
|
if (settled.status === "unanswered" && settled.reason !== "aborted") {
|
|
2141
2649
|
this.send({ type: "notice", level: "error", message: `No answer: ${settled.reason}` });
|
|
2142
2650
|
}
|
|
2143
2651
|
}, () => {});
|
|
2144
2652
|
return { submissionId: submission.id };
|
|
2145
2653
|
}
|
|
2654
|
+
case "renameConversation": {
|
|
2655
|
+
const { title } = args;
|
|
2656
|
+
const trimmed = title.trim();
|
|
2657
|
+
if (trimmed === "")
|
|
2658
|
+
throw new Error("Title cannot be empty");
|
|
2659
|
+
if (trimmed.length > 200)
|
|
2660
|
+
throw new Error("Title is too long (200 characters at most)");
|
|
2661
|
+
await this.#options.harness.commit(async (tx) => {
|
|
2662
|
+
const record = await tx.conversation(conversation.id);
|
|
2663
|
+
if (record === undefined)
|
|
2664
|
+
throw new Error(`Conversation ${conversation.id} does not exist`);
|
|
2665
|
+
if (record.owner !== undefined) {
|
|
2666
|
+
throw new Error("Subagent conversations are named by the call that started them");
|
|
2667
|
+
}
|
|
2668
|
+
const doc = await tx.doc(TitleDoc, conversation.id);
|
|
2669
|
+
doc.title = trimmed;
|
|
2670
|
+
doc.source = "manual";
|
|
2671
|
+
}, context2);
|
|
2672
|
+
return null;
|
|
2673
|
+
}
|
|
2146
2674
|
case "abort":
|
|
2147
|
-
await conversation.abort(
|
|
2675
|
+
await conversation.abort(context2);
|
|
2148
2676
|
return null;
|
|
2149
2677
|
case "compact": {
|
|
2150
2678
|
const { instructions } = args;
|
|
2151
|
-
const taskId = await conversation.compact(instructions,
|
|
2152
|
-
this.#options.harness.waitForTask(taskId,
|
|
2679
|
+
const taskId = await conversation.compact(instructions, context2);
|
|
2680
|
+
this.#options.harness.waitForTask(taskId, context2).then((receipt) => void this.send({ type: "notice", level: "info", message: `Compaction ${receipt.state.outcome.status}.` }), () => {});
|
|
2153
2681
|
return { taskId };
|
|
2154
2682
|
}
|
|
2155
2683
|
case "setModel": {
|
|
@@ -2158,7 +2686,7 @@ class GatewayClient {
|
|
|
2158
2686
|
if (model === undefined)
|
|
2159
2687
|
throw new Error(`Unknown model: ${ref.provider}/${ref.modelId}`);
|
|
2160
2688
|
const thinking = (await this.#agent(conversation.id)).thinkingLevel ?? "off";
|
|
2161
|
-
await conversation.configure({ model: ref, thinkingLevel: clampThinkingLevel3(model, thinking) },
|
|
2689
|
+
await conversation.configure({ model: ref, thinkingLevel: clampThinkingLevel3(model, thinking) }, context2);
|
|
2162
2690
|
return null;
|
|
2163
2691
|
}
|
|
2164
2692
|
case "setThinkingLevel": {
|
|
@@ -2170,20 +2698,39 @@ class GatewayClient {
|
|
|
2170
2698
|
if (!getSupportedThinkingLevels(model).includes(level)) {
|
|
2171
2699
|
throw new Error(`Thinking level ${level} is not supported by ${agent.model.provider}/${agent.model.modelId}`);
|
|
2172
2700
|
}
|
|
2173
|
-
await conversation.configure({ thinkingLevel: level },
|
|
2701
|
+
await conversation.configure({ thinkingLevel: level }, context2);
|
|
2174
2702
|
return null;
|
|
2175
2703
|
}
|
|
2176
2704
|
case "fork": {
|
|
2177
2705
|
const { entryId, removeTools = [] } = args;
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2706
|
+
return await serializedFork(this.#options.harness, async () => {
|
|
2707
|
+
const at = Number(entryId);
|
|
2708
|
+
await this.#options.harness.commit(async (tx) => {
|
|
2709
|
+
const record = await tx.conversation(conversation.id);
|
|
2710
|
+
if (record === undefined)
|
|
2711
|
+
throw new Error(`Conversation ${conversation.id} does not exist`);
|
|
2712
|
+
if (record.parent !== undefined || record.owner !== undefined) {
|
|
2713
|
+
throw new Error("Only top-level conversations can be forked");
|
|
2714
|
+
}
|
|
2715
|
+
let cursor;
|
|
2716
|
+
do {
|
|
2717
|
+
const page = await tx.scanConversations({}, 256, cursor);
|
|
2718
|
+
for (const candidate of page.items) {
|
|
2719
|
+
if (candidate.parent?.conversationId === conversation.id && candidate.parent.at === at) {
|
|
2720
|
+
throw new Error("This message already has a fork");
|
|
2721
|
+
}
|
|
2722
|
+
}
|
|
2723
|
+
cursor = page.next;
|
|
2724
|
+
} while (cursor !== undefined);
|
|
2725
|
+
}, context2);
|
|
2726
|
+
const agent = await conversation.agent(context2);
|
|
2727
|
+
const remove = agent.tools.filter((tool) => removeTools.includes(tool.name));
|
|
2728
|
+
const fork = await conversation.fork(at, {
|
|
2729
|
+
ownership: { kind: "ownerless" },
|
|
2730
|
+
...remove.length === 0 ? {} : { agent: { tools: { remove } } }
|
|
2731
|
+
}, context2);
|
|
2732
|
+
return { conversationId: fork.id };
|
|
2733
|
+
});
|
|
2187
2734
|
}
|
|
2188
2735
|
case "answer": {
|
|
2189
2736
|
const { kind, requestId, answers } = args;
|
|
@@ -2217,16 +2764,16 @@ class GatewayClient {
|
|
|
2217
2764
|
}
|
|
2218
2765
|
request.resolution = { outcome: "answered", answers, at: Date.now() };
|
|
2219
2766
|
return { first: true };
|
|
2220
|
-
},
|
|
2767
|
+
}, context2);
|
|
2221
2768
|
}
|
|
2222
2769
|
case "changes": {
|
|
2223
|
-
return await this.#changes(conversation,
|
|
2770
|
+
return await this.#changes(conversation, context2);
|
|
2224
2771
|
}
|
|
2225
2772
|
}
|
|
2226
2773
|
}
|
|
2227
|
-
async#dismissPendingQuestions(conversationId,
|
|
2774
|
+
async#dismissPendingQuestions(conversationId, context3) {
|
|
2228
2775
|
const questionDocs = (this.#options.docs ?? []).filter((doc) => doc.presentation === "pinomad.question");
|
|
2229
|
-
const snapshots = await Promise.all(questionDocs.map((doc) => this.#options.harness.snapshot(doc.token, conversationId,
|
|
2776
|
+
const snapshots = await Promise.all(questionDocs.map((doc) => this.#options.harness.snapshot(doc.token, conversationId, context3)));
|
|
2230
2777
|
const pending = snapshots.some((value) => Value2.Check(QuestionSchema, value) && value.requests.some((request) => request.resolution === undefined));
|
|
2231
2778
|
if (!pending)
|
|
2232
2779
|
return false;
|
|
@@ -2244,18 +2791,18 @@ class GatewayClient {
|
|
|
2244
2791
|
}
|
|
2245
2792
|
}
|
|
2246
2793
|
return dismissed;
|
|
2247
|
-
},
|
|
2794
|
+
}, context3);
|
|
2248
2795
|
}
|
|
2249
2796
|
get #chatsPrefix() {
|
|
2250
|
-
return
|
|
2797
|
+
return join6(this.#options.dataDir, "chats") + sep3;
|
|
2251
2798
|
}
|
|
2252
|
-
async#changes(conversation,
|
|
2253
|
-
const cwd = (await conversation.agent(
|
|
2799
|
+
async#changes(conversation, context3) {
|
|
2800
|
+
const cwd = (await conversation.agent(context3)).cwd;
|
|
2254
2801
|
if (cwd === undefined)
|
|
2255
2802
|
return { available: false, reason: "No working directory" };
|
|
2256
2803
|
if (cwd.startsWith(this.#chatsPrefix))
|
|
2257
2804
|
return { available: false, reason: "Chat conversations have no repository" };
|
|
2258
|
-
const index = await this.#options.harness.snapshot(IndexDoc,
|
|
2805
|
+
const index = await this.#options.harness.snapshot(IndexDoc, context3);
|
|
2259
2806
|
const record = checkoutAt(index?.checkouts, cwd);
|
|
2260
2807
|
if (record !== undefined) {
|
|
2261
2808
|
if (!await worktreeExists(record.path))
|
|
@@ -2267,43 +2814,8 @@ class GatewayClient {
|
|
|
2267
2814
|
return { available: false, reason: "Not in a git repository" };
|
|
2268
2815
|
return { available: true, ...await changesOf(base.repo, base.base) };
|
|
2269
2816
|
}
|
|
2270
|
-
async#forkCheckout(cwd) {
|
|
2271
|
-
if (cwd === undefined)
|
|
2272
|
-
return;
|
|
2273
|
-
const index = await this.#options.harness.snapshot(IndexDoc, context);
|
|
2274
|
-
const existing = checkoutAt(index?.checkouts, cwd);
|
|
2275
|
-
let source;
|
|
2276
|
-
if (existing !== undefined) {
|
|
2277
|
-
source = { repo: existing.repo, dir: existing.path, subdir: relative2(existing.path, cwd) };
|
|
2278
|
-
} else {
|
|
2279
|
-
if (cwd.startsWith(this.#chatsPrefix))
|
|
2280
|
-
return;
|
|
2281
|
-
const base2 = await gitBase(cwd);
|
|
2282
|
-
if (base2 === undefined)
|
|
2283
|
-
return;
|
|
2284
|
-
source = { repo: base2.repo, dir: base2.repo, subdir: base2.subdir };
|
|
2285
|
-
}
|
|
2286
|
-
const { base, snapshot } = await snapshotOf(source.dir);
|
|
2287
|
-
const suffix = branchSuffix();
|
|
2288
|
-
return async (tx, id) => {
|
|
2289
|
-
const path = worktreePath(this.#options.dataDir, id);
|
|
2290
|
-
const doc = await tx.doc(IndexDoc);
|
|
2291
|
-
if (doc.checkouts === undefined)
|
|
2292
|
-
doc.checkouts = [];
|
|
2293
|
-
doc.checkouts.push({
|
|
2294
|
-
conversationId: id,
|
|
2295
|
-
path,
|
|
2296
|
-
repo: source.repo,
|
|
2297
|
-
subdir: source.subdir,
|
|
2298
|
-
branch: worktreeBranch(id, suffix),
|
|
2299
|
-
base,
|
|
2300
|
-
snapshot
|
|
2301
|
-
});
|
|
2302
|
-
(await tx.doc(AgentDoc3, id)).cwd = join4(path, source.subdir);
|
|
2303
|
-
};
|
|
2304
|
-
}
|
|
2305
2817
|
async#agent(id) {
|
|
2306
|
-
return await this.#options.harness.snapshot(AgentDoc3, id,
|
|
2818
|
+
return await this.#options.harness.snapshot(AgentDoc3, id, context2) ?? {};
|
|
2307
2819
|
}
|
|
2308
2820
|
async dispose() {
|
|
2309
2821
|
if (this.#disposed)
|
|
@@ -2317,20 +2829,20 @@ class GatewayClient {
|
|
|
2317
2829
|
}
|
|
2318
2830
|
|
|
2319
2831
|
// apps/host/src/host.ts
|
|
2320
|
-
var
|
|
2832
|
+
var context3 = BACKGROUND_CONTEXT3;
|
|
2321
2833
|
async function hostToken(dataDir) {
|
|
2322
|
-
const path =
|
|
2834
|
+
const path = join7(dataDir, "token");
|
|
2323
2835
|
try {
|
|
2324
|
-
return (await
|
|
2836
|
+
return (await readFile4(path, "utf8")).trim();
|
|
2325
2837
|
} catch {
|
|
2326
|
-
const token =
|
|
2327
|
-
await
|
|
2838
|
+
const token = randomBytes3(24).toString("base64url");
|
|
2839
|
+
await writeFile3(path, `${token}
|
|
2328
2840
|
`, { mode: 384 });
|
|
2329
2841
|
return token;
|
|
2330
2842
|
}
|
|
2331
2843
|
}
|
|
2332
2844
|
async function openHost(options) {
|
|
2333
|
-
await
|
|
2845
|
+
await mkdir4(options.dataDir, { recursive: true });
|
|
2334
2846
|
const stale = Math.max(2000, options.lockStaleMs ?? 1e4);
|
|
2335
2847
|
const release = await lockfile.lock(options.dataDir, {
|
|
2336
2848
|
realpath: false,
|
|
@@ -2341,18 +2853,43 @@ async function openHost(options) {
|
|
|
2341
2853
|
});
|
|
2342
2854
|
let harness;
|
|
2343
2855
|
let mcp;
|
|
2856
|
+
let agentRoles;
|
|
2857
|
+
let extensions = [];
|
|
2344
2858
|
try {
|
|
2345
2859
|
const token = await hostToken(options.dataDir);
|
|
2346
2860
|
const reports = [];
|
|
2347
2861
|
let report = (error) => void reports.push(error);
|
|
2348
2862
|
const registry = createRegistry();
|
|
2349
|
-
|
|
2863
|
+
agentRoles = await openAgentRoles({ dir: join7(options.dataDir, "agents") });
|
|
2864
|
+
extensions = typeof options.extensions === "function" ? options.extensions({
|
|
2865
|
+
harness: () => {
|
|
2866
|
+
if (harness === undefined)
|
|
2867
|
+
throw new Error("harness is not open yet");
|
|
2868
|
+
return harness;
|
|
2869
|
+
},
|
|
2870
|
+
agentRoles
|
|
2871
|
+
}) : options.extensions ?? [];
|
|
2350
2872
|
for (const { extension } of extensions)
|
|
2351
2873
|
registry.install(extension);
|
|
2352
|
-
mcp = options.mcpConfig === undefined ? undefined : startMcp({
|
|
2874
|
+
mcp = options.mcpConfig === undefined ? undefined : startMcp({
|
|
2875
|
+
configPath: options.mcpConfig,
|
|
2876
|
+
registry,
|
|
2877
|
+
onNamespaces: (namespaces) => {
|
|
2878
|
+
for (const each of extensions) {
|
|
2879
|
+
const rebuilt = each.namespaced?.(namespaces);
|
|
2880
|
+
if (rebuilt !== undefined)
|
|
2881
|
+
registry.install(rebuilt);
|
|
2882
|
+
}
|
|
2883
|
+
}
|
|
2884
|
+
});
|
|
2885
|
+
const hostSettings = await openHostSettings({
|
|
2886
|
+
path: join7(options.dataDir, "settings.json"),
|
|
2887
|
+
fallback: options.settingsFallback,
|
|
2888
|
+
models: options.modelAuth?.models ?? constantValue(options.modelSummaries())
|
|
2889
|
+
});
|
|
2353
2890
|
const environments = new Map;
|
|
2354
|
-
const chatsDir =
|
|
2355
|
-
harness = await Harness.open(await openNodeSqliteStorage(
|
|
2891
|
+
const chatsDir = join7(options.dataDir, "chats");
|
|
2892
|
+
harness = await Harness.open(await (options.openStorage ?? openNodeSqliteStorage)(join7(options.dataDir, "session.sqlite")), {
|
|
2356
2893
|
models: options.models,
|
|
2357
2894
|
registry,
|
|
2358
2895
|
env: async (target) => {
|
|
@@ -2361,15 +2898,15 @@ async function openHost(options) {
|
|
|
2361
2898
|
return;
|
|
2362
2899
|
const worktrees = worktreeRoot(options.dataDir);
|
|
2363
2900
|
if (cwd === worktrees || cwd.startsWith(worktrees + sep4)) {
|
|
2364
|
-
const index = await target.read.snapshot(IndexDoc,
|
|
2901
|
+
const index = await target.read.snapshot(IndexDoc, context3);
|
|
2365
2902
|
const record = checkoutAt(index?.checkouts, cwd);
|
|
2366
2903
|
if (record !== undefined) {
|
|
2367
2904
|
await ensureWorktree(record);
|
|
2368
|
-
await
|
|
2905
|
+
await mkdir4(cwd, { recursive: true });
|
|
2369
2906
|
}
|
|
2370
2907
|
}
|
|
2371
2908
|
if (cwd.startsWith(`${chatsDir}/`))
|
|
2372
|
-
await
|
|
2909
|
+
await mkdir4(cwd, { recursive: true });
|
|
2373
2910
|
let env = environments.get(cwd);
|
|
2374
2911
|
if (env === undefined) {
|
|
2375
2912
|
env = new NodeExecutionEnv({ cwd });
|
|
@@ -2377,54 +2914,51 @@ async function openHost(options) {
|
|
|
2377
2914
|
}
|
|
2378
2915
|
return env;
|
|
2379
2916
|
},
|
|
2380
|
-
...options.settings === undefined ? {} : {
|
|
2917
|
+
...options.settings === undefined ? {} : {
|
|
2918
|
+
settings: typeof options.settings === "function" ? options.settings(hostSettings) : options.settings
|
|
2919
|
+
},
|
|
2381
2920
|
onReport: (error) => report(error)
|
|
2382
|
-
},
|
|
2383
|
-
|
|
2384
|
-
await
|
|
2385
|
-
await ensureDevices(harness, context2);
|
|
2921
|
+
}, context3);
|
|
2922
|
+
await ensureIndex(harness, context3);
|
|
2923
|
+
await ensureDevices(harness, context3);
|
|
2386
2924
|
for (const path of options.projects ?? [])
|
|
2387
|
-
await addProject(harness, path,
|
|
2388
|
-
const
|
|
2389
|
-
const
|
|
2390
|
-
const offers = remoteAccess ? pairingOffers(options.remote?.pairingTtlMs) : undefined;
|
|
2925
|
+
await addProject(harness, path, context3);
|
|
2926
|
+
const hostKey = await loadHostKey(options.dataDir);
|
|
2927
|
+
const offers = pairingOffers(options.remote?.pairingTtlMs);
|
|
2391
2928
|
const relayKey = options.relay === undefined ? undefined : await loadRelayKey(options.dataDir);
|
|
2392
|
-
const defaults = initialModel === undefined ? {} : {
|
|
2393
|
-
model: { provider: initialModel.provider, modelId: initialModel.modelId },
|
|
2394
|
-
...initialModel.thinkingLevel === undefined ? {} : { thinkingLevel: initialModel.thinkingLevel }
|
|
2395
|
-
};
|
|
2396
2929
|
const gateway = await startGateway({
|
|
2397
2930
|
harness,
|
|
2398
2931
|
models: options.models,
|
|
2399
2932
|
modelSummaries: options.modelSummaries,
|
|
2400
2933
|
session: { id: basename2(options.dataDir), directory: options.dataDir },
|
|
2401
2934
|
dataDir: options.dataDir,
|
|
2402
|
-
|
|
2935
|
+
settings: hostSettings,
|
|
2403
2936
|
token,
|
|
2404
2937
|
port: options.port,
|
|
2938
|
+
agents: agentRoles,
|
|
2405
2939
|
...options.browserOrigins === undefined ? {} : { browserOrigins: options.browserOrigins },
|
|
2406
2940
|
docs: extensions.flatMap((extension) => extension.docs ?? []),
|
|
2407
2941
|
toolPresentations: Object.assign({}, ...extensions.map((extension) => extension.tools ?? {})),
|
|
2408
2942
|
...mcp === undefined ? {} : { mcp: mcp.status },
|
|
2943
|
+
...options.modelAuth === undefined ? {} : { modelAuth: options.modelAuth },
|
|
2409
2944
|
...options.webRoot === undefined ? {} : { webRoot: options.webRoot },
|
|
2410
|
-
...
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
}
|
|
2945
|
+
...options.autoTitle === undefined ? {} : { autoTitle: options.autoTitle },
|
|
2946
|
+
secure: {
|
|
2947
|
+
hostKey,
|
|
2948
|
+
offers,
|
|
2949
|
+
...options.remote?.handshakeTimeoutMs === undefined ? {} : { handshakeTimeoutMs: options.remote.handshakeTimeoutMs },
|
|
2950
|
+
...options.remote === undefined ? {} : {
|
|
2951
|
+
lan: {
|
|
2952
|
+
port: options.remote.port,
|
|
2953
|
+
...options.remote.publicUrl === undefined ? {} : { publicUrl: options.remote.publicUrl }
|
|
2954
|
+
}
|
|
2955
|
+
},
|
|
2956
|
+
...options.relay === undefined || relayKey === undefined ? {} : {
|
|
2957
|
+
relay: {
|
|
2958
|
+
origin: options.relay.origin,
|
|
2959
|
+
signingKey: relayKey,
|
|
2960
|
+
...options.relay.reconnectDelayMs === undefined ? {} : { reconnectDelayMs: options.relay.reconnectDelayMs },
|
|
2961
|
+
...options.relay.pingIntervalMs === undefined ? {} : { pingIntervalMs: options.relay.pingIntervalMs }
|
|
2428
2962
|
}
|
|
2429
2963
|
}
|
|
2430
2964
|
}
|
|
@@ -2439,15 +2973,21 @@ async function openHost(options) {
|
|
|
2439
2973
|
url: gateway.url,
|
|
2440
2974
|
token,
|
|
2441
2975
|
harness,
|
|
2976
|
+
closed: opened.closed,
|
|
2977
|
+
settings: hostSettings,
|
|
2442
2978
|
...gateway.remote === undefined || hostKey === undefined ? {} : { remote: { url: gateway.remote.url, advertiseUrl: gateway.remote.advertiseUrl, hostKey: hostKey.publicKey } },
|
|
2443
2979
|
...gateway.relay === undefined ? {} : { relay: { origin: gateway.relay.origin, hostId: gateway.relay.hostId, state: () => gateway.relay.state() } },
|
|
2444
2980
|
close() {
|
|
2445
2981
|
closing ??= (async () => {
|
|
2446
2982
|
try {
|
|
2447
2983
|
await gateway.close();
|
|
2448
|
-
await opened.close(
|
|
2984
|
+
await opened.close(context3);
|
|
2449
2985
|
await mcp?.close();
|
|
2450
|
-
|
|
2986
|
+
agentRoles?.close();
|
|
2987
|
+
for (const each of extensions)
|
|
2988
|
+
await each.close?.();
|
|
2989
|
+
options.modelAuth?.close();
|
|
2990
|
+
await Promise.all([...environments.values()].map((env) => env.cleanup(context3)));
|
|
2451
2991
|
} finally {
|
|
2452
2992
|
await release();
|
|
2453
2993
|
}
|
|
@@ -2456,15 +2996,20 @@ async function openHost(options) {
|
|
|
2456
2996
|
}
|
|
2457
2997
|
};
|
|
2458
2998
|
} catch (error) {
|
|
2999
|
+
options.modelAuth?.close();
|
|
3000
|
+
agentRoles?.close();
|
|
3001
|
+
for (const each of extensions)
|
|
3002
|
+
await each.close?.().catch(() => {});
|
|
2459
3003
|
await mcp?.close().catch(() => {});
|
|
2460
|
-
await harness?.close(
|
|
3004
|
+
await harness?.close(context3).catch(() => {});
|
|
2461
3005
|
await release().catch(() => {});
|
|
2462
3006
|
throw error;
|
|
2463
3007
|
}
|
|
2464
3008
|
}
|
|
2465
3009
|
|
|
2466
|
-
// apps/host/src/
|
|
2467
|
-
import {
|
|
3010
|
+
// apps/host/src/model-auth.ts
|
|
3011
|
+
import { existsSync as existsSync2, watch as watch2 } from "node:fs";
|
|
3012
|
+
import { basename as basename3, dirname as dirname3 } from "node:path";
|
|
2468
3013
|
|
|
2469
3014
|
// apps/host/src/pi-setup.ts
|
|
2470
3015
|
import { getSupportedThinkingLevels as getSupportedThinkingLevels2 } from "@earendil-works/pi-ai";
|
|
@@ -2532,7 +3077,7 @@ function configureHarnessHttp(settingsManager) {
|
|
|
2532
3077
|
applyHttpProxySettings(settingsManager.getGlobalSettings().httpProxy);
|
|
2533
3078
|
configureHttpDispatcher(settingsManager.getHttpIdleTimeoutMs());
|
|
2534
3079
|
}
|
|
2535
|
-
function createHarnessSettings(settingsManager) {
|
|
3080
|
+
function createHarnessSettings(settingsManager, modes) {
|
|
2536
3081
|
return {
|
|
2537
3082
|
get stream() {
|
|
2538
3083
|
const provider = settingsManager.getProviderRetrySettings();
|
|
@@ -2550,10 +3095,10 @@ function createHarnessSettings(settingsManager) {
|
|
|
2550
3095
|
return settingsManager.getRetrySettings();
|
|
2551
3096
|
},
|
|
2552
3097
|
get steeringMode() {
|
|
2553
|
-
return
|
|
3098
|
+
return modes.value.steeringMode;
|
|
2554
3099
|
},
|
|
2555
3100
|
get followUpMode() {
|
|
2556
|
-
return
|
|
3101
|
+
return modes.value.followUpMode;
|
|
2557
3102
|
}
|
|
2558
3103
|
};
|
|
2559
3104
|
}
|
|
@@ -2573,11 +3118,81 @@ function modelSummaries(modelRuntime) {
|
|
|
2573
3118
|
modelId: model.id,
|
|
2574
3119
|
name: model.name,
|
|
2575
3120
|
contextWindow: model.contextWindow,
|
|
2576
|
-
thinkingLevels: getSupportedThinkingLevels2(model)
|
|
3121
|
+
thinkingLevels: getSupportedThinkingLevels2(model),
|
|
3122
|
+
images: model.input.includes("image")
|
|
2577
3123
|
}));
|
|
2578
3124
|
}
|
|
2579
3125
|
|
|
3126
|
+
// apps/host/src/model-auth.ts
|
|
3127
|
+
var sameModel2 = (a, b) => a.provider === b.provider && a.modelId === b.modelId && a.name === b.name && a.contextWindow === b.contextWindow && sameList(a.thinkingLevels, b.thinkingLevels, (x, y) => x === y);
|
|
3128
|
+
var sameLogin = (a, b) => a.providerId === b.providerId && a.name === b.name && a.kind === b.kind && a.subscription === b.subscription;
|
|
3129
|
+
async function storedLogins(modelRuntime) {
|
|
3130
|
+
const credentials = await modelRuntime.listCredentials();
|
|
3131
|
+
const logins = credentials.map((credential) => ({
|
|
3132
|
+
providerId: credential.providerId,
|
|
3133
|
+
name: modelRuntime.getProvider(credential.providerId)?.name ?? credential.providerId,
|
|
3134
|
+
kind: credential.type,
|
|
3135
|
+
subscription: modelRuntime.isUsingSubscription(credential.providerId)
|
|
3136
|
+
}));
|
|
3137
|
+
logins.sort((a, b) => a.providerId.localeCompare(b.providerId));
|
|
3138
|
+
return { logins };
|
|
3139
|
+
}
|
|
3140
|
+
async function watchModelAuth(modelRuntime, options) {
|
|
3141
|
+
const debounceMs = options.debounceMs ?? 150;
|
|
3142
|
+
const models = new Published(modelSummaries(modelRuntime), (a, b) => sameList(a, b, sameModel2));
|
|
3143
|
+
const auth = new Published(await storedLogins(modelRuntime), (a, b) => sameList(a.logins, b.logins, sameLogin));
|
|
3144
|
+
const fileName = basename3(options.authPath);
|
|
3145
|
+
let watcher;
|
|
3146
|
+
let timer;
|
|
3147
|
+
let reported = false;
|
|
3148
|
+
let closed = false;
|
|
3149
|
+
const refresh = async () => {
|
|
3150
|
+
try {
|
|
3151
|
+
await modelRuntime.getAvailable();
|
|
3152
|
+
models.set(modelSummaries(modelRuntime));
|
|
3153
|
+
auth.set(await storedLogins(modelRuntime));
|
|
3154
|
+
reported = false;
|
|
3155
|
+
} catch (error) {
|
|
3156
|
+
if (!reported)
|
|
3157
|
+
console.error(`model-auth refresh failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
3158
|
+
reported = true;
|
|
3159
|
+
}
|
|
3160
|
+
};
|
|
3161
|
+
const directory = dirname3(options.authPath);
|
|
3162
|
+
if (existsSync2(directory)) {
|
|
3163
|
+
watcher = watch2(directory, (_event, changed) => {
|
|
3164
|
+
if (closed || changed !== null && changed !== fileName)
|
|
3165
|
+
return;
|
|
3166
|
+
clearTimeout(timer);
|
|
3167
|
+
timer = setTimeout(() => void refresh(), debounceMs);
|
|
3168
|
+
});
|
|
3169
|
+
watcher.on("error", (error) => {
|
|
3170
|
+
if (!reported)
|
|
3171
|
+
console.error(`model-auth watch failed: ${error.message}`);
|
|
3172
|
+
reported = true;
|
|
3173
|
+
});
|
|
3174
|
+
}
|
|
3175
|
+
return {
|
|
3176
|
+
models,
|
|
3177
|
+
auth,
|
|
3178
|
+
async logout(providerId) {
|
|
3179
|
+
if (!auth.value.logins.some((login) => login.providerId === providerId)) {
|
|
3180
|
+
throw new Error(`No stored credential for provider "${providerId}"`);
|
|
3181
|
+
}
|
|
3182
|
+
await modelRuntime.logout(providerId);
|
|
3183
|
+
models.set(modelSummaries(modelRuntime));
|
|
3184
|
+
auth.set(await storedLogins(modelRuntime));
|
|
3185
|
+
},
|
|
3186
|
+
close() {
|
|
3187
|
+
closed = true;
|
|
3188
|
+
clearTimeout(timer);
|
|
3189
|
+
watcher?.close();
|
|
3190
|
+
}
|
|
3191
|
+
};
|
|
3192
|
+
}
|
|
3193
|
+
|
|
2580
3194
|
// apps/host/src/main.ts
|
|
3195
|
+
import { getSupportedThinkingLevels as getSupportedThinkingLevels3 } from "@earendil-works/pi-ai";
|
|
2581
3196
|
async function main(argv) {
|
|
2582
3197
|
const { values } = parseArgs({
|
|
2583
3198
|
args: [...argv],
|
|
@@ -2595,28 +3210,27 @@ async function main(argv) {
|
|
|
2595
3210
|
}
|
|
2596
3211
|
});
|
|
2597
3212
|
const dataDir = resolve3(values["data-dir"] ?? DEFAULT_DATA_DIR);
|
|
2598
|
-
const extensionsFor = (models, summaries) => (host2) =>
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
};
|
|
3213
|
+
const extensionsFor = (models, summaries, options2) => (host2) => [
|
|
3214
|
+
createContext({
|
|
3215
|
+
agentsHome: join8(homedir3(), ".agents"),
|
|
3216
|
+
checkout: (input, context4) => checkoutInfo(host2.harness(), input, context4)
|
|
3217
|
+
}),
|
|
3218
|
+
createCoding(options2),
|
|
3219
|
+
todo,
|
|
3220
|
+
question,
|
|
3221
|
+
createSubagent({
|
|
3222
|
+
models,
|
|
3223
|
+
modelSummaries: summaries,
|
|
3224
|
+
exclude: [question.extension],
|
|
3225
|
+
roles: () => host2.agentRoles.value.roles
|
|
3226
|
+
}),
|
|
3227
|
+
createCodemode()
|
|
3228
|
+
];
|
|
2615
3229
|
const common = {
|
|
2616
3230
|
dataDir,
|
|
2617
3231
|
projects: values.project ?? [],
|
|
2618
3232
|
port: Number(values.port),
|
|
2619
|
-
mcpConfig:
|
|
3233
|
+
mcpConfig: join8(homedir3(), ".agents", "mcp.json"),
|
|
2620
3234
|
browserOrigins: values["browser-origin"] ?? ["http://127.0.0.1:5199"],
|
|
2621
3235
|
webRoot,
|
|
2622
3236
|
...values["lock-stale-ms"] === undefined ? {} : { lockStaleMs: Number(values["lock-stale-ms"]) },
|
|
@@ -2640,7 +3254,8 @@ async function main(argv) {
|
|
|
2640
3254
|
modelId: model2.id,
|
|
2641
3255
|
name: model2.name,
|
|
2642
3256
|
contextWindow: model2.contextWindow,
|
|
2643
|
-
thinkingLevels: getSupportedThinkingLevels3(model2)
|
|
3257
|
+
thinkingLevels: getSupportedThinkingLevels3(model2),
|
|
3258
|
+
images: model2.input.includes("image")
|
|
2644
3259
|
}
|
|
2645
3260
|
];
|
|
2646
3261
|
return {
|
|
@@ -2648,32 +3263,49 @@ async function main(argv) {
|
|
|
2648
3263
|
models,
|
|
2649
3264
|
modelSummaries: summaries,
|
|
2650
3265
|
extensions: extensionsFor(models, summaries),
|
|
2651
|
-
|
|
3266
|
+
autoTitle: false,
|
|
3267
|
+
settingsFallback: {
|
|
3268
|
+
defaultModel: () => ({ provider: model2.provider, modelId: model2.id }),
|
|
3269
|
+
defaultThinkingLevel: () => null,
|
|
3270
|
+
steeringMode: () => "one-at-a-time",
|
|
3271
|
+
followUpMode: () => "one-at-a-time"
|
|
3272
|
+
}
|
|
2652
3273
|
};
|
|
2653
3274
|
}
|
|
2654
3275
|
async function piOptions() {
|
|
2655
3276
|
const modelRuntime = await ModelRuntime.create();
|
|
3277
|
+
const modelAuth = await watchModelAuth(modelRuntime, { authPath: join8(getAgentDir(), "auth.json") });
|
|
2656
3278
|
const settingsManager = SettingsManager.create(process.cwd());
|
|
2657
3279
|
configureHarnessHttp(settingsManager);
|
|
2658
|
-
const
|
|
2659
|
-
const
|
|
3280
|
+
const summaries = () => modelAuth.models.value;
|
|
3281
|
+
const images = await createNodePhotonImages();
|
|
2660
3282
|
return {
|
|
2661
3283
|
...common,
|
|
2662
3284
|
models: modelRuntime,
|
|
2663
3285
|
modelSummaries: summaries,
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
3286
|
+
modelAuth,
|
|
3287
|
+
extensions: extensionsFor(modelRuntime, summaries, { images }),
|
|
3288
|
+
settings: (store) => createHarnessSettings(settingsManager, store),
|
|
3289
|
+
settingsFallback: {
|
|
3290
|
+
defaultModel: () => {
|
|
3291
|
+
const model2 = defaultModel(settingsManager, modelRuntime);
|
|
3292
|
+
return model2 === undefined ? null : { provider: model2.provider, modelId: model2.modelId };
|
|
3293
|
+
},
|
|
3294
|
+
defaultThinkingLevel: () => settingsManager.getDefaultThinkingLevel() ?? null,
|
|
3295
|
+
steeringMode: () => settingsManager.getSteeringMode(),
|
|
3296
|
+
followUpMode: () => settingsManager.getFollowUpMode()
|
|
3297
|
+
}
|
|
2667
3298
|
};
|
|
2668
3299
|
}
|
|
2669
3300
|
const options = values.faux !== undefined ? fauxOptions(() => () => fauxAssistantMessage(values.faux)) : await piOptions();
|
|
2670
3301
|
const host = await openHost(options);
|
|
2671
|
-
const
|
|
3302
|
+
const { defaultModel: effectiveModel } = host.settings.value;
|
|
3303
|
+
const model = effectiveModel === null ? null : `${effectiveModel.provider}/${effectiveModel.modelId}`;
|
|
2672
3304
|
const web = packagedVersion === undefined ? "http://127.0.0.1:5199/" : new URL(host.url.replace(/^ws/, "http")).href;
|
|
2673
3305
|
console.log(JSON.stringify({
|
|
2674
3306
|
event: "ready",
|
|
2675
3307
|
url: host.url,
|
|
2676
|
-
tokenFile:
|
|
3308
|
+
tokenFile: join8(dataDir, "token"),
|
|
2677
3309
|
dataDir,
|
|
2678
3310
|
model,
|
|
2679
3311
|
web,
|
|
@@ -2689,6 +3321,15 @@ async function main(argv) {
|
|
|
2689
3321
|
};
|
|
2690
3322
|
process.on("SIGINT", stop);
|
|
2691
3323
|
process.on("SIGTERM", stop);
|
|
3324
|
+
host.closed.then((end) => {
|
|
3325
|
+
if (end.reason !== "failed" || stopping)
|
|
3326
|
+
return;
|
|
3327
|
+
stopping = true;
|
|
3328
|
+
const error = end.error instanceof Error ? end.error.message : String(end.error);
|
|
3329
|
+
process.stderr.write(`${JSON.stringify({ event: "fatal", error })}
|
|
3330
|
+
`);
|
|
3331
|
+
host.close().finally(() => process.exit(1));
|
|
3332
|
+
});
|
|
2692
3333
|
}
|
|
2693
3334
|
if (false)
|
|
2694
3335
|
;
|