pinomad 0.2.0 → 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 -182
- package/dist/{chunk-haycqqp1.js → chunk-3qn8gbjb.js} +1 -1
- package/dist/{chunk-rkfdsacq.js → chunk-3tf23vjq.js} +1 -1
- package/dist/{chunk-qdxhqva8.js → chunk-4dj2vb8t.js} +1 -1
- package/dist/{chunk-5h97zxag.js → chunk-bggesvnj.js} +137 -31
- package/dist/chunk-c155p0t0.js +154 -0
- package/dist/{chunk-5712fvyp.js → chunk-kxws8b83.js} +4 -4
- package/dist/{chunk-n4bvmcy8.js → chunk-n60kkf1m.js} +4 -4
- package/dist/{chunk-q26n39mf.js → chunk-syf0z68f.js} +2 -2
- package/dist/{chunk-d9q0dz8s.js → chunk-t9y2wfk5.js} +2 -2
- package/dist/{chunk-fhtkkcgt.js → chunk-tfqc1djy.js} +1023 -614
- package/dist/pinomad.js +7 -7
- package/package.json +7 -7
- package/web/assets/index-CBXdQLua.js +383 -0
- package/web/assets/{index-KwICnYux.css → index-CG33GBxM.css} +1 -1
- package/web/index.html +2 -2
- package/dist/chunk-h35fermn.js +0 -92
- package/web/assets/index-4p91UVoc.js +0 -383
|
@@ -5,13 +5,17 @@ import {
|
|
|
5
5
|
import {
|
|
6
6
|
packagedVersion,
|
|
7
7
|
webRoot
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-3tf23vjq.js";
|
|
9
9
|
import {
|
|
10
|
+
HOST_SETTING_KEYS,
|
|
10
11
|
INVALID_FRAME_CLOSE_CODE,
|
|
11
12
|
PROTOCOL,
|
|
13
|
+
THINKING_LEVELS,
|
|
12
14
|
UNAUTHORIZED_CLOSE_CODE,
|
|
15
|
+
isQueueMode,
|
|
16
|
+
isThinkingLevel,
|
|
13
17
|
parseClientFrame
|
|
14
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-c155p0t0.js";
|
|
15
19
|
import {
|
|
16
20
|
DevicesDoc,
|
|
17
21
|
ensureDevices,
|
|
@@ -41,12 +45,13 @@ import {
|
|
|
41
45
|
import"./chunk-df32avw6.js";
|
|
42
46
|
|
|
43
47
|
// apps/host/src/main.ts
|
|
44
|
-
import { homedir as
|
|
45
|
-
import { join as
|
|
48
|
+
import { homedir as homedir3 } from "node:os";
|
|
49
|
+
import { join as join8, resolve as resolve3 } from "node:path";
|
|
46
50
|
import { parseArgs } from "node:util";
|
|
47
51
|
import { createModels } from "@earendil-works/pi-ai/models";
|
|
48
52
|
import { fauxAssistantMessage, fauxProvider } from "@earendil-works/pi-ai/providers/faux";
|
|
49
|
-
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";
|
|
50
55
|
|
|
51
56
|
// apps/host/src/extensions/question.ts
|
|
52
57
|
import {
|
|
@@ -93,6 +98,7 @@ var QuestionRequestSchema = Type.Object({
|
|
|
93
98
|
var QuestionSchema = Type.Object({ requests: Type.Array(QuestionRequestSchema) });
|
|
94
99
|
var DiffSchema = Type.Object({ patch: Type.String() });
|
|
95
100
|
var CodemodeCallSchema = Type.Object({
|
|
101
|
+
id: Type.Optional(Type.Number()),
|
|
96
102
|
name: Type.String(),
|
|
97
103
|
args: Type.String(),
|
|
98
104
|
status: Type.Union([Type.Literal("running"), Type.Literal("ok"), Type.Literal("error"), Type.Literal("cancelled")]),
|
|
@@ -100,7 +106,7 @@ var CodemodeCallSchema = Type.Object({
|
|
|
100
106
|
error: Type.Optional(Type.String()),
|
|
101
107
|
details: Type.Optional(Type.Unknown())
|
|
102
108
|
});
|
|
103
|
-
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) });
|
|
104
110
|
|
|
105
111
|
// apps/host/src/extensions/question.ts
|
|
106
112
|
var QuestionDoc = defineDoc({
|
|
@@ -169,10 +175,11 @@ function result(questions, resolution) {
|
|
|
169
175
|
} else {
|
|
170
176
|
text = "Question cancelled.";
|
|
171
177
|
}
|
|
172
|
-
return {
|
|
178
|
+
return { output: [{ type: "text", text }], details: { resolution } };
|
|
173
179
|
}
|
|
174
180
|
var askUserQuestion = defineTool({
|
|
175
181
|
name: "ask_user_question",
|
|
182
|
+
callers: ["model"],
|
|
176
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.",
|
|
177
184
|
parameters: Type2.Object({
|
|
178
185
|
questions: Type2.Array(QuestionItemSchema, { minItems: 1, maxItems: 4 })
|
|
@@ -199,13 +206,12 @@ var askUserQuestion = defineTool({
|
|
|
199
206
|
});
|
|
200
207
|
var question = {
|
|
201
208
|
extension: defineExtension({ name: "question", tools: [askUserQuestion], tasks: [QuestionWait] }),
|
|
202
|
-
modelOnly: ["ask_user_question"],
|
|
203
209
|
docs: [{ token: QuestionDoc, presentation: "pinomad.question" }]
|
|
204
210
|
};
|
|
205
211
|
|
|
206
212
|
// apps/host/src/extensions/coding.ts
|
|
207
213
|
import { wrapTool } from "@earendil-works/pi-durable";
|
|
208
|
-
import {
|
|
214
|
+
import { createCodingTools, createWriteTool } from "@earendil-works/pi-durable/tools";
|
|
209
215
|
import { createPatch } from "diff";
|
|
210
216
|
var writeWithDiff = wrapTool(createWriteTool(), (tool) => ({
|
|
211
217
|
...tool,
|
|
@@ -221,403 +227,32 @@ var writeWithDiff = wrapTool(createWriteTool(), (tool) => ({
|
|
|
221
227
|
return { ...result2, details: { patch: createPatch(args.path, oldContent, args.content) } };
|
|
222
228
|
}
|
|
223
229
|
}));
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
}
|
|
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();
|
|
228
237
|
|
|
229
238
|
// apps/host/src/extensions/codemode.ts
|
|
230
|
-
import {
|
|
231
|
-
import {
|
|
232
|
-
CodemodeSandbox,
|
|
233
|
-
CodemodeSourceError,
|
|
234
|
-
loadQuickJSWasm,
|
|
235
|
-
parseCodemodeSource,
|
|
236
|
-
renderToolSignature
|
|
237
|
-
} from "@earendil-works/pi-codemode";
|
|
238
|
-
import { validateToolArguments } from "@earendil-works/pi-ai/utils/validation";
|
|
239
|
-
import {
|
|
240
|
-
defineExtension as defineExtension2,
|
|
241
|
-
defineTool as defineTool2,
|
|
242
|
-
section
|
|
243
|
-
} from "@earendil-works/pi-durable";
|
|
244
|
-
import { Type as Type3 } from "typebox";
|
|
239
|
+
import { CodemodeSandbox, loadQuickJSWasm } from "@earendil-works/pi-codemode";
|
|
240
|
+
import { createCodemodeExtension } from "@earendil-works/pi-durable/codemode";
|
|
245
241
|
var wasm = loadQuickJSWasm();
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
class NestedOutput {
|
|
250
|
-
#head = "";
|
|
251
|
-
#tail = "";
|
|
252
|
-
#dropped = 0;
|
|
253
|
-
#decoder = new TextDecoder;
|
|
254
|
-
push(chunk) {
|
|
255
|
-
const text = typeof chunk === "string" ? chunk : this.#decoder.decode(chunk, { stream: true });
|
|
256
|
-
if (this.#dropped === 0 && this.#head.length + text.length <= OUTPUT_CAP) {
|
|
257
|
-
this.#head += text;
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
if (this.#dropped === 0) {
|
|
261
|
-
const rest = this.#head.slice(OUTPUT_HALF) + text;
|
|
262
|
-
this.#head = this.#head.slice(0, OUTPUT_HALF);
|
|
263
|
-
this.#dropped = Math.max(0, rest.length - OUTPUT_HALF);
|
|
264
|
-
this.#tail = rest.slice(-OUTPUT_HALF);
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
const merged = this.#tail + text;
|
|
268
|
-
this.#dropped += Math.max(0, merged.length - OUTPUT_HALF);
|
|
269
|
-
this.#tail = merged.slice(-OUTPUT_HALF);
|
|
270
|
-
}
|
|
271
|
-
text() {
|
|
272
|
-
const tail = this.#tail + this.#decoder.decode();
|
|
273
|
-
return this.#dropped === 0 ? this.#head + tail : `${this.#head}
|
|
274
|
-
[... ${this.#dropped} characters omitted ...]
|
|
275
|
-
${tail}`;
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
var summarizeArgs = (args) => {
|
|
279
|
-
let text;
|
|
280
|
-
try {
|
|
281
|
-
text = JSON.stringify(args) ?? "null";
|
|
282
|
-
} catch {
|
|
283
|
-
text = String(args);
|
|
284
|
-
}
|
|
285
|
-
return text.length <= 200 ? text : `${text.slice(0, 199)}…`;
|
|
286
|
-
};
|
|
287
|
-
var errorText = (error) => error instanceof Error ? error.message : String(error);
|
|
288
|
-
var textOf = (content) => content.flatMap((block) => block.type === "text" ? [block.text] : []).join("");
|
|
289
|
-
var TOKENIZE = /[^a-z0-9]+/;
|
|
290
|
-
var tokens = (text) => text.replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase().split(TOKENIZE).filter(Boolean);
|
|
291
|
-
function search(callables, query, limit, namespace) {
|
|
292
|
-
const docs = [...callables].filter((tool) => namespace === undefined || tool.namespace?.name === namespace).map((tool) => {
|
|
293
|
-
const text = [tool.name, tool.description, tool.namespace?.name, tool.namespace?.description].filter(Boolean).join(" ");
|
|
294
|
-
return { tool, terms: tokens(text) };
|
|
295
|
-
});
|
|
296
|
-
const avgLength = docs.length === 0 ? 1 : docs.reduce((sum, doc) => sum + doc.terms.length, 0) / docs.length;
|
|
297
|
-
const frequency = new Map;
|
|
298
|
-
for (const doc of docs)
|
|
299
|
-
for (const term of new Set(doc.terms))
|
|
300
|
-
frequency.set(term, (frequency.get(term) ?? 0) + 1);
|
|
301
|
-
const scored = docs.map((doc) => {
|
|
302
|
-
const counts = new Map;
|
|
303
|
-
for (const term of doc.terms)
|
|
304
|
-
counts.set(term, (counts.get(term) ?? 0) + 1);
|
|
305
|
-
let score = 0;
|
|
306
|
-
for (const term of tokens(query)) {
|
|
307
|
-
const df = frequency.get(term);
|
|
308
|
-
if (df === undefined)
|
|
309
|
-
continue;
|
|
310
|
-
const idf = Math.log(1 + (docs.length - df + 0.5) / (df + 0.5));
|
|
311
|
-
const tf = counts.get(term) ?? 0;
|
|
312
|
-
score += idf * tf * 2.2 / (tf + 1.2 * (0.25 + 0.75 * doc.terms.length / avgLength));
|
|
313
|
-
}
|
|
314
|
-
return { tool: doc.tool, score };
|
|
315
|
-
});
|
|
316
|
-
return scored.filter((entry) => entry.score > 0).sort((a, b) => b.score - a.score).slice(0, limit).map((entry) => ({
|
|
317
|
-
name: entry.tool.name,
|
|
318
|
-
description: entry.tool.description.length <= 300 ? entry.tool.description : `${entry.tool.description.slice(0, 299)}…`
|
|
319
|
-
}));
|
|
320
|
-
}
|
|
321
|
-
function createCodemode(options) {
|
|
322
|
-
const excluded = new Set([...options.modelOnly, "codemode"]);
|
|
323
|
-
const modelOnlyList = [...excluded].join(", ");
|
|
324
|
-
const tool = defineTool2({
|
|
325
|
-
name: "codemode",
|
|
326
|
-
description: DESCRIPTION.replace("{MODEL_ONLY}", modelOnlyList),
|
|
327
|
-
parameters: Type3.Object({
|
|
328
|
-
code: Type3.String({ minLength: 1, description: "Raw JavaScript source; see the tool description." })
|
|
329
|
-
}),
|
|
330
|
-
execute: async (args, api, context) => {
|
|
331
|
-
let parsed;
|
|
332
|
-
try {
|
|
333
|
-
parsed = parseCodemodeSource(args.code);
|
|
334
|
-
} catch (error) {
|
|
335
|
-
if (error instanceof CodemodeSourceError) {
|
|
336
|
-
return { content: [{ type: "text", text: `Script failed: ${error.message}` }], isError: true };
|
|
337
|
-
}
|
|
338
|
-
throw error;
|
|
339
|
-
}
|
|
340
|
-
const agent = await api.agent(context);
|
|
341
|
-
const callables = new Map;
|
|
342
|
-
for (const offered of agent.tools) {
|
|
343
|
-
if (!excluded.has(offered.name))
|
|
344
|
-
callables.set(offered.name, { kind: "agent", tool: offered });
|
|
345
|
-
}
|
|
346
|
-
for (const scriptTool of options.scriptTools())
|
|
347
|
-
callables.set(scriptTool.name, { kind: "script", tool: scriptTool });
|
|
348
|
-
const calls = [];
|
|
349
|
-
let detailsBudget = 512 * 1024;
|
|
350
|
-
const snapshot = () => ({
|
|
351
|
-
code: parsed.code,
|
|
352
|
-
calls: calls.map((call) => ({ ...call }))
|
|
353
|
-
});
|
|
354
|
-
const publish = () => {
|
|
355
|
-
api.details(snapshot(), context).catch(() => {});
|
|
356
|
-
};
|
|
357
|
-
const executeNested = async (name, callArgs, signal) => {
|
|
358
|
-
const record = { name, args: summarizeArgs(callArgs), status: "running" };
|
|
359
|
-
calls.push(record);
|
|
360
|
-
publish();
|
|
361
|
-
const started2 = Date.now();
|
|
362
|
-
try {
|
|
363
|
-
const callable = callables.get(name);
|
|
364
|
-
if (callable === undefined)
|
|
365
|
-
throw new Error(`Unknown tool "${name}"`);
|
|
366
|
-
const value = callable.kind === "script" ? await callable.tool.call(callArgs, signal) : await runAgentTool(callable.tool, callArgs, signal, record, calls.length - 1);
|
|
367
|
-
record.status = signal.aborted ? "cancelled" : "ok";
|
|
368
|
-
return value;
|
|
369
|
-
} catch (error) {
|
|
370
|
-
record.status = signal.aborted ? "cancelled" : "error";
|
|
371
|
-
record.error = errorText(error).slice(0, 500);
|
|
372
|
-
throw error;
|
|
373
|
-
} finally {
|
|
374
|
-
record.durationMs = Date.now() - started2;
|
|
375
|
-
publish();
|
|
376
|
-
}
|
|
377
|
-
};
|
|
378
|
-
const runAgentTool = async (offered, callArgs, signal, record, index) => {
|
|
379
|
-
const prepared = offered.prepareArguments === undefined ? callArgs : offered.prepareArguments(callArgs);
|
|
380
|
-
const validated = validateToolArguments(offered, {
|
|
381
|
-
type: "toolCall",
|
|
382
|
-
id: `${api.callId}/${index}`,
|
|
383
|
-
name: offered.name,
|
|
384
|
-
arguments: prepared
|
|
385
|
-
});
|
|
386
|
-
const output = new NestedOutput;
|
|
387
|
-
const diagnostics = [];
|
|
388
|
-
let reported;
|
|
389
|
-
const derived = {
|
|
390
|
-
...api,
|
|
391
|
-
callId: `${api.callId}/${index}`,
|
|
392
|
-
output: (chunk) => output.push(chunk),
|
|
393
|
-
outputWindow: undefined,
|
|
394
|
-
diagnostic: (diagnostic) => diagnostics.push(`[${diagnostic.severity}] ${diagnostic.message}`),
|
|
395
|
-
details: async (value) => {
|
|
396
|
-
reported = value;
|
|
397
|
-
}
|
|
398
|
-
};
|
|
399
|
-
let result2;
|
|
400
|
-
try {
|
|
401
|
-
result2 = await offered.execute(validated, derived, withAbortSignal(signal, context));
|
|
402
|
-
} catch (error) {
|
|
403
|
-
if (signal.aborted)
|
|
404
|
-
throw error;
|
|
405
|
-
throw new Error(`${[output.text(), errorText(error)].filter(Boolean).join(`
|
|
406
|
-
`)}`.trim());
|
|
407
|
-
}
|
|
408
|
-
const captured = result2.details ?? reported;
|
|
409
|
-
if (captured !== undefined && options.presentations[offered.name] !== undefined) {
|
|
410
|
-
try {
|
|
411
|
-
const size = JSON.stringify(captured).length;
|
|
412
|
-
if (size <= 64 * 1024 && detailsBudget >= size) {
|
|
413
|
-
detailsBudget -= size;
|
|
414
|
-
record.details = captured;
|
|
415
|
-
}
|
|
416
|
-
} catch {}
|
|
417
|
-
}
|
|
418
|
-
const collected = [output.text(), ...diagnostics].filter(Boolean).join(`
|
|
419
|
-
`);
|
|
420
|
-
const content = result2.content ?? [{ type: "text", text: collected }];
|
|
421
|
-
if (result2.isError === true)
|
|
422
|
-
throw new Error(textOf(content) || collected);
|
|
423
|
-
const texts = content.filter((block) => block.type === "text");
|
|
424
|
-
return texts.length === content.length ? texts.map((block) => block.text).join("") : content;
|
|
425
|
-
};
|
|
426
|
-
const globals = [
|
|
427
|
-
{
|
|
428
|
-
name: "searchTools",
|
|
429
|
-
spread: true,
|
|
430
|
-
description: "Search callable tools by relevance.",
|
|
431
|
-
execute: (callArgs) => {
|
|
432
|
-
const [query, opts] = callArgs;
|
|
433
|
-
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);
|
|
434
|
-
}
|
|
435
|
-
},
|
|
436
|
-
{
|
|
437
|
-
name: "describeTool",
|
|
438
|
-
description: "The declaration of one callable tool, or undefined.",
|
|
439
|
-
execute: (callArgs) => {
|
|
440
|
-
const callable = callables.get(String(callArgs));
|
|
441
|
-
if (callable === undefined)
|
|
442
|
-
return;
|
|
443
|
-
return {
|
|
444
|
-
name: callable.tool.name,
|
|
445
|
-
description: callable.tool.description,
|
|
446
|
-
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 })
|
|
447
|
-
};
|
|
448
|
-
}
|
|
449
|
-
},
|
|
450
|
-
{
|
|
451
|
-
name: "describeNamespace",
|
|
452
|
-
description: "The tools of one script-only namespace, or undefined.",
|
|
453
|
-
execute: (callArgs) => {
|
|
454
|
-
const wanted = String(callArgs);
|
|
455
|
-
const members = options.scriptTools().filter((tool2) => tool2.namespace?.name === wanted);
|
|
456
|
-
if (members.length === 0)
|
|
457
|
-
return;
|
|
458
|
-
const first = members[0].namespace;
|
|
459
|
-
return {
|
|
460
|
-
name: wanted,
|
|
461
|
-
...first.description === undefined ? {} : { description: first.description },
|
|
462
|
-
...first.instructions === undefined ? {} : { instructions: first.instructions },
|
|
463
|
-
tools: members.map((tool2) => ({ name: tool2.name, description: tool2.description }))
|
|
464
|
-
};
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
];
|
|
468
|
-
const sandbox = new CodemodeSandbox({
|
|
469
|
-
tools: [...callables.values()].map((callable) => ({
|
|
470
|
-
name: callable.tool.name,
|
|
471
|
-
...callable.tool.description === undefined ? {} : { description: callable.tool.description },
|
|
472
|
-
inputSchema: callable.kind === "script" ? callable.tool.inputSchema : callable.tool.parameters,
|
|
473
|
-
...callable.kind === "script" && callable.tool.outputSchema !== undefined ? { outputSchema: callable.tool.outputSchema } : {},
|
|
474
|
-
execute: (callArgs, { signal }) => executeNested(callable.tool.name, callArgs, signal)
|
|
475
|
-
})),
|
|
476
|
-
globals,
|
|
477
|
-
memoryLimitBytes: 256 * 1024 * 1024,
|
|
478
|
-
timeoutMs: parsed.options.timeoutMs ?? Infinity,
|
|
479
|
-
wasm
|
|
480
|
-
});
|
|
481
|
-
const started = Date.now();
|
|
482
|
-
try {
|
|
483
|
-
const result2 = await sandbox.execute(parsed.code, { signal: context.abortSignal });
|
|
484
|
-
context.abortSignal?.throwIfAborted();
|
|
485
|
-
return {
|
|
486
|
-
content: resultContent(result2, started, parsed.options.maxOutputTokens),
|
|
487
|
-
isError: !result2.ok,
|
|
488
|
-
details: snapshot()
|
|
489
|
-
};
|
|
490
|
-
} finally {
|
|
491
|
-
await sandbox.close();
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
});
|
|
242
|
+
function createCodemode() {
|
|
243
|
+
const sandbox = new CodemodeSandbox({ wasm, memoryLimitBytes: 256 * 1024 * 1024 });
|
|
244
|
+
const namespaced = (namespaces) => createCodemodeExtension({ sandbox, namespaces });
|
|
495
245
|
return {
|
|
496
|
-
extension:
|
|
497
|
-
name: "codemode",
|
|
498
|
-
tools: [tool],
|
|
499
|
-
sections: [
|
|
500
|
-
section("codemode", (input) => {
|
|
501
|
-
const offered = new Set(input.agent.tools.map((each) => each.name));
|
|
502
|
-
const namespaces = new Map;
|
|
503
|
-
for (const tool2 of options.scriptTools()) {
|
|
504
|
-
if (tool2.namespace === undefined)
|
|
505
|
-
continue;
|
|
506
|
-
const entry = namespaces.get(tool2.namespace.name) ?? { scriptOnly: 0 };
|
|
507
|
-
namespaces.set(tool2.namespace.name, {
|
|
508
|
-
description: entry.description ?? tool2.namespace.description,
|
|
509
|
-
scriptOnly: entry.scriptOnly + (offered.has(tool2.name) ? 0 : 1)
|
|
510
|
-
});
|
|
511
|
-
}
|
|
512
|
-
const scriptOnly = [...namespaces.entries()].filter(([, entry]) => entry.scriptOnly > 0).sort(([a], [b]) => a.localeCompare(b));
|
|
513
|
-
let text = `Every tool declared to you except ${modelOnlyList} can also be called from codemode scripts as tools.<name>(args), with the same arguments.`;
|
|
514
|
-
if (scriptOnly.length > 0) {
|
|
515
|
-
const lines = scriptOnly.map(([name, entry]) => {
|
|
516
|
-
const firstLine = entry.description?.split(`
|
|
517
|
-
`)[0];
|
|
518
|
-
const clipped = firstLine === undefined ? "" : `: ${firstLine.slice(0, 200)}`;
|
|
519
|
-
return `- ${name} (${entry.scriptOnly} tools)${clipped}`;
|
|
520
|
-
});
|
|
521
|
-
text += `
|
|
522
|
-
|
|
523
|
-
These namespaces are reachable only from codemode scripts; find their tools with describeNamespace(name) or searchTools(query):
|
|
524
|
-
${lines.join(`
|
|
525
|
-
`)}`;
|
|
526
|
-
}
|
|
527
|
-
return text;
|
|
528
|
-
})
|
|
529
|
-
]
|
|
530
|
-
}),
|
|
246
|
+
extension: namespaced([]),
|
|
531
247
|
tools: { codemode: "pinomad.codemode" },
|
|
532
|
-
|
|
248
|
+
namespaced,
|
|
249
|
+
close: () => sandbox.close()
|
|
533
250
|
};
|
|
534
251
|
}
|
|
535
|
-
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.
|
|
536
|
-
|
|
537
|
-
\`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.
|
|
538
|
-
|
|
539
|
-
Globals:
|
|
540
|
-
- 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.
|
|
541
|
-
- text(value): add to the output; strings as is, other values as JSON. console.log(...) and a top-level \`return value\` do the same.
|
|
542
|
-
- image(block): add an image from an image block { type: "image", data, mimeType } or a base64 data: URL.
|
|
543
|
-
- exit(): end the script successfully.
|
|
544
|
-
- ALL_TOOLS: every callable tool as { name, description }.
|
|
545
|
-
- searchTools(query, { limit?, namespace? }): resolves to the callable tools ranked by relevance (default limit 8), as { name, description }[].
|
|
546
|
-
- describeTool(name): resolves to { name, description, declaration } with the tool's TypeScript declaration, or undefined.
|
|
547
|
-
- describeNamespace(name): resolves to { name, description?, instructions?, tools } for a namespace such as an MCP server, or undefined.
|
|
548
|
-
These three return promises: await them.
|
|
549
|
-
|
|
550
|
-
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.
|
|
551
|
-
|
|
552
|
-
Call describeTool before using a tool whose arguments you have not seen. Scripts cannot call {MODEL_ONLY}.`;
|
|
553
|
-
function resultContent(result2, started, maxOutputTokens) {
|
|
554
|
-
const seconds = ((Date.now() - started) / 1000).toFixed(1);
|
|
555
|
-
const items = [
|
|
556
|
-
{ type: "text", text: result2.ok ? `Script completed in ${seconds}s` : `Script failed in ${seconds}s` }
|
|
557
|
-
];
|
|
558
|
-
for (const item of result2.output) {
|
|
559
|
-
items.push(item.type === "text" ? { type: "text", text: item.text } : { type: "image", data: item.data, mimeType: item.mimeType });
|
|
560
|
-
}
|
|
561
|
-
if (result2.ok && result2.value !== undefined) {
|
|
562
|
-
items.push({ type: "text", text: typeof result2.value === "string" ? result2.value : JSON.stringify(result2.value) });
|
|
563
|
-
}
|
|
564
|
-
if (!result2.ok) {
|
|
565
|
-
const error = result2.error;
|
|
566
|
-
items.push({
|
|
567
|
-
type: "text",
|
|
568
|
-
text: error.kind === "timeout" ? "Script error: timed out" : error.kind === "aborted" ? "Script error: aborted" : `Script error: ${error.name ?? "Error"}: ${error.message}`
|
|
569
|
-
});
|
|
570
|
-
}
|
|
571
|
-
return truncateText(mergeText(items), (maxOutputTokens ?? 1e4) * 4);
|
|
572
|
-
}
|
|
573
|
-
function mergeText(items) {
|
|
574
|
-
const merged = [];
|
|
575
|
-
for (const item of items) {
|
|
576
|
-
const last = merged.at(-1);
|
|
577
|
-
if (item.type === "text" && last?.type === "text")
|
|
578
|
-
merged[merged.length - 1] = { type: "text", text: `${last.text}
|
|
579
|
-
${item.text}` };
|
|
580
|
-
else
|
|
581
|
-
merged.push(item);
|
|
582
|
-
}
|
|
583
|
-
return merged;
|
|
584
|
-
}
|
|
585
|
-
function truncateText(items, budget) {
|
|
586
|
-
const total = items.reduce((sum, item) => sum + (item.type === "text" ? item.text.length : 0), 0);
|
|
587
|
-
if (total <= budget)
|
|
588
|
-
return items;
|
|
589
|
-
const headBudget = Math.floor(budget / 2);
|
|
590
|
-
const tailStart = total - (budget - headBudget);
|
|
591
|
-
const omitted = total - headBudget - (total - tailStart);
|
|
592
|
-
const out = [];
|
|
593
|
-
let offset = 0;
|
|
594
|
-
let marked = false;
|
|
595
|
-
for (const item of items) {
|
|
596
|
-
if (item.type !== "text") {
|
|
597
|
-
out.push(item);
|
|
598
|
-
continue;
|
|
599
|
-
}
|
|
600
|
-
const end = offset + item.text.length;
|
|
601
|
-
const head = item.text.slice(0, Math.max(headBudget - offset, 0));
|
|
602
|
-
const tail = end > tailStart ? item.text.slice(Math.max(tailStart - offset, 0)) : "";
|
|
603
|
-
if (head !== "")
|
|
604
|
-
out.push({ type: "text", text: head });
|
|
605
|
-
if (!marked && end >= headBudget) {
|
|
606
|
-
out.push({ type: "text", text: `
|
|
607
|
-
[... ${omitted} characters omitted ...]
|
|
608
|
-
` });
|
|
609
|
-
marked = true;
|
|
610
|
-
}
|
|
611
|
-
if (tail !== "")
|
|
612
|
-
out.push({ type: "text", text: tail });
|
|
613
|
-
offset = end;
|
|
614
|
-
}
|
|
615
|
-
return mergeText(out);
|
|
616
|
-
}
|
|
617
252
|
|
|
618
253
|
// apps/host/src/extensions/context.ts
|
|
619
254
|
import { dirname, join, sep } from "node:path";
|
|
620
|
-
import { defineExtension as
|
|
255
|
+
import { defineExtension as defineExtension2, section } from "@earendil-works/pi-durable";
|
|
621
256
|
import { formatSkillsForPrompt, loadProjectContextFiles, loadSkills } from "@earendil-works/pi-coding-agent";
|
|
622
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.";
|
|
623
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.";
|
|
@@ -625,11 +260,11 @@ function createContext(options) {
|
|
|
625
260
|
const { agentsHome } = options;
|
|
626
261
|
const resolveCheckout = (input, context) => options.checkout?.(input, context);
|
|
627
262
|
return {
|
|
628
|
-
extension:
|
|
263
|
+
extension: defineExtension2({
|
|
629
264
|
name: "context",
|
|
630
265
|
sections: [
|
|
631
|
-
|
|
632
|
-
|
|
266
|
+
section("preamble", () => PREAMBLE, { tag: false }),
|
|
267
|
+
section("environment", async (input, context) => {
|
|
633
268
|
const cwd = input.agent.cwd;
|
|
634
269
|
if (cwd === undefined)
|
|
635
270
|
return;
|
|
@@ -649,7 +284,7 @@ function createContext(options) {
|
|
|
649
284
|
].join(`
|
|
650
285
|
`);
|
|
651
286
|
}),
|
|
652
|
-
|
|
287
|
+
section("project_context", async (input, context) => {
|
|
653
288
|
const cwd = input.agent.cwd;
|
|
654
289
|
if (cwd === undefined)
|
|
655
290
|
return;
|
|
@@ -675,7 +310,7 @@ ${file.content}`).join(`
|
|
|
675
310
|
|
|
676
311
|
`);
|
|
677
312
|
}),
|
|
678
|
-
|
|
313
|
+
section("skills", (input) => {
|
|
679
314
|
const cwd = input.agent.cwd;
|
|
680
315
|
if (cwd === undefined)
|
|
681
316
|
return;
|
|
@@ -699,10 +334,11 @@ import {
|
|
|
699
334
|
AgentDoc,
|
|
700
335
|
AssistantEntry,
|
|
701
336
|
configure,
|
|
702
|
-
defineExtension as
|
|
703
|
-
defineTool as
|
|
337
|
+
defineExtension as defineExtension3,
|
|
338
|
+
defineTool as defineTool2,
|
|
339
|
+
section as section2
|
|
704
340
|
} from "@earendil-works/pi-durable";
|
|
705
|
-
import { Type as
|
|
341
|
+
import { Type as Type3 } from "typebox";
|
|
706
342
|
|
|
707
343
|
// apps/host/src/subagent-doc.ts
|
|
708
344
|
import { defineDoc as defineDoc2 } from "@earendil-works/pi-durable";
|
|
@@ -717,20 +353,32 @@ var SubagentDoc = defineDoc2({
|
|
|
717
353
|
|
|
718
354
|
// apps/host/src/extensions/subagent.ts
|
|
719
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.";
|
|
720
|
-
var
|
|
356
|
+
var THINKING_LEVELS2 = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
721
357
|
function createSubagent(options) {
|
|
722
358
|
const available = () => options.modelSummaries().map((model) => `${model.provider}/${model.modelId}`).join(", ");
|
|
723
|
-
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({
|
|
724
368
|
name: "subagent",
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
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({
|
|
728
373
|
minLength: 1,
|
|
729
374
|
description: "Complete, self-contained instructions. The subagent sees none of this conversation."
|
|
730
375
|
}),
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
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." }))
|
|
734
382
|
}),
|
|
735
383
|
replay: "safe",
|
|
736
384
|
execute: async (args, api, context) => {
|
|
@@ -744,31 +392,43 @@ function createSubagent(options) {
|
|
|
744
392
|
...doc2?.startedAt === undefined ? {} : { startedAt: doc2.startedAt }
|
|
745
393
|
};
|
|
746
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;
|
|
747
404
|
let model;
|
|
748
405
|
let thinkingLevel;
|
|
749
406
|
let override;
|
|
750
|
-
if (
|
|
751
|
-
const slash =
|
|
752
|
-
const provider = slash === -1 ? "" :
|
|
753
|
-
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);
|
|
754
411
|
override = provider === "" || modelId === "" ? undefined : options.models.getModel(provider, modelId);
|
|
755
412
|
const listed = options.modelSummaries().some((each) => each.provider === provider && each.modelId === modelId);
|
|
756
|
-
if (override === undefined || !listed)
|
|
757
|
-
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
|
+
}
|
|
758
416
|
model = { provider, modelId };
|
|
759
417
|
}
|
|
760
|
-
if (
|
|
418
|
+
if (levelString !== undefined || override !== undefined) {
|
|
761
419
|
const parent = await tx.doc(AgentDoc, api.conversationId);
|
|
762
420
|
const effective = override ?? (parent.model === undefined ? undefined : options.models.getModel(parent.model.provider, parent.model.modelId));
|
|
763
|
-
if (
|
|
764
|
-
thinkingLevel = effective === undefined ?
|
|
421
|
+
if (levelString !== undefined) {
|
|
422
|
+
thinkingLevel = effective === undefined ? levelString : clampThinkingLevel(effective, levelString);
|
|
765
423
|
} else if (parent.thinkingLevel !== undefined) {
|
|
766
424
|
thinkingLevel = clampThinkingLevel(override, parent.thinkingLevel);
|
|
767
425
|
}
|
|
768
426
|
}
|
|
769
427
|
const change = {
|
|
770
428
|
extensions: { remove: [extension, ...options.exclude] },
|
|
771
|
-
instructions: INSTRUCTIONS
|
|
429
|
+
instructions: role === undefined || role.prompt === "" ? INSTRUCTIONS : `${INSTRUCTIONS}
|
|
430
|
+
|
|
431
|
+
${role.prompt}`,
|
|
772
432
|
...model === undefined ? {} : { model },
|
|
773
433
|
...thinkingLevel === undefined ? {} : { thinkingLevel }
|
|
774
434
|
};
|
|
@@ -781,6 +441,8 @@ function createSubagent(options) {
|
|
|
781
441
|
doc.startedAt = startedAt;
|
|
782
442
|
if (ref !== undefined)
|
|
783
443
|
doc.model = `${ref.provider}/${ref.modelId}`;
|
|
444
|
+
if (role !== undefined)
|
|
445
|
+
doc.role = role.name;
|
|
784
446
|
const label = args.description?.trim();
|
|
785
447
|
if (label !== undefined && label !== "")
|
|
786
448
|
doc.label = label;
|
|
@@ -798,7 +460,7 @@ function createSubagent(options) {
|
|
|
798
460
|
const message = answer?.model?.[0];
|
|
799
461
|
const text = message?.role === "assistant" ? message.content.flatMap((block) => block.type === "text" ? [block.text] : []).join("") : "";
|
|
800
462
|
const result2 = {
|
|
801
|
-
|
|
463
|
+
output: [{ type: "text", text: text === "" ? "(The subagent returned no text.)" : text }],
|
|
802
464
|
details: {
|
|
803
465
|
conversationId: child.id,
|
|
804
466
|
...child.model === undefined ? {} : { model: child.model },
|
|
@@ -808,13 +470,28 @@ function createSubagent(options) {
|
|
|
808
470
|
return result2;
|
|
809
471
|
}
|
|
810
472
|
});
|
|
811
|
-
const extension =
|
|
812
|
-
|
|
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 };
|
|
813
490
|
}
|
|
814
491
|
|
|
815
492
|
// apps/host/src/extensions/todo.ts
|
|
816
|
-
import { defineDoc as defineDoc3, defineExtension as
|
|
817
|
-
import { Type as
|
|
493
|
+
import { defineDoc as defineDoc3, defineExtension as defineExtension4, defineTool as defineTool3 } from "@earendil-works/pi-durable";
|
|
494
|
+
import { Type as Type4 } from "typebox";
|
|
818
495
|
var TodoDoc = defineDoc3({
|
|
819
496
|
kind: "todo.list",
|
|
820
497
|
version: 1,
|
|
@@ -823,33 +500,30 @@ var TodoDoc = defineDoc3({
|
|
|
823
500
|
fork: "asOf",
|
|
824
501
|
initial: () => ({ items: [] })
|
|
825
502
|
});
|
|
826
|
-
var todoWrite =
|
|
503
|
+
var todoWrite = defineTool3({
|
|
827
504
|
name: "todo_write",
|
|
828
505
|
description: "Replace the todo list shown to the user with the given items.",
|
|
829
|
-
parameters:
|
|
506
|
+
parameters: Type4.Object({ items: Type4.Array(TodoItemSchema) }),
|
|
830
507
|
replay: "safe",
|
|
831
508
|
execute: async (args, api, context) => {
|
|
832
509
|
await api.commit(async (tx) => {
|
|
833
510
|
const doc = await tx.doc(TodoDoc, api.conversationId);
|
|
834
511
|
doc.items = args.items.map((item) => ({ ...item }));
|
|
835
512
|
}, context);
|
|
836
|
-
return {
|
|
513
|
+
return { output: [{ type: "text", text: `Todo list updated: ${args.items.length} item(s).` }] };
|
|
837
514
|
}
|
|
838
515
|
});
|
|
839
516
|
var todo = {
|
|
840
|
-
extension:
|
|
517
|
+
extension: defineExtension4({ name: "todo", tools: [todoWrite] }),
|
|
841
518
|
docs: [{ token: TodoDoc, presentation: "pinomad.todo" }]
|
|
842
519
|
};
|
|
843
520
|
|
|
844
521
|
// apps/host/src/host.ts
|
|
845
|
-
import { randomBytes as
|
|
846
|
-
import { mkdir as
|
|
847
|
-
import { basename as basename2, join as
|
|
848
|
-
import { BACKGROUND_CONTEXT as
|
|
849
|
-
import {
|
|
850
|
-
createRegistry,
|
|
851
|
-
Harness
|
|
852
|
-
} 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";
|
|
853
527
|
import { NodeExecutionEnv } from "@earendil-works/pi-durable/env/node";
|
|
854
528
|
|
|
855
529
|
// apps/host/src/organization.ts
|
|
@@ -939,7 +613,7 @@ async function changesOf(dir, base, maxBytes = 1024 * 1024) {
|
|
|
939
613
|
try {
|
|
940
614
|
await git(dir, ["read-tree", "HEAD"], env);
|
|
941
615
|
await git(dir, ["add", "-A"], env);
|
|
942
|
-
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);
|
|
943
617
|
if (Buffer.byteLength(stdout) <= maxBytes)
|
|
944
618
|
return { base, patch: stdout, truncated: false };
|
|
945
619
|
const cut = stdout.slice(0, maxBytes);
|
|
@@ -951,6 +625,16 @@ async function changesOf(dir, base, maxBytes = 1024 * 1024) {
|
|
|
951
625
|
}
|
|
952
626
|
}
|
|
953
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
|
+
|
|
954
638
|
// apps/host/src/organization.ts
|
|
955
639
|
var IndexDoc = defineDoc4({
|
|
956
640
|
kind: "pinomad.index",
|
|
@@ -1003,7 +687,7 @@ async function createConversation(harness, dataDir, input, defaults, models, con
|
|
|
1003
687
|
if (prior !== undefined) {
|
|
1004
688
|
const existing = await harness.conversation(prior.id, context);
|
|
1005
689
|
if (existing !== undefined) {
|
|
1006
|
-
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);
|
|
1007
691
|
}
|
|
1008
692
|
return prior.id;
|
|
1009
693
|
}
|
|
@@ -1055,14 +739,14 @@ async function createConversation(harness, dataDir, input, defaults, models, con
|
|
|
1055
739
|
}, context);
|
|
1056
740
|
if (home.kind === "chat")
|
|
1057
741
|
await mkdir(join3(dataDir, "chats", String(conversation.id)), { recursive: true });
|
|
1058
|
-
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);
|
|
1059
743
|
return conversation.id;
|
|
1060
744
|
} catch (error) {
|
|
1061
745
|
if (!(error instanceof DuplicateRequest))
|
|
1062
746
|
throw error;
|
|
1063
747
|
const existing = await harness.conversation(error.conversationId, context);
|
|
1064
748
|
if (existing !== undefined) {
|
|
1065
|
-
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);
|
|
1066
750
|
}
|
|
1067
751
|
return error.conversationId;
|
|
1068
752
|
}
|
|
@@ -1166,8 +850,8 @@ import {
|
|
|
1166
850
|
StreamableHttpTransport,
|
|
1167
851
|
toLlmContent
|
|
1168
852
|
} from "@earendil-works/pi-mcp";
|
|
1169
|
-
import { defineExtension as
|
|
1170
|
-
import { Type as
|
|
853
|
+
import { defineExtension as defineExtension5, defineTool as defineTool4 } from "@earendil-works/pi-durable";
|
|
854
|
+
import { Type as Type5 } from "typebox";
|
|
1171
855
|
var ENV_VAR = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
1172
856
|
var isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1173
857
|
function expand(value) {
|
|
@@ -1289,32 +973,23 @@ var callToolResultSchema = (tool) => ({
|
|
|
1289
973
|
},
|
|
1290
974
|
required: ["content"]
|
|
1291
975
|
});
|
|
1292
|
-
function
|
|
1293
|
-
return {
|
|
976
|
+
function mcpTool(client, timeoutMs, exposure, name, tool) {
|
|
977
|
+
return defineTool4({
|
|
1294
978
|
name,
|
|
1295
979
|
description: tool.description ?? tool.title ?? tool.name,
|
|
1296
|
-
|
|
1297
|
-
outputSchema: callToolResultSchema(tool),
|
|
1298
|
-
namespace: {
|
|
1299
|
-
name: server,
|
|
1300
|
-
...description === undefined ? {} : { description },
|
|
1301
|
-
...client.instructions === undefined ? {} : { instructions: client.instructions }
|
|
1302
|
-
},
|
|
1303
|
-
call: (args, signal) => client.callTool(tool.name, args, { signal, timeoutMs })
|
|
1304
|
-
};
|
|
1305
|
-
}
|
|
1306
|
-
function asModelTool(tool) {
|
|
1307
|
-
return defineTool5({
|
|
1308
|
-
name: tool.name,
|
|
1309
|
-
description: tool.description,
|
|
1310
|
-
parameters: Type6.Unsafe({
|
|
980
|
+
parameters: Type5.Unsafe({
|
|
1311
981
|
...tool.inputSchema,
|
|
1312
982
|
type: "object",
|
|
1313
983
|
properties: tool.inputSchema.properties ?? {}
|
|
1314
984
|
}),
|
|
985
|
+
...exposure === "direct" ? {} : { callers: ["tools"] },
|
|
986
|
+
structuredOutputSchema: Type5.Unsafe(callToolResultSchema(tool)),
|
|
1315
987
|
execute: async (args, _api, context) => {
|
|
1316
|
-
const result2 = await tool.
|
|
1317
|
-
|
|
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 };
|
|
1318
993
|
}
|
|
1319
994
|
});
|
|
1320
995
|
}
|
|
@@ -1323,6 +998,7 @@ function startMcp(options) {
|
|
|
1323
998
|
const listeners = new Set;
|
|
1324
999
|
const servers = new Map;
|
|
1325
1000
|
const toolsets = new Map;
|
|
1001
|
+
const namespaces = new Map;
|
|
1326
1002
|
const exposures = new Map;
|
|
1327
1003
|
const live = new Set;
|
|
1328
1004
|
let errors = [];
|
|
@@ -1333,8 +1009,8 @@ function startMcp(options) {
|
|
|
1333
1009
|
listener(value);
|
|
1334
1010
|
};
|
|
1335
1011
|
const install = () => {
|
|
1336
|
-
|
|
1337
|
-
options.
|
|
1012
|
+
options.registry.install(defineExtension5({ name: "mcp", tools: [...toolsets.values()].flat() }));
|
|
1013
|
+
options.onNamespaces?.([...namespaces.values()]);
|
|
1338
1014
|
};
|
|
1339
1015
|
const set = (name, patch) => {
|
|
1340
1016
|
servers.set(name, { name, tools: 0, ...servers.get(name), ...patch });
|
|
@@ -1351,16 +1027,23 @@ function startMcp(options) {
|
|
|
1351
1027
|
continue;
|
|
1352
1028
|
}
|
|
1353
1029
|
taken.add(wire);
|
|
1354
|
-
tools.push(
|
|
1030
|
+
tools.push(mcpTool(client, timeoutMs, exposures.get(name) ?? "codemode", wire, tool));
|
|
1355
1031
|
}
|
|
1356
1032
|
if (closed)
|
|
1357
1033
|
return;
|
|
1358
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
|
+
});
|
|
1359
1041
|
set(name, { state: "connected", tools: tools.length, error: collisions.length === 0 ? undefined : collisions.join("; ") });
|
|
1360
1042
|
install();
|
|
1361
1043
|
};
|
|
1362
1044
|
const fail = (name, error) => {
|
|
1363
1045
|
toolsets.delete(name);
|
|
1046
|
+
namespaces.delete(name);
|
|
1364
1047
|
set(name, { state: "failed", error, tools: 0 });
|
|
1365
1048
|
install();
|
|
1366
1049
|
};
|
|
@@ -1435,7 +1118,6 @@ ${stderr}`;
|
|
|
1435
1118
|
});
|
|
1436
1119
|
return {
|
|
1437
1120
|
status,
|
|
1438
|
-
scriptTools: () => [...toolsets.values()].flat(),
|
|
1439
1121
|
async close() {
|
|
1440
1122
|
closed = true;
|
|
1441
1123
|
await Promise.all([...live].map((client) => client.close().catch(() => {})));
|
|
@@ -1443,15 +1125,340 @@ ${stderr}`;
|
|
|
1443
1125
|
};
|
|
1444
1126
|
}
|
|
1445
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
|
+
|
|
1446
1453
|
// apps/host/src/host.ts
|
|
1447
1454
|
import { openNodeSqliteStorage } from "@earendil-works/pi-durable/storage/sqlite/node";
|
|
1448
1455
|
import lockfile from "proper-lockfile";
|
|
1449
1456
|
|
|
1450
1457
|
// apps/host/src/gateway.ts
|
|
1451
1458
|
import { createServer } from "node:http";
|
|
1452
|
-
import { networkInterfaces } from "node:os";
|
|
1453
|
-
import { join as
|
|
1454
|
-
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";
|
|
1455
1462
|
import { clampThinkingLevel as clampThinkingLevel3, getSupportedThinkingLevels } from "@earendil-works/pi-ai";
|
|
1456
1463
|
import {
|
|
1457
1464
|
AgentDoc as AgentDoc3,
|
|
@@ -1460,6 +1467,190 @@ import {
|
|
|
1460
1467
|
import { WebSocket as WebSocket2, WebSocketServer } from "ws";
|
|
1461
1468
|
import { Value as Value2 } from "typebox/value";
|
|
1462
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
|
+
|
|
1463
1654
|
// packages/protocol/src/tool-target.ts
|
|
1464
1655
|
var TARGET_KEYS = [
|
|
1465
1656
|
"path",
|
|
@@ -1536,7 +1727,7 @@ function startRelayLink(options) {
|
|
|
1536
1727
|
}
|
|
1537
1728
|
}
|
|
1538
1729
|
}, pingIntervalMs);
|
|
1539
|
-
function
|
|
1730
|
+
function watch2(socket) {
|
|
1540
1731
|
watched.add(socket);
|
|
1541
1732
|
ponged.set(socket, true);
|
|
1542
1733
|
socket.on("pong", () => ponged.set(socket, true));
|
|
@@ -1548,7 +1739,7 @@ function startRelayLink(options) {
|
|
|
1548
1739
|
return;
|
|
1549
1740
|
const socket = new WebSocket(`${wsBase}/host`, { handshakeTimeout: pingIntervalMs });
|
|
1550
1741
|
control = socket;
|
|
1551
|
-
|
|
1742
|
+
watch2(socket);
|
|
1552
1743
|
socket.on("message", onControlMessage);
|
|
1553
1744
|
socket.once("close", onControlClosed);
|
|
1554
1745
|
}
|
|
@@ -1587,7 +1778,7 @@ function startRelayLink(options) {
|
|
|
1587
1778
|
break;
|
|
1588
1779
|
pendingAccepts += 1;
|
|
1589
1780
|
const data2 = new WebSocket(`${wsBase}/accept/${frame.connId}`, { handshakeTimeout: pingIntervalMs });
|
|
1590
|
-
|
|
1781
|
+
watch2(data2);
|
|
1591
1782
|
const release = () => {
|
|
1592
1783
|
pendingAccepts -= 1;
|
|
1593
1784
|
clearTimeout(ageOut);
|
|
@@ -1634,7 +1825,7 @@ function startRelayLink(options) {
|
|
|
1634
1825
|
}
|
|
1635
1826
|
|
|
1636
1827
|
// apps/host/src/gateway.ts
|
|
1637
|
-
var
|
|
1828
|
+
var context2 = BACKGROUND_CONTEXT2;
|
|
1638
1829
|
var forkTails = new WeakMap;
|
|
1639
1830
|
function serializedFork(harness, work) {
|
|
1640
1831
|
const next = (forkTails.get(harness) ?? Promise.resolve()).then(work, work);
|
|
@@ -1649,6 +1840,11 @@ function serializedFork(harness, work) {
|
|
|
1649
1840
|
var LOOPBACK_SECURE_PATH = "/secure";
|
|
1650
1841
|
async function startGateway(options) {
|
|
1651
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
|
+
});
|
|
1652
1848
|
const http = createServer((request, response) => {
|
|
1653
1849
|
serveWebClient(request, response, options.webRoot).catch(() => {
|
|
1654
1850
|
if (!response.headersSent)
|
|
@@ -1752,6 +1948,7 @@ async function startGateway(options) {
|
|
|
1752
1948
|
broadcast,
|
|
1753
1949
|
async close() {
|
|
1754
1950
|
await relay?.close();
|
|
1951
|
+
detachAutoTitle?.();
|
|
1755
1952
|
conversations.dispose();
|
|
1756
1953
|
await Promise.all([...clients].map((client) => client.dispose()));
|
|
1757
1954
|
for (const socket of server.clients)
|
|
@@ -1852,12 +2049,12 @@ function secureHandshake(socket, secure, harness, register) {
|
|
|
1852
2049
|
return;
|
|
1853
2050
|
}
|
|
1854
2051
|
const deviceKey = toBase64Url(remoteStatic);
|
|
1855
|
-
if (!await isRegistered(harness, deviceKey,
|
|
2052
|
+
if (!await isRegistered(harness, deviceKey, context2)) {
|
|
1856
2053
|
if (hello.pair === undefined || !secure.offers.consume(hello.pair.secret)) {
|
|
1857
2054
|
socket.close(UNAUTHORIZED_CLOSE_CODE, "unauthorized");
|
|
1858
2055
|
return;
|
|
1859
2056
|
}
|
|
1860
|
-
await registerDevice(harness, { publicKey: deviceKey, name: hello.pair.name },
|
|
2057
|
+
await registerDevice(harness, { publicKey: deviceKey, name: hello.pair.name }, context2);
|
|
1861
2058
|
}
|
|
1862
2059
|
const { message, result: result2 } = responder.writeMessage2(encodeHostHello({ v: 1 }));
|
|
1863
2060
|
return { result: result2, message, deviceKey };
|
|
@@ -1905,6 +2102,7 @@ class ConversationList {
|
|
|
1905
2102
|
#records = new Map;
|
|
1906
2103
|
#facts = new Map;
|
|
1907
2104
|
#index = new Map;
|
|
2105
|
+
#runEnd = new Set;
|
|
1908
2106
|
#list = [];
|
|
1909
2107
|
#unsubscribe = () => {};
|
|
1910
2108
|
#notifying = false;
|
|
@@ -1917,7 +2115,7 @@ class ConversationList {
|
|
|
1917
2115
|
const summaries = [];
|
|
1918
2116
|
let cursor;
|
|
1919
2117
|
do {
|
|
1920
|
-
const page = await harness.commit((tx) => tx.scanConversations({}, 256, cursor),
|
|
2118
|
+
const page = await harness.commit((tx) => tx.scanConversations({}, 256, cursor), context2);
|
|
1921
2119
|
for (const record of page.items) {
|
|
1922
2120
|
list.#records.set(record.id, record);
|
|
1923
2121
|
const facts = await list.#load(harness, record);
|
|
@@ -1934,24 +2132,30 @@ class ConversationList {
|
|
|
1934
2132
|
async#load(harness, record) {
|
|
1935
2133
|
const history = await historyFacts(harness, record.id);
|
|
1936
2134
|
const facts = { pending: new Set, ...history };
|
|
1937
|
-
const live = await harness.snapshot(LiveDoc, record.id,
|
|
2135
|
+
const live = await harness.snapshot(LiveDoc, record.id, context2);
|
|
1938
2136
|
facts.running = live?.run !== undefined;
|
|
1939
2137
|
facts.tools = live?.tools;
|
|
1940
2138
|
for (const token of this.#questionTokens) {
|
|
1941
|
-
const value = await harness.snapshot(token, record.id,
|
|
2139
|
+
const value = await harness.snapshot(token, record.id, context2);
|
|
1942
2140
|
if (Value2.Check(QuestionSchema, value) && value.requests.some((request) => request.resolution === undefined)) {
|
|
1943
2141
|
facts.pending.add(token.definition.kind);
|
|
1944
2142
|
}
|
|
1945
2143
|
}
|
|
1946
2144
|
if (record.owner !== undefined) {
|
|
1947
|
-
const
|
|
1948
|
-
if (typeof label === "string" && label !== "")
|
|
1949
|
-
facts.label = label;
|
|
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;
|
|
1950
2150
|
}
|
|
2151
|
+
const named = (await harness.snapshot(TitleDoc, record.id, context2))?.title;
|
|
2152
|
+
if (typeof named === "string" && named !== "")
|
|
2153
|
+
facts.named = named;
|
|
1951
2154
|
return facts;
|
|
1952
2155
|
}
|
|
1953
2156
|
#observe(publication) {
|
|
1954
2157
|
const touched = new Set;
|
|
2158
|
+
const ended = new Set;
|
|
1955
2159
|
for (const change of publication.changes) {
|
|
1956
2160
|
if (change.type === "conversation") {
|
|
1957
2161
|
this.#records.set(change.value.id, change.value);
|
|
@@ -1981,9 +2185,12 @@ class ConversationList {
|
|
|
1981
2185
|
const facts = this.#fact(id);
|
|
1982
2186
|
if (change.record.kind === "pi.live") {
|
|
1983
2187
|
const live = change.value;
|
|
2188
|
+
const wasRunning = facts.running === true;
|
|
1984
2189
|
facts.running = live?.run !== undefined;
|
|
1985
2190
|
facts.tools = live?.tools;
|
|
1986
2191
|
touched.add(id);
|
|
2192
|
+
if (wasRunning && !facts.running && this.#records.get(id)?.owner === undefined)
|
|
2193
|
+
ended.add(id);
|
|
1987
2194
|
} else if (this.#questionKinds.has(change.record.kind)) {
|
|
1988
2195
|
const value = change.value;
|
|
1989
2196
|
const pending = Value2.Check(QuestionSchema, value) && value.requests.some((request) => request.resolution === undefined);
|
|
@@ -1995,8 +2202,13 @@ class ConversationList {
|
|
|
1995
2202
|
facts.pending = next2;
|
|
1996
2203
|
touched.add(id);
|
|
1997
2204
|
} else if (change.record.kind === SubagentDoc.definition.kind) {
|
|
1998
|
-
const
|
|
1999
|
-
facts.label = typeof label === "string" && label !== "" ? label : undefined;
|
|
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;
|
|
2000
2212
|
touched.add(id);
|
|
2001
2213
|
}
|
|
2002
2214
|
}
|
|
@@ -2019,6 +2231,13 @@ class ConversationList {
|
|
|
2019
2231
|
}
|
|
2020
2232
|
if (next !== undefined)
|
|
2021
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);
|
|
2022
2241
|
}
|
|
2023
2242
|
#fact(id) {
|
|
2024
2243
|
let facts = this.#facts.get(id);
|
|
@@ -2062,31 +2281,33 @@ function summaryOf(record, facts) {
|
|
|
2062
2281
|
kind: record.owner !== undefined ? "subagent" : record.parent !== undefined ? "fork" : "conversation",
|
|
2063
2282
|
...parent === undefined ? {} : { parent },
|
|
2064
2283
|
...record.parent === undefined ? {} : { forkedAt: String(record.parent.at) },
|
|
2065
|
-
...facts.title === undefined ? {} : { title: facts.title },
|
|
2284
|
+
...(facts.named ?? facts.title) === undefined ? {} : { title: facts.named ?? facts.title },
|
|
2066
2285
|
...status === undefined ? {} : { status },
|
|
2067
2286
|
...facts.updatedAt === undefined ? {} : { updatedAt: facts.updatedAt },
|
|
2068
2287
|
...facts.label === undefined ? {} : { label: facts.label },
|
|
2288
|
+
...facts.role === undefined ? {} : { role: facts.role },
|
|
2069
2289
|
...activity === undefined ? {} : { activity }
|
|
2070
2290
|
};
|
|
2071
2291
|
}
|
|
2072
2292
|
function sameSummary(a, b) {
|
|
2073
|
-
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.activity?.tool === b.activity?.tool && a.activity?.target === b.activity?.target;
|
|
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;
|
|
2074
2294
|
}
|
|
2075
2295
|
function titleOf(entry) {
|
|
2076
2296
|
const message = entry?.model?.[0];
|
|
2077
2297
|
if (message?.role !== "user")
|
|
2078
2298
|
return {};
|
|
2079
2299
|
const text = typeof message.content === "string" ? message.content : message.content.flatMap((block) => block.type === "text" ? [block.text] : []).join(" ");
|
|
2080
|
-
|
|
2300
|
+
const title = text.replace(/\s+/g, " ").trim();
|
|
2301
|
+
return title === "" ? {} : { title };
|
|
2081
2302
|
}
|
|
2082
2303
|
async function historyFacts(harness, id) {
|
|
2083
|
-
const conversation = await harness.conversation(id,
|
|
2304
|
+
const conversation = await harness.conversation(id, context2);
|
|
2084
2305
|
let first;
|
|
2085
2306
|
let newest;
|
|
2086
2307
|
let cursor;
|
|
2087
2308
|
do {
|
|
2088
|
-
const page = await conversation.entries({}, 256, cursor,
|
|
2089
|
-
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;
|
|
2090
2311
|
newest ??= page.items.find((entry) => entry.conversationId === id && (entry.kind === "pi.user" || entry.kind === "pi.assistant"));
|
|
2091
2312
|
cursor = page.next;
|
|
2092
2313
|
} while (cursor !== undefined);
|
|
@@ -2120,13 +2341,20 @@ class GatewayClient {
|
|
|
2120
2341
|
#connection;
|
|
2121
2342
|
#hooks;
|
|
2122
2343
|
#options;
|
|
2123
|
-
#
|
|
2344
|
+
#hostValues;
|
|
2124
2345
|
#subscriptions = new Map;
|
|
2125
2346
|
#disposed = false;
|
|
2126
2347
|
constructor(connection, options, conversations, hooks) {
|
|
2127
2348
|
this.#connection = connection;
|
|
2128
2349
|
this.#options = options;
|
|
2129
|
-
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
|
+
};
|
|
2130
2358
|
this.#hooks = hooks;
|
|
2131
2359
|
this.devicePublicKey = hooks.devicePublicKey;
|
|
2132
2360
|
connection.handle({
|
|
@@ -2165,7 +2393,7 @@ class GatewayClient {
|
|
|
2165
2393
|
...packagedVersion === undefined ? {} : { hostVersion: packagedVersion },
|
|
2166
2394
|
session: options.session,
|
|
2167
2395
|
models: options.modelSummaries(),
|
|
2168
|
-
defaults: options.
|
|
2396
|
+
defaults: settingsDefaults(options.settings.value),
|
|
2169
2397
|
docs: (options.docs ?? []).map((doc) => ({
|
|
2170
2398
|
kind: doc.token.definition.kind,
|
|
2171
2399
|
...doc.presentation === undefined ? {} : { presentation: doc.presentation }
|
|
@@ -2216,36 +2444,34 @@ class GatewayClient {
|
|
|
2216
2444
|
state.subscription = subscription;
|
|
2217
2445
|
}
|
|
2218
2446
|
async#open(stream, send) {
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
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 })) };
|
|
2223
2451
|
}
|
|
2224
2452
|
if (stream === "index") {
|
|
2225
|
-
const
|
|
2226
|
-
if (
|
|
2453
|
+
const watch2 = await this.#options.harness.watchDoc(IndexDoc, context2);
|
|
2454
|
+
if (watch2 === undefined)
|
|
2227
2455
|
throw new Error("Index document is missing");
|
|
2228
|
-
return this.#forward(stream,
|
|
2456
|
+
return this.#forward(stream, watch2, send);
|
|
2229
2457
|
}
|
|
2230
2458
|
if (stream === "devices") {
|
|
2231
|
-
const
|
|
2232
|
-
if (
|
|
2459
|
+
const watch2 = await this.#options.harness.watchDoc(DevicesDoc, context2);
|
|
2460
|
+
if (watch2 === undefined)
|
|
2233
2461
|
throw new Error("Devices document is missing");
|
|
2234
|
-
return this.#forward(stream,
|
|
2462
|
+
return this.#forward(stream, watch2, send);
|
|
2235
2463
|
}
|
|
2236
2464
|
if (stream === "tasks")
|
|
2237
|
-
return this.#forward(stream, await this.#options.harness.watchTaskGraph(
|
|
2238
|
-
if (stream === "mcp") {
|
|
2239
|
-
await send({ type: "snapshot", stream, value: this.#options.mcp?.value ?? null });
|
|
2240
|
-
const mcp = this.#options.mcp;
|
|
2241
|
-
return mcp === undefined ? { stop: () => {} } : { stop: mcp.subscribe((value) => void send({ type: "snapshot", stream, value })) };
|
|
2242
|
-
}
|
|
2465
|
+
return this.#forward(stream, await this.#options.harness.watchTaskGraph(context2), send);
|
|
2243
2466
|
if (stream.startsWith("doc:")) {
|
|
2244
2467
|
const at = stream.lastIndexOf(":");
|
|
2245
2468
|
return this.#openDoc(stream, stream.slice("doc:".length, at), Number(stream.slice(at + 1)), send);
|
|
2246
2469
|
}
|
|
2247
2470
|
const conversation = await this.#conversation(Number(stream.slice("conversation:".length)));
|
|
2248
|
-
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;
|
|
2249
2475
|
}
|
|
2250
2476
|
async#openDoc(stream, kind, id, send) {
|
|
2251
2477
|
const doc = this.#options.docs?.find((candidate) => candidate.token.definition.kind === kind);
|
|
@@ -2268,7 +2494,7 @@ class GatewayClient {
|
|
|
2268
2494
|
setImmediate(() => signalCreation());
|
|
2269
2495
|
});
|
|
2270
2496
|
try {
|
|
2271
|
-
const existing = await harness.watchDoc(token, id,
|
|
2497
|
+
const existing = await harness.watchDoc(token, id, context2);
|
|
2272
2498
|
if (existing !== undefined) {
|
|
2273
2499
|
unsubscribe();
|
|
2274
2500
|
return this.#forward(stream, existing, send);
|
|
@@ -2281,13 +2507,13 @@ class GatewayClient {
|
|
|
2281
2507
|
created.then(async () => {
|
|
2282
2508
|
if (stopped)
|
|
2283
2509
|
return;
|
|
2284
|
-
const
|
|
2285
|
-
if (
|
|
2510
|
+
const watch2 = await harness.watchDoc(token, id, context2);
|
|
2511
|
+
if (watch2 === undefined)
|
|
2286
2512
|
return;
|
|
2287
2513
|
if (stopped)
|
|
2288
|
-
await
|
|
2514
|
+
await watch2.stop();
|
|
2289
2515
|
else {
|
|
2290
|
-
inner = await this.#forward(stream,
|
|
2516
|
+
inner = await this.#forward(stream, watch2, send);
|
|
2291
2517
|
if (stopped)
|
|
2292
2518
|
await inner.stop();
|
|
2293
2519
|
}
|
|
@@ -2304,10 +2530,10 @@ class GatewayClient {
|
|
|
2304
2530
|
}
|
|
2305
2531
|
};
|
|
2306
2532
|
}
|
|
2307
|
-
async#forward(stream,
|
|
2308
|
-
await send({ type: "snapshot", stream, value:
|
|
2309
|
-
|
|
2310
|
-
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;
|
|
2311
2537
|
}
|
|
2312
2538
|
async#unsubscribe(stream) {
|
|
2313
2539
|
const subscription = this.#subscriptions.get(stream);
|
|
@@ -2315,7 +2541,7 @@ class GatewayClient {
|
|
|
2315
2541
|
await subscription?.subscription?.stop();
|
|
2316
2542
|
}
|
|
2317
2543
|
async#conversation(id) {
|
|
2318
|
-
const conversation = await this.#options.harness.conversation(id,
|
|
2544
|
+
const conversation = await this.#options.harness.conversation(id, context2);
|
|
2319
2545
|
if (conversation === undefined)
|
|
2320
2546
|
throw new Error(`Conversation ${id} does not exist`);
|
|
2321
2547
|
return conversation;
|
|
@@ -2331,27 +2557,31 @@ class GatewayClient {
|
|
|
2331
2557
|
async#call(method, args) {
|
|
2332
2558
|
switch (method) {
|
|
2333
2559
|
case "addProject":
|
|
2334
|
-
return addProject(this.#options.harness, args.path,
|
|
2560
|
+
return addProject(this.#options.harness, args.path, context2);
|
|
2335
2561
|
case "removeProject":
|
|
2336
|
-
await removeProject(this.#options.harness, args.path,
|
|
2562
|
+
await removeProject(this.#options.harness, args.path, context2);
|
|
2337
2563
|
return null;
|
|
2338
2564
|
case "createConversation": {
|
|
2339
2565
|
const create = args;
|
|
2566
|
+
const problem = imageProblem(create.images ?? []);
|
|
2567
|
+
if (problem !== undefined)
|
|
2568
|
+
throw new Error(problem);
|
|
2340
2569
|
const conversationId = await createConversation(this.#options.harness, this.#options.dataDir, {
|
|
2341
2570
|
home: create.home,
|
|
2342
2571
|
text: create.text,
|
|
2343
2572
|
requestId: create.requestId,
|
|
2573
|
+
...create.images === undefined ? {} : { images: create.images },
|
|
2344
2574
|
...create.checkout === undefined ? {} : { checkout: create.checkout },
|
|
2345
2575
|
...create.model === undefined ? {} : { model: create.model },
|
|
2346
2576
|
...create.thinkingLevel === undefined ? {} : { thinkingLevel: create.thinkingLevel }
|
|
2347
|
-
}, this.#options.
|
|
2577
|
+
}, settingsDefaults(this.#options.settings.value), this.#options.models, context2);
|
|
2348
2578
|
return { conversationId };
|
|
2349
2579
|
}
|
|
2350
2580
|
case "archive": {
|
|
2351
2581
|
const input = args;
|
|
2352
|
-
await archive(this.#options.harness, input.conversationId, input.archived,
|
|
2582
|
+
await archive(this.#options.harness, input.conversationId, input.archived, context2);
|
|
2353
2583
|
if (input.archived) {
|
|
2354
|
-
const kept = await cleanupArchivedWorktrees(this.#options.harness, input.conversationId,
|
|
2584
|
+
const kept = await cleanupArchivedWorktrees(this.#options.harness, input.conversationId, context2);
|
|
2355
2585
|
for (const { record, reason } of kept) {
|
|
2356
2586
|
await this.send({ type: "notice", level: "warning", message: `Kept worktree ${record.path} (branch ${record.branch}): ${reason}` });
|
|
2357
2587
|
}
|
|
@@ -2366,31 +2596,88 @@ class GatewayClient {
|
|
|
2366
2596
|
}
|
|
2367
2597
|
case "revokeDevice": {
|
|
2368
2598
|
const { publicKey } = args;
|
|
2369
|
-
await revokeDevice(this.#options.harness, publicKey,
|
|
2599
|
+
await revokeDevice(this.#options.harness, publicKey, context2);
|
|
2370
2600
|
this.#hooks.closeDevice(publicKey);
|
|
2371
2601
|
return null;
|
|
2372
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());
|
|
2373
2637
|
}
|
|
2374
2638
|
const conversation = await this.#conversation(args.conversationId);
|
|
2375
2639
|
switch (method) {
|
|
2376
2640
|
case "submit": {
|
|
2377
|
-
const { text, whenBusy, requestId } = args;
|
|
2378
|
-
const
|
|
2379
|
-
|
|
2380
|
-
|
|
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) => {
|
|
2381
2648
|
if (settled.status === "unanswered" && settled.reason !== "aborted") {
|
|
2382
2649
|
this.send({ type: "notice", level: "error", message: `No answer: ${settled.reason}` });
|
|
2383
2650
|
}
|
|
2384
2651
|
}, () => {});
|
|
2385
2652
|
return { submissionId: submission.id };
|
|
2386
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
|
+
}
|
|
2387
2674
|
case "abort":
|
|
2388
|
-
await conversation.abort(
|
|
2675
|
+
await conversation.abort(context2);
|
|
2389
2676
|
return null;
|
|
2390
2677
|
case "compact": {
|
|
2391
2678
|
const { instructions } = args;
|
|
2392
|
-
const taskId = await conversation.compact(instructions,
|
|
2393
|
-
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}.` }), () => {});
|
|
2394
2681
|
return { taskId };
|
|
2395
2682
|
}
|
|
2396
2683
|
case "setModel": {
|
|
@@ -2399,7 +2686,7 @@ class GatewayClient {
|
|
|
2399
2686
|
if (model === undefined)
|
|
2400
2687
|
throw new Error(`Unknown model: ${ref.provider}/${ref.modelId}`);
|
|
2401
2688
|
const thinking = (await this.#agent(conversation.id)).thinkingLevel ?? "off";
|
|
2402
|
-
await conversation.configure({ model: ref, thinkingLevel: clampThinkingLevel3(model, thinking) },
|
|
2689
|
+
await conversation.configure({ model: ref, thinkingLevel: clampThinkingLevel3(model, thinking) }, context2);
|
|
2403
2690
|
return null;
|
|
2404
2691
|
}
|
|
2405
2692
|
case "setThinkingLevel": {
|
|
@@ -2411,7 +2698,7 @@ class GatewayClient {
|
|
|
2411
2698
|
if (!getSupportedThinkingLevels(model).includes(level)) {
|
|
2412
2699
|
throw new Error(`Thinking level ${level} is not supported by ${agent.model.provider}/${agent.model.modelId}`);
|
|
2413
2700
|
}
|
|
2414
|
-
await conversation.configure({ thinkingLevel: level },
|
|
2701
|
+
await conversation.configure({ thinkingLevel: level }, context2);
|
|
2415
2702
|
return null;
|
|
2416
2703
|
}
|
|
2417
2704
|
case "fork": {
|
|
@@ -2435,13 +2722,13 @@ class GatewayClient {
|
|
|
2435
2722
|
}
|
|
2436
2723
|
cursor = page.next;
|
|
2437
2724
|
} while (cursor !== undefined);
|
|
2438
|
-
},
|
|
2439
|
-
const agent = await conversation.agent(
|
|
2725
|
+
}, context2);
|
|
2726
|
+
const agent = await conversation.agent(context2);
|
|
2440
2727
|
const remove = agent.tools.filter((tool) => removeTools.includes(tool.name));
|
|
2441
2728
|
const fork = await conversation.fork(at, {
|
|
2442
2729
|
ownership: { kind: "ownerless" },
|
|
2443
2730
|
...remove.length === 0 ? {} : { agent: { tools: { remove } } }
|
|
2444
|
-
},
|
|
2731
|
+
}, context2);
|
|
2445
2732
|
return { conversationId: fork.id };
|
|
2446
2733
|
});
|
|
2447
2734
|
}
|
|
@@ -2477,16 +2764,16 @@ class GatewayClient {
|
|
|
2477
2764
|
}
|
|
2478
2765
|
request.resolution = { outcome: "answered", answers, at: Date.now() };
|
|
2479
2766
|
return { first: true };
|
|
2480
|
-
},
|
|
2767
|
+
}, context2);
|
|
2481
2768
|
}
|
|
2482
2769
|
case "changes": {
|
|
2483
|
-
return await this.#changes(conversation,
|
|
2770
|
+
return await this.#changes(conversation, context2);
|
|
2484
2771
|
}
|
|
2485
2772
|
}
|
|
2486
2773
|
}
|
|
2487
|
-
async#dismissPendingQuestions(conversationId,
|
|
2774
|
+
async#dismissPendingQuestions(conversationId, context3) {
|
|
2488
2775
|
const questionDocs = (this.#options.docs ?? []).filter((doc) => doc.presentation === "pinomad.question");
|
|
2489
|
-
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)));
|
|
2490
2777
|
const pending = snapshots.some((value) => Value2.Check(QuestionSchema, value) && value.requests.some((request) => request.resolution === undefined));
|
|
2491
2778
|
if (!pending)
|
|
2492
2779
|
return false;
|
|
@@ -2504,18 +2791,18 @@ class GatewayClient {
|
|
|
2504
2791
|
}
|
|
2505
2792
|
}
|
|
2506
2793
|
return dismissed;
|
|
2507
|
-
},
|
|
2794
|
+
}, context3);
|
|
2508
2795
|
}
|
|
2509
2796
|
get #chatsPrefix() {
|
|
2510
|
-
return
|
|
2797
|
+
return join6(this.#options.dataDir, "chats") + sep3;
|
|
2511
2798
|
}
|
|
2512
|
-
async#changes(conversation,
|
|
2513
|
-
const cwd = (await conversation.agent(
|
|
2799
|
+
async#changes(conversation, context3) {
|
|
2800
|
+
const cwd = (await conversation.agent(context3)).cwd;
|
|
2514
2801
|
if (cwd === undefined)
|
|
2515
2802
|
return { available: false, reason: "No working directory" };
|
|
2516
2803
|
if (cwd.startsWith(this.#chatsPrefix))
|
|
2517
2804
|
return { available: false, reason: "Chat conversations have no repository" };
|
|
2518
|
-
const index = await this.#options.harness.snapshot(IndexDoc,
|
|
2805
|
+
const index = await this.#options.harness.snapshot(IndexDoc, context3);
|
|
2519
2806
|
const record = checkoutAt(index?.checkouts, cwd);
|
|
2520
2807
|
if (record !== undefined) {
|
|
2521
2808
|
if (!await worktreeExists(record.path))
|
|
@@ -2528,7 +2815,7 @@ class GatewayClient {
|
|
|
2528
2815
|
return { available: true, ...await changesOf(base.repo, base.base) };
|
|
2529
2816
|
}
|
|
2530
2817
|
async#agent(id) {
|
|
2531
|
-
return await this.#options.harness.snapshot(AgentDoc3, id,
|
|
2818
|
+
return await this.#options.harness.snapshot(AgentDoc3, id, context2) ?? {};
|
|
2532
2819
|
}
|
|
2533
2820
|
async dispose() {
|
|
2534
2821
|
if (this.#disposed)
|
|
@@ -2542,20 +2829,20 @@ class GatewayClient {
|
|
|
2542
2829
|
}
|
|
2543
2830
|
|
|
2544
2831
|
// apps/host/src/host.ts
|
|
2545
|
-
var
|
|
2832
|
+
var context3 = BACKGROUND_CONTEXT3;
|
|
2546
2833
|
async function hostToken(dataDir) {
|
|
2547
|
-
const path =
|
|
2834
|
+
const path = join7(dataDir, "token");
|
|
2548
2835
|
try {
|
|
2549
|
-
return (await
|
|
2836
|
+
return (await readFile4(path, "utf8")).trim();
|
|
2550
2837
|
} catch {
|
|
2551
|
-
const token =
|
|
2552
|
-
await
|
|
2838
|
+
const token = randomBytes3(24).toString("base64url");
|
|
2839
|
+
await writeFile3(path, `${token}
|
|
2553
2840
|
`, { mode: 384 });
|
|
2554
2841
|
return token;
|
|
2555
2842
|
}
|
|
2556
2843
|
}
|
|
2557
2844
|
async function openHost(options) {
|
|
2558
|
-
await
|
|
2845
|
+
await mkdir4(options.dataDir, { recursive: true });
|
|
2559
2846
|
const stale = Math.max(2000, options.lockStaleMs ?? 1e4);
|
|
2560
2847
|
const release = await lockfile.lock(options.dataDir, {
|
|
2561
2848
|
realpath: false,
|
|
@@ -2566,25 +2853,43 @@ async function openHost(options) {
|
|
|
2566
2853
|
});
|
|
2567
2854
|
let harness;
|
|
2568
2855
|
let mcp;
|
|
2856
|
+
let agentRoles;
|
|
2857
|
+
let extensions = [];
|
|
2569
2858
|
try {
|
|
2570
2859
|
const token = await hostToken(options.dataDir);
|
|
2571
2860
|
const reports = [];
|
|
2572
2861
|
let report = (error) => void reports.push(error);
|
|
2573
2862
|
const registry = createRegistry();
|
|
2574
|
-
|
|
2575
|
-
|
|
2863
|
+
agentRoles = await openAgentRoles({ dir: join7(options.dataDir, "agents") });
|
|
2864
|
+
extensions = typeof options.extensions === "function" ? options.extensions({
|
|
2576
2865
|
harness: () => {
|
|
2577
2866
|
if (harness === undefined)
|
|
2578
2867
|
throw new Error("harness is not open yet");
|
|
2579
2868
|
return harness;
|
|
2580
|
-
}
|
|
2869
|
+
},
|
|
2870
|
+
agentRoles
|
|
2581
2871
|
}) : options.extensions ?? [];
|
|
2582
2872
|
for (const { extension } of extensions)
|
|
2583
2873
|
registry.install(extension);
|
|
2584
|
-
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
|
+
});
|
|
2585
2890
|
const environments = new Map;
|
|
2586
|
-
const chatsDir =
|
|
2587
|
-
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")), {
|
|
2588
2893
|
models: options.models,
|
|
2589
2894
|
registry,
|
|
2590
2895
|
env: async (target) => {
|
|
@@ -2593,15 +2898,15 @@ async function openHost(options) {
|
|
|
2593
2898
|
return;
|
|
2594
2899
|
const worktrees = worktreeRoot(options.dataDir);
|
|
2595
2900
|
if (cwd === worktrees || cwd.startsWith(worktrees + sep4)) {
|
|
2596
|
-
const index = await target.read.snapshot(IndexDoc,
|
|
2901
|
+
const index = await target.read.snapshot(IndexDoc, context3);
|
|
2597
2902
|
const record = checkoutAt(index?.checkouts, cwd);
|
|
2598
2903
|
if (record !== undefined) {
|
|
2599
2904
|
await ensureWorktree(record);
|
|
2600
|
-
await
|
|
2905
|
+
await mkdir4(cwd, { recursive: true });
|
|
2601
2906
|
}
|
|
2602
2907
|
}
|
|
2603
2908
|
if (cwd.startsWith(`${chatsDir}/`))
|
|
2604
|
-
await
|
|
2909
|
+
await mkdir4(cwd, { recursive: true });
|
|
2605
2910
|
let env = environments.get(cwd);
|
|
2606
2911
|
if (env === undefined) {
|
|
2607
2912
|
env = new NodeExecutionEnv({ cwd });
|
|
@@ -2609,35 +2914,35 @@ async function openHost(options) {
|
|
|
2609
2914
|
}
|
|
2610
2915
|
return env;
|
|
2611
2916
|
},
|
|
2612
|
-
...options.settings === undefined ? {} : {
|
|
2917
|
+
...options.settings === undefined ? {} : {
|
|
2918
|
+
settings: typeof options.settings === "function" ? options.settings(hostSettings) : options.settings
|
|
2919
|
+
},
|
|
2613
2920
|
onReport: (error) => report(error)
|
|
2614
|
-
},
|
|
2615
|
-
|
|
2616
|
-
await
|
|
2617
|
-
await ensureDevices(harness, context2);
|
|
2921
|
+
}, context3);
|
|
2922
|
+
await ensureIndex(harness, context3);
|
|
2923
|
+
await ensureDevices(harness, context3);
|
|
2618
2924
|
for (const path of options.projects ?? [])
|
|
2619
|
-
await addProject(harness, path,
|
|
2925
|
+
await addProject(harness, path, context3);
|
|
2620
2926
|
const hostKey = await loadHostKey(options.dataDir);
|
|
2621
2927
|
const offers = pairingOffers(options.remote?.pairingTtlMs);
|
|
2622
2928
|
const relayKey = options.relay === undefined ? undefined : await loadRelayKey(options.dataDir);
|
|
2623
|
-
const defaults = initialModel === undefined ? {} : {
|
|
2624
|
-
model: { provider: initialModel.provider, modelId: initialModel.modelId },
|
|
2625
|
-
...initialModel.thinkingLevel === undefined ? {} : { thinkingLevel: initialModel.thinkingLevel }
|
|
2626
|
-
};
|
|
2627
2929
|
const gateway = await startGateway({
|
|
2628
2930
|
harness,
|
|
2629
2931
|
models: options.models,
|
|
2630
2932
|
modelSummaries: options.modelSummaries,
|
|
2631
2933
|
session: { id: basename2(options.dataDir), directory: options.dataDir },
|
|
2632
2934
|
dataDir: options.dataDir,
|
|
2633
|
-
|
|
2935
|
+
settings: hostSettings,
|
|
2634
2936
|
token,
|
|
2635
2937
|
port: options.port,
|
|
2938
|
+
agents: agentRoles,
|
|
2636
2939
|
...options.browserOrigins === undefined ? {} : { browserOrigins: options.browserOrigins },
|
|
2637
2940
|
docs: extensions.flatMap((extension) => extension.docs ?? []),
|
|
2638
2941
|
toolPresentations: Object.assign({}, ...extensions.map((extension) => extension.tools ?? {})),
|
|
2639
2942
|
...mcp === undefined ? {} : { mcp: mcp.status },
|
|
2943
|
+
...options.modelAuth === undefined ? {} : { modelAuth: options.modelAuth },
|
|
2640
2944
|
...options.webRoot === undefined ? {} : { webRoot: options.webRoot },
|
|
2945
|
+
...options.autoTitle === undefined ? {} : { autoTitle: options.autoTitle },
|
|
2641
2946
|
secure: {
|
|
2642
2947
|
hostKey,
|
|
2643
2948
|
offers,
|
|
@@ -2668,15 +2973,21 @@ async function openHost(options) {
|
|
|
2668
2973
|
url: gateway.url,
|
|
2669
2974
|
token,
|
|
2670
2975
|
harness,
|
|
2976
|
+
closed: opened.closed,
|
|
2977
|
+
settings: hostSettings,
|
|
2671
2978
|
...gateway.remote === undefined || hostKey === undefined ? {} : { remote: { url: gateway.remote.url, advertiseUrl: gateway.remote.advertiseUrl, hostKey: hostKey.publicKey } },
|
|
2672
2979
|
...gateway.relay === undefined ? {} : { relay: { origin: gateway.relay.origin, hostId: gateway.relay.hostId, state: () => gateway.relay.state() } },
|
|
2673
2980
|
close() {
|
|
2674
2981
|
closing ??= (async () => {
|
|
2675
2982
|
try {
|
|
2676
2983
|
await gateway.close();
|
|
2677
|
-
await opened.close(
|
|
2984
|
+
await opened.close(context3);
|
|
2678
2985
|
await mcp?.close();
|
|
2679
|
-
|
|
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)));
|
|
2680
2991
|
} finally {
|
|
2681
2992
|
await release();
|
|
2682
2993
|
}
|
|
@@ -2685,15 +2996,20 @@ async function openHost(options) {
|
|
|
2685
2996
|
}
|
|
2686
2997
|
};
|
|
2687
2998
|
} catch (error) {
|
|
2999
|
+
options.modelAuth?.close();
|
|
3000
|
+
agentRoles?.close();
|
|
3001
|
+
for (const each of extensions)
|
|
3002
|
+
await each.close?.().catch(() => {});
|
|
2688
3003
|
await mcp?.close().catch(() => {});
|
|
2689
|
-
await harness?.close(
|
|
3004
|
+
await harness?.close(context3).catch(() => {});
|
|
2690
3005
|
await release().catch(() => {});
|
|
2691
3006
|
throw error;
|
|
2692
3007
|
}
|
|
2693
3008
|
}
|
|
2694
3009
|
|
|
2695
|
-
// apps/host/src/
|
|
2696
|
-
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";
|
|
2697
3013
|
|
|
2698
3014
|
// apps/host/src/pi-setup.ts
|
|
2699
3015
|
import { getSupportedThinkingLevels as getSupportedThinkingLevels2 } from "@earendil-works/pi-ai";
|
|
@@ -2761,7 +3077,7 @@ function configureHarnessHttp(settingsManager) {
|
|
|
2761
3077
|
applyHttpProxySettings(settingsManager.getGlobalSettings().httpProxy);
|
|
2762
3078
|
configureHttpDispatcher(settingsManager.getHttpIdleTimeoutMs());
|
|
2763
3079
|
}
|
|
2764
|
-
function createHarnessSettings(settingsManager) {
|
|
3080
|
+
function createHarnessSettings(settingsManager, modes) {
|
|
2765
3081
|
return {
|
|
2766
3082
|
get stream() {
|
|
2767
3083
|
const provider = settingsManager.getProviderRetrySettings();
|
|
@@ -2779,10 +3095,10 @@ function createHarnessSettings(settingsManager) {
|
|
|
2779
3095
|
return settingsManager.getRetrySettings();
|
|
2780
3096
|
},
|
|
2781
3097
|
get steeringMode() {
|
|
2782
|
-
return
|
|
3098
|
+
return modes.value.steeringMode;
|
|
2783
3099
|
},
|
|
2784
3100
|
get followUpMode() {
|
|
2785
|
-
return
|
|
3101
|
+
return modes.value.followUpMode;
|
|
2786
3102
|
}
|
|
2787
3103
|
};
|
|
2788
3104
|
}
|
|
@@ -2802,11 +3118,81 @@ function modelSummaries(modelRuntime) {
|
|
|
2802
3118
|
modelId: model.id,
|
|
2803
3119
|
name: model.name,
|
|
2804
3120
|
contextWindow: model.contextWindow,
|
|
2805
|
-
thinkingLevels: getSupportedThinkingLevels2(model)
|
|
3121
|
+
thinkingLevels: getSupportedThinkingLevels2(model),
|
|
3122
|
+
images: model.input.includes("image")
|
|
2806
3123
|
}));
|
|
2807
3124
|
}
|
|
2808
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
|
+
|
|
2809
3194
|
// apps/host/src/main.ts
|
|
3195
|
+
import { getSupportedThinkingLevels as getSupportedThinkingLevels3 } from "@earendil-works/pi-ai";
|
|
2810
3196
|
async function main(argv) {
|
|
2811
3197
|
const { values } = parseArgs({
|
|
2812
3198
|
args: [...argv],
|
|
@@ -2824,31 +3210,27 @@ async function main(argv) {
|
|
|
2824
3210
|
}
|
|
2825
3211
|
});
|
|
2826
3212
|
const dataDir = resolve3(values["data-dir"] ?? DEFAULT_DATA_DIR);
|
|
2827
|
-
const extensionsFor = (models, summaries) => (host2) =>
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
presentations: Object.assign({}, ...others.map((extension) => extension.tools ?? {}))
|
|
2844
|
-
})
|
|
2845
|
-
];
|
|
2846
|
-
};
|
|
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
|
+
];
|
|
2847
3229
|
const common = {
|
|
2848
3230
|
dataDir,
|
|
2849
3231
|
projects: values.project ?? [],
|
|
2850
3232
|
port: Number(values.port),
|
|
2851
|
-
mcpConfig:
|
|
3233
|
+
mcpConfig: join8(homedir3(), ".agents", "mcp.json"),
|
|
2852
3234
|
browserOrigins: values["browser-origin"] ?? ["http://127.0.0.1:5199"],
|
|
2853
3235
|
webRoot,
|
|
2854
3236
|
...values["lock-stale-ms"] === undefined ? {} : { lockStaleMs: Number(values["lock-stale-ms"]) },
|
|
@@ -2872,7 +3254,8 @@ async function main(argv) {
|
|
|
2872
3254
|
modelId: model2.id,
|
|
2873
3255
|
name: model2.name,
|
|
2874
3256
|
contextWindow: model2.contextWindow,
|
|
2875
|
-
thinkingLevels: getSupportedThinkingLevels3(model2)
|
|
3257
|
+
thinkingLevels: getSupportedThinkingLevels3(model2),
|
|
3258
|
+
images: model2.input.includes("image")
|
|
2876
3259
|
}
|
|
2877
3260
|
];
|
|
2878
3261
|
return {
|
|
@@ -2880,32 +3263,49 @@ async function main(argv) {
|
|
|
2880
3263
|
models,
|
|
2881
3264
|
modelSummaries: summaries,
|
|
2882
3265
|
extensions: extensionsFor(models, summaries),
|
|
2883
|
-
|
|
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
|
+
}
|
|
2884
3273
|
};
|
|
2885
3274
|
}
|
|
2886
3275
|
async function piOptions() {
|
|
2887
3276
|
const modelRuntime = await ModelRuntime.create();
|
|
3277
|
+
const modelAuth = await watchModelAuth(modelRuntime, { authPath: join8(getAgentDir(), "auth.json") });
|
|
2888
3278
|
const settingsManager = SettingsManager.create(process.cwd());
|
|
2889
3279
|
configureHarnessHttp(settingsManager);
|
|
2890
|
-
const
|
|
2891
|
-
const
|
|
3280
|
+
const summaries = () => modelAuth.models.value;
|
|
3281
|
+
const images = await createNodePhotonImages();
|
|
2892
3282
|
return {
|
|
2893
3283
|
...common,
|
|
2894
3284
|
models: modelRuntime,
|
|
2895
3285
|
modelSummaries: summaries,
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
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
|
+
}
|
|
2899
3298
|
};
|
|
2900
3299
|
}
|
|
2901
3300
|
const options = values.faux !== undefined ? fauxOptions(() => () => fauxAssistantMessage(values.faux)) : await piOptions();
|
|
2902
3301
|
const host = await openHost(options);
|
|
2903
|
-
const
|
|
3302
|
+
const { defaultModel: effectiveModel } = host.settings.value;
|
|
3303
|
+
const model = effectiveModel === null ? null : `${effectiveModel.provider}/${effectiveModel.modelId}`;
|
|
2904
3304
|
const web = packagedVersion === undefined ? "http://127.0.0.1:5199/" : new URL(host.url.replace(/^ws/, "http")).href;
|
|
2905
3305
|
console.log(JSON.stringify({
|
|
2906
3306
|
event: "ready",
|
|
2907
3307
|
url: host.url,
|
|
2908
|
-
tokenFile:
|
|
3308
|
+
tokenFile: join8(dataDir, "token"),
|
|
2909
3309
|
dataDir,
|
|
2910
3310
|
model,
|
|
2911
3311
|
web,
|
|
@@ -2921,6 +3321,15 @@ async function main(argv) {
|
|
|
2921
3321
|
};
|
|
2922
3322
|
process.on("SIGINT", stop);
|
|
2923
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
|
+
});
|
|
2924
3333
|
}
|
|
2925
3334
|
if (false)
|
|
2926
3335
|
;
|