pinomad 0.6.0 → 0.7.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 +2 -2
- package/dist/{chunk-3dnnaz4j.js → chunk-07769pqx.js} +4 -4
- package/dist/{chunk-d5ypmmr7.js → chunk-12kaxggg.js} +2 -2
- package/dist/{chunk-4btj08we.js → chunk-1nyzr0mg.js} +2264 -926
- package/dist/{chunk-ggb1taqy.js → chunk-1t58fh0k.js} +4 -4
- package/dist/{chunk-wqvp1fwh.js → chunk-6ysezfy5.js} +28 -11
- package/dist/{chunk-367n9je8.js → chunk-8yme2wep.js} +1 -1
- package/dist/{chunk-n4tjhcb7.js → chunk-e4ahjbmz.js} +198 -36
- package/dist/{chunk-k2s1hfq5.js → chunk-fbkd73zr.js} +4 -4
- package/dist/{chunk-7s1f4z4g.js → chunk-m4c5gvce.js} +2 -2
- package/dist/{chunk-c8nxym2z.js → chunk-rtwxjb90.js} +1 -1
- package/dist/{chunk-d93kvxvc.js → chunk-sj7rnvh4.js} +114 -5
- package/dist/{chunk-jn7y815x.js → chunk-sz3rzk55.js} +4 -4
- package/dist/pinomad.js +10 -10
- package/package.json +1 -1
- package/web/assets/index-C0rwAo0H.css +1 -0
- package/web/assets/index-DGee2kNU.js +394 -0
- package/web/index.html +2 -2
- package/web/assets/index-C_aU_ZiV.js +0 -383
- package/web/assets/index-DaPcXZPM.css +0 -1
- package/web/assets/montserrat-latin-ext-wght-normal-BsZE-iaG.woff2 +0 -0
- package/web/assets/montserrat-latin-wght-normal-l_AIctKy.woff2 +0 -0
|
@@ -35,14 +35,25 @@ import {
|
|
|
35
35
|
import {
|
|
36
36
|
packagedVersion,
|
|
37
37
|
webRoot
|
|
38
|
-
} from "./chunk-
|
|
38
|
+
} from "./chunk-rtwxjb90.js";
|
|
39
39
|
import {
|
|
40
|
+
ATTACHMENT_TTL_MS,
|
|
40
41
|
INVALID_FRAME_CLOSE_CODE,
|
|
42
|
+
MAX_ATTACHMENTS_PER_MESSAGE,
|
|
43
|
+
MAX_ATTACHMENT_NAME_LENGTH,
|
|
44
|
+
MAX_PENDING_ATTACHMENTS,
|
|
45
|
+
MAX_PENDING_ATTACHMENT_BYTES,
|
|
41
46
|
PROTOCOL,
|
|
42
47
|
UNAUTHORIZED_CLOSE_CODE,
|
|
48
|
+
attachmentBeginProblem,
|
|
49
|
+
attachmentBlock,
|
|
50
|
+
attachmentChunkProblem,
|
|
51
|
+
base64DecodedLength,
|
|
52
|
+
isAttachmentId,
|
|
43
53
|
organize,
|
|
54
|
+
parseAttachmentBlock,
|
|
44
55
|
parseClientFrame
|
|
45
|
-
} from "./chunk-
|
|
56
|
+
} from "./chunk-e4ahjbmz.js";
|
|
46
57
|
import {
|
|
47
58
|
THINKING_LEVELS,
|
|
48
59
|
isThinkingLevel
|
|
@@ -54,7 +65,7 @@ import"./chunk-df32avw6.js";
|
|
|
54
65
|
|
|
55
66
|
// apps/host/src/main.ts
|
|
56
67
|
import { homedir as homedir3 } from "node:os";
|
|
57
|
-
import { join as
|
|
68
|
+
import { join as join12, resolve as resolve7 } from "node:path";
|
|
58
69
|
import { parseArgs } from "node:util";
|
|
59
70
|
import { createModels } from "@earendil-works/pi-ai/models";
|
|
60
71
|
import { fauxAssistantMessage, fauxProvider } from "@earendil-works/pi-ai/providers/faux";
|
|
@@ -259,480 +270,237 @@ function createCodemode() {
|
|
|
259
270
|
}
|
|
260
271
|
|
|
261
272
|
// apps/host/src/extensions/context.ts
|
|
262
|
-
import { dirname
|
|
273
|
+
import { dirname as dirname2, sep as sep4 } from "node:path";
|
|
263
274
|
import { defineExtension as defineExtension2, section } from "@earendil-works/pi-durable";
|
|
264
|
-
import { formatSkillsForPrompt, loadProjectContextFiles
|
|
265
|
-
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.";
|
|
266
|
-
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.";
|
|
267
|
-
function createContext(options) {
|
|
268
|
-
const { agentsHome } = options;
|
|
269
|
-
const resolveCheckout = (input, context) => options.checkout?.(input, context);
|
|
270
|
-
return {
|
|
271
|
-
extension: defineExtension2({
|
|
272
|
-
name: "context",
|
|
273
|
-
sections: [
|
|
274
|
-
section("preamble", () => PREAMBLE, { tag: false }),
|
|
275
|
-
section("environment", async (input, context) => {
|
|
276
|
-
const cwd = input.agent.cwd;
|
|
277
|
-
if (cwd === undefined)
|
|
278
|
-
return;
|
|
279
|
-
const resolved = await resolveCheckout({ conversationId: input.conversationId, read: input.read }, context);
|
|
280
|
-
const checkout = resolved?.checkout;
|
|
281
|
-
const forkLine = resolved?.fork === true && checkout?.own !== true ? [FORK_SHARED_DIR] : [];
|
|
282
|
-
if (checkout === undefined) {
|
|
283
|
-
return [`Working directory: ${cwd}`, ...forkLine].join(`
|
|
284
|
-
`);
|
|
285
|
-
}
|
|
286
|
-
return [
|
|
287
|
-
`Working directory: ${cwd}`,
|
|
288
|
-
checkout.own ? `This directory is a git worktree PiNomad created for this conversation, on branch ${checkout.branch} starting from commit ${checkout.base.slice(0, 12)}.` : `This directory is a git worktree PiNomad created for the conversation this one belongs to, on branch ${checkout.branch} starting from commit ${checkout.base.slice(0, 12)}.`,
|
|
289
|
-
`The user's project directory is ${checkout.projectPath} and must not be modified.`,
|
|
290
|
-
"This is a fresh checkout: ignored files (dependencies such as node_modules, or .env) are absent — install dependencies per AGENTS.md when needed.",
|
|
291
|
-
...forkLine
|
|
292
|
-
].join(`
|
|
293
|
-
`);
|
|
294
|
-
}),
|
|
295
|
-
section("file_references", () => "When the user writes @./path in their message, it is a project file the user references; the path is relative to your current working directory. Its contents are not expanded for you — read the file yourself if the task needs them.", { tag: false }),
|
|
296
|
-
section("project_context", async (input, context) => {
|
|
297
|
-
const cwd = input.agent.cwd;
|
|
298
|
-
if (cwd === undefined)
|
|
299
|
-
return;
|
|
300
|
-
const checkout = (await resolveCheckout({ conversationId: input.conversationId, read: input.read }, context))?.checkout;
|
|
301
|
-
let files;
|
|
302
|
-
if (checkout === undefined) {
|
|
303
|
-
files = loadProjectContextFiles({ cwd, agentDir: agentsHome });
|
|
304
|
-
} else {
|
|
305
|
-
const seen = new Set;
|
|
306
|
-
files = [
|
|
307
|
-
...loadProjectContextFiles({ cwd: dirname(checkout.repo), agentDir: agentsHome }),
|
|
308
|
-
...loadProjectContextFiles({ cwd, agentDir: agentsHome }).filter((file) => file.path.startsWith(checkout.worktreeRoot + sep))
|
|
309
|
-
].filter((file) => {
|
|
310
|
-
if (seen.has(file.path))
|
|
311
|
-
return false;
|
|
312
|
-
seen.add(file.path);
|
|
313
|
-
return true;
|
|
314
|
-
});
|
|
315
|
-
}
|
|
316
|
-
return files.length === 0 ? undefined : files.map((file) => `## ${file.path}
|
|
275
|
+
import { formatSkillsForPrompt, loadProjectContextFiles } from "@earendil-works/pi-coding-agent";
|
|
317
276
|
|
|
318
|
-
|
|
277
|
+
// apps/host/src/skills.ts
|
|
278
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
279
|
+
import { existsSync } from "node:fs";
|
|
280
|
+
import { realpath as realpath4, stat as stat4 } from "node:fs/promises";
|
|
281
|
+
import { dirname, join as join4, relative as relative2, resolve as resolve3, sep as sep3 } from "node:path";
|
|
282
|
+
import { promisify as promisify2 } from "node:util";
|
|
283
|
+
import { AgentDoc as AgentDoc3 } from "@earendil-works/pi-durable";
|
|
284
|
+
import { loadSkills } from "@earendil-works/pi-coding-agent";
|
|
319
285
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
286
|
+
// apps/host/src/checkout.ts
|
|
287
|
+
import { execFile, spawn } from "node:child_process";
|
|
288
|
+
import { randomBytes } from "node:crypto";
|
|
289
|
+
import { realpath, rm, stat } from "node:fs/promises";
|
|
290
|
+
import { tmpdir } from "node:os";
|
|
291
|
+
import { join, relative, sep } from "node:path";
|
|
292
|
+
import { promisify } from "node:util";
|
|
293
|
+
var run = promisify(execFile);
|
|
294
|
+
var git = (dir, args, env) => run("git", ["-C", dir, ...args], env === undefined ? {} : { env: { ...process.env, ...env } });
|
|
295
|
+
function gitCapped(dir, args, maxBytes, env) {
|
|
296
|
+
return new Promise((resolve, reject) => {
|
|
297
|
+
const child = spawn("git", ["-C", dir, ...args], env === undefined ? {} : { env: { ...process.env, ...env } });
|
|
298
|
+
const chunks = [];
|
|
299
|
+
let size = 0;
|
|
300
|
+
let killed = false;
|
|
301
|
+
let stderr = "";
|
|
302
|
+
child.stdout.on("data", (chunk) => {
|
|
303
|
+
if (killed)
|
|
304
|
+
return;
|
|
305
|
+
chunks.push(chunk);
|
|
306
|
+
size += chunk.length;
|
|
307
|
+
if (size > maxBytes) {
|
|
308
|
+
killed = true;
|
|
309
|
+
child.kill();
|
|
310
|
+
child.stdout.destroy();
|
|
311
|
+
child.stderr.destroy();
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
child.stderr.on("data", (chunk) => {
|
|
315
|
+
if (stderr.length < 8192)
|
|
316
|
+
stderr += chunk.toString("utf8");
|
|
317
|
+
});
|
|
318
|
+
child.on("error", reject);
|
|
319
|
+
child.on("close", (code) => {
|
|
320
|
+
const stdout = Buffer.concat(chunks).toString("utf8");
|
|
321
|
+
if (killed)
|
|
322
|
+
resolve({ stdout, truncated: true });
|
|
323
|
+
else if (code === 0)
|
|
324
|
+
resolve({ stdout, truncated: false });
|
|
325
|
+
else
|
|
326
|
+
reject(Object.assign(new Error(`Command failed: git ${args.join(" ")}
|
|
327
|
+
${stderr}`), { code, stderr }));
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
var worktreeRoot = (dataDir) => join(dataDir, "worktrees");
|
|
332
|
+
var worktreePath = (dataDir, id) => join(worktreeRoot(dataDir), String(id));
|
|
333
|
+
var worktreeBranch = (id, suffix) => `pinomad/${id}-${suffix}`;
|
|
334
|
+
var branchSuffix = () => randomBytes(2).toString("hex");
|
|
335
|
+
var checkoutAt = (checkouts, cwd) => checkouts?.find((checkout) => cwd === checkout.path || cwd.startsWith(checkout.path + sep));
|
|
336
|
+
async function gitBase(projectPath) {
|
|
337
|
+
const top = await git(projectPath, ["rev-parse", "--show-toplevel"]).then(({ stdout }) => stdout.trim(), () => {
|
|
338
|
+
return;
|
|
339
|
+
});
|
|
340
|
+
if (top === undefined)
|
|
341
|
+
return;
|
|
342
|
+
const repo = await realpath(top).catch(() => top);
|
|
343
|
+
const base = await git(repo, ["rev-parse", "--verify", "HEAD"]).then(({ stdout }) => stdout.trim(), () => {
|
|
344
|
+
return;
|
|
345
|
+
});
|
|
346
|
+
if (base === undefined)
|
|
347
|
+
return;
|
|
348
|
+
return { repo, subdir: relative(repo, projectPath), base };
|
|
349
|
+
}
|
|
350
|
+
async function branchOf(dir) {
|
|
351
|
+
const ref = await git(dir, ["symbolic-ref", "--quiet", "HEAD"]).then(({ stdout }) => stdout.trim(), () => {
|
|
352
|
+
return;
|
|
353
|
+
});
|
|
354
|
+
const prefix = "refs/heads/";
|
|
355
|
+
if (ref === undefined || !ref.startsWith(prefix))
|
|
356
|
+
return;
|
|
357
|
+
return ref.slice(prefix.length) || undefined;
|
|
358
|
+
}
|
|
359
|
+
var repoLocks = new Map;
|
|
360
|
+
function locked(repo, work) {
|
|
361
|
+
const next = (repoLocks.get(repo) ?? Promise.resolve()).then(work, work);
|
|
362
|
+
const settled = next.then(() => {}, () => {});
|
|
363
|
+
repoLocks.set(repo, settled);
|
|
364
|
+
settled.then(() => {
|
|
365
|
+
if (repoLocks.get(repo) === settled)
|
|
366
|
+
repoLocks.delete(repo);
|
|
367
|
+
});
|
|
368
|
+
return next;
|
|
369
|
+
}
|
|
370
|
+
var worktreeExists = async (path) => stat(join(path, ".git")).then(() => true, () => false);
|
|
371
|
+
var worktreeSettled = (record) => locked(record.repo, () => worktreeExists(record.path));
|
|
372
|
+
async function ensureWorktree(record) {
|
|
373
|
+
await locked(record.repo, async () => {
|
|
374
|
+
if (await worktreeExists(record.path))
|
|
375
|
+
return;
|
|
376
|
+
await git(record.repo, ["worktree", "prune"]);
|
|
377
|
+
const branchExists = await git(record.repo, ["show-ref", "--verify", "--quiet", `refs/heads/${record.branch}`]).then(() => true, () => false);
|
|
378
|
+
if (branchExists) {
|
|
379
|
+
await git(record.repo, ["worktree", "add", record.path, record.branch]);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
await git(record.repo, ["worktree", "add", "-b", record.branch, record.path, record.base]);
|
|
383
|
+
if (record.snapshot !== undefined) {
|
|
384
|
+
await git(record.path, ["read-tree", "-u", "--reset", record.snapshot]);
|
|
385
|
+
await git(record.path, ["reset", "-q"]);
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
async function removeWorktree(record) {
|
|
390
|
+
return await locked(record.repo, async () => {
|
|
391
|
+
if (!await worktreeExists(record.path))
|
|
392
|
+
return;
|
|
393
|
+
return await git(record.repo, ["worktree", "remove", record.path]).then(() => {
|
|
394
|
+
return;
|
|
395
|
+
}, (error) => reasonOf(error));
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
function reasonOf(error) {
|
|
399
|
+
const stderr = error?.stderr;
|
|
400
|
+
const text = typeof stderr === "string" && stderr.trim() !== "" ? stderr.trim() : error instanceof Error ? error.message : String(error);
|
|
401
|
+
return text.split(`
|
|
402
|
+
`).filter((line) => line.trim() !== "").at(-1) ?? text;
|
|
403
|
+
}
|
|
404
|
+
async function changesOf(dir, base, maxBytes = 1024 * 1024) {
|
|
405
|
+
const indexFile = join(tmpdir(), `pinomad-index-${randomBytes(8).toString("hex")}`);
|
|
406
|
+
const env = { GIT_INDEX_FILE: indexFile };
|
|
407
|
+
try {
|
|
408
|
+
await git(dir, ["read-tree", "HEAD"], env);
|
|
409
|
+
await git(dir, ["add", "-A"], env);
|
|
410
|
+
const { stdout, truncated } = await gitCapped(dir, ["diff", "--cached", "--no-color", "--find-renames", "--src-prefix=a/", "--dst-prefix=b/", base], maxBytes, env);
|
|
411
|
+
if (!truncated && Buffer.byteLength(stdout) <= maxBytes)
|
|
412
|
+
return { base, patch: stdout, truncated: false };
|
|
413
|
+
const cut = Buffer.from(stdout, "utf8").subarray(0, maxBytes);
|
|
414
|
+
const patch = cut.subarray(0, cut.lastIndexOf(10) + 1).toString("utf8");
|
|
415
|
+
return { base, patch, truncated: true };
|
|
416
|
+
} finally {
|
|
417
|
+
await rm(indexFile, { force: true });
|
|
418
|
+
}
|
|
338
419
|
}
|
|
339
420
|
|
|
340
|
-
// apps/host/src/
|
|
421
|
+
// apps/host/src/files.ts
|
|
422
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
423
|
+
import { opendir, realpath as realpath3, stat as stat3 } from "node:fs/promises";
|
|
424
|
+
import { join as join3, resolve as resolve2, sep as sep2 } from "node:path";
|
|
425
|
+
import { AgentDoc as AgentDoc2 } from "@earendil-works/pi-durable";
|
|
426
|
+
|
|
427
|
+
// apps/host/src/organization.ts
|
|
428
|
+
import { mkdir, realpath as realpath2, stat as stat2 } from "node:fs/promises";
|
|
429
|
+
import { basename, join as join2, resolve } from "node:path";
|
|
341
430
|
import { clampThinkingLevel } from "@earendil-works/pi-ai";
|
|
342
431
|
import {
|
|
343
432
|
AgentDoc,
|
|
344
|
-
|
|
345
|
-
configure
|
|
346
|
-
defineExtension as defineExtension3,
|
|
347
|
-
defineTool as defineTool2,
|
|
348
|
-
section as section2
|
|
433
|
+
defineDoc as defineDoc3,
|
|
434
|
+
configure
|
|
349
435
|
} from "@earendil-works/pi-durable";
|
|
350
|
-
import { Type as Type3 } from "typebox";
|
|
351
436
|
|
|
352
|
-
// apps/host/src/
|
|
437
|
+
// apps/host/src/message-content.ts
|
|
438
|
+
function messageContent(text, images, attachmentText) {
|
|
439
|
+
if ((images === undefined || images.length === 0) && attachmentText === undefined)
|
|
440
|
+
return text;
|
|
441
|
+
return [
|
|
442
|
+
...text === "" ? [] : [{ type: "text", text }],
|
|
443
|
+
...attachmentText === undefined ? [] : [{ type: "text", text: attachmentText }],
|
|
444
|
+
...(images ?? []).map(({ data, mimeType }) => ({ type: "image", data, mimeType }))
|
|
445
|
+
];
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// apps/host/src/role-doc.ts
|
|
353
449
|
import { defineDoc as defineDoc2 } from "@earendil-works/pi-durable";
|
|
354
|
-
var
|
|
355
|
-
kind: "pinomad.
|
|
450
|
+
var RoleDoc = defineDoc2({
|
|
451
|
+
kind: "pinomad.role",
|
|
356
452
|
version: 1,
|
|
357
453
|
scope: "conversation",
|
|
358
|
-
history: "
|
|
359
|
-
fork: "
|
|
454
|
+
history: "rewindable",
|
|
455
|
+
fork: "asOf",
|
|
360
456
|
initial: () => ({})
|
|
361
457
|
});
|
|
362
458
|
|
|
363
|
-
// apps/host/src/
|
|
364
|
-
var
|
|
365
|
-
|
|
366
|
-
function createSubagent(options) {
|
|
367
|
-
const available = () => options.modelSummaries().map((model) => `${model.provider}/${model.modelId}`).join(", ");
|
|
368
|
-
const roles = options.roles ?? (() => []);
|
|
369
|
-
const roleNamed = (name) => {
|
|
370
|
-
const found = roles().find((each) => each.name === name);
|
|
371
|
-
if (found !== undefined)
|
|
372
|
-
return found;
|
|
373
|
-
const defined = roles();
|
|
374
|
-
throw new Error(defined.length === 0 ? "No named agents are defined" : `Unknown agent "${name}". Available agents: ${defined.map((each) => each.name).join(", ")}`);
|
|
375
|
-
};
|
|
376
|
-
const tool = defineTool2({
|
|
377
|
-
name: "subagent",
|
|
378
|
-
callers: ["model"],
|
|
379
|
-
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()}.`,
|
|
380
|
-
parameters: Type3.Object({
|
|
381
|
-
task: Type3.String({
|
|
382
|
-
minLength: 1,
|
|
383
|
-
description: "Complete, self-contained instructions. The subagent sees none of this conversation."
|
|
384
|
-
}),
|
|
385
|
-
role: Type3.Optional(Type3.String({
|
|
386
|
-
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."
|
|
387
|
-
})),
|
|
388
|
-
description: Type3.Optional(Type3.String({ description: 'A short 3-6 word label for the user, e.g. "Audit auth middleware".' })),
|
|
389
|
-
model: Type3.Optional(Type3.String({ description: "Optional `provider/modelId` to use instead of yours." })),
|
|
390
|
-
thinkingLevel: Type3.Optional(Type3.Union(THINKING_LEVELS2.map((level) => Type3.Literal(level)), { description: "Optional thinking level instead of yours." }))
|
|
391
|
-
}),
|
|
392
|
-
replay: "safe",
|
|
393
|
-
execute: async (args, api, context) => {
|
|
394
|
-
const child = await api.commit(async (tx) => {
|
|
395
|
-
const existing = (await tx.scanConversations({ ownerTaskId: api.taskId }, 1)).items[0];
|
|
396
|
-
if (existing !== undefined) {
|
|
397
|
-
const doc2 = await tx.doc(SubagentDoc, existing.id);
|
|
398
|
-
return {
|
|
399
|
-
id: existing.id,
|
|
400
|
-
...doc2?.model === undefined ? {} : { model: doc2.model },
|
|
401
|
-
...doc2?.startedAt === undefined ? {} : { startedAt: doc2.startedAt }
|
|
402
|
-
};
|
|
403
|
-
}
|
|
404
|
-
const role = args.role === undefined ? undefined : roleNamed(args.role);
|
|
405
|
-
if (role?.model !== undefined && args.model !== undefined) {
|
|
406
|
-
throw new Error(`Agent "${role.name}" pins model ${role.model}; omit \`model\``);
|
|
407
|
-
}
|
|
408
|
-
if (role?.thinkingLevel !== undefined && args.thinkingLevel !== undefined) {
|
|
409
|
-
throw new Error(`Agent "${role.name}" pins thinking level ${role.thinkingLevel}; omit \`thinkingLevel\``);
|
|
410
|
-
}
|
|
411
|
-
const modelString = role?.model ?? args.model;
|
|
412
|
-
const levelString = role?.thinkingLevel ?? args.thinkingLevel;
|
|
413
|
-
let model;
|
|
414
|
-
let thinkingLevel;
|
|
415
|
-
let override;
|
|
416
|
-
if (modelString !== undefined) {
|
|
417
|
-
const slash = modelString.indexOf("/");
|
|
418
|
-
const provider = slash === -1 ? "" : modelString.slice(0, slash);
|
|
419
|
-
const modelId = slash === -1 ? "" : modelString.slice(slash + 1);
|
|
420
|
-
override = provider === "" || modelId === "" ? undefined : options.models.getModel(provider, modelId);
|
|
421
|
-
const listed = options.modelSummaries().some((each) => each.provider === provider && each.modelId === modelId);
|
|
422
|
-
if (override === undefined || !listed) {
|
|
423
|
-
throw new Error(role === undefined ? `Unknown model "${modelString}". Available models: ${available()}` : `Agent "${role.name}" uses model "${modelString}", which is not available. Available models: ${available()}`);
|
|
424
|
-
}
|
|
425
|
-
model = { provider, modelId };
|
|
426
|
-
}
|
|
427
|
-
if (levelString !== undefined || override !== undefined) {
|
|
428
|
-
const parent = await tx.doc(AgentDoc, api.conversationId);
|
|
429
|
-
const effective = override ?? (parent.model === undefined ? undefined : options.models.getModel(parent.model.provider, parent.model.modelId));
|
|
430
|
-
if (levelString !== undefined) {
|
|
431
|
-
thinkingLevel = effective === undefined ? levelString : clampThinkingLevel(effective, levelString);
|
|
432
|
-
} else if (parent.thinkingLevel !== undefined) {
|
|
433
|
-
thinkingLevel = clampThinkingLevel(override, parent.thinkingLevel);
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
const change = {
|
|
437
|
-
extensions: { remove: [extension, ...options.exclude] },
|
|
438
|
-
instructions: role === undefined || role.prompt === "" ? INSTRUCTIONS : `${INSTRUCTIONS}
|
|
439
|
-
|
|
440
|
-
${role.prompt}`,
|
|
441
|
-
...model === undefined ? {} : { model },
|
|
442
|
-
...thinkingLevel === undefined ? {} : { thinkingLevel }
|
|
443
|
-
};
|
|
444
|
-
const created = await tx.createConversation({ ownership: { kind: "task", taskId: api.taskId } });
|
|
445
|
-
await configure(tx, created.id, change);
|
|
446
|
-
const childAgent = await tx.doc(AgentDoc, created.id);
|
|
447
|
-
const ref = childAgent.model ?? model ?? (await tx.doc(AgentDoc, api.conversationId)).model;
|
|
448
|
-
const startedAt = Date.now();
|
|
449
|
-
const doc = await tx.doc(SubagentDoc, created.id);
|
|
450
|
-
doc.startedAt = startedAt;
|
|
451
|
-
if (ref !== undefined)
|
|
452
|
-
doc.model = `${ref.provider}/${ref.modelId}`;
|
|
453
|
-
if (role !== undefined)
|
|
454
|
-
doc.role = role.name;
|
|
455
|
-
const label = args.description?.trim();
|
|
456
|
-
if (label !== undefined && label !== "")
|
|
457
|
-
doc.label = label;
|
|
458
|
-
return { id: created.id, ...ref === undefined ? {} : { model: doc.model }, startedAt };
|
|
459
|
-
}, context);
|
|
460
|
-
await api.details({ conversationId: child.id, ...child.model === undefined ? {} : { model: child.model }, ...child.startedAt === undefined ? {} : { startedAt: child.startedAt } }, context);
|
|
461
|
-
const handle = await api.conversation(child.id, context);
|
|
462
|
-
if (handle === undefined)
|
|
463
|
-
throw new Error(`Subagent conversation ${child.id} does not exist`);
|
|
464
|
-
const request = { type: "input", content: args.task, requestId: `subagent:${api.taskId}` };
|
|
465
|
-
const settled = await (await handle.submit(request, context)).wait(context);
|
|
466
|
-
if (settled.status !== "done" || settled.type !== "input")
|
|
467
|
-
throw new Error(`Subagent failed: ${settled.status}`);
|
|
468
|
-
const answer = await api.commit((tx) => tx.entry(AssistantEntry, settled.answer), context);
|
|
469
|
-
const message = answer?.model?.[0];
|
|
470
|
-
const text = message?.role === "assistant" ? message.content.flatMap((block) => block.type === "text" ? [block.text] : []).join("") : "";
|
|
471
|
-
const result2 = {
|
|
472
|
-
output: [{ type: "text", text: text === "" ? "(The subagent returned no text.)" : text }],
|
|
473
|
-
details: {
|
|
474
|
-
conversationId: child.id,
|
|
475
|
-
...child.model === undefined ? {} : { model: child.model },
|
|
476
|
-
...child.startedAt === undefined ? {} : { startedAt: child.startedAt }
|
|
477
|
-
}
|
|
478
|
-
};
|
|
479
|
-
return result2;
|
|
480
|
-
}
|
|
481
|
-
});
|
|
482
|
-
const extension = defineExtension3({
|
|
483
|
-
name: "subagent",
|
|
484
|
-
tools: [tool],
|
|
485
|
-
sections: [
|
|
486
|
-
section2("agent_roles", () => {
|
|
487
|
-
const defined = roles();
|
|
488
|
-
if (defined.length === 0)
|
|
489
|
-
return;
|
|
490
|
-
return [
|
|
491
|
-
"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`.",
|
|
492
|
-
...defined.map((each) => `- ${each.name}: ${each.description.replace(/\s+/g, " ")}`)
|
|
493
|
-
].join(`
|
|
494
|
-
`);
|
|
495
|
-
})
|
|
496
|
-
]
|
|
497
|
-
});
|
|
498
|
-
return { extension };
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
// apps/host/src/extensions/todo.ts
|
|
502
|
-
import { defineDoc as defineDoc3, defineExtension as defineExtension4, defineTool as defineTool3 } from "@earendil-works/pi-durable";
|
|
503
|
-
import { Type as Type4 } from "typebox";
|
|
504
|
-
var TodoDoc = defineDoc3({
|
|
505
|
-
kind: "todo.list",
|
|
459
|
+
// apps/host/src/organization.ts
|
|
460
|
+
var IndexDoc = defineDoc3({
|
|
461
|
+
kind: "pinomad.index",
|
|
506
462
|
version: 1,
|
|
507
|
-
scope: "
|
|
508
|
-
|
|
509
|
-
fork: "asOf",
|
|
510
|
-
initial: () => ({ items: [] })
|
|
511
|
-
});
|
|
512
|
-
var todoWrite = defineTool3({
|
|
513
|
-
name: "todo_write",
|
|
514
|
-
description: "Replace the todo list shown to the user with the given items.",
|
|
515
|
-
parameters: Type4.Object({ items: Type4.Array(TodoItemSchema) }),
|
|
516
|
-
replay: "safe",
|
|
517
|
-
execute: async (args, api, context) => {
|
|
518
|
-
await api.commit(async (tx) => {
|
|
519
|
-
const doc = await tx.doc(TodoDoc, api.conversationId);
|
|
520
|
-
doc.items = args.items.map((item) => ({ ...item }));
|
|
521
|
-
}, context);
|
|
522
|
-
return { output: [{ type: "text", text: `Todo list updated: ${args.items.length} item(s).` }] };
|
|
523
|
-
}
|
|
463
|
+
scope: "session",
|
|
464
|
+
initial: () => ({ projects: [], conversations: [] })
|
|
524
465
|
});
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
};
|
|
529
|
-
|
|
530
|
-
// apps/host/src/host.ts
|
|
531
|
-
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
532
|
-
import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
|
|
533
|
-
import { basename as basename2, join as join8, sep as sep5 } from "node:path";
|
|
534
|
-
import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT4 } from "@earendil-works/chord/context";
|
|
535
|
-
import { createRegistry, Harness } from "@earendil-works/pi-durable";
|
|
536
|
-
import { NodeExecutionEnv } from "@earendil-works/pi-durable/env/node";
|
|
537
|
-
|
|
538
|
-
// apps/host/src/organization.ts
|
|
539
|
-
import { mkdir, realpath as realpath2, stat as stat2 } from "node:fs/promises";
|
|
540
|
-
import { basename, join as join3, resolve } from "node:path";
|
|
541
|
-
import { clampThinkingLevel as clampThinkingLevel2 } from "@earendil-works/pi-ai";
|
|
542
|
-
import {
|
|
543
|
-
AgentDoc as AgentDoc2,
|
|
544
|
-
defineDoc as defineDoc4
|
|
545
|
-
} from "@earendil-works/pi-durable";
|
|
546
|
-
|
|
547
|
-
// apps/host/src/checkout.ts
|
|
548
|
-
import { execFile, spawn } from "node:child_process";
|
|
549
|
-
import { randomBytes } from "node:crypto";
|
|
550
|
-
import { realpath, rm, stat } from "node:fs/promises";
|
|
551
|
-
import { tmpdir } from "node:os";
|
|
552
|
-
import { join as join2, relative, sep as sep2 } from "node:path";
|
|
553
|
-
import { promisify } from "node:util";
|
|
554
|
-
var run = promisify(execFile);
|
|
555
|
-
var git = (dir, args, env) => run("git", ["-C", dir, ...args], env === undefined ? {} : { env: { ...process.env, ...env } });
|
|
556
|
-
function gitCapped(dir, args, maxBytes, env) {
|
|
557
|
-
return new Promise((resolve, reject) => {
|
|
558
|
-
const child = spawn("git", ["-C", dir, ...args], env === undefined ? {} : { env: { ...process.env, ...env } });
|
|
559
|
-
const chunks = [];
|
|
560
|
-
let size = 0;
|
|
561
|
-
let killed = false;
|
|
562
|
-
let stderr = "";
|
|
563
|
-
child.stdout.on("data", (chunk) => {
|
|
564
|
-
if (killed)
|
|
565
|
-
return;
|
|
566
|
-
chunks.push(chunk);
|
|
567
|
-
size += chunk.length;
|
|
568
|
-
if (size > maxBytes) {
|
|
569
|
-
killed = true;
|
|
570
|
-
child.kill();
|
|
571
|
-
child.stdout.destroy();
|
|
572
|
-
child.stderr.destroy();
|
|
573
|
-
}
|
|
574
|
-
});
|
|
575
|
-
child.stderr.on("data", (chunk) => {
|
|
576
|
-
if (stderr.length < 8192)
|
|
577
|
-
stderr += chunk.toString("utf8");
|
|
578
|
-
});
|
|
579
|
-
child.on("error", reject);
|
|
580
|
-
child.on("close", (code) => {
|
|
581
|
-
const stdout = Buffer.concat(chunks).toString("utf8");
|
|
582
|
-
if (killed)
|
|
583
|
-
resolve({ stdout, truncated: true });
|
|
584
|
-
else if (code === 0)
|
|
585
|
-
resolve({ stdout, truncated: false });
|
|
586
|
-
else
|
|
587
|
-
reject(Object.assign(new Error(`Command failed: git ${args.join(" ")}
|
|
588
|
-
${stderr}`), { code, stderr }));
|
|
589
|
-
});
|
|
590
|
-
});
|
|
466
|
+
async function ensureIndex(harness, context) {
|
|
467
|
+
await harness.commit(async (tx) => {
|
|
468
|
+
await tx.doc(IndexDoc);
|
|
469
|
+
}, context);
|
|
591
470
|
}
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
var worktreeBranch = (id, suffix) => `pinomad/${id}-${suffix}`;
|
|
595
|
-
var branchSuffix = () => randomBytes(2).toString("hex");
|
|
596
|
-
var checkoutAt = (checkouts, cwd) => checkouts?.find((checkout) => cwd === checkout.path || cwd.startsWith(checkout.path + sep2));
|
|
597
|
-
async function gitBase(projectPath) {
|
|
598
|
-
const top = await git(projectPath, ["rev-parse", "--show-toplevel"]).then(({ stdout }) => stdout.trim(), () => {
|
|
599
|
-
return;
|
|
600
|
-
});
|
|
601
|
-
if (top === undefined)
|
|
602
|
-
return;
|
|
603
|
-
const repo = await realpath(top).catch(() => top);
|
|
604
|
-
const base = await git(repo, ["rev-parse", "--verify", "HEAD"]).then(({ stdout }) => stdout.trim(), () => {
|
|
605
|
-
return;
|
|
606
|
-
});
|
|
607
|
-
if (base === undefined)
|
|
608
|
-
return;
|
|
609
|
-
return { repo, subdir: relative(repo, projectPath), base };
|
|
471
|
+
async function normalizePath(path) {
|
|
472
|
+
return await realpath2(resolve(path));
|
|
610
473
|
}
|
|
611
|
-
async function
|
|
612
|
-
const
|
|
613
|
-
|
|
474
|
+
async function addProject(harness, path, context) {
|
|
475
|
+
const normalized = await normalizePath(path).catch(() => {
|
|
476
|
+
throw new Error(`No such directory: ${path}`);
|
|
614
477
|
});
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
478
|
+
if (!(await stat2(normalized)).isDirectory())
|
|
479
|
+
throw new Error(`Not a directory: ${path}`);
|
|
480
|
+
return await harness.commit(async (tx) => {
|
|
481
|
+
const doc = await tx.doc(IndexDoc);
|
|
482
|
+
const existing = doc.projects.find((project2) => project2.path === normalized);
|
|
483
|
+
if (existing !== undefined)
|
|
484
|
+
return { ...existing };
|
|
485
|
+
const project = { path: normalized, name: basename(normalized), addedAt: Date.now() };
|
|
486
|
+
doc.projects.push(project);
|
|
487
|
+
return project;
|
|
488
|
+
}, context);
|
|
619
489
|
}
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
if (repoLocks.get(repo) === settled)
|
|
627
|
-
repoLocks.delete(repo);
|
|
628
|
-
});
|
|
629
|
-
return next;
|
|
490
|
+
async function removeProject(harness, path, context) {
|
|
491
|
+
const normalized = await normalizePath(path).catch(() => resolve(path));
|
|
492
|
+
await harness.commit(async (tx) => {
|
|
493
|
+
const doc = await tx.doc(IndexDoc);
|
|
494
|
+
doc.projects = doc.projects.filter((project) => project.path !== normalized);
|
|
495
|
+
}, context);
|
|
630
496
|
}
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
if (branchExists) {
|
|
639
|
-
await git(record.repo, ["worktree", "add", record.path, record.branch]);
|
|
640
|
-
return;
|
|
641
|
-
}
|
|
642
|
-
await git(record.repo, ["worktree", "add", "-b", record.branch, record.path, record.base]);
|
|
643
|
-
if (record.snapshot !== undefined) {
|
|
644
|
-
await git(record.path, ["read-tree", "-u", "--reset", record.snapshot]);
|
|
645
|
-
await git(record.path, ["reset", "-q"]);
|
|
646
|
-
}
|
|
647
|
-
});
|
|
648
|
-
}
|
|
649
|
-
async function removeWorktree(record) {
|
|
650
|
-
return await locked(record.repo, async () => {
|
|
651
|
-
if (!await worktreeExists(record.path))
|
|
652
|
-
return;
|
|
653
|
-
return await git(record.repo, ["worktree", "remove", record.path]).then(() => {
|
|
654
|
-
return;
|
|
655
|
-
}, (error) => reasonOf(error));
|
|
656
|
-
});
|
|
657
|
-
}
|
|
658
|
-
function reasonOf(error) {
|
|
659
|
-
const stderr = error?.stderr;
|
|
660
|
-
const text = typeof stderr === "string" && stderr.trim() !== "" ? stderr.trim() : error instanceof Error ? error.message : String(error);
|
|
661
|
-
return text.split(`
|
|
662
|
-
`).filter((line) => line.trim() !== "").at(-1) ?? text;
|
|
663
|
-
}
|
|
664
|
-
async function changesOf(dir, base, maxBytes = 1024 * 1024) {
|
|
665
|
-
const indexFile = join2(tmpdir(), `pinomad-index-${randomBytes(8).toString("hex")}`);
|
|
666
|
-
const env = { GIT_INDEX_FILE: indexFile };
|
|
667
|
-
try {
|
|
668
|
-
await git(dir, ["read-tree", "HEAD"], env);
|
|
669
|
-
await git(dir, ["add", "-A"], env);
|
|
670
|
-
const { stdout, truncated } = await gitCapped(dir, ["diff", "--cached", "--no-color", "--find-renames", "--src-prefix=a/", "--dst-prefix=b/", base], maxBytes, env);
|
|
671
|
-
if (!truncated && Buffer.byteLength(stdout) <= maxBytes)
|
|
672
|
-
return { base, patch: stdout, truncated: false };
|
|
673
|
-
const cut = Buffer.from(stdout, "utf8").subarray(0, maxBytes);
|
|
674
|
-
const patch = cut.subarray(0, cut.lastIndexOf(10) + 1).toString("utf8");
|
|
675
|
-
return { base, patch, truncated: true };
|
|
676
|
-
} finally {
|
|
677
|
-
await rm(indexFile, { force: true });
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
// apps/host/src/message-content.ts
|
|
682
|
-
function messageContent(text, images) {
|
|
683
|
-
if (images === undefined || images.length === 0)
|
|
684
|
-
return text;
|
|
685
|
-
return [
|
|
686
|
-
...text === "" ? [] : [{ type: "text", text }],
|
|
687
|
-
...images.map(({ data, mimeType }) => ({ type: "image", data, mimeType }))
|
|
688
|
-
];
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
// apps/host/src/organization.ts
|
|
692
|
-
var IndexDoc = defineDoc4({
|
|
693
|
-
kind: "pinomad.index",
|
|
694
|
-
version: 1,
|
|
695
|
-
scope: "session",
|
|
696
|
-
initial: () => ({ projects: [], conversations: [] })
|
|
697
|
-
});
|
|
698
|
-
async function ensureIndex(harness, context) {
|
|
699
|
-
await harness.commit(async (tx) => {
|
|
700
|
-
await tx.doc(IndexDoc);
|
|
701
|
-
}, context);
|
|
702
|
-
}
|
|
703
|
-
async function normalizePath(path) {
|
|
704
|
-
return await realpath2(resolve(path));
|
|
705
|
-
}
|
|
706
|
-
async function addProject(harness, path, context) {
|
|
707
|
-
const normalized = await normalizePath(path).catch(() => {
|
|
708
|
-
throw new Error(`No such directory: ${path}`);
|
|
709
|
-
});
|
|
710
|
-
if (!(await stat2(normalized)).isDirectory())
|
|
711
|
-
throw new Error(`Not a directory: ${path}`);
|
|
712
|
-
return await harness.commit(async (tx) => {
|
|
713
|
-
const doc = await tx.doc(IndexDoc);
|
|
714
|
-
const existing = doc.projects.find((project2) => project2.path === normalized);
|
|
715
|
-
if (existing !== undefined)
|
|
716
|
-
return { ...existing };
|
|
717
|
-
const project = { path: normalized, name: basename(normalized), addedAt: Date.now() };
|
|
718
|
-
doc.projects.push(project);
|
|
719
|
-
return project;
|
|
720
|
-
}, context);
|
|
721
|
-
}
|
|
722
|
-
async function removeProject(harness, path, context) {
|
|
723
|
-
const normalized = await normalizePath(path).catch(() => resolve(path));
|
|
724
|
-
await harness.commit(async (tx) => {
|
|
725
|
-
const doc = await tx.doc(IndexDoc);
|
|
726
|
-
doc.projects = doc.projects.filter((project) => project.path !== normalized);
|
|
727
|
-
}, context);
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
class DuplicateRequest extends Error {
|
|
731
|
-
conversationId;
|
|
732
|
-
constructor(conversationId) {
|
|
733
|
-
super("conversation already created");
|
|
734
|
-
this.conversationId = conversationId;
|
|
735
|
-
}
|
|
497
|
+
|
|
498
|
+
class DuplicateRequest extends Error {
|
|
499
|
+
conversationId;
|
|
500
|
+
constructor(conversationId) {
|
|
501
|
+
super("conversation already created");
|
|
502
|
+
this.conversationId = conversationId;
|
|
503
|
+
}
|
|
736
504
|
}
|
|
737
505
|
function requireRole(agents, name) {
|
|
738
506
|
const found = agents.roles.find((role) => role.name === name);
|
|
@@ -756,7 +524,7 @@ async function createConversation(harness, dataDir, input, defaults, models, mod
|
|
|
756
524
|
if (prior !== undefined) {
|
|
757
525
|
const existing = await harness.conversation(prior.id, context);
|
|
758
526
|
if (existing !== undefined) {
|
|
759
|
-
await existing.submit({ type: "input", content: messageContent(input.text, input.images), whenBusy: "followUp", requestId: input.requestId }, context);
|
|
527
|
+
await existing.submit({ type: "input", content: messageContent(input.text, input.images, input.attachmentText), whenBusy: "followUp", requestId: input.requestId }, context);
|
|
760
528
|
}
|
|
761
529
|
return prior.id;
|
|
762
530
|
}
|
|
@@ -777,7 +545,7 @@ async function createConversation(harness, dataDir, input, defaults, models, mod
|
|
|
777
545
|
throw new Error(`Model ${modelRef.provider}/${modelRef.modelId} is not available${suffix2}`);
|
|
778
546
|
}
|
|
779
547
|
}
|
|
780
|
-
const thinkingLevel = model === undefined ? input.thinkingLevel ?? role?.thinkingLevel ?? defaults.thinkingLevel :
|
|
548
|
+
const thinkingLevel = model === undefined ? input.thinkingLevel ?? role?.thinkingLevel ?? defaults.thinkingLevel : clampThinkingLevel(model, input.thinkingLevel ?? role?.thinkingLevel ?? defaults.thinkingLevel ?? "off");
|
|
781
549
|
const home = input.home;
|
|
782
550
|
const projectPath = home.kind === "project" ? await normalizePath(home.path).catch(() => home.path) : undefined;
|
|
783
551
|
const repo = home.kind === "project" && input.checkout !== "project" && projectPath !== undefined ? await gitBase(projectPath) : undefined;
|
|
@@ -807,6 +575,7 @@ async function createConversation(harness, dataDir, input, defaults, models, mod
|
|
|
807
575
|
requestId: input.requestId,
|
|
808
576
|
...role === undefined ? {} : { role: role.name }
|
|
809
577
|
});
|
|
578
|
+
(await tx.doc(RoleDoc, id)).name = role?.name ?? null;
|
|
810
579
|
if (repo !== undefined && suffix !== undefined) {
|
|
811
580
|
const path = worktreePath(dataDir, id);
|
|
812
581
|
if (doc.checkouts === undefined)
|
|
@@ -819,26 +588,59 @@ async function createConversation(harness, dataDir, input, defaults, models, mod
|
|
|
819
588
|
branch: worktreeBranch(id, suffix),
|
|
820
589
|
base: repo.base
|
|
821
590
|
});
|
|
822
|
-
(await tx.doc(
|
|
591
|
+
(await tx.doc(AgentDoc, id)).cwd = join2(path, repo.subdir);
|
|
823
592
|
} else if (home.kind === "chat") {
|
|
824
|
-
(await tx.doc(
|
|
593
|
+
(await tx.doc(AgentDoc, id)).cwd = join2(dataDir, "chats", String(id));
|
|
825
594
|
}
|
|
826
595
|
}
|
|
827
596
|
}, context);
|
|
828
597
|
if (home.kind === "chat")
|
|
829
|
-
await mkdir(
|
|
830
|
-
await conversation.submit({ type: "input", content: messageContent(input.text, input.images), whenBusy: "followUp", requestId: input.requestId }, context);
|
|
598
|
+
await mkdir(join2(dataDir, "chats", String(conversation.id)), { recursive: true });
|
|
599
|
+
await conversation.submit({ type: "input", content: messageContent(input.text, input.images, input.attachmentText), whenBusy: "followUp", requestId: input.requestId }, context);
|
|
831
600
|
return conversation.id;
|
|
832
601
|
} catch (error) {
|
|
833
602
|
if (!(error instanceof DuplicateRequest))
|
|
834
603
|
throw error;
|
|
835
604
|
const existing = await harness.conversation(error.conversationId, context);
|
|
836
605
|
if (existing !== undefined) {
|
|
837
|
-
await existing.submit({ type: "input", content: messageContent(input.text, input.images), whenBusy: "followUp", requestId: input.requestId }, context);
|
|
606
|
+
await existing.submit({ type: "input", content: messageContent(input.text, input.images, input.attachmentText), whenBusy: "followUp", requestId: input.requestId }, context);
|
|
838
607
|
}
|
|
839
608
|
return error.conversationId;
|
|
840
609
|
}
|
|
841
610
|
}
|
|
611
|
+
async function setAgent(harness, input, models, modelSummaries, agents, context) {
|
|
612
|
+
await harness.commit(async (tx) => {
|
|
613
|
+
const record = await tx.conversation(input.conversationId);
|
|
614
|
+
if (record === undefined)
|
|
615
|
+
throw new Error(`Conversation ${input.conversationId} does not exist`);
|
|
616
|
+
if (record.owner !== undefined)
|
|
617
|
+
throw new Error("Subagent conversations cannot switch agent");
|
|
618
|
+
const role = input.agent === null ? undefined : requireRole(agents, input.agent);
|
|
619
|
+
const state = await tx.doc(AgentDoc, input.conversationId);
|
|
620
|
+
let change;
|
|
621
|
+
if (role === undefined) {
|
|
622
|
+
change = { instructions: null };
|
|
623
|
+
} else {
|
|
624
|
+
const modelRef = roleModelRef(role) ?? state.model;
|
|
625
|
+
const listed = modelSummaries().map((model2) => `${model2.provider}/${model2.modelId}`).join(", ");
|
|
626
|
+
const suffix = listed === "" ? "" : `. Available models: ${listed}`;
|
|
627
|
+
if (modelRef === undefined) {
|
|
628
|
+
throw new Error(`Agent "${role.name}" has no model: the role pins none and the conversation has none${suffix}`);
|
|
629
|
+
}
|
|
630
|
+
const model = models.getModel(modelRef.provider, modelRef.modelId);
|
|
631
|
+
if (model === undefined || !modelSummaries().some((each) => each.provider === modelRef.provider && each.modelId === modelRef.modelId)) {
|
|
632
|
+
throw new Error(`Model ${modelRef.provider}/${modelRef.modelId} is not available${suffix}`);
|
|
633
|
+
}
|
|
634
|
+
change = {
|
|
635
|
+
model: modelRef,
|
|
636
|
+
thinkingLevel: clampThinkingLevel(model, role.thinkingLevel ?? state.thinkingLevel ?? "off"),
|
|
637
|
+
instructions: role.prompt === "" ? null : role.prompt
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
await configure(tx, input.conversationId, change);
|
|
641
|
+
(await tx.doc(RoleDoc, input.conversationId)).name = role?.name ?? null;
|
|
642
|
+
}, context);
|
|
643
|
+
}
|
|
842
644
|
async function archive(harness, id, archived, context) {
|
|
843
645
|
await harness.commit(async (tx) => {
|
|
844
646
|
const doc = await tx.doc(IndexDoc);
|
|
@@ -863,7 +665,7 @@ async function forkFlagOf(harness, id, context) {
|
|
|
863
665
|
return record.parent !== undefined;
|
|
864
666
|
}
|
|
865
667
|
async function checkoutInfo(harness, input, context) {
|
|
866
|
-
const agent = await input.read.snapshot(
|
|
668
|
+
const agent = await input.read.snapshot(AgentDoc, input.conversationId, context);
|
|
867
669
|
const cwd = agent?.cwd;
|
|
868
670
|
if (cwd === undefined)
|
|
869
671
|
return;
|
|
@@ -878,7 +680,7 @@ async function checkoutInfo(harness, input, context) {
|
|
|
878
680
|
checkout: {
|
|
879
681
|
branch: checkout.branch,
|
|
880
682
|
base: checkout.base,
|
|
881
|
-
projectPath:
|
|
683
|
+
projectPath: join2(checkout.repo, checkout.subdir),
|
|
882
684
|
repo: checkout.repo,
|
|
883
685
|
worktreeRoot: checkout.path,
|
|
884
686
|
own: checkout.conversationId === input.conversationId
|
|
@@ -916,20 +718,724 @@ async function cleanupArchivedWorktrees(harness, rootId, context) {
|
|
|
916
718
|
queue.push(child);
|
|
917
719
|
}
|
|
918
720
|
}
|
|
919
|
-
}
|
|
920
|
-
const kept = [];
|
|
921
|
-
for (const record of checkouts.filter((checkout) => subtree.has(checkout.conversationId))) {
|
|
922
|
-
const reason = await removeWorktree(record);
|
|
923
|
-
if (reason !== undefined)
|
|
924
|
-
kept.push({ record, reason });
|
|
925
|
-
}
|
|
926
|
-
return kept;
|
|
721
|
+
}
|
|
722
|
+
const kept = [];
|
|
723
|
+
for (const record of checkouts.filter((checkout) => subtree.has(checkout.conversationId))) {
|
|
724
|
+
const reason = await removeWorktree(record);
|
|
725
|
+
if (reason !== undefined)
|
|
726
|
+
kept.push({ record, reason });
|
|
727
|
+
}
|
|
728
|
+
return kept;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// apps/host/src/files.ts
|
|
732
|
+
var CACHE_TTL_MS = 3000;
|
|
733
|
+
var MAX_CACHED_ROOTS = 64;
|
|
734
|
+
var MAX_CONCURRENT_SCANS = 4;
|
|
735
|
+
var SCAN_BUDGET_MS = 5000;
|
|
736
|
+
var MAX_OUTPUT_BYTES = 4 * 1024 * 1024;
|
|
737
|
+
var MAX_WALK_ENTRIES = 20000;
|
|
738
|
+
var MAX_RESULTS = 50;
|
|
739
|
+
var UNUSABLE_PATH = /[\s`\x00-\x1f\x7f-\x9f]/;
|
|
740
|
+
function createCandidateLister(enumerate, options = {}) {
|
|
741
|
+
const now = options.now ?? Date.now;
|
|
742
|
+
const ttlMs = options.ttlMs ?? CACHE_TTL_MS;
|
|
743
|
+
const maxConcurrent = options.maxConcurrent ?? MAX_CONCURRENT_SCANS;
|
|
744
|
+
const maxCached = options.maxCached ?? MAX_CACHED_ROOTS;
|
|
745
|
+
const cache = new Map;
|
|
746
|
+
const inflight = new Map;
|
|
747
|
+
let running = 0;
|
|
748
|
+
const waiting = [];
|
|
749
|
+
return (root, mode) => {
|
|
750
|
+
const key = `${mode}${root}`;
|
|
751
|
+
const hit = cache.get(key);
|
|
752
|
+
if (hit !== undefined) {
|
|
753
|
+
if (hit.expiresAt > now())
|
|
754
|
+
return Promise.resolve(hit.value);
|
|
755
|
+
cache.delete(key);
|
|
756
|
+
}
|
|
757
|
+
const pending = inflight.get(key);
|
|
758
|
+
if (pending !== undefined)
|
|
759
|
+
return pending;
|
|
760
|
+
const scan = (async () => {
|
|
761
|
+
if (running >= maxConcurrent)
|
|
762
|
+
await new Promise((release) => waiting.push(release));
|
|
763
|
+
running += 1;
|
|
764
|
+
try {
|
|
765
|
+
return await enumerate(root, mode);
|
|
766
|
+
} finally {
|
|
767
|
+
running -= 1;
|
|
768
|
+
waiting.shift()?.();
|
|
769
|
+
}
|
|
770
|
+
})();
|
|
771
|
+
inflight.set(key, scan);
|
|
772
|
+
scan.then((value) => {
|
|
773
|
+
const at = now();
|
|
774
|
+
for (const [cached, entry] of cache) {
|
|
775
|
+
if (entry.expiresAt <= at)
|
|
776
|
+
cache.delete(cached);
|
|
777
|
+
}
|
|
778
|
+
while (cache.size >= maxCached)
|
|
779
|
+
cache.delete(cache.keys().next().value);
|
|
780
|
+
cache.set(key, { value, expiresAt: at + ttlMs });
|
|
781
|
+
}, () => {});
|
|
782
|
+
return scan.finally(() => inflight.delete(key));
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
function gitLines(cwd, args, limits) {
|
|
786
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
787
|
+
const child = spawn2("git", ["-C", cwd, ...args], { stdio: ["ignore", "pipe", "pipe"] });
|
|
788
|
+
const chunks = [];
|
|
789
|
+
let bytes = 0;
|
|
790
|
+
let truncated = false;
|
|
791
|
+
let settled = false;
|
|
792
|
+
const kill = (markTruncated) => {
|
|
793
|
+
truncated ||= markTruncated;
|
|
794
|
+
child.kill("SIGKILL");
|
|
795
|
+
};
|
|
796
|
+
const timer = setTimeout(() => kill(true), Math.max(0, limits.deadline - limits.now()));
|
|
797
|
+
const finish = (error) => {
|
|
798
|
+
if (settled)
|
|
799
|
+
return;
|
|
800
|
+
settled = true;
|
|
801
|
+
clearTimeout(timer);
|
|
802
|
+
if (error !== undefined) {
|
|
803
|
+
rejectPromise(error);
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
const output = Buffer.concat(chunks).toString("utf8");
|
|
807
|
+
const parts = output.split("\x00");
|
|
808
|
+
if (truncated)
|
|
809
|
+
parts.pop();
|
|
810
|
+
resolvePromise({ lines: parts.filter((part) => part !== ""), truncated });
|
|
811
|
+
};
|
|
812
|
+
child.stdout.on("data", (chunk) => {
|
|
813
|
+
bytes += chunk.length;
|
|
814
|
+
if (bytes > limits.maxBytes) {
|
|
815
|
+
kill(true);
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
chunks.push(chunk);
|
|
819
|
+
if (limits.now() > limits.deadline)
|
|
820
|
+
kill(true);
|
|
821
|
+
});
|
|
822
|
+
child.on("error", (error) => finish(error));
|
|
823
|
+
child.stderr.resume();
|
|
824
|
+
child.on("close", (code) => {
|
|
825
|
+
if (code === 0 || truncated) {
|
|
826
|
+
finish(undefined);
|
|
827
|
+
} else {
|
|
828
|
+
finish(new Error(`git ${args.join(" ")} exited with code ${code}`));
|
|
829
|
+
}
|
|
830
|
+
});
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
async function inWorkTree(root) {
|
|
834
|
+
return await new Promise((resolvePromise) => {
|
|
835
|
+
const child = spawn2("git", ["-C", root, "rev-parse", "--is-inside-work-tree"], { stdio: ["ignore", "pipe", "ignore"] });
|
|
836
|
+
let out = "";
|
|
837
|
+
child.stdout.on("data", (chunk) => out += chunk.toString("utf8"));
|
|
838
|
+
child.on("error", () => resolvePromise(false));
|
|
839
|
+
child.on("close", (code) => resolvePromise(code === 0 && out.trim() === "true"));
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
async function hasHead(root) {
|
|
843
|
+
return await new Promise((resolvePromise) => {
|
|
844
|
+
const child = spawn2("git", ["-C", root, "rev-parse", "--verify", "-q", "HEAD"], { stdio: ["ignore", "ignore", "ignore"] });
|
|
845
|
+
child.on("error", () => resolvePromise(false));
|
|
846
|
+
child.on("close", (code) => resolvePromise(code === 0));
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
var STAGED_RECORD = /^([0-7]{6}) [0-9a-f]{40,64} [0-7]\t/;
|
|
850
|
+
async function gitWorkingFiles(root, limits) {
|
|
851
|
+
const listing = await gitLines(root, ["ls-files", "-z", "-s", "--cached", "--others", "--exclude-standard"], limits);
|
|
852
|
+
const deleted = await gitLines(root, ["ls-files", "-z", "--deleted"], limits);
|
|
853
|
+
const gone = new Set(deleted.lines);
|
|
854
|
+
const files = new Set;
|
|
855
|
+
for (const line of listing.lines) {
|
|
856
|
+
const staged = STAGED_RECORD.exec(line);
|
|
857
|
+
const path = staged === null ? line : line.slice(staged[0].length);
|
|
858
|
+
if (staged !== null && staged[1] === "160000")
|
|
859
|
+
continue;
|
|
860
|
+
if (path === "" || path.endsWith("/") || gone.has(path))
|
|
861
|
+
continue;
|
|
862
|
+
files.add(path);
|
|
863
|
+
}
|
|
864
|
+
return { files: [...files], truncated: listing.truncated || deleted.truncated };
|
|
865
|
+
}
|
|
866
|
+
async function gitHeadFiles(root, limits) {
|
|
867
|
+
const listing = await gitLines(root, ["ls-tree", "-r", "-z", "HEAD"], limits);
|
|
868
|
+
const files = [];
|
|
869
|
+
for (const line of listing.lines) {
|
|
870
|
+
const tab = line.indexOf("\t");
|
|
871
|
+
if (tab === -1)
|
|
872
|
+
continue;
|
|
873
|
+
const kind = line.slice(0, tab).split(" ")[1];
|
|
874
|
+
if (kind === "blob")
|
|
875
|
+
files.push(line.slice(tab + 1));
|
|
876
|
+
}
|
|
877
|
+
return { files, truncated: listing.truncated };
|
|
878
|
+
}
|
|
879
|
+
async function walkFiles(root, limits) {
|
|
880
|
+
const files = [];
|
|
881
|
+
let entries = 0;
|
|
882
|
+
let truncated = false;
|
|
883
|
+
const dirs = [{ dir: root, prefix: "" }];
|
|
884
|
+
while (dirs.length > 0) {
|
|
885
|
+
if (limits.now() > limits.deadline) {
|
|
886
|
+
truncated = true;
|
|
887
|
+
break;
|
|
888
|
+
}
|
|
889
|
+
const { dir, prefix } = dirs.pop();
|
|
890
|
+
let handle;
|
|
891
|
+
try {
|
|
892
|
+
handle = await opendir(dir);
|
|
893
|
+
} catch (error) {
|
|
894
|
+
if (prefix === "")
|
|
895
|
+
throw error;
|
|
896
|
+
truncated = true;
|
|
897
|
+
continue;
|
|
898
|
+
}
|
|
899
|
+
for await (const entry of handle) {
|
|
900
|
+
if (entries++ >= limits.maxEntries || limits.now() > limits.deadline) {
|
|
901
|
+
truncated = true;
|
|
902
|
+
break;
|
|
903
|
+
}
|
|
904
|
+
if (entry.isDirectory()) {
|
|
905
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules")
|
|
906
|
+
continue;
|
|
907
|
+
dirs.push({ dir: join3(dir, entry.name), prefix: `${prefix}${entry.name}/` });
|
|
908
|
+
} else if (entry.isFile() || entry.isSymbolicLink()) {
|
|
909
|
+
files.push(`${prefix}${entry.name}`);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
if (truncated)
|
|
913
|
+
break;
|
|
914
|
+
}
|
|
915
|
+
return { files, truncated };
|
|
916
|
+
}
|
|
917
|
+
async function enumerateRoot(root, mode, limits = {}) {
|
|
918
|
+
const now = limits.now ?? Date.now;
|
|
919
|
+
const bounded = {
|
|
920
|
+
now,
|
|
921
|
+
deadline: now() + (limits.budgetMs ?? SCAN_BUDGET_MS),
|
|
922
|
+
maxBytes: limits.maxBytes ?? MAX_OUTPUT_BYTES,
|
|
923
|
+
maxEntries: limits.maxEntries ?? MAX_WALK_ENTRIES
|
|
924
|
+
};
|
|
925
|
+
if (!await inWorkTree(root)) {
|
|
926
|
+
return walkFiles(root, bounded);
|
|
927
|
+
}
|
|
928
|
+
if (mode === "head" && !await hasHead(root)) {
|
|
929
|
+
return walkFiles(root, bounded);
|
|
930
|
+
}
|
|
931
|
+
return mode === "head" ? gitHeadFiles(root, bounded) : gitWorkingFiles(root, bounded);
|
|
932
|
+
}
|
|
933
|
+
function matchFiles(candidates, query, max = MAX_RESULTS) {
|
|
934
|
+
const usable = candidates.filter((path) => !UNUSABLE_PATH.test(path));
|
|
935
|
+
if (query === "") {
|
|
936
|
+
const sorted = [...usable].sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
937
|
+
return { files: sorted.slice(0, max), truncated: sorted.length > max };
|
|
938
|
+
}
|
|
939
|
+
const needle = query.toLowerCase();
|
|
940
|
+
const tiers = [[], [], []];
|
|
941
|
+
for (const path of usable) {
|
|
942
|
+
const base = path.slice(path.lastIndexOf("/") + 1).toLowerCase();
|
|
943
|
+
const lower = path.toLowerCase();
|
|
944
|
+
if (base.startsWith(needle))
|
|
945
|
+
tiers[0].push(path);
|
|
946
|
+
else if (base.includes(needle))
|
|
947
|
+
tiers[1].push(path);
|
|
948
|
+
else if (lower.includes(needle))
|
|
949
|
+
tiers[2].push(path);
|
|
950
|
+
}
|
|
951
|
+
const shorter = (a, b) => a.length - b.length || (a < b ? -1 : a > b ? 1 : 0);
|
|
952
|
+
const matched = [...tiers[0].sort(shorter), ...tiers[1].sort(shorter), ...tiers[2].sort(shorter)];
|
|
953
|
+
return { files: matched.slice(0, max), truncated: matched.length > max };
|
|
954
|
+
}
|
|
955
|
+
async function listFiles(harness, dataDir, enumerate, scope, query, context) {
|
|
956
|
+
let root;
|
|
957
|
+
let mode = "live";
|
|
958
|
+
if (scope.kind === "conversation") {
|
|
959
|
+
const agent = await harness.snapshot(AgentDoc2, scope.conversationId, context);
|
|
960
|
+
if (agent === undefined)
|
|
961
|
+
throw new Error(`Conversation ${scope.conversationId} does not exist`);
|
|
962
|
+
const cwd = agent.cwd;
|
|
963
|
+
if (cwd === undefined)
|
|
964
|
+
throw new Error(`Conversation ${scope.conversationId} has no checkout`);
|
|
965
|
+
if (cwd.startsWith(join3(dataDir, "chats") + sep2))
|
|
966
|
+
return { files: [], truncated: false };
|
|
967
|
+
const record = checkoutAt((await harness.snapshot(IndexDoc, context))?.checkouts, cwd);
|
|
968
|
+
const ready = record === undefined || await worktreeSettled(record);
|
|
969
|
+
const info = ready ? await stat3(cwd).catch(() => {
|
|
970
|
+
return;
|
|
971
|
+
}) : undefined;
|
|
972
|
+
if (info === undefined || !info.isDirectory()) {
|
|
973
|
+
throw new Error(`Conversation ${scope.conversationId}'s checkout is unavailable: ${cwd}`);
|
|
974
|
+
}
|
|
975
|
+
root = cwd;
|
|
976
|
+
} else {
|
|
977
|
+
const normalized = await realpath3(resolve2(scope.path)).catch(() => resolve2(scope.path));
|
|
978
|
+
const index = await harness.snapshot(IndexDoc, context);
|
|
979
|
+
if (!index?.projects.some((project) => project.path === normalized)) {
|
|
980
|
+
throw new Error(`Unknown project: ${scope.path}`);
|
|
981
|
+
}
|
|
982
|
+
const info = await stat3(normalized).catch(() => {
|
|
983
|
+
return;
|
|
984
|
+
});
|
|
985
|
+
if (info === undefined || !info.isDirectory()) {
|
|
986
|
+
throw new Error(`Not a directory: ${normalized}`);
|
|
987
|
+
}
|
|
988
|
+
root = normalized;
|
|
989
|
+
mode = scope.checkout === "worktree" ? "head" : "live";
|
|
990
|
+
}
|
|
991
|
+
const candidates = await enumerate(root, mode);
|
|
992
|
+
const matched = matchFiles(candidates.files, query);
|
|
993
|
+
return { files: matched.files, truncated: matched.truncated || candidates.truncated };
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// apps/host/src/skills.ts
|
|
997
|
+
var exec = promisify2(execFile2);
|
|
998
|
+
function invocableSkillName(name) {
|
|
999
|
+
return name.length <= 64 && /^[a-z0-9-]+$/.test(name) && !name.startsWith("-") && !name.endsWith("-") && !name.includes("--");
|
|
1000
|
+
}
|
|
1001
|
+
var reportedDiagnostics = new Set;
|
|
1002
|
+
function reportDiagnostics(diagnostics) {
|
|
1003
|
+
for (const diagnostic of diagnostics) {
|
|
1004
|
+
const key = `${diagnostic.path ?? ""}\x00${diagnostic.message}`;
|
|
1005
|
+
if (reportedDiagnostics.has(key))
|
|
1006
|
+
continue;
|
|
1007
|
+
reportedDiagnostics.add(key);
|
|
1008
|
+
console.error(JSON.stringify({
|
|
1009
|
+
event: "skill",
|
|
1010
|
+
level: diagnostic.type === "error" ? "error" : "warning",
|
|
1011
|
+
...diagnostic.path === undefined ? {} : { path: diagnostic.path },
|
|
1012
|
+
message: diagnostic.message
|
|
1013
|
+
}));
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
function entrySkills(dir, source, agentsHome) {
|
|
1017
|
+
if (!existsSync(dir))
|
|
1018
|
+
return [];
|
|
1019
|
+
const { skills, diagnostics } = loadSkills({ cwd: dir, agentDir: agentsHome, includeDefaults: false, skillPaths: [dir] });
|
|
1020
|
+
reportDiagnostics(diagnostics);
|
|
1021
|
+
return skills.map((skill) => ({ skill, source }));
|
|
1022
|
+
}
|
|
1023
|
+
function projectSkillLayers(root) {
|
|
1024
|
+
const start = resolve3(root);
|
|
1025
|
+
const dirs = [start];
|
|
1026
|
+
let dir = start;
|
|
1027
|
+
for (;; ) {
|
|
1028
|
+
if (existsSync(join4(dir, ".git")))
|
|
1029
|
+
return { dirs, gitRoot: dir };
|
|
1030
|
+
const parent = dirname(dir);
|
|
1031
|
+
if (parent === dir)
|
|
1032
|
+
return { dirs: [start] };
|
|
1033
|
+
dir = parent;
|
|
1034
|
+
dirs.push(dir);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
function projectSkills(dirs, agentsHome) {
|
|
1038
|
+
return dirs.flatMap((dir) => entrySkills(join4(dir, ".agents", "skills"), "project", agentsHome));
|
|
1039
|
+
}
|
|
1040
|
+
function mergeSkills(entries) {
|
|
1041
|
+
const seen = new Set;
|
|
1042
|
+
const merged = [];
|
|
1043
|
+
for (const entry of entries) {
|
|
1044
|
+
for (const scoped of entry) {
|
|
1045
|
+
if (seen.has(scoped.skill.name))
|
|
1046
|
+
continue;
|
|
1047
|
+
seen.add(scoped.skill.name);
|
|
1048
|
+
merged.push(scoped);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
return merged;
|
|
1052
|
+
}
|
|
1053
|
+
function scopedSkills(root, agentsHome) {
|
|
1054
|
+
const project = root === undefined ? [] : projectSkills(projectSkillLayers(root).dirs, agentsHome);
|
|
1055
|
+
return mergeSkills([project, entrySkills(join4(agentsHome, "skills"), "user", agentsHome)]);
|
|
1056
|
+
}
|
|
1057
|
+
async function committedSkillFiles(gitRoot, layers) {
|
|
1058
|
+
if (!await inWorkTree(gitRoot) || !await hasHead(gitRoot))
|
|
1059
|
+
return null;
|
|
1060
|
+
const pathspecs = layers.map((dir) => {
|
|
1061
|
+
const rel = relative2(gitRoot, dir).split(sep3).join("/");
|
|
1062
|
+
return rel === "" ? ".agents/skills" : `${rel}/.agents/skills`;
|
|
1063
|
+
});
|
|
1064
|
+
const { stdout } = await exec("git", ["-C", gitRoot, "ls-tree", "-r", "-z", "--full-name", "--name-only", "HEAD", "--", ...pathspecs], { maxBuffer: 4 * 1024 * 1024 });
|
|
1065
|
+
return new Set(stdout.split("\x00").filter((path) => path !== ""));
|
|
1066
|
+
}
|
|
1067
|
+
async function enumerateSkills(root, mode, agentsHome) {
|
|
1068
|
+
let project = [];
|
|
1069
|
+
if (root !== undefined) {
|
|
1070
|
+
const layers = projectSkillLayers(root);
|
|
1071
|
+
project = projectSkills(layers.dirs, agentsHome);
|
|
1072
|
+
const gitRoot = layers.gitRoot;
|
|
1073
|
+
if (mode === "head" && gitRoot !== undefined) {
|
|
1074
|
+
const committed = await committedSkillFiles(gitRoot, layers.dirs);
|
|
1075
|
+
if (committed !== null) {
|
|
1076
|
+
project = project.filter((scoped) => committed.has(relative2(gitRoot, scoped.skill.filePath).split(sep3).join("/")));
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
const merged = mergeSkills([project, entrySkills(join4(agentsHome, "skills"), "user", agentsHome)]);
|
|
1081
|
+
return merged.filter((scoped) => invocableSkillName(scoped.skill.name)).map(({ skill, source }) => ({
|
|
1082
|
+
name: skill.name,
|
|
1083
|
+
description: skill.description,
|
|
1084
|
+
source,
|
|
1085
|
+
filePath: skill.filePath,
|
|
1086
|
+
baseDir: skill.baseDir
|
|
1087
|
+
}));
|
|
1088
|
+
}
|
|
1089
|
+
var CACHE_TTL_MS2 = 3000;
|
|
1090
|
+
var MAX_CACHED_SCOPES = 64;
|
|
1091
|
+
function createSkillLister(enumerate, options = {}) {
|
|
1092
|
+
const now = options.now ?? Date.now;
|
|
1093
|
+
const ttlMs = options.ttlMs ?? CACHE_TTL_MS2;
|
|
1094
|
+
const maxCached = options.maxCached ?? MAX_CACHED_SCOPES;
|
|
1095
|
+
const cache = new Map;
|
|
1096
|
+
const inflight = new Map;
|
|
1097
|
+
return (root, mode) => {
|
|
1098
|
+
const key = `${mode}${root ?? ""}`;
|
|
1099
|
+
const hit = cache.get(key);
|
|
1100
|
+
if (hit !== undefined) {
|
|
1101
|
+
if (hit.expiresAt > now())
|
|
1102
|
+
return Promise.resolve(hit.value);
|
|
1103
|
+
cache.delete(key);
|
|
1104
|
+
}
|
|
1105
|
+
const pending = inflight.get(key);
|
|
1106
|
+
if (pending !== undefined)
|
|
1107
|
+
return pending;
|
|
1108
|
+
const scan = enumerate(root, mode);
|
|
1109
|
+
inflight.set(key, scan);
|
|
1110
|
+
scan.then((value) => {
|
|
1111
|
+
const at = now();
|
|
1112
|
+
for (const [cached, entry] of cache) {
|
|
1113
|
+
if (entry.expiresAt <= at)
|
|
1114
|
+
cache.delete(cached);
|
|
1115
|
+
}
|
|
1116
|
+
while (cache.size >= maxCached)
|
|
1117
|
+
cache.delete(cache.keys().next().value);
|
|
1118
|
+
cache.set(key, { value, expiresAt: at + ttlMs });
|
|
1119
|
+
}, () => {});
|
|
1120
|
+
return scan.finally(() => inflight.delete(key));
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
async function scopeSkills(harness, lister, scope, context) {
|
|
1124
|
+
if (scope.kind === "chat")
|
|
1125
|
+
return lister(undefined, "live");
|
|
1126
|
+
let root;
|
|
1127
|
+
let mode = "live";
|
|
1128
|
+
if (scope.kind === "conversation") {
|
|
1129
|
+
const agent = await harness.snapshot(AgentDoc3, scope.conversationId, context);
|
|
1130
|
+
if (agent === undefined)
|
|
1131
|
+
throw new Error(`Conversation ${scope.conversationId} does not exist`);
|
|
1132
|
+
const cwd = agent.cwd;
|
|
1133
|
+
if (cwd === undefined)
|
|
1134
|
+
throw new Error(`Conversation ${scope.conversationId} has no checkout`);
|
|
1135
|
+
const record = checkoutAt((await harness.snapshot(IndexDoc, context))?.checkouts, cwd);
|
|
1136
|
+
const ready = record === undefined || await worktreeSettled(record);
|
|
1137
|
+
const info = ready ? await stat4(cwd).catch(() => {
|
|
1138
|
+
return;
|
|
1139
|
+
}) : undefined;
|
|
1140
|
+
if (info === undefined || !info.isDirectory()) {
|
|
1141
|
+
throw new Error(`Conversation ${scope.conversationId}'s checkout is unavailable: ${cwd}`);
|
|
1142
|
+
}
|
|
1143
|
+
root = cwd;
|
|
1144
|
+
} else {
|
|
1145
|
+
const normalized = await realpath4(resolve3(scope.path)).catch(() => resolve3(scope.path));
|
|
1146
|
+
const index = await harness.snapshot(IndexDoc, context);
|
|
1147
|
+
if (!index?.projects.some((project) => project.path === normalized)) {
|
|
1148
|
+
throw new Error(`Unknown project: ${scope.path}`);
|
|
1149
|
+
}
|
|
1150
|
+
const info = await stat4(normalized).catch(() => {
|
|
1151
|
+
return;
|
|
1152
|
+
});
|
|
1153
|
+
if (info === undefined || !info.isDirectory()) {
|
|
1154
|
+
throw new Error(`Not a directory: ${normalized}`);
|
|
1155
|
+
}
|
|
1156
|
+
root = normalized;
|
|
1157
|
+
mode = scope.checkout === "worktree" ? "head" : "live";
|
|
1158
|
+
}
|
|
1159
|
+
return lister(root, mode);
|
|
1160
|
+
}
|
|
1161
|
+
async function listSkills(harness, lister, scope, context) {
|
|
1162
|
+
const skills = await scopeSkills(harness, lister, scope, context);
|
|
1163
|
+
return { skills: skills.map(({ name, description, source }) => ({ name, description, source })) };
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
// apps/host/src/extensions/context.ts
|
|
1167
|
+
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.";
|
|
1168
|
+
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.";
|
|
1169
|
+
function createContext(options) {
|
|
1170
|
+
const { agentsHome } = options;
|
|
1171
|
+
const resolveCheckout = (input, context) => options.checkout?.(input, context);
|
|
1172
|
+
return {
|
|
1173
|
+
extension: defineExtension2({
|
|
1174
|
+
name: "context",
|
|
1175
|
+
sections: [
|
|
1176
|
+
section("preamble", () => PREAMBLE, { tag: false }),
|
|
1177
|
+
section("environment", async (input, context) => {
|
|
1178
|
+
const cwd = input.agent.cwd;
|
|
1179
|
+
if (cwd === undefined)
|
|
1180
|
+
return;
|
|
1181
|
+
const resolved = await resolveCheckout({ conversationId: input.conversationId, read: input.read }, context);
|
|
1182
|
+
const checkout = resolved?.checkout;
|
|
1183
|
+
const forkLine = resolved?.fork === true && checkout?.own !== true ? [FORK_SHARED_DIR] : [];
|
|
1184
|
+
if (checkout === undefined) {
|
|
1185
|
+
return [`Working directory: ${cwd}`, ...forkLine].join(`
|
|
1186
|
+
`);
|
|
1187
|
+
}
|
|
1188
|
+
return [
|
|
1189
|
+
`Working directory: ${cwd}`,
|
|
1190
|
+
checkout.own ? `This directory is a git worktree PiNomad created for this conversation, on branch ${checkout.branch} starting from commit ${checkout.base.slice(0, 12)}.` : `This directory is a git worktree PiNomad created for the conversation this one belongs to, on branch ${checkout.branch} starting from commit ${checkout.base.slice(0, 12)}.`,
|
|
1191
|
+
`The user's project directory is ${checkout.projectPath} and must not be modified.`,
|
|
1192
|
+
"This is a fresh checkout: ignored files (dependencies such as node_modules, or .env) are absent — install dependencies per AGENTS.md when needed.",
|
|
1193
|
+
...forkLine
|
|
1194
|
+
].join(`
|
|
1195
|
+
`);
|
|
1196
|
+
}),
|
|
1197
|
+
section("file_references", () => "When the user writes @./path in their message, it is a project file the user references; the path is relative to your current working directory. Its contents are not expanded for you — read the file yourself if the task needs them.", { tag: false }),
|
|
1198
|
+
section("project_context", async (input, context) => {
|
|
1199
|
+
const cwd = input.agent.cwd;
|
|
1200
|
+
if (cwd === undefined)
|
|
1201
|
+
return;
|
|
1202
|
+
const checkout = (await resolveCheckout({ conversationId: input.conversationId, read: input.read }, context))?.checkout;
|
|
1203
|
+
let files;
|
|
1204
|
+
if (checkout === undefined) {
|
|
1205
|
+
files = loadProjectContextFiles({ cwd, agentDir: agentsHome });
|
|
1206
|
+
} else {
|
|
1207
|
+
const seen = new Set;
|
|
1208
|
+
files = [
|
|
1209
|
+
...loadProjectContextFiles({ cwd: dirname2(checkout.repo), agentDir: agentsHome }),
|
|
1210
|
+
...loadProjectContextFiles({ cwd, agentDir: agentsHome }).filter((file) => file.path.startsWith(checkout.worktreeRoot + sep4))
|
|
1211
|
+
].filter((file) => {
|
|
1212
|
+
if (seen.has(file.path))
|
|
1213
|
+
return false;
|
|
1214
|
+
seen.add(file.path);
|
|
1215
|
+
return true;
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
return files.length === 0 ? undefined : files.map((file) => `## ${file.path}
|
|
1219
|
+
|
|
1220
|
+
${file.content}`).join(`
|
|
1221
|
+
|
|
1222
|
+
`);
|
|
1223
|
+
}),
|
|
1224
|
+
section("skills", (input) => {
|
|
1225
|
+
const cwd = input.agent.cwd;
|
|
1226
|
+
if (cwd === undefined)
|
|
1227
|
+
return;
|
|
1228
|
+
const skills = scopedSkills(cwd, agentsHome).map((scoped) => scoped.skill);
|
|
1229
|
+
const text = formatSkillsForPrompt(skills, "read");
|
|
1230
|
+
return text === "" ? undefined : text;
|
|
1231
|
+
}, { tag: false })
|
|
1232
|
+
]
|
|
1233
|
+
})
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
// apps/host/src/extensions/subagent.ts
|
|
1238
|
+
import { clampThinkingLevel as clampThinkingLevel2 } from "@earendil-works/pi-ai";
|
|
1239
|
+
import {
|
|
1240
|
+
AgentDoc as AgentDoc4,
|
|
1241
|
+
AssistantEntry,
|
|
1242
|
+
configure as configure2,
|
|
1243
|
+
defineExtension as defineExtension3,
|
|
1244
|
+
defineTool as defineTool2,
|
|
1245
|
+
section as section2
|
|
1246
|
+
} from "@earendil-works/pi-durable";
|
|
1247
|
+
import { Type as Type3 } from "typebox";
|
|
1248
|
+
|
|
1249
|
+
// apps/host/src/subagent-doc.ts
|
|
1250
|
+
import { defineDoc as defineDoc4 } from "@earendil-works/pi-durable";
|
|
1251
|
+
var SubagentDoc = defineDoc4({
|
|
1252
|
+
kind: "pinomad.subagent",
|
|
1253
|
+
version: 1,
|
|
1254
|
+
scope: "conversation",
|
|
1255
|
+
history: "latest",
|
|
1256
|
+
fork: "initial",
|
|
1257
|
+
initial: () => ({})
|
|
1258
|
+
});
|
|
1259
|
+
|
|
1260
|
+
// apps/host/src/extensions/subagent.ts
|
|
1261
|
+
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.";
|
|
1262
|
+
var THINKING_LEVELS2 = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
1263
|
+
function createSubagent(options) {
|
|
1264
|
+
const available = () => options.modelSummaries().map((model) => `${model.provider}/${model.modelId}`).join(", ");
|
|
1265
|
+
const roles = options.roles ?? (() => []);
|
|
1266
|
+
const roleNamed = (name) => {
|
|
1267
|
+
const found = roles().find((each) => each.name === name);
|
|
1268
|
+
if (found !== undefined)
|
|
1269
|
+
return found;
|
|
1270
|
+
const defined = roles();
|
|
1271
|
+
throw new Error(defined.length === 0 ? "No named agents are defined" : `Unknown agent "${name}". Available agents: ${defined.map((each) => each.name).join(", ")}`);
|
|
1272
|
+
};
|
|
1273
|
+
const tool = defineTool2({
|
|
1274
|
+
name: "subagent",
|
|
1275
|
+
callers: ["model"],
|
|
1276
|
+
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()}.`,
|
|
1277
|
+
parameters: Type3.Object({
|
|
1278
|
+
task: Type3.String({
|
|
1279
|
+
minLength: 1,
|
|
1280
|
+
description: "Complete, self-contained instructions. The subagent sees none of this conversation."
|
|
1281
|
+
}),
|
|
1282
|
+
role: Type3.Optional(Type3.String({
|
|
1283
|
+
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."
|
|
1284
|
+
})),
|
|
1285
|
+
description: Type3.Optional(Type3.String({ description: 'A short 3-6 word label for the user, e.g. "Audit auth middleware".' })),
|
|
1286
|
+
model: Type3.Optional(Type3.String({ description: "Optional `provider/modelId` to use instead of yours." })),
|
|
1287
|
+
thinkingLevel: Type3.Optional(Type3.Union(THINKING_LEVELS2.map((level) => Type3.Literal(level)), { description: "Optional thinking level instead of yours." }))
|
|
1288
|
+
}),
|
|
1289
|
+
replay: "safe",
|
|
1290
|
+
execute: async (args, api, context) => {
|
|
1291
|
+
const child = await api.commit(async (tx) => {
|
|
1292
|
+
const existing = (await tx.scanConversations({ ownerTaskId: api.taskId }, 1)).items[0];
|
|
1293
|
+
if (existing !== undefined) {
|
|
1294
|
+
const doc2 = await tx.doc(SubagentDoc, existing.id);
|
|
1295
|
+
return {
|
|
1296
|
+
id: existing.id,
|
|
1297
|
+
...doc2?.model === undefined ? {} : { model: doc2.model },
|
|
1298
|
+
...doc2?.startedAt === undefined ? {} : { startedAt: doc2.startedAt }
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
const role = args.role === undefined ? undefined : roleNamed(args.role);
|
|
1302
|
+
if (role?.model !== undefined && args.model !== undefined) {
|
|
1303
|
+
throw new Error(`Agent "${role.name}" pins model ${role.model}; omit \`model\``);
|
|
1304
|
+
}
|
|
1305
|
+
if (role?.thinkingLevel !== undefined && args.thinkingLevel !== undefined) {
|
|
1306
|
+
throw new Error(`Agent "${role.name}" pins thinking level ${role.thinkingLevel}; omit \`thinkingLevel\``);
|
|
1307
|
+
}
|
|
1308
|
+
const modelString = role?.model ?? args.model;
|
|
1309
|
+
const levelString = role?.thinkingLevel ?? args.thinkingLevel;
|
|
1310
|
+
let model;
|
|
1311
|
+
let thinkingLevel;
|
|
1312
|
+
let override;
|
|
1313
|
+
if (modelString !== undefined) {
|
|
1314
|
+
const slash = modelString.indexOf("/");
|
|
1315
|
+
const provider = slash === -1 ? "" : modelString.slice(0, slash);
|
|
1316
|
+
const modelId = slash === -1 ? "" : modelString.slice(slash + 1);
|
|
1317
|
+
override = provider === "" || modelId === "" ? undefined : options.models.getModel(provider, modelId);
|
|
1318
|
+
const listed = options.modelSummaries().some((each) => each.provider === provider && each.modelId === modelId);
|
|
1319
|
+
if (override === undefined || !listed) {
|
|
1320
|
+
throw new Error(role === undefined ? `Unknown model "${modelString}". Available models: ${available()}` : `Agent "${role.name}" uses model "${modelString}", which is not available. Available models: ${available()}`);
|
|
1321
|
+
}
|
|
1322
|
+
model = { provider, modelId };
|
|
1323
|
+
}
|
|
1324
|
+
if (levelString !== undefined || override !== undefined) {
|
|
1325
|
+
const parent = await tx.doc(AgentDoc4, api.conversationId);
|
|
1326
|
+
const effective = override ?? (parent.model === undefined ? undefined : options.models.getModel(parent.model.provider, parent.model.modelId));
|
|
1327
|
+
if (levelString !== undefined) {
|
|
1328
|
+
thinkingLevel = effective === undefined ? levelString : clampThinkingLevel2(effective, levelString);
|
|
1329
|
+
} else if (parent.thinkingLevel !== undefined) {
|
|
1330
|
+
thinkingLevel = clampThinkingLevel2(override, parent.thinkingLevel);
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
const change = {
|
|
1334
|
+
extensions: { remove: [extension, ...options.exclude] },
|
|
1335
|
+
instructions: role === undefined || role.prompt === "" ? INSTRUCTIONS : `${INSTRUCTIONS}
|
|
1336
|
+
|
|
1337
|
+
${role.prompt}`,
|
|
1338
|
+
...model === undefined ? {} : { model },
|
|
1339
|
+
...thinkingLevel === undefined ? {} : { thinkingLevel }
|
|
1340
|
+
};
|
|
1341
|
+
const created = await tx.createConversation({ ownership: { kind: "task", taskId: api.taskId } });
|
|
1342
|
+
await configure2(tx, created.id, change);
|
|
1343
|
+
const childAgent = await tx.doc(AgentDoc4, created.id);
|
|
1344
|
+
const ref = childAgent.model ?? model ?? (await tx.doc(AgentDoc4, api.conversationId)).model;
|
|
1345
|
+
const startedAt = Date.now();
|
|
1346
|
+
const doc = await tx.doc(SubagentDoc, created.id);
|
|
1347
|
+
doc.startedAt = startedAt;
|
|
1348
|
+
if (ref !== undefined)
|
|
1349
|
+
doc.model = `${ref.provider}/${ref.modelId}`;
|
|
1350
|
+
if (role !== undefined)
|
|
1351
|
+
doc.role = role.name;
|
|
1352
|
+
const label = args.description?.trim();
|
|
1353
|
+
if (label !== undefined && label !== "")
|
|
1354
|
+
doc.label = label;
|
|
1355
|
+
return { id: created.id, ...ref === undefined ? {} : { model: doc.model }, startedAt };
|
|
1356
|
+
}, context);
|
|
1357
|
+
await api.details({ conversationId: child.id, ...child.model === undefined ? {} : { model: child.model }, ...child.startedAt === undefined ? {} : { startedAt: child.startedAt } }, context);
|
|
1358
|
+
const handle = await api.conversation(child.id, context);
|
|
1359
|
+
if (handle === undefined)
|
|
1360
|
+
throw new Error(`Subagent conversation ${child.id} does not exist`);
|
|
1361
|
+
const request = { type: "input", content: args.task, requestId: `subagent:${api.taskId}` };
|
|
1362
|
+
const settled = await (await handle.submit(request, context)).wait(context);
|
|
1363
|
+
if (settled.status !== "done" || settled.type !== "input")
|
|
1364
|
+
throw new Error(`Subagent failed: ${settled.status}`);
|
|
1365
|
+
const answer = await api.commit((tx) => tx.entry(AssistantEntry, settled.answer), context);
|
|
1366
|
+
const message = answer?.model?.[0];
|
|
1367
|
+
const text = message?.role === "assistant" ? message.content.flatMap((block) => block.type === "text" ? [block.text] : []).join("") : "";
|
|
1368
|
+
const result2 = {
|
|
1369
|
+
output: [{ type: "text", text: text === "" ? "(The subagent returned no text.)" : text }],
|
|
1370
|
+
details: {
|
|
1371
|
+
conversationId: child.id,
|
|
1372
|
+
...child.model === undefined ? {} : { model: child.model },
|
|
1373
|
+
...child.startedAt === undefined ? {} : { startedAt: child.startedAt }
|
|
1374
|
+
}
|
|
1375
|
+
};
|
|
1376
|
+
return result2;
|
|
1377
|
+
}
|
|
1378
|
+
});
|
|
1379
|
+
const extension = defineExtension3({
|
|
1380
|
+
name: "subagent",
|
|
1381
|
+
tools: [tool],
|
|
1382
|
+
sections: [
|
|
1383
|
+
section2("agent_roles", () => {
|
|
1384
|
+
const defined = roles();
|
|
1385
|
+
if (defined.length === 0)
|
|
1386
|
+
return;
|
|
1387
|
+
return [
|
|
1388
|
+
"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`.",
|
|
1389
|
+
...defined.map((each) => `- ${each.name}: ${each.description.replace(/\s+/g, " ")}`)
|
|
1390
|
+
].join(`
|
|
1391
|
+
`);
|
|
1392
|
+
})
|
|
1393
|
+
]
|
|
1394
|
+
});
|
|
1395
|
+
return { extension };
|
|
927
1396
|
}
|
|
928
1397
|
|
|
1398
|
+
// apps/host/src/extensions/todo.ts
|
|
1399
|
+
import { defineDoc as defineDoc5, defineExtension as defineExtension4, defineTool as defineTool3 } from "@earendil-works/pi-durable";
|
|
1400
|
+
import { Type as Type4 } from "typebox";
|
|
1401
|
+
var TodoDoc = defineDoc5({
|
|
1402
|
+
kind: "todo.list",
|
|
1403
|
+
version: 1,
|
|
1404
|
+
scope: "conversation",
|
|
1405
|
+
history: "rewindable",
|
|
1406
|
+
fork: "asOf",
|
|
1407
|
+
initial: () => ({ items: [] })
|
|
1408
|
+
});
|
|
1409
|
+
var todoWrite = defineTool3({
|
|
1410
|
+
name: "todo_write",
|
|
1411
|
+
description: "Replace the todo list shown to the user with the given items.",
|
|
1412
|
+
parameters: Type4.Object({ items: Type4.Array(TodoItemSchema) }),
|
|
1413
|
+
replay: "safe",
|
|
1414
|
+
execute: async (args, api, context) => {
|
|
1415
|
+
await api.commit(async (tx) => {
|
|
1416
|
+
const doc = await tx.doc(TodoDoc, api.conversationId);
|
|
1417
|
+
doc.items = args.items.map((item) => ({ ...item }));
|
|
1418
|
+
}, context);
|
|
1419
|
+
return { output: [{ type: "text", text: `Todo list updated: ${args.items.length} item(s).` }] };
|
|
1420
|
+
}
|
|
1421
|
+
});
|
|
1422
|
+
var todo = {
|
|
1423
|
+
extension: defineExtension4({ name: "todo", tools: [todoWrite] }),
|
|
1424
|
+
docs: [{ token: TodoDoc, presentation: "pinomad.todo" }]
|
|
1425
|
+
};
|
|
1426
|
+
|
|
1427
|
+
// apps/host/src/host.ts
|
|
1428
|
+
import { randomBytes as randomBytes4 } from "node:crypto";
|
|
1429
|
+
import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile3 } from "node:fs/promises";
|
|
1430
|
+
import { basename as basename3, join as join10, sep as sep6 } from "node:path";
|
|
1431
|
+
import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT4 } from "@earendil-works/chord/context";
|
|
1432
|
+
import { createRegistry, Harness } from "@earendil-works/pi-durable";
|
|
1433
|
+
import { NodeExecutionEnv } from "@earendil-works/pi-durable/env/node";
|
|
1434
|
+
|
|
929
1435
|
// apps/host/src/mcp.ts
|
|
930
1436
|
import { readFile } from "node:fs/promises";
|
|
931
1437
|
import { homedir } from "node:os";
|
|
932
|
-
import { isAbsolute, resolve as
|
|
1438
|
+
import { isAbsolute, resolve as resolve4 } from "node:path";
|
|
933
1439
|
import {
|
|
934
1440
|
McpAuthRequiredError,
|
|
935
1441
|
McpClient,
|
|
@@ -1004,7 +1510,7 @@ function parseServer(name, raw, error) {
|
|
|
1004
1510
|
const env = expandAll(raw.env, "env", (message) => error(`${name}: ${message}`));
|
|
1005
1511
|
if (raw.env !== undefined && env === undefined)
|
|
1006
1512
|
return;
|
|
1007
|
-
const cwd = raw.cwd === undefined ? homedir() : isAbsolute(raw.cwd) ? raw.cwd :
|
|
1513
|
+
const cwd = raw.cwd === undefined ? homedir() : isAbsolute(raw.cwd) ? raw.cwd : resolve4(homedir(), raw.cwd);
|
|
1008
1514
|
return {
|
|
1009
1515
|
name,
|
|
1010
1516
|
spec: { kind: "stdio", command: raw.command, args: raw.args, env, cwd },
|
|
@@ -1215,9 +1721,9 @@ ${stderr}`;
|
|
|
1215
1721
|
|
|
1216
1722
|
// apps/host/src/agent-roles.ts
|
|
1217
1723
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
1218
|
-
import { existsSync, watch } from "node:fs";
|
|
1724
|
+
import { existsSync as existsSync2, watch } from "node:fs";
|
|
1219
1725
|
import { mkdir as mkdir2, readdir, readFile as readFile2, rename, rm as rm2, writeFile } from "node:fs/promises";
|
|
1220
|
-
import { join as
|
|
1726
|
+
import { join as join6 } from "node:path";
|
|
1221
1727
|
import { parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
1222
1728
|
|
|
1223
1729
|
// packages/protocol/src/agents.ts
|
|
@@ -1261,7 +1767,7 @@ async function load(dir) {
|
|
|
1261
1767
|
const problem = (reason2) => void problems.push({ file, reason: reason2 });
|
|
1262
1768
|
let parsed;
|
|
1263
1769
|
try {
|
|
1264
|
-
parsed = parseFrontmatter(await readFile2(
|
|
1770
|
+
parsed = parseFrontmatter(await readFile2(join6(dir, file), "utf8"));
|
|
1265
1771
|
} catch (error) {
|
|
1266
1772
|
problem(`Couldn't parse the file: ${error instanceof Error ? error.message : String(error)}`);
|
|
1267
1773
|
continue;
|
|
@@ -1319,11 +1825,14 @@ async function openAgentRoles(options) {
|
|
|
1319
1825
|
const dir = options.dir;
|
|
1320
1826
|
await mkdir2(dir, { recursive: true });
|
|
1321
1827
|
const debounceMs = options.debounceMs ?? 150;
|
|
1828
|
+
const rescanMs = options.rescanMs ?? 1000;
|
|
1322
1829
|
const published = new Published({ roles: [], problems: [] }, (a, b) => sameList(a.roles, b.roles, sameRole) && sameList(a.problems, b.problems, sameProblem));
|
|
1323
1830
|
let closed = false;
|
|
1324
1831
|
let reported = false;
|
|
1325
1832
|
let timer;
|
|
1326
|
-
|
|
1833
|
+
let queued = false;
|
|
1834
|
+
let chain = Promise.resolve();
|
|
1835
|
+
const loadOnce = async () => {
|
|
1327
1836
|
try {
|
|
1328
1837
|
published.set(await load(dir));
|
|
1329
1838
|
reported = false;
|
|
@@ -1333,7 +1842,18 @@ async function openAgentRoles(options) {
|
|
|
1333
1842
|
reported = true;
|
|
1334
1843
|
}
|
|
1335
1844
|
};
|
|
1336
|
-
|
|
1845
|
+
const reload = () => {
|
|
1846
|
+
if (closed)
|
|
1847
|
+
return Promise.resolve();
|
|
1848
|
+
queued = true;
|
|
1849
|
+
chain = chain.then(async () => {
|
|
1850
|
+
if (!queued || closed)
|
|
1851
|
+
return;
|
|
1852
|
+
queued = false;
|
|
1853
|
+
await loadOnce();
|
|
1854
|
+
});
|
|
1855
|
+
return chain;
|
|
1856
|
+
};
|
|
1337
1857
|
const watcher = watch(dir, () => {
|
|
1338
1858
|
if (closed)
|
|
1339
1859
|
return;
|
|
@@ -1344,7 +1864,10 @@ async function openAgentRoles(options) {
|
|
|
1344
1864
|
if (!reported)
|
|
1345
1865
|
console.error(`agent-roles watch failed: ${error.message}`);
|
|
1346
1866
|
reported = true;
|
|
1867
|
+
reload();
|
|
1347
1868
|
});
|
|
1869
|
+
await reload();
|
|
1870
|
+
const rescan = setInterval(() => void reload(), rescanMs);
|
|
1348
1871
|
let pending = Promise.resolve();
|
|
1349
1872
|
const enqueue = (operation) => {
|
|
1350
1873
|
const run2 = pending.then(operation, operation);
|
|
@@ -1356,13 +1879,13 @@ async function openAgentRoles(options) {
|
|
|
1356
1879
|
if (problem !== undefined)
|
|
1357
1880
|
throw new Error(problem);
|
|
1358
1881
|
await enqueue(async () => {
|
|
1359
|
-
const target =
|
|
1360
|
-
const exists =
|
|
1882
|
+
const target = join6(dir, `${role.name}.md`);
|
|
1883
|
+
const exists = existsSync2(target);
|
|
1361
1884
|
if (mustExist && !exists)
|
|
1362
1885
|
throw new Error(`No such agent: "${role.name}"`);
|
|
1363
1886
|
if (!mustExist && exists)
|
|
1364
1887
|
throw new Error(`Agent "${role.name}" already exists`);
|
|
1365
|
-
const temp =
|
|
1888
|
+
const temp = join6(dir, `.${role.name}.${randomBytes2(6).toString("hex")}.tmp`);
|
|
1366
1889
|
try {
|
|
1367
1890
|
await writeFile(temp, serialize(role));
|
|
1368
1891
|
await rename(temp, target);
|
|
@@ -1383,377 +1906,618 @@ async function openAgentRoles(options) {
|
|
|
1383
1906
|
async remove(name) {
|
|
1384
1907
|
const problem = agentNameProblem(name);
|
|
1385
1908
|
if (problem !== undefined)
|
|
1386
|
-
throw new Error(problem);
|
|
1387
|
-
await enqueue(async () => {
|
|
1388
|
-
const target =
|
|
1389
|
-
if (!
|
|
1390
|
-
throw new Error(`No such agent: "${name}"`);
|
|
1391
|
-
await rm2(target);
|
|
1392
|
-
await reload();
|
|
1393
|
-
});
|
|
1394
|
-
},
|
|
1395
|
-
close() {
|
|
1396
|
-
closed = true;
|
|
1397
|
-
clearTimeout(timer);
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
}
|
|
1402
|
-
|
|
1403
|
-
// apps/host/src/host.ts
|
|
1404
|
-
import { openNodeSqliteStorage } from "@earendil-works/pi-durable/storage/sqlite/node";
|
|
1405
|
-
import lockfile from "proper-lockfile";
|
|
1406
|
-
|
|
1407
|
-
// apps/host/src/gateway.ts
|
|
1408
|
-
import { createServer } from "node:http";
|
|
1409
|
-
import { homedir as homedir2, networkInterfaces } from "node:os";
|
|
1410
|
-
import { join as join7, sep as sep4 } from "node:path";
|
|
1411
|
-
import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT3 } from "@earendil-works/chord/context";
|
|
1412
|
-
import { clampThinkingLevel as clampThinkingLevel3, getSupportedThinkingLevels } from "@earendil-works/pi-ai";
|
|
1413
|
-
import {
|
|
1414
|
-
AgentDoc as AgentDoc4,
|
|
1415
|
-
LiveDoc
|
|
1416
|
-
} from "@earendil-works/pi-durable";
|
|
1417
|
-
import { WebSocket, WebSocketServer } from "ws";
|
|
1418
|
-
import { Value as Value2 } from "typebox/value";
|
|
1419
|
-
|
|
1420
|
-
// apps/host/src/folders.ts
|
|
1421
|
-
import { readdir as readdir2, realpath as realpath3, stat as stat3 } from "node:fs/promises";
|
|
1422
|
-
import { isAbsolute as isAbsolute2, join as join5 } from "node:path";
|
|
1423
|
-
var denied = (error) => {
|
|
1424
|
-
const code = error?.code;
|
|
1425
|
-
return code === "EACCES" || code === "EPERM";
|
|
1426
|
-
};
|
|
1427
|
-
async function listFolders(path, home) {
|
|
1428
|
-
if (path !== undefined && !isAbsolute2(path))
|
|
1429
|
-
throw new Error(`Not an absolute path: ${path}`);
|
|
1430
|
-
const given = path ?? home;
|
|
1431
|
-
const dir = await realpath3(given).catch((error) => {
|
|
1432
|
-
if (denied(error))
|
|
1433
|
-
throw new Error(`Permission denied: ${given}`);
|
|
1434
|
-
throw new Error(`No such directory: ${given}`);
|
|
1435
|
-
});
|
|
1436
|
-
if (!(await stat3(dir)).isDirectory())
|
|
1437
|
-
throw new Error(`Not a directory: ${given}`);
|
|
1438
|
-
let entries;
|
|
1439
|
-
try {
|
|
1440
|
-
entries = await readdir2(dir, { withFileTypes: true });
|
|
1441
|
-
} catch (error) {
|
|
1442
|
-
if (denied(error))
|
|
1443
|
-
throw new Error(`Permission denied: ${given}`);
|
|
1444
|
-
throw error;
|
|
1445
|
-
}
|
|
1446
|
-
const folders = [];
|
|
1447
|
-
for (const entry of entries) {
|
|
1448
|
-
if (entry.name.startsWith("."))
|
|
1449
|
-
continue;
|
|
1450
|
-
let isDirectory = entry.isDirectory();
|
|
1451
|
-
if (!isDirectory && entry.isSymbolicLink()) {
|
|
1452
|
-
isDirectory = await stat3(join5(dir, entry.name)).then((target) => target.isDirectory(), () => false);
|
|
1453
|
-
}
|
|
1454
|
-
if (isDirectory)
|
|
1455
|
-
folders.push({ name: entry.name, path: join5(dir, entry.name) });
|
|
1456
|
-
}
|
|
1457
|
-
folders.sort((a, b) => a.name.localeCompare(b.name, "en", { numeric: true, sensitivity: "base" }));
|
|
1458
|
-
return { path: dir, home: await realpath3(home).catch(() => home), folders };
|
|
1459
|
-
}
|
|
1460
|
-
|
|
1461
|
-
// apps/host/src/files.ts
|
|
1462
|
-
import { spawn as spawn2 } from "node:child_process";
|
|
1463
|
-
import { opendir, realpath as realpath4, stat as stat4 } from "node:fs/promises";
|
|
1464
|
-
import { join as join6, resolve as resolve3, sep as sep3 } from "node:path";
|
|
1465
|
-
import { AgentDoc as AgentDoc3 } from "@earendil-works/pi-durable";
|
|
1466
|
-
var CACHE_TTL_MS = 3000;
|
|
1467
|
-
var MAX_CACHED_ROOTS = 64;
|
|
1468
|
-
var MAX_CONCURRENT_SCANS = 4;
|
|
1469
|
-
var SCAN_BUDGET_MS = 5000;
|
|
1470
|
-
var MAX_OUTPUT_BYTES = 4 * 1024 * 1024;
|
|
1471
|
-
var MAX_WALK_ENTRIES = 20000;
|
|
1472
|
-
var MAX_RESULTS = 50;
|
|
1473
|
-
var UNUSABLE_PATH = /[\s`\x00-\x1f\x7f-\x9f]/;
|
|
1474
|
-
function createCandidateLister(enumerate, options = {}) {
|
|
1475
|
-
const now = options.now ?? Date.now;
|
|
1476
|
-
const ttlMs = options.ttlMs ?? CACHE_TTL_MS;
|
|
1477
|
-
const maxConcurrent = options.maxConcurrent ?? MAX_CONCURRENT_SCANS;
|
|
1478
|
-
const maxCached = options.maxCached ?? MAX_CACHED_ROOTS;
|
|
1479
|
-
const cache = new Map;
|
|
1480
|
-
const inflight = new Map;
|
|
1481
|
-
let running = 0;
|
|
1482
|
-
const waiting = [];
|
|
1483
|
-
return (root, mode) => {
|
|
1484
|
-
const key = `${mode}${root}`;
|
|
1485
|
-
const hit = cache.get(key);
|
|
1486
|
-
if (hit !== undefined) {
|
|
1487
|
-
if (hit.expiresAt > now())
|
|
1488
|
-
return Promise.resolve(hit.value);
|
|
1489
|
-
cache.delete(key);
|
|
1490
|
-
}
|
|
1491
|
-
const pending = inflight.get(key);
|
|
1492
|
-
if (pending !== undefined)
|
|
1493
|
-
return pending;
|
|
1494
|
-
const scan = (async () => {
|
|
1495
|
-
if (running >= maxConcurrent)
|
|
1496
|
-
await new Promise((release) => waiting.push(release));
|
|
1497
|
-
running += 1;
|
|
1498
|
-
try {
|
|
1499
|
-
return await enumerate(root, mode);
|
|
1500
|
-
} finally {
|
|
1501
|
-
running -= 1;
|
|
1502
|
-
waiting.shift()?.();
|
|
1503
|
-
}
|
|
1504
|
-
})();
|
|
1505
|
-
inflight.set(key, scan);
|
|
1506
|
-
scan.then((value) => {
|
|
1507
|
-
const at = now();
|
|
1508
|
-
for (const [cached, entry] of cache) {
|
|
1509
|
-
if (entry.expiresAt <= at)
|
|
1510
|
-
cache.delete(cached);
|
|
1511
|
-
}
|
|
1512
|
-
while (cache.size >= maxCached)
|
|
1513
|
-
cache.delete(cache.keys().next().value);
|
|
1514
|
-
cache.set(key, { value, expiresAt: at + ttlMs });
|
|
1515
|
-
}, () => {});
|
|
1516
|
-
return scan.finally(() => inflight.delete(key));
|
|
1517
|
-
};
|
|
1518
|
-
}
|
|
1519
|
-
function gitLines(cwd, args, limits) {
|
|
1520
|
-
return new Promise((resolvePromise, rejectPromise) => {
|
|
1521
|
-
const child = spawn2("git", ["-C", cwd, ...args], { stdio: ["ignore", "pipe", "pipe"] });
|
|
1522
|
-
const chunks = [];
|
|
1523
|
-
let bytes = 0;
|
|
1524
|
-
let truncated = false;
|
|
1525
|
-
let settled = false;
|
|
1526
|
-
const kill = (markTruncated) => {
|
|
1527
|
-
truncated ||= markTruncated;
|
|
1528
|
-
child.kill("SIGKILL");
|
|
1529
|
-
};
|
|
1530
|
-
const timer = setTimeout(() => kill(true), Math.max(0, limits.deadline - limits.now()));
|
|
1531
|
-
const finish = (error) => {
|
|
1532
|
-
if (settled)
|
|
1533
|
-
return;
|
|
1534
|
-
settled = true;
|
|
1535
|
-
clearTimeout(timer);
|
|
1536
|
-
if (error !== undefined) {
|
|
1537
|
-
rejectPromise(error);
|
|
1538
|
-
return;
|
|
1539
|
-
}
|
|
1540
|
-
const output = Buffer.concat(chunks).toString("utf8");
|
|
1541
|
-
const parts = output.split("\x00");
|
|
1542
|
-
if (truncated)
|
|
1543
|
-
parts.pop();
|
|
1544
|
-
resolvePromise({ lines: parts.filter((part) => part !== ""), truncated });
|
|
1545
|
-
};
|
|
1546
|
-
child.stdout.on("data", (chunk) => {
|
|
1547
|
-
bytes += chunk.length;
|
|
1548
|
-
if (bytes > limits.maxBytes) {
|
|
1549
|
-
kill(true);
|
|
1550
|
-
return;
|
|
1551
|
-
}
|
|
1552
|
-
chunks.push(chunk);
|
|
1553
|
-
if (limits.now() > limits.deadline)
|
|
1554
|
-
kill(true);
|
|
1555
|
-
});
|
|
1556
|
-
child.on("error", (error) => finish(error));
|
|
1557
|
-
child.stderr.resume();
|
|
1558
|
-
child.on("close", (code) => {
|
|
1559
|
-
if (code === 0 || truncated) {
|
|
1560
|
-
finish(undefined);
|
|
1561
|
-
} else {
|
|
1562
|
-
finish(new Error(`git ${args.join(" ")} exited with code ${code}`));
|
|
1563
|
-
}
|
|
1564
|
-
});
|
|
1565
|
-
});
|
|
1909
|
+
throw new Error(problem);
|
|
1910
|
+
await enqueue(async () => {
|
|
1911
|
+
const target = join6(dir, `${name}.md`);
|
|
1912
|
+
if (!existsSync2(target))
|
|
1913
|
+
throw new Error(`No such agent: "${name}"`);
|
|
1914
|
+
await rm2(target);
|
|
1915
|
+
await reload();
|
|
1916
|
+
});
|
|
1917
|
+
},
|
|
1918
|
+
close() {
|
|
1919
|
+
closed = true;
|
|
1920
|
+
clearTimeout(timer);
|
|
1921
|
+
clearInterval(rescan);
|
|
1922
|
+
watcher.close();
|
|
1923
|
+
}
|
|
1924
|
+
};
|
|
1566
1925
|
}
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1926
|
+
|
|
1927
|
+
// apps/host/src/host.ts
|
|
1928
|
+
import { openNodeSqliteStorage } from "@earendil-works/pi-durable/storage/sqlite/node";
|
|
1929
|
+
import lockfile from "proper-lockfile";
|
|
1930
|
+
|
|
1931
|
+
// apps/host/src/gateway.ts
|
|
1932
|
+
import { createServer } from "node:http";
|
|
1933
|
+
import { homedir as homedir2, networkInterfaces } from "node:os";
|
|
1934
|
+
import { join as join8, sep as sep5 } from "node:path";
|
|
1935
|
+
import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT3 } from "@earendil-works/chord/context";
|
|
1936
|
+
import { clampThinkingLevel as clampThinkingLevel3, getSupportedThinkingLevels } from "@earendil-works/pi-ai";
|
|
1937
|
+
import {
|
|
1938
|
+
AgentDoc as AgentDoc5,
|
|
1939
|
+
configure as configure3,
|
|
1940
|
+
InboxDoc,
|
|
1941
|
+
LiveDoc
|
|
1942
|
+
} from "@earendil-works/pi-durable";
|
|
1943
|
+
import { WebSocket, WebSocketServer } from "ws";
|
|
1944
|
+
import { Value as Value2 } from "typebox/value";
|
|
1945
|
+
|
|
1946
|
+
// apps/host/src/folders.ts
|
|
1947
|
+
import { readdir as readdir2, realpath as realpath5, stat as stat5 } from "node:fs/promises";
|
|
1948
|
+
import { isAbsolute as isAbsolute2, join as join7 } from "node:path";
|
|
1949
|
+
var denied = (error) => {
|
|
1950
|
+
const code = error?.code;
|
|
1951
|
+
return code === "EACCES" || code === "EPERM";
|
|
1952
|
+
};
|
|
1953
|
+
async function listFolders(path, home) {
|
|
1954
|
+
if (path !== undefined && !isAbsolute2(path))
|
|
1955
|
+
throw new Error(`Not an absolute path: ${path}`);
|
|
1956
|
+
const given = path ?? home;
|
|
1957
|
+
const dir = await realpath5(given).catch((error) => {
|
|
1958
|
+
if (denied(error))
|
|
1959
|
+
throw new Error(`Permission denied: ${given}`);
|
|
1960
|
+
throw new Error(`No such directory: ${given}`);
|
|
1574
1961
|
});
|
|
1962
|
+
if (!(await stat5(dir)).isDirectory())
|
|
1963
|
+
throw new Error(`Not a directory: ${given}`);
|
|
1964
|
+
let entries;
|
|
1965
|
+
try {
|
|
1966
|
+
entries = await readdir2(dir, { withFileTypes: true });
|
|
1967
|
+
} catch (error) {
|
|
1968
|
+
if (denied(error))
|
|
1969
|
+
throw new Error(`Permission denied: ${given}`);
|
|
1970
|
+
throw error;
|
|
1971
|
+
}
|
|
1972
|
+
const folders = [];
|
|
1973
|
+
for (const entry of entries) {
|
|
1974
|
+
if (entry.name.startsWith("."))
|
|
1975
|
+
continue;
|
|
1976
|
+
let isDirectory = entry.isDirectory();
|
|
1977
|
+
if (!isDirectory && entry.isSymbolicLink()) {
|
|
1978
|
+
isDirectory = await stat5(join7(dir, entry.name)).then((target) => target.isDirectory(), () => false);
|
|
1979
|
+
}
|
|
1980
|
+
if (isDirectory)
|
|
1981
|
+
folders.push({ name: entry.name, path: join7(dir, entry.name) });
|
|
1982
|
+
}
|
|
1983
|
+
folders.sort((a, b) => a.name.localeCompare(b.name, "en", { numeric: true, sensitivity: "base" }));
|
|
1984
|
+
return { path: dir, home: await realpath5(home).catch(() => home), folders };
|
|
1575
1985
|
}
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1986
|
+
|
|
1987
|
+
// apps/host/src/skill-expansion.ts
|
|
1988
|
+
import { readFile as readFile3 } from "node:fs/promises";
|
|
1989
|
+
import { stripFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
1990
|
+
|
|
1991
|
+
// packages/client/src/skill-blocks.ts
|
|
1992
|
+
var OPEN = '<skill name="';
|
|
1993
|
+
var LOCATION = '" location="';
|
|
1994
|
+
var REFERENCES = `">
|
|
1995
|
+
References are relative to `;
|
|
1996
|
+
var CLOSE = `
|
|
1997
|
+
</skill>`;
|
|
1998
|
+
var SEPARATOR = `
|
|
1999
|
+
|
|
2000
|
+
`;
|
|
2001
|
+
function withSkillBlocks(blocks, text) {
|
|
2002
|
+
if (blocks.length === 0)
|
|
2003
|
+
return text;
|
|
2004
|
+
const written = blocks.map((block) => `${OPEN}${block.name}${LOCATION}${block.location}${REFERENCES}${block.baseDir}.
|
|
2005
|
+
|
|
2006
|
+
${block.body}${CLOSE}`);
|
|
2007
|
+
return [...written, text].join(SEPARATOR);
|
|
1582
2008
|
}
|
|
1583
|
-
var
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
const
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
2009
|
+
var isNameUnit = (unit) => unit >= 48 && unit <= 57 || unit >= 97 && unit <= 122 || unit === 45;
|
|
2010
|
+
function blockEnd(text, at) {
|
|
2011
|
+
if (!text.startsWith(OPEN, at))
|
|
2012
|
+
return -1;
|
|
2013
|
+
let i = at + OPEN.length;
|
|
2014
|
+
const name = i;
|
|
2015
|
+
while (i < text.length && isNameUnit(text.charCodeAt(i)))
|
|
2016
|
+
i += 1;
|
|
2017
|
+
if (i === name || !text.startsWith(LOCATION, i))
|
|
2018
|
+
return -1;
|
|
2019
|
+
i += LOCATION.length;
|
|
2020
|
+
const location = i;
|
|
2021
|
+
while (i < text.length && text[i] !== '"' && text[i] !== `
|
|
2022
|
+
`)
|
|
2023
|
+
i += 1;
|
|
2024
|
+
if (i === location || !text.startsWith(REFERENCES, i))
|
|
2025
|
+
return -1;
|
|
2026
|
+
i += REFERENCES.length;
|
|
2027
|
+
const lineEnd = text.indexOf(`
|
|
2028
|
+
`, i);
|
|
2029
|
+
if (lineEnd - i < 2 || text[lineEnd - 1] !== "." || text[lineEnd + 1] !== `
|
|
2030
|
+
`)
|
|
2031
|
+
return -1;
|
|
2032
|
+
const close = text.indexOf(CLOSE, lineEnd + 1);
|
|
2033
|
+
return close === -1 ? -1 : close + CLOSE.length;
|
|
2034
|
+
}
|
|
2035
|
+
function stripSkillPrefix(text) {
|
|
2036
|
+
const skills = [];
|
|
2037
|
+
let at = 0;
|
|
2038
|
+
for (;; ) {
|
|
2039
|
+
const end = blockEnd(text, at);
|
|
2040
|
+
if (end === -1 || !text.startsWith(SEPARATOR, end) || end + SEPARATOR.length === text.length) {
|
|
2041
|
+
return { text: text.slice(at), skills };
|
|
2042
|
+
}
|
|
2043
|
+
skills.push(text.slice(at + OPEN.length, text.indexOf('"', at + OPEN.length)));
|
|
2044
|
+
at = end + SEPARATOR.length;
|
|
1597
2045
|
}
|
|
1598
|
-
return { files: [...files], truncated: listing.truncated || deleted.truncated };
|
|
1599
2046
|
}
|
|
1600
|
-
|
|
1601
|
-
const
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
2047
|
+
function stripUserBlocks(content) {
|
|
2048
|
+
const attachments = [];
|
|
2049
|
+
let joined = "";
|
|
2050
|
+
if (typeof content === "string") {
|
|
2051
|
+
joined = content;
|
|
2052
|
+
} else {
|
|
2053
|
+
for (const block of content) {
|
|
2054
|
+
if (block.type !== "text" || typeof block.text !== "string")
|
|
2055
|
+
continue;
|
|
2056
|
+
const files = parseAttachmentBlock(block.text);
|
|
2057
|
+
if (files !== undefined)
|
|
2058
|
+
attachments.push(...files);
|
|
2059
|
+
else
|
|
2060
|
+
joined += block.text;
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
const typed = stripSkillPrefix(joined);
|
|
2064
|
+
return { text: typed.text, skills: typed.skills, attachments };
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
// packages/client/src/skill-mentions.ts
|
|
2068
|
+
var WHITESPACE = /\p{White_Space}/u;
|
|
2069
|
+
var WORD_CHAR = /[\p{Alphabetic}\p{N}_$]/u;
|
|
2070
|
+
var isNameUnit2 = (unit) => unit >= 48 && unit <= 57 || unit >= 97 && unit <= 122 || unit === 45;
|
|
2071
|
+
function skillMentionRanges(text, names) {
|
|
2072
|
+
const known = new Set(names);
|
|
2073
|
+
const ranges = [];
|
|
2074
|
+
for (let i = 0;i < text.length; i += 1) {
|
|
2075
|
+
if (text.charCodeAt(i) !== 36)
|
|
1606
2076
|
continue;
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
2077
|
+
if (i > 0 && !WHITESPACE.test(text[i - 1]))
|
|
2078
|
+
continue;
|
|
2079
|
+
let end = i + 1;
|
|
2080
|
+
while (end < text.length && isNameUnit2(text.charCodeAt(end)))
|
|
2081
|
+
end += 1;
|
|
2082
|
+
if (!known.has(text.slice(i + 1, end)))
|
|
2083
|
+
continue;
|
|
2084
|
+
if (end < text.length && WORD_CHAR.test(String.fromCodePoint(text.codePointAt(end))))
|
|
2085
|
+
continue;
|
|
2086
|
+
ranges.push({ start: i, end });
|
|
1610
2087
|
}
|
|
1611
|
-
return
|
|
2088
|
+
return ranges;
|
|
1612
2089
|
}
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
2090
|
+
|
|
2091
|
+
// apps/host/src/skill-expansion.ts
|
|
2092
|
+
function mentionedSkillNames(text, names) {
|
|
2093
|
+
return [...new Set(skillMentionRanges(text, names).map((range) => text.slice(range.start + 1, range.end)))];
|
|
2094
|
+
}
|
|
2095
|
+
async function expandSkills(text, names, skills) {
|
|
2096
|
+
const byName = new Map(skills.map((skill) => [skill.name, skill]));
|
|
2097
|
+
const blocks = [];
|
|
2098
|
+
const missing = [];
|
|
2099
|
+
const failures = [];
|
|
2100
|
+
for (const name of new Set(names)) {
|
|
2101
|
+
const skill = byName.get(name);
|
|
2102
|
+
if (skill === undefined) {
|
|
2103
|
+
missing.push(name);
|
|
2104
|
+
continue;
|
|
1622
2105
|
}
|
|
1623
|
-
const { dir, prefix } = dirs.pop();
|
|
1624
|
-
let handle;
|
|
1625
2106
|
try {
|
|
1626
|
-
|
|
2107
|
+
const body = stripFrontmatter(await readFile3(skill.filePath, "utf8")).trim();
|
|
2108
|
+
blocks.push({ name, location: skill.filePath, baseDir: skill.baseDir, body });
|
|
1627
2109
|
} catch (error) {
|
|
1628
|
-
|
|
1629
|
-
throw error;
|
|
1630
|
-
truncated = true;
|
|
1631
|
-
continue;
|
|
2110
|
+
failures.push({ name, filePath: skill.filePath, message: error instanceof Error ? error.message : String(error) });
|
|
1632
2111
|
}
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
2112
|
+
}
|
|
2113
|
+
return { text: withSkillBlocks(blocks, text), missing, failures };
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
// apps/host/src/commands.ts
|
|
2117
|
+
var BUILTIN_COMMANDS = [
|
|
2118
|
+
{
|
|
2119
|
+
info: {
|
|
2120
|
+
name: "compact",
|
|
2121
|
+
description: "Summarize older context to free up space",
|
|
2122
|
+
argumentHint: "[instructions]",
|
|
2123
|
+
source: "builtin"
|
|
2124
|
+
},
|
|
2125
|
+
run: async (target, args) => {
|
|
2126
|
+
await target.compact(args?.trim() || undefined);
|
|
1645
2127
|
}
|
|
1646
|
-
if (truncated)
|
|
1647
|
-
break;
|
|
1648
2128
|
}
|
|
1649
|
-
|
|
2129
|
+
];
|
|
2130
|
+
function listCommands() {
|
|
2131
|
+
return { commands: BUILTIN_COMMANDS.map((command) => command.info) };
|
|
1650
2132
|
}
|
|
1651
|
-
async function
|
|
1652
|
-
const
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
2133
|
+
async function runCommand(target, name, args) {
|
|
2134
|
+
const command = BUILTIN_COMMANDS.find((candidate) => candidate.info.name === name);
|
|
2135
|
+
if (command === undefined)
|
|
2136
|
+
throw new Error(`Unknown command: /${name}`);
|
|
2137
|
+
await command.run(target, args);
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
// packages/protocol/src/images.ts
|
|
2141
|
+
var IMAGE_MIME_TYPES = ["image/jpeg", "image/png", "image/gif", "image/webp"];
|
|
2142
|
+
var REF_ENCODER = new TextEncoder;
|
|
2143
|
+
var MAX_IMAGES_PER_MESSAGE = 4;
|
|
2144
|
+
var MAX_IMAGE_BYTES = 5 * 1024 * 1024;
|
|
2145
|
+
var BASE64 = /^[A-Za-z0-9+/]*={0,2}$/;
|
|
2146
|
+
function imageProblem(images) {
|
|
2147
|
+
if (images.length > MAX_IMAGES_PER_MESSAGE)
|
|
2148
|
+
return `At most ${MAX_IMAGES_PER_MESSAGE} images per message`;
|
|
2149
|
+
for (let i = 0;i < images.length; i++) {
|
|
2150
|
+
const image = images[i];
|
|
2151
|
+
if (!IMAGE_MIME_TYPES.includes(image.mimeType)) {
|
|
2152
|
+
return `Unsupported image type: ${image.mimeType} (use JPEG, PNG, GIF or WebP)`;
|
|
2153
|
+
}
|
|
2154
|
+
if (image.data === "" || image.data.length % 4 !== 0 || !BASE64.test(image.data)) {
|
|
2155
|
+
return `Image ${i + 1} is not valid base64`;
|
|
2156
|
+
}
|
|
2157
|
+
const padding = image.data.endsWith("==") ? 2 : image.data.endsWith("=") ? 1 : 0;
|
|
2158
|
+
if (image.data.length * 3 / 4 - padding > MAX_IMAGE_BYTES)
|
|
2159
|
+
return `Image ${i + 1} is larger than 5 MB`;
|
|
1661
2160
|
}
|
|
1662
|
-
|
|
1663
|
-
|
|
2161
|
+
return;
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
// apps/host/src/image-refs.ts
|
|
2165
|
+
import { createHash } from "node:crypto";
|
|
2166
|
+
import { applyImmutable } from "@earendil-works/chord/delta";
|
|
2167
|
+
var isObject2 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2168
|
+
var isImageBlock = (value) => value.type === "image" && typeof value.data === "string" && typeof value.mimeType === "string";
|
|
2169
|
+
var hashed = new WeakMap;
|
|
2170
|
+
function refOf(block) {
|
|
2171
|
+
let ref = hashed.get(block);
|
|
2172
|
+
if (ref === undefined) {
|
|
2173
|
+
ref = createHash("sha256").update(block.mimeType).update(`
|
|
2174
|
+
`).update(block.data).digest("hex");
|
|
2175
|
+
hashed.set(block, ref);
|
|
1664
2176
|
}
|
|
1665
|
-
return
|
|
2177
|
+
return ref;
|
|
1666
2178
|
}
|
|
1667
|
-
function
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
2179
|
+
function slotAt(path) {
|
|
2180
|
+
if (path[0] === "entries") {
|
|
2181
|
+
if (path.length === 1)
|
|
2182
|
+
return "entries";
|
|
2183
|
+
if (path.length === 2 && typeof path[1] === "number")
|
|
2184
|
+
return "entry";
|
|
2185
|
+
if (path.length === 3 && typeof path[1] === "number" && path[2] === "model")
|
|
2186
|
+
return "messages";
|
|
2187
|
+
if (path.length === 4 && typeof path[1] === "number" && path[2] === "model" && typeof path[3] === "number")
|
|
2188
|
+
return "message";
|
|
2189
|
+
if (path.length === 5 && typeof path[1] === "number" && path[2] === "model" && typeof path[3] === "number" && path[4] === "content")
|
|
2190
|
+
return "content";
|
|
2191
|
+
if (path.length === 6 && typeof path[1] === "number" && path[2] === "model" && typeof path[3] === "number" && path[4] === "content" && typeof path[5] === "number")
|
|
2192
|
+
return "block";
|
|
2193
|
+
return "none";
|
|
1672
2194
|
}
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
if (
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
2195
|
+
if (path[0] !== "docs")
|
|
2196
|
+
return "none";
|
|
2197
|
+
if (path.length === 1)
|
|
2198
|
+
return "docs";
|
|
2199
|
+
if (path[1] === "pi.inbox") {
|
|
2200
|
+
if (path.length === 2)
|
|
2201
|
+
return "inbox";
|
|
2202
|
+
if (path.length === 3 && path[2] === "items")
|
|
2203
|
+
return "items";
|
|
2204
|
+
if (path.length === 4 && path[2] === "items" && typeof path[3] === "number")
|
|
2205
|
+
return "item";
|
|
2206
|
+
if (path.length === 5 && path[2] === "items" && typeof path[3] === "number" && path[4] === "content")
|
|
2207
|
+
return "content";
|
|
2208
|
+
if (path.length === 6 && path[2] === "items" && typeof path[3] === "number" && path[4] === "content" && typeof path[5] === "number")
|
|
2209
|
+
return "block";
|
|
2210
|
+
return "none";
|
|
1684
2211
|
}
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
2212
|
+
if (path[1] === "pi.live") {
|
|
2213
|
+
if (path.length === 2)
|
|
2214
|
+
return "live";
|
|
2215
|
+
if (path.length === 3 && path[2] === "generation")
|
|
2216
|
+
return "generation";
|
|
2217
|
+
if (path.length === 4 && path[2] === "generation" && path[3] === "message")
|
|
2218
|
+
return "message";
|
|
2219
|
+
if (path.length === 5 && path[2] === "generation" && path[3] === "message" && path[4] === "content")
|
|
2220
|
+
return "content";
|
|
2221
|
+
if (path.length === 6 && path[2] === "generation" && path[3] === "message" && path[4] === "content" && typeof path[5] === "number")
|
|
2222
|
+
return "block";
|
|
2223
|
+
return "none";
|
|
2224
|
+
}
|
|
2225
|
+
return "none";
|
|
1688
2226
|
}
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
if (
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
2227
|
+
function withRef(block) {
|
|
2228
|
+
const ref = refOf(block);
|
|
2229
|
+
const { data: _data, ...rest } = block;
|
|
2230
|
+
return { ...rest, ref };
|
|
2231
|
+
}
|
|
2232
|
+
var HASH_YIELD_BYTES = 8 * 1024 * 1024;
|
|
2233
|
+
var yieldToEventLoop = () => new Promise((resolve5) => setImmediate(resolve5));
|
|
2234
|
+
async function hashBlocks(blocks) {
|
|
2235
|
+
let bytes = 0;
|
|
2236
|
+
for (const block of blocks) {
|
|
2237
|
+
if (hashed.has(block))
|
|
2238
|
+
continue;
|
|
2239
|
+
if (bytes >= HASH_YIELD_BYTES) {
|
|
2240
|
+
bytes = 0;
|
|
2241
|
+
await yieldToEventLoop();
|
|
2242
|
+
}
|
|
2243
|
+
refOf(block);
|
|
2244
|
+
bytes += block.data.length;
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
function rewriteEntries(value) {
|
|
2248
|
+
if (!Array.isArray(value))
|
|
2249
|
+
return value;
|
|
2250
|
+
let out;
|
|
2251
|
+
for (let i = 0;i < value.length; i++) {
|
|
2252
|
+
const next = rewriteEntry(value[i]);
|
|
2253
|
+
if (next !== value[i])
|
|
2254
|
+
(out ??= value.slice())[i] = next;
|
|
2255
|
+
}
|
|
2256
|
+
return out ?? value;
|
|
2257
|
+
}
|
|
2258
|
+
function rewriteEntry(value) {
|
|
2259
|
+
if (!isObject2(value) || value.model === undefined)
|
|
2260
|
+
return value;
|
|
2261
|
+
const model = rewriteMessages(value.model);
|
|
2262
|
+
return model === value.model ? value : { ...value, model };
|
|
2263
|
+
}
|
|
2264
|
+
function rewriteMessages(value) {
|
|
2265
|
+
if (!Array.isArray(value))
|
|
2266
|
+
return value;
|
|
2267
|
+
let out;
|
|
2268
|
+
for (let i = 0;i < value.length; i++) {
|
|
2269
|
+
const next = rewriteMessage(value[i]);
|
|
2270
|
+
if (next !== value[i])
|
|
2271
|
+
(out ??= value.slice())[i] = next;
|
|
2272
|
+
}
|
|
2273
|
+
return out ?? value;
|
|
2274
|
+
}
|
|
2275
|
+
function rewriteMessage(value) {
|
|
2276
|
+
if (!isObject2(value) || value.content === undefined)
|
|
2277
|
+
return value;
|
|
2278
|
+
const content = rewriteContent(value.content);
|
|
2279
|
+
return content === value.content ? value : { ...value, content };
|
|
2280
|
+
}
|
|
2281
|
+
function rewriteContent(value) {
|
|
2282
|
+
if (!Array.isArray(value))
|
|
2283
|
+
return value;
|
|
2284
|
+
let out;
|
|
2285
|
+
for (let i = 0;i < value.length; i++) {
|
|
2286
|
+
const block = value[i];
|
|
2287
|
+
const next = isObject2(block) && isImageBlock(block) ? withRef(block) : block;
|
|
2288
|
+
if (next !== block)
|
|
2289
|
+
(out ??= value.slice())[i] = next;
|
|
2290
|
+
}
|
|
2291
|
+
return out ?? value;
|
|
2292
|
+
}
|
|
2293
|
+
function rewriteItems(value) {
|
|
2294
|
+
if (!Array.isArray(value))
|
|
2295
|
+
return value;
|
|
2296
|
+
let out;
|
|
2297
|
+
for (let i = 0;i < value.length; i++) {
|
|
2298
|
+
const next = rewriteItem(value[i]);
|
|
2299
|
+
if (next !== value[i])
|
|
2300
|
+
(out ??= value.slice())[i] = next;
|
|
2301
|
+
}
|
|
2302
|
+
return out ?? value;
|
|
2303
|
+
}
|
|
2304
|
+
function rewriteItem(value) {
|
|
2305
|
+
if (!isObject2(value) || value.content === undefined)
|
|
2306
|
+
return value;
|
|
2307
|
+
const content = rewriteContent(value.content);
|
|
2308
|
+
return content === value.content ? value : { ...value, content };
|
|
2309
|
+
}
|
|
2310
|
+
function rewriteInbox(value) {
|
|
2311
|
+
if (!isObject2(value) || value.items === undefined)
|
|
2312
|
+
return value;
|
|
2313
|
+
const items = rewriteItems(value.items);
|
|
2314
|
+
return items === value.items ? value : { ...value, items };
|
|
2315
|
+
}
|
|
2316
|
+
function rewriteGeneration(value) {
|
|
2317
|
+
if (!isObject2(value) || value.message === undefined)
|
|
2318
|
+
return value;
|
|
2319
|
+
const message = rewriteMessage(value.message);
|
|
2320
|
+
return message === value.message ? value : { ...value, message };
|
|
2321
|
+
}
|
|
2322
|
+
function rewriteLive(value) {
|
|
2323
|
+
if (!isObject2(value) || value.generation === undefined)
|
|
2324
|
+
return value;
|
|
2325
|
+
const generation = rewriteGeneration(value.generation);
|
|
2326
|
+
return generation === value.generation ? value : { ...value, generation };
|
|
2327
|
+
}
|
|
2328
|
+
function rewriteDocs(value) {
|
|
2329
|
+
if (!isObject2(value))
|
|
2330
|
+
return value;
|
|
2331
|
+
let out;
|
|
2332
|
+
for (const key of ["pi.inbox", "pi.live"]) {
|
|
2333
|
+
const doc = value[key];
|
|
2334
|
+
if (doc === undefined)
|
|
2335
|
+
continue;
|
|
2336
|
+
const next = key === "pi.inbox" ? rewriteInbox(doc) : rewriteLive(doc);
|
|
2337
|
+
if (next !== doc)
|
|
2338
|
+
(out ??= { ...value })[key] = next;
|
|
2339
|
+
}
|
|
2340
|
+
return out ?? value;
|
|
2341
|
+
}
|
|
2342
|
+
function rewriteView(value) {
|
|
2343
|
+
if (!isObject2(value))
|
|
2344
|
+
return value;
|
|
2345
|
+
let out;
|
|
2346
|
+
if (value.entries !== undefined) {
|
|
2347
|
+
const entries = rewriteEntries(value.entries);
|
|
2348
|
+
if (entries !== value.entries)
|
|
2349
|
+
(out ??= { ...value }).entries = entries;
|
|
2350
|
+
}
|
|
2351
|
+
if (value.docs !== undefined) {
|
|
2352
|
+
const docs = rewriteDocs(value.docs);
|
|
2353
|
+
if (docs !== value.docs)
|
|
2354
|
+
(out ??= { ...value }).docs = docs;
|
|
2355
|
+
}
|
|
2356
|
+
return out ?? value;
|
|
2357
|
+
}
|
|
2358
|
+
function rewriteSlot(slot, value) {
|
|
2359
|
+
switch (slot) {
|
|
2360
|
+
case "entries":
|
|
2361
|
+
return rewriteEntries(value);
|
|
2362
|
+
case "entry":
|
|
2363
|
+
return rewriteEntry(value);
|
|
2364
|
+
case "messages":
|
|
2365
|
+
return rewriteMessages(value);
|
|
2366
|
+
case "message":
|
|
2367
|
+
return rewriteMessage(value);
|
|
2368
|
+
case "content":
|
|
2369
|
+
return rewriteContent(value);
|
|
2370
|
+
case "block":
|
|
2371
|
+
return isObject2(value) && isImageBlock(value) ? withRef(value) : value;
|
|
2372
|
+
case "docs":
|
|
2373
|
+
return rewriteDocs(value);
|
|
2374
|
+
case "inbox":
|
|
2375
|
+
return rewriteInbox(value);
|
|
2376
|
+
case "items":
|
|
2377
|
+
return rewriteItems(value);
|
|
2378
|
+
case "item":
|
|
2379
|
+
return rewriteItem(value);
|
|
2380
|
+
case "live":
|
|
2381
|
+
return rewriteLive(value);
|
|
2382
|
+
case "generation":
|
|
2383
|
+
return rewriteGeneration(value);
|
|
2384
|
+
default:
|
|
2385
|
+
return value;
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
function rewriteOp(op) {
|
|
2389
|
+
switch (op[0]) {
|
|
2390
|
+
case "r": {
|
|
2391
|
+
const value = rewriteView(op[1]);
|
|
2392
|
+
return value === op[1] ? op : ["r", value];
|
|
2393
|
+
}
|
|
2394
|
+
case "s": {
|
|
2395
|
+
const slot = slotAt(op[1]);
|
|
2396
|
+
if (slot === "none")
|
|
2397
|
+
return op;
|
|
2398
|
+
const value = rewriteSlot(slot, op[2]);
|
|
2399
|
+
return value === op[2] ? op : ["s", op[1], value];
|
|
1706
2400
|
}
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
2401
|
+
case "p": {
|
|
2402
|
+
const path = op[1];
|
|
2403
|
+
const start = op[2];
|
|
2404
|
+
let items;
|
|
2405
|
+
for (let i = 0;i < op[4].length; i++) {
|
|
2406
|
+
const item = op[4][i];
|
|
2407
|
+
const slot = slotAt([...path, start + i]);
|
|
2408
|
+
const next = slot === "none" ? item : rewriteSlot(slot, item);
|
|
2409
|
+
if (next !== item)
|
|
2410
|
+
(items ??= op[4].slice())[i] = next;
|
|
2411
|
+
}
|
|
2412
|
+
return items === undefined ? op : ["p", op[1], op[2], op[3], items];
|
|
1713
2413
|
}
|
|
1714
|
-
|
|
2414
|
+
default:
|
|
2415
|
+
return op;
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
function resolve5(root, path, length) {
|
|
2419
|
+
let node = root;
|
|
2420
|
+
for (let i = 0;i < length; i++) {
|
|
2421
|
+
if (node === null || typeof node !== "object")
|
|
1715
2422
|
return;
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
2423
|
+
node = node[path[i]];
|
|
2424
|
+
}
|
|
2425
|
+
return node;
|
|
2426
|
+
}
|
|
2427
|
+
function reachesIntoImage(op, root) {
|
|
2428
|
+
if (op[0] === "r" || op[0] === "m")
|
|
2429
|
+
return false;
|
|
2430
|
+
const path = op[1];
|
|
2431
|
+
const depth = op[0] === "p" ? path.length : path.length - 1;
|
|
2432
|
+
for (let length = 1;length <= depth; length++) {
|
|
2433
|
+
if (slotAt(path.slice(0, length)) !== "block")
|
|
2434
|
+
continue;
|
|
2435
|
+
const node = resolve5(root, path, length);
|
|
2436
|
+
if (isObject2(node) && isImageBlock(node))
|
|
2437
|
+
return true;
|
|
2438
|
+
}
|
|
2439
|
+
return false;
|
|
2440
|
+
}
|
|
2441
|
+
function* contentArrays(view) {
|
|
2442
|
+
if (!isObject2(view))
|
|
2443
|
+
return;
|
|
2444
|
+
if (Array.isArray(view.entries)) {
|
|
2445
|
+
for (const entry of view.entries) {
|
|
2446
|
+
if (!isObject2(entry) || !Array.isArray(entry.model))
|
|
2447
|
+
continue;
|
|
2448
|
+
for (const message of entry.model) {
|
|
2449
|
+
if (isObject2(message) && Array.isArray(message.content))
|
|
2450
|
+
yield message.content;
|
|
2451
|
+
}
|
|
1719
2452
|
}
|
|
1720
|
-
root = normalized;
|
|
1721
|
-
mode = scope.checkout === "worktree" ? "head" : "live";
|
|
1722
2453
|
}
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
2454
|
+
if (!isObject2(view.docs))
|
|
2455
|
+
return;
|
|
2456
|
+
const inbox = view.docs["pi.inbox"];
|
|
2457
|
+
if (isObject2(inbox) && Array.isArray(inbox.items)) {
|
|
2458
|
+
for (const item of inbox.items) {
|
|
2459
|
+
if (isObject2(item) && Array.isArray(item.content))
|
|
2460
|
+
yield item.content;
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
const live = view.docs["pi.live"];
|
|
2464
|
+
if (isObject2(live) && isObject2(live.generation) && isObject2(live.generation.message) && Array.isArray(live.generation.message.content)) {
|
|
2465
|
+
yield live.generation.message.content;
|
|
2466
|
+
}
|
|
1726
2467
|
}
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
function imageProblem(images) {
|
|
1734
|
-
if (images.length > MAX_IMAGES_PER_MESSAGE)
|
|
1735
|
-
return `At most ${MAX_IMAGES_PER_MESSAGE} images per message`;
|
|
1736
|
-
for (let i = 0;i < images.length; i++) {
|
|
1737
|
-
const image = images[i];
|
|
1738
|
-
if (!IMAGE_MIME_TYPES.includes(image.mimeType)) {
|
|
1739
|
-
return `Unsupported image type: ${image.mimeType} (use JPEG, PNG, GIF or WebP)`;
|
|
2468
|
+
function imageBlocks(view) {
|
|
2469
|
+
const blocks = [];
|
|
2470
|
+
for (const content of contentArrays(view)) {
|
|
2471
|
+
for (const block of content) {
|
|
2472
|
+
if (isObject2(block) && isImageBlock(block))
|
|
2473
|
+
blocks.push(block);
|
|
1740
2474
|
}
|
|
1741
|
-
|
|
1742
|
-
|
|
2475
|
+
}
|
|
2476
|
+
return blocks;
|
|
2477
|
+
}
|
|
2478
|
+
function findImage(view, ref) {
|
|
2479
|
+
for (const block of imageBlocks(view)) {
|
|
2480
|
+
if (refOf(block) === ref) {
|
|
2481
|
+
return { data: block.data, mimeType: block.mimeType };
|
|
1743
2482
|
}
|
|
1744
|
-
const padding = image.data.endsWith("==") ? 2 : image.data.endsWith("=") ? 1 : 0;
|
|
1745
|
-
if (image.data.length * 3 / 4 - padding > MAX_IMAGE_BYTES)
|
|
1746
|
-
return `Image ${i + 1} is larger than 5 MB`;
|
|
1747
2483
|
}
|
|
1748
2484
|
return;
|
|
1749
2485
|
}
|
|
1750
2486
|
|
|
2487
|
+
class ImageRefs {
|
|
2488
|
+
async value(view) {
|
|
2489
|
+
const blocks = imageBlocks(view);
|
|
2490
|
+
if (blocks.length > 0)
|
|
2491
|
+
await hashBlocks(blocks);
|
|
2492
|
+
return rewriteView(view);
|
|
2493
|
+
}
|
|
2494
|
+
image(view, ref) {
|
|
2495
|
+
return findImage(view, ref);
|
|
2496
|
+
}
|
|
2497
|
+
async ops(ops, before) {
|
|
2498
|
+
let state = before;
|
|
2499
|
+
let touched = false;
|
|
2500
|
+
for (const op of ops) {
|
|
2501
|
+
if (!touched && reachesIntoImage(op, state))
|
|
2502
|
+
touched = true;
|
|
2503
|
+
state = applyImmutable(state, [op]);
|
|
2504
|
+
}
|
|
2505
|
+
if (touched) {
|
|
2506
|
+
await hashBlocks(imageBlocks(state));
|
|
2507
|
+
return [["r", rewriteView(state)]];
|
|
2508
|
+
}
|
|
2509
|
+
if (ops.some((op) => op[0] === "r"))
|
|
2510
|
+
await hashBlocks(imageBlocks(state));
|
|
2511
|
+
return ops.map(rewriteOp);
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
|
|
1751
2515
|
// apps/host/src/auto-title.ts
|
|
1752
2516
|
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
|
1753
2517
|
|
|
1754
2518
|
// apps/host/src/title-doc.ts
|
|
1755
|
-
import { defineDoc as
|
|
1756
|
-
var TitleDoc =
|
|
2519
|
+
import { defineDoc as defineDoc6 } from "@earendil-works/pi-durable";
|
|
2520
|
+
var TitleDoc = defineDoc6({
|
|
1757
2521
|
kind: "pinomad.title",
|
|
1758
2522
|
version: 1,
|
|
1759
2523
|
scope: "conversation",
|
|
@@ -1822,7 +2586,8 @@ function attachAutoTitle(runs, deps) {
|
|
|
1822
2586
|
const model = ref === null ? undefined : deps.models.getModel(ref.provider, ref.modelId);
|
|
1823
2587
|
if (model === undefined)
|
|
1824
2588
|
return;
|
|
1825
|
-
const
|
|
2589
|
+
const firstMessage = first.model?.[0];
|
|
2590
|
+
const firstText = (stripUserBlocks(firstMessage?.content ?? "").text || "(images or files only)").slice(0, TITLE_INPUT_LIMIT);
|
|
1826
2591
|
const answer = textOf(last.content).slice(0, TITLE_INPUT_LIMIT);
|
|
1827
2592
|
const request = {
|
|
1828
2593
|
systemPrompt: TITLE_PROMPT,
|
|
@@ -1869,9 +2634,9 @@ ${answer}`}`,
|
|
|
1869
2634
|
}
|
|
1870
2635
|
|
|
1871
2636
|
// apps/host/src/push.ts
|
|
1872
|
-
import { createHash } from "node:crypto";
|
|
2637
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
1873
2638
|
import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT2 } from "@earendil-works/chord/context";
|
|
1874
|
-
import { defineDoc as
|
|
2639
|
+
import { defineDoc as defineDoc7 } from "@earendil-works/pi-durable";
|
|
1875
2640
|
|
|
1876
2641
|
// packages/client/src/family.ts
|
|
1877
2642
|
var SUBAGENT_TASK_PREFIX = /^\s*in the working directory\s+\S+,\s*/i;
|
|
@@ -1929,7 +2694,7 @@ function diffRunNotices(previous, organized) {
|
|
|
1929
2694
|
|
|
1930
2695
|
// apps/host/src/push.ts
|
|
1931
2696
|
var context2 = BACKGROUND_CONTEXT2;
|
|
1932
|
-
var PushDoc =
|
|
2697
|
+
var PushDoc = defineDoc7({
|
|
1933
2698
|
kind: "pinomad.push",
|
|
1934
2699
|
version: 2,
|
|
1935
2700
|
scope: "session",
|
|
@@ -2012,7 +2777,7 @@ var realTimers = {
|
|
|
2012
2777
|
clearTimeout: (handle) => clearTimeout(handle)
|
|
2013
2778
|
};
|
|
2014
2779
|
var tokenHint = (token) => `…${token.slice(-6)}`;
|
|
2015
|
-
var collapseId = (hostKey, id) =>
|
|
2780
|
+
var collapseId = (hostKey, id) => createHash2("sha256").update(`${hostKey}:${id}`).digest("hex").slice(0, 32);
|
|
2016
2781
|
async function attachPushSender(options) {
|
|
2017
2782
|
const { harness, conversations } = options;
|
|
2018
2783
|
const transport = options.transport ?? UNAVAILABLE_TRANSPORT;
|
|
@@ -2062,7 +2827,7 @@ async function attachPushSender(options) {
|
|
|
2062
2827
|
return named;
|
|
2063
2828
|
if (notice.body !== "Finished" || disposed)
|
|
2064
2829
|
return FALLBACK_TITLE;
|
|
2065
|
-
return await new Promise((
|
|
2830
|
+
return await new Promise((resolve6) => {
|
|
2066
2831
|
const waiters = titleWaiters.get(notice.id) ?? new Set;
|
|
2067
2832
|
titleWaiters.set(notice.id, waiters);
|
|
2068
2833
|
const settle = (title) => {
|
|
@@ -2070,7 +2835,7 @@ async function attachPushSender(options) {
|
|
|
2070
2835
|
if (waiters.size === 0)
|
|
2071
2836
|
titleWaiters.delete(notice.id);
|
|
2072
2837
|
timers.clearTimeout(timer);
|
|
2073
|
-
|
|
2838
|
+
resolve6(title);
|
|
2074
2839
|
};
|
|
2075
2840
|
const timer = timers.setTimeout(() => settle(FALLBACK_TITLE), TITLE_WAIT_MS);
|
|
2076
2841
|
waiters.add(settle);
|
|
@@ -2270,6 +3035,28 @@ function toolTarget(args) {
|
|
|
2270
3035
|
// apps/host/src/gateway.ts
|
|
2271
3036
|
var context3 = BACKGROUND_CONTEXT3;
|
|
2272
3037
|
var forkTails = new WeakMap;
|
|
3038
|
+
function assertQueuedInput(record, conversationId) {
|
|
3039
|
+
if (record.conversationId !== conversationId) {
|
|
3040
|
+
throw new Error(`Submission ${record.id} belongs to another conversation`);
|
|
3041
|
+
}
|
|
3042
|
+
if (record.type !== "input")
|
|
3043
|
+
throw new Error(`Submission ${record.id} is a write, not a message`);
|
|
3044
|
+
}
|
|
3045
|
+
async function dismissPendingQuestions(tx, docs, conversationId) {
|
|
3046
|
+
let dismissed = false;
|
|
3047
|
+
for (const doc of docs) {
|
|
3048
|
+
const value = await tx.doc(doc.token, conversationId);
|
|
3049
|
+
if (!Value2.Check(QuestionSchema, value))
|
|
3050
|
+
continue;
|
|
3051
|
+
for (const request of value.requests) {
|
|
3052
|
+
if (request.resolution === undefined) {
|
|
3053
|
+
request.resolution = { outcome: "dismissed", at: Date.now() };
|
|
3054
|
+
dismissed = true;
|
|
3055
|
+
}
|
|
3056
|
+
}
|
|
3057
|
+
}
|
|
3058
|
+
return dismissed;
|
|
3059
|
+
}
|
|
2273
3060
|
function serializedFork(harness, work) {
|
|
2274
3061
|
const next = (forkTails.get(harness) ?? Promise.resolve()).then(work, work);
|
|
2275
3062
|
const settled = next.then(() => {}, () => {});
|
|
@@ -2284,6 +3071,7 @@ var LOOPBACK_SECURE_PATH = "/secure";
|
|
|
2284
3071
|
async function startGateway(options) {
|
|
2285
3072
|
const conversations = await ConversationList.open(options.harness, options.docs);
|
|
2286
3073
|
const fileLister = createCandidateLister(enumerateRoot);
|
|
3074
|
+
const skillLister = createSkillLister((root, mode) => enumerateSkills(root, mode, options.agentsHome));
|
|
2287
3075
|
const detachAutoTitle = options.autoTitle === false ? undefined : attachAutoTitle(conversations, {
|
|
2288
3076
|
harness: options.harness,
|
|
2289
3077
|
models: options.models,
|
|
@@ -2296,12 +3084,12 @@ async function startGateway(options) {
|
|
|
2296
3084
|
response.end();
|
|
2297
3085
|
});
|
|
2298
3086
|
});
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
http.once("listening", resolve4);
|
|
3087
|
+
await new Promise((resolve6, reject) => {
|
|
3088
|
+
http.once("listening", resolve6);
|
|
2302
3089
|
http.once("error", reject);
|
|
2303
3090
|
http.listen(options.port, "127.0.0.1");
|
|
2304
3091
|
});
|
|
3092
|
+
const server = new WebSocketServer({ server: http });
|
|
2305
3093
|
const { port } = http.address();
|
|
2306
3094
|
const browserOrigins = [...options.browserOrigins ?? [], `http://127.0.0.1:${port}`];
|
|
2307
3095
|
const clients = new Set;
|
|
@@ -2315,7 +3103,7 @@ async function startGateway(options) {
|
|
|
2315
3103
|
if (joined?.source === "account") {
|
|
2316
3104
|
broadcast("info", `New device "${joined.name}" joined through your account`);
|
|
2317
3105
|
}
|
|
2318
|
-
const client = new GatewayClient(connection, options, conversations, fileLister, {
|
|
3106
|
+
const client = new GatewayClient(connection, options, conversations, fileLister, skillLister, {
|
|
2319
3107
|
devicePublicKey,
|
|
2320
3108
|
closed: (self) => {
|
|
2321
3109
|
clients.delete(self);
|
|
@@ -2375,7 +3163,11 @@ async function startGateway(options) {
|
|
|
2375
3163
|
}
|
|
2376
3164
|
register(tokenConnection(socket));
|
|
2377
3165
|
});
|
|
2378
|
-
remote = await startRemote(options, admission, register)
|
|
3166
|
+
remote = await startRemote(options, admission, register).catch(async (error) => {
|
|
3167
|
+
await new Promise((resolve6) => server.close(() => resolve6()));
|
|
3168
|
+
await new Promise((resolve6) => http.close(() => resolve6()));
|
|
3169
|
+
throw error;
|
|
3170
|
+
});
|
|
2379
3171
|
const secure = options.secure;
|
|
2380
3172
|
const detachSettingsWatch = options.settings.subscribe((settings) => {
|
|
2381
3173
|
if (!settings.acceptAccountDevices)
|
|
@@ -2447,8 +3239,8 @@ async function startGateway(options) {
|
|
|
2447
3239
|
for (const socket of server.clients)
|
|
2448
3240
|
socket.terminate();
|
|
2449
3241
|
await remote?.close();
|
|
2450
|
-
await new Promise((
|
|
2451
|
-
await new Promise((
|
|
3242
|
+
await new Promise((resolve6) => server.close(() => resolve6()));
|
|
3243
|
+
await new Promise((resolve6) => http.close(() => resolve6()));
|
|
2452
3244
|
}
|
|
2453
3245
|
};
|
|
2454
3246
|
}
|
|
@@ -2457,7 +3249,7 @@ function tokenConnection(socket) {
|
|
|
2457
3249
|
get open() {
|
|
2458
3250
|
return socket.readyState === WebSocket.OPEN;
|
|
2459
3251
|
},
|
|
2460
|
-
send: (text) => socket.readyState === WebSocket.OPEN ? new Promise((
|
|
3252
|
+
send: (text) => socket.readyState === WebSocket.OPEN ? new Promise((resolve6) => socket.send(text, () => resolve6())) : Promise.resolve(),
|
|
2461
3253
|
close: (code, reason) => socket.close(code, reason),
|
|
2462
3254
|
handle(handlers) {
|
|
2463
3255
|
socket.on("message", (data) => handlers.message(String(data)));
|
|
@@ -2470,9 +3262,9 @@ function secureConnection(socket, session) {
|
|
|
2470
3262
|
get open() {
|
|
2471
3263
|
return socket.readyState === WebSocket.OPEN;
|
|
2472
3264
|
},
|
|
2473
|
-
send: (text) => socket.readyState === WebSocket.OPEN ? new Promise((
|
|
3265
|
+
send: (text) => socket.readyState === WebSocket.OPEN ? new Promise((resolve6) => {
|
|
2474
3266
|
const chunks = session.seal(text);
|
|
2475
|
-
chunks.forEach((chunk, index) => socket.send(chunk, index === chunks.length - 1 ? () =>
|
|
3267
|
+
chunks.forEach((chunk, index) => socket.send(chunk, index === chunks.length - 1 ? () => resolve6() : undefined));
|
|
2476
3268
|
}) : Promise.resolve(),
|
|
2477
3269
|
close: (code, reason) => socket.close(code, reason),
|
|
2478
3270
|
handle(handlers) {
|
|
@@ -2575,13 +3367,13 @@ async function startRemote(options, admission, register) {
|
|
|
2575
3367
|
response.end();
|
|
2576
3368
|
});
|
|
2577
3369
|
});
|
|
2578
|
-
const secure = new WebSocketServer({ server: http });
|
|
2579
|
-
secure.on("connection", (socket) => secureHandshake(socket, spec, admission, register));
|
|
2580
3370
|
await new Promise((resolveListen, reject) => {
|
|
2581
3371
|
http.once("listening", resolveListen);
|
|
2582
3372
|
http.once("error", reject);
|
|
2583
3373
|
http.listen(remote.port, "0.0.0.0");
|
|
2584
3374
|
});
|
|
3375
|
+
const secure = new WebSocketServer({ server: http });
|
|
3376
|
+
secure.on("connection", (socket) => secureHandshake(socket, spec, admission, register));
|
|
2585
3377
|
const { port } = http.address();
|
|
2586
3378
|
const url = `ws://127.0.0.1:${port}`;
|
|
2587
3379
|
const advertiseUrl = remote.publicUrl?.replace(/\/+$/, "") ?? `http://${pickLanAddress(networkInterfaces()) ?? "127.0.0.1"}:${port}`;
|
|
@@ -2591,8 +3383,8 @@ async function startRemote(options, admission, register) {
|
|
|
2591
3383
|
async close() {
|
|
2592
3384
|
for (const socket of secure.clients)
|
|
2593
3385
|
socket.terminate();
|
|
2594
|
-
await new Promise((
|
|
2595
|
-
await new Promise((
|
|
3386
|
+
await new Promise((resolve6) => secure.close(() => resolve6()));
|
|
3387
|
+
await new Promise((resolve6) => http.close(() => resolve6()));
|
|
2596
3388
|
}
|
|
2597
3389
|
};
|
|
2598
3390
|
}
|
|
@@ -2658,6 +3450,7 @@ class ConversationList {
|
|
|
2658
3450
|
if (typeof subagent?.role === "string" && subagent.role !== "")
|
|
2659
3451
|
facts.role = subagent.role;
|
|
2660
3452
|
}
|
|
3453
|
+
facts.agent = roleNameOf(await harness.snapshot(RoleDoc, record.id, context3));
|
|
2661
3454
|
const named = (await harness.snapshot(TitleDoc, record.id, context3))?.title;
|
|
2662
3455
|
if (typeof named === "string" && named !== "")
|
|
2663
3456
|
facts.named = named;
|
|
@@ -2714,6 +3507,9 @@ class ConversationList {
|
|
|
2714
3507
|
next.delete(change.record.kind);
|
|
2715
3508
|
facts.pending = next;
|
|
2716
3509
|
touched.add(id);
|
|
3510
|
+
} else if (change.record.kind === RoleDoc.definition.kind) {
|
|
3511
|
+
facts.agent = roleNameOf(change.value);
|
|
3512
|
+
touched.add(id);
|
|
2717
3513
|
} else if (change.record.kind === SubagentDoc.definition.kind) {
|
|
2718
3514
|
const value = change.value;
|
|
2719
3515
|
facts.label = typeof value?.label === "string" && value.label !== "" ? value.label : undefined;
|
|
@@ -2746,7 +3542,10 @@ class ConversationList {
|
|
|
2746
3542
|
}
|
|
2747
3543
|
#summaryOf(record) {
|
|
2748
3544
|
const facts = this.#fact(record.id);
|
|
2749
|
-
|
|
3545
|
+
if (record.owner !== undefined)
|
|
3546
|
+
return summaryOf(record, facts, facts.role, facts.role ?? null);
|
|
3547
|
+
const role = topLevelRole(record, this.#records, this.#roles);
|
|
3548
|
+
return summaryOf(record, facts, role, (facts.agent === undefined ? role : facts.agent) ?? null);
|
|
2750
3549
|
}
|
|
2751
3550
|
#refreshSummaries(touched) {
|
|
2752
3551
|
let next;
|
|
@@ -2797,6 +3596,11 @@ class ConversationList {
|
|
|
2797
3596
|
this.#listeners.clear();
|
|
2798
3597
|
}
|
|
2799
3598
|
}
|
|
3599
|
+
function roleNameOf(value) {
|
|
3600
|
+
if (value === null || value === undefined)
|
|
3601
|
+
return;
|
|
3602
|
+
return typeof value.name === "string" ? value.name : value.name === null ? null : undefined;
|
|
3603
|
+
}
|
|
2800
3604
|
function rolesOf(index) {
|
|
2801
3605
|
const roles = new Map;
|
|
2802
3606
|
for (const entry of index?.conversations ?? []) {
|
|
@@ -2816,7 +3620,7 @@ function topLevelRole(record, records, roles) {
|
|
|
2816
3620
|
}
|
|
2817
3621
|
return;
|
|
2818
3622
|
}
|
|
2819
|
-
function summaryOf(record, facts, role) {
|
|
3623
|
+
function summaryOf(record, facts, role, currentAgent) {
|
|
2820
3624
|
const parent = record.parent?.conversationId ?? record.owner?.conversationId;
|
|
2821
3625
|
const status = facts.pending.size > 0 ? "needs-answer" : facts.running === true ? "running" : facts.failed === true ? "failed" : undefined;
|
|
2822
3626
|
const runningSlot = facts.running === true ? facts.tools?.find((slot) => slot.status === "running") : undefined;
|
|
@@ -2834,18 +3638,18 @@ function summaryOf(record, facts, role) {
|
|
|
2834
3638
|
...facts.updatedAt === undefined ? {} : { updatedAt: facts.updatedAt },
|
|
2835
3639
|
...facts.label === undefined ? {} : { label: facts.label },
|
|
2836
3640
|
...role === undefined ? {} : { role },
|
|
3641
|
+
currentAgent,
|
|
2837
3642
|
...activity === undefined ? {} : { activity }
|
|
2838
3643
|
};
|
|
2839
3644
|
}
|
|
2840
3645
|
function sameSummary(a, b) {
|
|
2841
|
-
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;
|
|
3646
|
+
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.currentAgent === b.currentAgent && a.activity?.tool === b.activity?.tool && a.activity?.target === b.activity?.target;
|
|
2842
3647
|
}
|
|
2843
3648
|
function titleOf(entry) {
|
|
2844
3649
|
const message = entry?.model?.[0];
|
|
2845
3650
|
if (message?.role !== "user")
|
|
2846
3651
|
return {};
|
|
2847
|
-
const
|
|
2848
|
-
const title = text.replace(/\s+/g, " ").trim();
|
|
3652
|
+
const title = stripUserBlocks(message.content).text.replace(/\s+/g, " ").trim();
|
|
2849
3653
|
return title === "" ? {} : { title };
|
|
2850
3654
|
}
|
|
2851
3655
|
async function historyFacts(harness, id) {
|
|
@@ -2890,13 +3694,15 @@ class GatewayClient {
|
|
|
2890
3694
|
#hooks;
|
|
2891
3695
|
#options;
|
|
2892
3696
|
#files;
|
|
3697
|
+
#skills;
|
|
2893
3698
|
#hostValues;
|
|
2894
3699
|
#subscriptions = new Map;
|
|
2895
3700
|
#disposed = false;
|
|
2896
|
-
constructor(connection, options, conversations, files, hooks) {
|
|
3701
|
+
constructor(connection, options, conversations, files, skills, hooks) {
|
|
2897
3702
|
this.#connection = connection;
|
|
2898
3703
|
this.#options = options;
|
|
2899
3704
|
this.#files = files;
|
|
3705
|
+
this.#skills = skills;
|
|
2900
3706
|
this.#hostValues = {
|
|
2901
3707
|
conversations,
|
|
2902
3708
|
mcp: options.mcp ?? constantValue(null),
|
|
@@ -2961,15 +3767,15 @@ class GatewayClient {
|
|
|
2961
3767
|
}
|
|
2962
3768
|
async#receive(frame) {
|
|
2963
3769
|
if (frame.type === "subscribe")
|
|
2964
|
-
await this.#subscribe(frame.stream);
|
|
3770
|
+
await this.#subscribe(frame.stream, frame.images === "ref");
|
|
2965
3771
|
else if (frame.type === "unsubscribe")
|
|
2966
3772
|
await this.#unsubscribe(frame.stream);
|
|
2967
3773
|
else
|
|
2968
3774
|
await this.#answer(frame.id, frame.method, frame.args);
|
|
2969
3775
|
}
|
|
2970
|
-
async#subscribe(stream) {
|
|
3776
|
+
async#subscribe(stream, imageRefs) {
|
|
2971
3777
|
const previous = this.#subscriptions.get(stream);
|
|
2972
|
-
const state = {};
|
|
3778
|
+
const state = imageRefs && stream.startsWith("conversation:") ? { images: new ImageRefs } : {};
|
|
2973
3779
|
this.#subscriptions.set(stream, state);
|
|
2974
3780
|
const current = () => !this.#disposed && this.#subscriptions.get(stream) === state;
|
|
2975
3781
|
const send = (frame) => current() ? this.send(frame) : Promise.resolve();
|
|
@@ -2978,7 +3784,7 @@ class GatewayClient {
|
|
|
2978
3784
|
await previous?.subscription?.stop();
|
|
2979
3785
|
if (!current())
|
|
2980
3786
|
return;
|
|
2981
|
-
subscription = await this.#open(stream, send);
|
|
3787
|
+
subscription = await this.#open(stream, send, state);
|
|
2982
3788
|
} catch (error) {
|
|
2983
3789
|
if (current()) {
|
|
2984
3790
|
await send({ type: "ended", stream, reason: error instanceof Error ? error.message : String(error) });
|
|
@@ -2993,7 +3799,7 @@ class GatewayClient {
|
|
|
2993
3799
|
}
|
|
2994
3800
|
state.subscription = subscription;
|
|
2995
3801
|
}
|
|
2996
|
-
async#open(stream, send) {
|
|
3802
|
+
async#open(stream, send, state) {
|
|
2997
3803
|
const source = this.#hostValue(stream);
|
|
2998
3804
|
if (source !== undefined) {
|
|
2999
3805
|
send({ type: "snapshot", stream, value: source.value });
|
|
@@ -3018,7 +3824,7 @@ class GatewayClient {
|
|
|
3018
3824
|
return this.#openDoc(stream, stream.slice("doc:".length, at), Number(stream.slice(at + 1)), send);
|
|
3019
3825
|
}
|
|
3020
3826
|
const conversation = await this.#conversation(Number(stream.slice("conversation:".length)));
|
|
3021
|
-
return this.#forward(stream, await conversation.watch(context3), send);
|
|
3827
|
+
return this.#forward(stream, await conversation.watch(context3), send, state);
|
|
3022
3828
|
}
|
|
3023
3829
|
#hostValue(stream) {
|
|
3024
3830
|
return Object.hasOwn(this.#hostValues, stream) ? this.#hostValues[stream] : undefined;
|
|
@@ -3031,8 +3837,8 @@ class GatewayClient {
|
|
|
3031
3837
|
await this.#conversation(id);
|
|
3032
3838
|
const { harness } = this.#options;
|
|
3033
3839
|
let signalCreation;
|
|
3034
|
-
const created = new Promise((
|
|
3035
|
-
signalCreation =
|
|
3840
|
+
const created = new Promise((resolve6) => {
|
|
3841
|
+
signalCreation = resolve6;
|
|
3036
3842
|
});
|
|
3037
3843
|
let inner;
|
|
3038
3844
|
let stopped = false;
|
|
@@ -3080,11 +3886,60 @@ class GatewayClient {
|
|
|
3080
3886
|
}
|
|
3081
3887
|
};
|
|
3082
3888
|
}
|
|
3083
|
-
async#forward(stream, watch2, send) {
|
|
3084
|
-
|
|
3085
|
-
|
|
3889
|
+
async#forward(stream, watch2, send, state) {
|
|
3890
|
+
if (state?.images === undefined) {
|
|
3891
|
+
await send({ type: "snapshot", stream, value: watch2.value });
|
|
3892
|
+
watch2.start((_value, ops) => send({ type: "ops", stream, ops }));
|
|
3893
|
+
return watch2;
|
|
3894
|
+
}
|
|
3895
|
+
const images = state.images;
|
|
3896
|
+
let before = watch2.value;
|
|
3897
|
+
state.view = before;
|
|
3898
|
+
await send({ type: "snapshot", stream, value: await images.value(before) });
|
|
3899
|
+
watch2.start(async (value, ops) => {
|
|
3900
|
+
const sent = await images.ops(ops, before);
|
|
3901
|
+
before = value;
|
|
3902
|
+
state.view = before;
|
|
3903
|
+
return send({ type: "ops", stream, ops: sent });
|
|
3904
|
+
});
|
|
3086
3905
|
return watch2;
|
|
3087
3906
|
}
|
|
3907
|
+
#shownImage(ref) {
|
|
3908
|
+
for (const state of this.#subscriptions.values()) {
|
|
3909
|
+
if (state.images === undefined || state.view === undefined)
|
|
3910
|
+
continue;
|
|
3911
|
+
const image = state.images.image(state.view, ref);
|
|
3912
|
+
if (image !== undefined)
|
|
3913
|
+
return image;
|
|
3914
|
+
}
|
|
3915
|
+
return;
|
|
3916
|
+
}
|
|
3917
|
+
#attachmentStore() {
|
|
3918
|
+
const store = this.#options.attachments;
|
|
3919
|
+
if (store === undefined)
|
|
3920
|
+
throw new Error("Attachments are not available on this host");
|
|
3921
|
+
return store;
|
|
3922
|
+
}
|
|
3923
|
+
async#attachmentText(ids) {
|
|
3924
|
+
if (ids === undefined || ids.length === 0)
|
|
3925
|
+
return;
|
|
3926
|
+
const files = await this.#attachmentStore().use(ids);
|
|
3927
|
+
return files === undefined ? undefined : attachmentBlock(files);
|
|
3928
|
+
}
|
|
3929
|
+
#attached(images) {
|
|
3930
|
+
const resolved = images?.map((image, index) => {
|
|
3931
|
+
if ("data" in image)
|
|
3932
|
+
return image;
|
|
3933
|
+
const shown = this.#shownImage(image.ref);
|
|
3934
|
+
if (shown === undefined)
|
|
3935
|
+
throw new Error(`Image ${index + 1} is no longer available`);
|
|
3936
|
+
return shown;
|
|
3937
|
+
});
|
|
3938
|
+
const problem = imageProblem(resolved ?? []);
|
|
3939
|
+
if (problem !== undefined)
|
|
3940
|
+
throw new Error(problem);
|
|
3941
|
+
return resolved;
|
|
3942
|
+
}
|
|
3088
3943
|
async#unsubscribe(stream) {
|
|
3089
3944
|
const subscription = this.#subscriptions.get(stream);
|
|
3090
3945
|
this.#subscriptions.delete(stream);
|
|
@@ -3096,6 +3951,11 @@ class GatewayClient {
|
|
|
3096
3951
|
throw new Error(`Conversation ${id} does not exist`);
|
|
3097
3952
|
return conversation;
|
|
3098
3953
|
}
|
|
3954
|
+
async#compact(conversation, instructions) {
|
|
3955
|
+
const taskId = await conversation.compact(instructions, context3);
|
|
3956
|
+
this.#options.harness.waitForTask(taskId, context3).then((receipt) => void this.send({ type: "notice", level: "info", message: `Compaction ${receipt.state.outcome.status}.` }), () => {});
|
|
3957
|
+
return taskId;
|
|
3958
|
+
}
|
|
3099
3959
|
async#answer(id, method, args) {
|
|
3100
3960
|
try {
|
|
3101
3961
|
const value = await this.#call(method, args);
|
|
@@ -3111,16 +3971,37 @@ class GatewayClient {
|
|
|
3111
3971
|
case "removeProject":
|
|
3112
3972
|
await removeProject(this.#options.harness, args.path, context3);
|
|
3113
3973
|
return null;
|
|
3974
|
+
case "submit": {
|
|
3975
|
+
const input = args;
|
|
3976
|
+
const images = this.#attached(input.images);
|
|
3977
|
+
const conversation2 = await this.#conversation(input.conversationId);
|
|
3978
|
+
const attachments = await this.#attachmentText(input.attachments);
|
|
3979
|
+
const text = await this.#expandSkills(input.text, { kind: "conversation", conversationId: conversation2.id }, input.skills);
|
|
3980
|
+
const dismissed = await this.#dismissPendingQuestions(conversation2.id, context3);
|
|
3981
|
+
const submission = await conversation2.submit({
|
|
3982
|
+
type: "input",
|
|
3983
|
+
content: messageContent(text, images, attachments),
|
|
3984
|
+
whenBusy: dismissed ? "steer" : input.whenBusy,
|
|
3985
|
+
requestId: input.requestId
|
|
3986
|
+
}, context3);
|
|
3987
|
+
submission.wait(context3).then((settled) => {
|
|
3988
|
+
if (settled.status === "unanswered" && settled.reason !== "aborted") {
|
|
3989
|
+
this.send({ type: "notice", level: "error", message: `No answer: ${settled.reason}` });
|
|
3990
|
+
}
|
|
3991
|
+
}, () => {});
|
|
3992
|
+
return { submissionId: submission.id };
|
|
3993
|
+
}
|
|
3114
3994
|
case "createConversation": {
|
|
3115
3995
|
const create = args;
|
|
3116
|
-
const
|
|
3117
|
-
|
|
3118
|
-
|
|
3996
|
+
const images = this.#attached(create.images);
|
|
3997
|
+
const attachments = await this.#attachmentText(create.attachments);
|
|
3998
|
+
const text = await this.#expandSkills(create.text, create.home.kind === "chat" ? { kind: "chat" } : { kind: "project", path: create.home.path, checkout: create.checkout ?? "worktree" }, create.skills);
|
|
3119
3999
|
const conversationId = await createConversation(this.#options.harness, this.#options.dataDir, {
|
|
3120
4000
|
home: create.home,
|
|
3121
|
-
text
|
|
4001
|
+
text,
|
|
3122
4002
|
requestId: create.requestId,
|
|
3123
|
-
...
|
|
4003
|
+
...images === undefined ? {} : { images },
|
|
4004
|
+
...attachments === undefined ? {} : { attachmentText: attachments },
|
|
3124
4005
|
...create.checkout === undefined ? {} : { checkout: create.checkout },
|
|
3125
4006
|
...create.model === undefined ? {} : { model: create.model },
|
|
3126
4007
|
...create.thinkingLevel === undefined ? {} : { thinkingLevel: create.thinkingLevel },
|
|
@@ -3199,22 +4080,64 @@ class GatewayClient {
|
|
|
3199
4080
|
const input = args;
|
|
3200
4081
|
return listFiles(this.#options.harness, this.#options.dataDir, this.#files, input.scope, input.query, context3);
|
|
3201
4082
|
}
|
|
4083
|
+
case "listSkills": {
|
|
4084
|
+
const input = args;
|
|
4085
|
+
return listSkills(this.#options.harness, this.#skills, input.scope, context3);
|
|
4086
|
+
}
|
|
4087
|
+
case "image": {
|
|
4088
|
+
const image = this.#shownImage(args.ref);
|
|
4089
|
+
if (image === undefined)
|
|
4090
|
+
throw new Error("Unknown image");
|
|
4091
|
+
return image;
|
|
4092
|
+
}
|
|
4093
|
+
case "beginAttachment": {
|
|
4094
|
+
const input = args;
|
|
4095
|
+
const store = this.#attachmentStore();
|
|
4096
|
+
return { attachmentId: await store.begin(input.name, input.size) };
|
|
4097
|
+
}
|
|
4098
|
+
case "appendAttachment": {
|
|
4099
|
+
const input = args;
|
|
4100
|
+
const store = this.#attachmentStore();
|
|
4101
|
+
return await store.append(input.attachmentId, input.offset, input.data);
|
|
4102
|
+
}
|
|
3202
4103
|
}
|
|
3203
4104
|
const conversation = await this.#conversation(args.conversationId);
|
|
3204
4105
|
switch (method) {
|
|
3205
|
-
case "
|
|
3206
|
-
const {
|
|
3207
|
-
const
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
4106
|
+
case "withdrawQueued": {
|
|
4107
|
+
const { conversationId, submissionId } = args;
|
|
4108
|
+
const id = submissionId;
|
|
4109
|
+
const submission = await this.#options.harness.submission(id, context3);
|
|
4110
|
+
if (submission === undefined)
|
|
4111
|
+
throw new Error(`Submission ${submissionId} does not exist`);
|
|
4112
|
+
assertQueuedInput(await submission.status(context3), conversationId);
|
|
4113
|
+
await this.#options.harness.abortSubmission(id, context3, conversationId);
|
|
4114
|
+
const after = await this.#options.harness.submission(id, context3);
|
|
4115
|
+
if (after === undefined)
|
|
4116
|
+
throw new Error(`Submission ${submissionId} does not exist`);
|
|
4117
|
+
return { outcome: (await after.status(context3)).entry === undefined ? "withdrawn" : "sent" };
|
|
4118
|
+
}
|
|
4119
|
+
case "steerQueued": {
|
|
4120
|
+
const { conversationId, submissionId } = args;
|
|
4121
|
+
const id = submissionId;
|
|
4122
|
+
return await this.#options.harness.commit(async (tx) => {
|
|
4123
|
+
const record = await tx.submission(id);
|
|
4124
|
+
if (record === undefined)
|
|
4125
|
+
throw new Error(`Submission ${submissionId} does not exist`);
|
|
4126
|
+
assertQueuedInput(record, conversationId);
|
|
4127
|
+
if (record.entry !== undefined)
|
|
4128
|
+
return { outcome: "sent" };
|
|
4129
|
+
if (record.status !== "queued")
|
|
4130
|
+
return { outcome: "unavailable" };
|
|
4131
|
+
const inbox = await tx.doc(InboxDoc, conversationId);
|
|
4132
|
+
const item = inbox.items.find((candidate) => candidate.id === id);
|
|
4133
|
+
if (item === undefined) {
|
|
4134
|
+
throw new Error(`Submission ${submissionId} is not queued in the inbox`);
|
|
3215
4135
|
}
|
|
3216
|
-
|
|
3217
|
-
|
|
4136
|
+
await dismissPendingQuestions(tx, this.#questionDocs(), conversationId);
|
|
4137
|
+
if (item.mode === "followUp")
|
|
4138
|
+
item.mode = "steer";
|
|
4139
|
+
return { outcome: "steered" };
|
|
4140
|
+
}, context3);
|
|
3218
4141
|
}
|
|
3219
4142
|
case "renameConversation": {
|
|
3220
4143
|
const { title } = args;
|
|
@@ -3241,29 +4164,44 @@ class GatewayClient {
|
|
|
3241
4164
|
return null;
|
|
3242
4165
|
case "compact": {
|
|
3243
4166
|
const { instructions } = args;
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
4167
|
+
return { taskId: await this.#compact(conversation, instructions) };
|
|
4168
|
+
}
|
|
4169
|
+
case "listCommands":
|
|
4170
|
+
return listCommands();
|
|
4171
|
+
case "runCommand": {
|
|
4172
|
+
const { name, args: input } = args;
|
|
4173
|
+
await runCommand({ compact: (instructions) => this.#compact(conversation, instructions) }, name, input);
|
|
4174
|
+
return null;
|
|
3247
4175
|
}
|
|
3248
4176
|
case "setModel": {
|
|
3249
4177
|
const { model: ref } = args;
|
|
3250
4178
|
const model = this.#options.models.getModel(ref.provider, ref.modelId);
|
|
3251
4179
|
if (model === undefined)
|
|
3252
4180
|
throw new Error(`Unknown model: ${ref.provider}/${ref.modelId}`);
|
|
3253
|
-
|
|
3254
|
-
|
|
4181
|
+
await this.#options.harness.commit(async (tx) => {
|
|
4182
|
+
const state = await tx.doc(AgentDoc5, conversation.id);
|
|
4183
|
+
const thinking = state.thinkingLevel ?? "off";
|
|
4184
|
+
await configure3(tx, conversation.id, { model: ref, thinkingLevel: clampThinkingLevel3(model, thinking) });
|
|
4185
|
+
}, context3);
|
|
3255
4186
|
return null;
|
|
3256
4187
|
}
|
|
3257
4188
|
case "setThinkingLevel": {
|
|
3258
4189
|
const { level } = args;
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
4190
|
+
await this.#options.harness.commit(async (tx) => {
|
|
4191
|
+
const state = await tx.doc(AgentDoc5, conversation.id);
|
|
4192
|
+
const model = state.model === undefined ? undefined : this.#options.models.getModel(state.model.provider, state.model.modelId);
|
|
4193
|
+
if (model === undefined)
|
|
4194
|
+
throw new Error("No model selected");
|
|
4195
|
+
if (!getSupportedThinkingLevels(model).includes(level)) {
|
|
4196
|
+
throw new Error(`Thinking level ${level} is not supported by ${state.model.provider}/${state.model.modelId}`);
|
|
4197
|
+
}
|
|
4198
|
+
await configure3(tx, conversation.id, { thinkingLevel: level });
|
|
4199
|
+
}, context3);
|
|
4200
|
+
return null;
|
|
4201
|
+
}
|
|
4202
|
+
case "setAgent": {
|
|
4203
|
+
const { conversationId, agent } = args;
|
|
4204
|
+
await setAgent(this.#options.harness, { conversationId, agent }, this.#options.models, this.#options.modelSummaries, this.#options.agents?.value ?? { roles: [], problems: [] }, context3);
|
|
3267
4205
|
return null;
|
|
3268
4206
|
}
|
|
3269
4207
|
case "fork": {
|
|
@@ -3292,7 +4230,10 @@ class GatewayClient {
|
|
|
3292
4230
|
const remove = agent.tools.filter((tool) => removeTools.includes(tool.name));
|
|
3293
4231
|
const fork = await conversation.fork(at, {
|
|
3294
4232
|
ownership: { kind: "ownerless" },
|
|
3295
|
-
...remove.length === 0 ? {} : { agent: { tools: { remove } } }
|
|
4233
|
+
...remove.length === 0 ? {} : { agent: { tools: { remove } } },
|
|
4234
|
+
init: async (tx, id) => {
|
|
4235
|
+
await tx.doc(RoleDoc, id);
|
|
4236
|
+
}
|
|
3296
4237
|
}, context3);
|
|
3297
4238
|
return { conversationId: fork.id };
|
|
3298
4239
|
});
|
|
@@ -3336,30 +4277,47 @@ class GatewayClient {
|
|
|
3336
4277
|
}
|
|
3337
4278
|
}
|
|
3338
4279
|
}
|
|
4280
|
+
async#expandSkills(text, scope, bound) {
|
|
4281
|
+
if (bound === undefined ? !text.includes("$") : bound.length === 0)
|
|
4282
|
+
return text;
|
|
4283
|
+
let skills;
|
|
4284
|
+
try {
|
|
4285
|
+
skills = await scopeSkills(this.#options.harness, this.#skills, scope, context3);
|
|
4286
|
+
} catch (error) {
|
|
4287
|
+
this.send({
|
|
4288
|
+
type: "notice",
|
|
4289
|
+
level: "warning",
|
|
4290
|
+
message: `Skills were not expanded: ${error instanceof Error ? error.message : String(error)}`
|
|
4291
|
+
});
|
|
4292
|
+
return text;
|
|
4293
|
+
}
|
|
4294
|
+
const names = bound ?? mentionedSkillNames(text, skills.map((skill) => skill.name));
|
|
4295
|
+
const expanded = await expandSkills(text, names, skills);
|
|
4296
|
+
for (const name of expanded.missing) {
|
|
4297
|
+
this.send({ type: "notice", level: "warning", message: `Skill "${name}" is not available here; sent without it` });
|
|
4298
|
+
}
|
|
4299
|
+
for (const failure of expanded.failures) {
|
|
4300
|
+
this.send({
|
|
4301
|
+
type: "notice",
|
|
4302
|
+
level: "error",
|
|
4303
|
+
message: `Could not read skill "${failure.name}" (${failure.filePath}): ${failure.message}`
|
|
4304
|
+
});
|
|
4305
|
+
}
|
|
4306
|
+
return expanded.text;
|
|
4307
|
+
}
|
|
3339
4308
|
async#dismissPendingQuestions(conversationId, context4) {
|
|
3340
|
-
const questionDocs =
|
|
4309
|
+
const questionDocs = this.#questionDocs();
|
|
3341
4310
|
const snapshots = await Promise.all(questionDocs.map((doc) => this.#options.harness.snapshot(doc.token, conversationId, context4)));
|
|
3342
4311
|
const pending = snapshots.some((value) => Value2.Check(QuestionSchema, value) && value.requests.some((request) => request.resolution === undefined));
|
|
3343
4312
|
if (!pending)
|
|
3344
4313
|
return false;
|
|
3345
|
-
return await this.#options.harness.commit(
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
if (!Value2.Check(QuestionSchema, value))
|
|
3350
|
-
continue;
|
|
3351
|
-
for (const request of value.requests) {
|
|
3352
|
-
if (request.resolution === undefined) {
|
|
3353
|
-
request.resolution = { outcome: "dismissed", at: Date.now() };
|
|
3354
|
-
dismissed = true;
|
|
3355
|
-
}
|
|
3356
|
-
}
|
|
3357
|
-
}
|
|
3358
|
-
return dismissed;
|
|
3359
|
-
}, context4);
|
|
4314
|
+
return await this.#options.harness.commit((tx) => dismissPendingQuestions(tx, questionDocs, conversationId), context4);
|
|
4315
|
+
}
|
|
4316
|
+
#questionDocs() {
|
|
4317
|
+
return (this.#options.docs ?? []).filter((doc) => doc.presentation === "pinomad.question");
|
|
3360
4318
|
}
|
|
3361
4319
|
get #chatsPrefix() {
|
|
3362
|
-
return
|
|
4320
|
+
return join8(this.#options.dataDir, "chats") + sep5;
|
|
3363
4321
|
}
|
|
3364
4322
|
async#changes(conversation, context4) {
|
|
3365
4323
|
const cwd = (await conversation.agent(context4)).cwd;
|
|
@@ -3380,9 +4338,6 @@ class GatewayClient {
|
|
|
3380
4338
|
return { available: false, reason: "Not in a git repository", branch };
|
|
3381
4339
|
return { available: true, ...await changesOf(base.repo, base.base), branch };
|
|
3382
4340
|
}
|
|
3383
|
-
async#agent(id) {
|
|
3384
|
-
return await this.#options.harness.snapshot(AgentDoc4, id, context3) ?? {};
|
|
3385
|
-
}
|
|
3386
4341
|
async dispose() {
|
|
3387
4342
|
if (this.#disposed)
|
|
3388
4343
|
return;
|
|
@@ -3394,21 +4349,330 @@ class GatewayClient {
|
|
|
3394
4349
|
}
|
|
3395
4350
|
}
|
|
3396
4351
|
|
|
4352
|
+
// apps/host/src/attachments.ts
|
|
4353
|
+
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
4354
|
+
import {
|
|
4355
|
+
chmod,
|
|
4356
|
+
lstat,
|
|
4357
|
+
mkdir as mkdir3,
|
|
4358
|
+
open,
|
|
4359
|
+
readFile as readFile4,
|
|
4360
|
+
readdir as readdir3,
|
|
4361
|
+
rename as rename2,
|
|
4362
|
+
rm as rm3,
|
|
4363
|
+
stat as stat6,
|
|
4364
|
+
writeFile as writeFile2
|
|
4365
|
+
} from "node:fs/promises";
|
|
4366
|
+
import { basename as basename2, join as join9, resolve as resolve6 } from "node:path";
|
|
4367
|
+
var META = "meta.json";
|
|
4368
|
+
var DATA = "file";
|
|
4369
|
+
var CLEANUP_INTERVAL_MS = 6 * 60 * 60 * 1000;
|
|
4370
|
+
var FILE_NAME_BYTES = 200;
|
|
4371
|
+
function metaOf(value, id) {
|
|
4372
|
+
if (value === null || typeof value !== "object")
|
|
4373
|
+
return;
|
|
4374
|
+
const meta = value;
|
|
4375
|
+
if (meta.v !== 1)
|
|
4376
|
+
return;
|
|
4377
|
+
if (typeof meta.name !== "string" || typeof meta.fileName !== "string")
|
|
4378
|
+
return;
|
|
4379
|
+
if (typeof meta.size !== "number" || !Number.isSafeInteger(meta.size) || meta.size < 0)
|
|
4380
|
+
return;
|
|
4381
|
+
if (meta.state !== "uploading" && meta.state !== "complete" && meta.state !== "used")
|
|
4382
|
+
return;
|
|
4383
|
+
if (typeof meta.createdAt !== "number" || typeof meta.expiresAt !== "number")
|
|
4384
|
+
return;
|
|
4385
|
+
if (meta.fileName === "" || basename2(meta.fileName) !== meta.fileName || meta.fileName === "." || meta.fileName === "..") {
|
|
4386
|
+
return;
|
|
4387
|
+
}
|
|
4388
|
+
return { ...meta, id };
|
|
4389
|
+
}
|
|
4390
|
+
function displayName2(name) {
|
|
4391
|
+
const chars = Array.from(name);
|
|
4392
|
+
return chars.length <= MAX_ATTACHMENT_NAME_LENGTH ? name : chars.slice(0, MAX_ATTACHMENT_NAME_LENGTH).join("");
|
|
4393
|
+
}
|
|
4394
|
+
function truncateUtf8(text, limit) {
|
|
4395
|
+
let out = "";
|
|
4396
|
+
let bytes = 0;
|
|
4397
|
+
for (const char of text) {
|
|
4398
|
+
const size = Buffer.byteLength(char, "utf8");
|
|
4399
|
+
if (bytes + size > limit)
|
|
4400
|
+
break;
|
|
4401
|
+
out += char;
|
|
4402
|
+
bytes += size;
|
|
4403
|
+
}
|
|
4404
|
+
return out;
|
|
4405
|
+
}
|
|
4406
|
+
function attachmentFileName(name) {
|
|
4407
|
+
let clean = (name.split(/[\\/]/).pop() ?? "").replace(/[\u0000-\u001f\u007f]/g, "_").trim();
|
|
4408
|
+
clean = clean.replace(/^\.+/, "");
|
|
4409
|
+
if (clean === "")
|
|
4410
|
+
return "file";
|
|
4411
|
+
const dot = clean.lastIndexOf(".");
|
|
4412
|
+
const rawExtension = dot > 0 ? clean.slice(dot) : "";
|
|
4413
|
+
const extension = Buffer.byteLength(rawExtension, "utf8") <= 32 ? rawExtension : "";
|
|
4414
|
+
const stem = extension === "" ? clean : clean.slice(0, -extension.length);
|
|
4415
|
+
const extensionBytes = Buffer.byteLength(extension, "utf8");
|
|
4416
|
+
const keptStem = truncateUtf8(stem, Math.max(0, FILE_NAME_BYTES - extensionBytes));
|
|
4417
|
+
const trimmedStem = keptStem.replace(/[\s.]+$/u, "");
|
|
4418
|
+
const candidate = trimmedStem + (trimmedStem === "" ? "" : extension);
|
|
4419
|
+
return candidate === "" ? "file" : candidate;
|
|
4420
|
+
}
|
|
4421
|
+
|
|
4422
|
+
class AttachmentStore {
|
|
4423
|
+
#root;
|
|
4424
|
+
#now;
|
|
4425
|
+
#ttlMs;
|
|
4426
|
+
#pendingLimit;
|
|
4427
|
+
#pendingBytesLimit;
|
|
4428
|
+
#cleanupIntervalMs;
|
|
4429
|
+
#lock = Promise.resolve();
|
|
4430
|
+
#pending = new Map;
|
|
4431
|
+
#timer;
|
|
4432
|
+
constructor(options) {
|
|
4433
|
+
this.#root = resolve6(options.dir);
|
|
4434
|
+
this.#now = options.now ?? Date.now;
|
|
4435
|
+
this.#ttlMs = options.ttlMs ?? ATTACHMENT_TTL_MS;
|
|
4436
|
+
this.#pendingLimit = options.pendingLimit ?? MAX_PENDING_ATTACHMENTS;
|
|
4437
|
+
this.#pendingBytesLimit = options.pendingBytesLimit ?? MAX_PENDING_ATTACHMENT_BYTES;
|
|
4438
|
+
this.#cleanupIntervalMs = options.cleanupIntervalMs ?? CLEANUP_INTERVAL_MS;
|
|
4439
|
+
}
|
|
4440
|
+
get root() {
|
|
4441
|
+
return this.#root;
|
|
4442
|
+
}
|
|
4443
|
+
async open() {
|
|
4444
|
+
await mkdir3(this.#root, { recursive: true });
|
|
4445
|
+
await this.#rebuildPending();
|
|
4446
|
+
await this.sweep();
|
|
4447
|
+
this.#timer = setInterval(() => void this.sweep().catch(() => {}), this.#cleanupIntervalMs);
|
|
4448
|
+
this.#timer.unref();
|
|
4449
|
+
}
|
|
4450
|
+
close() {
|
|
4451
|
+
if (this.#timer !== undefined)
|
|
4452
|
+
clearInterval(this.#timer);
|
|
4453
|
+
this.#timer = undefined;
|
|
4454
|
+
}
|
|
4455
|
+
#run(work) {
|
|
4456
|
+
const next = this.#lock.then(work, work);
|
|
4457
|
+
this.#lock = next.then(() => {}, () => {});
|
|
4458
|
+
return next;
|
|
4459
|
+
}
|
|
4460
|
+
#directory(id) {
|
|
4461
|
+
return join9(this.#root, id);
|
|
4462
|
+
}
|
|
4463
|
+
#dataPath(meta) {
|
|
4464
|
+
return join9(this.#directory(meta.id), DATA, meta.fileName);
|
|
4465
|
+
}
|
|
4466
|
+
async#readMeta(id) {
|
|
4467
|
+
if (!isAttachmentId(id))
|
|
4468
|
+
return;
|
|
4469
|
+
try {
|
|
4470
|
+
return metaOf(JSON.parse(await readFile4(join9(this.#directory(id), META), "utf8")), id);
|
|
4471
|
+
} catch {
|
|
4472
|
+
return;
|
|
4473
|
+
}
|
|
4474
|
+
}
|
|
4475
|
+
async#writeMeta(meta) {
|
|
4476
|
+
const { id, ...value } = meta;
|
|
4477
|
+
const directory = this.#directory(id);
|
|
4478
|
+
const temporary = join9(directory, `.meta.${randomBytes3(6).toString("hex")}.tmp`);
|
|
4479
|
+
await writeFile2(temporary, `${JSON.stringify(value)}
|
|
4480
|
+
`, { mode: 384 });
|
|
4481
|
+
await rename2(temporary, join9(directory, META));
|
|
4482
|
+
}
|
|
4483
|
+
async#rebuildPending() {
|
|
4484
|
+
this.#pending.clear();
|
|
4485
|
+
let entries;
|
|
4486
|
+
try {
|
|
4487
|
+
entries = await readdir3(this.#root, { withFileTypes: true });
|
|
4488
|
+
} catch {
|
|
4489
|
+
return;
|
|
4490
|
+
}
|
|
4491
|
+
for (const entry of entries) {
|
|
4492
|
+
if (!entry.isDirectory())
|
|
4493
|
+
continue;
|
|
4494
|
+
const meta = await this.#readMeta(entry.name);
|
|
4495
|
+
if (meta !== undefined && meta.state !== "used")
|
|
4496
|
+
this.#pending.set(meta.id, meta.size);
|
|
4497
|
+
}
|
|
4498
|
+
}
|
|
4499
|
+
begin(name, size) {
|
|
4500
|
+
return this.#run(async () => {
|
|
4501
|
+
const problem = attachmentBeginProblem(name, size);
|
|
4502
|
+
if (problem !== undefined)
|
|
4503
|
+
throw new Error(problem);
|
|
4504
|
+
let pendingBytes = 0;
|
|
4505
|
+
for (const bytes of this.#pending.values())
|
|
4506
|
+
pendingBytes += bytes;
|
|
4507
|
+
if (this.#pending.size + 1 > this.#pendingLimit) {
|
|
4508
|
+
throw new Error(`Too many pending attachments (${this.#pendingLimit} at most); try again after unused uploads expire`);
|
|
4509
|
+
}
|
|
4510
|
+
if (pendingBytes + size > this.#pendingBytesLimit) {
|
|
4511
|
+
throw new Error(`Pending attachments exceed the ${this.#pendingBytesLimit / (1024 * 1024)} MB budget; try again after unused uploads expire`);
|
|
4512
|
+
}
|
|
4513
|
+
const id = randomBytes3(16).toString("base64url");
|
|
4514
|
+
const directory = this.#directory(id);
|
|
4515
|
+
const meta = {
|
|
4516
|
+
id,
|
|
4517
|
+
v: 1,
|
|
4518
|
+
name: displayName2(name),
|
|
4519
|
+
fileName: attachmentFileName(name),
|
|
4520
|
+
size,
|
|
4521
|
+
state: size === 0 ? "complete" : "uploading",
|
|
4522
|
+
createdAt: this.#now(),
|
|
4523
|
+
expiresAt: this.#now() + this.#ttlMs
|
|
4524
|
+
};
|
|
4525
|
+
try {
|
|
4526
|
+
await mkdir3(join9(directory, DATA), { recursive: true });
|
|
4527
|
+
await writeFile2(this.#dataPath(meta), new Uint8Array(0), { mode: 384 });
|
|
4528
|
+
if (size === 0)
|
|
4529
|
+
await chmod(this.#dataPath(meta), 292);
|
|
4530
|
+
await this.#writeMeta(meta);
|
|
4531
|
+
} catch (error) {
|
|
4532
|
+
await rm3(directory, { recursive: true, force: true }).catch(() => {});
|
|
4533
|
+
throw error;
|
|
4534
|
+
}
|
|
4535
|
+
this.#pending.set(id, size);
|
|
4536
|
+
return id;
|
|
4537
|
+
});
|
|
4538
|
+
}
|
|
4539
|
+
append(id, offset, data) {
|
|
4540
|
+
return this.#run(async () => {
|
|
4541
|
+
const meta = await this.#readMeta(id);
|
|
4542
|
+
if (meta === undefined)
|
|
4543
|
+
throw new Error("Attachment not found or expired; attach it again");
|
|
4544
|
+
if (meta.state === "complete" || meta.state === "used") {
|
|
4545
|
+
return { received: meta.size, complete: true };
|
|
4546
|
+
}
|
|
4547
|
+
const problem = attachmentChunkProblem(data);
|
|
4548
|
+
if (problem !== undefined)
|
|
4549
|
+
throw new Error(problem);
|
|
4550
|
+
const decoded = base64DecodedLength(data);
|
|
4551
|
+
const path = this.#dataPath(meta);
|
|
4552
|
+
const received = (await stat6(path)).size;
|
|
4553
|
+
if (received === meta.size) {
|
|
4554
|
+
await chmod(path, 292);
|
|
4555
|
+
await this.#writeMeta({ ...meta, state: "complete" });
|
|
4556
|
+
return { received, complete: true };
|
|
4557
|
+
}
|
|
4558
|
+
if (offset !== received)
|
|
4559
|
+
return { received, complete: false };
|
|
4560
|
+
if (received + decoded > meta.size)
|
|
4561
|
+
throw new Error("Attachment chunk is larger than the remaining size");
|
|
4562
|
+
const bytes = Buffer.from(data, "base64");
|
|
4563
|
+
const handle = await open(path, "r+");
|
|
4564
|
+
try {
|
|
4565
|
+
const { bytesWritten } = await handle.write(bytes, 0, bytes.length, received);
|
|
4566
|
+
if (bytesWritten !== bytes.length)
|
|
4567
|
+
throw new Error("Could not write the whole attachment chunk");
|
|
4568
|
+
} finally {
|
|
4569
|
+
await handle.close();
|
|
4570
|
+
}
|
|
4571
|
+
const after = received + bytes.length;
|
|
4572
|
+
if (after === meta.size) {
|
|
4573
|
+
await chmod(path, 292);
|
|
4574
|
+
await this.#writeMeta({ ...meta, state: "complete" });
|
|
4575
|
+
}
|
|
4576
|
+
return { received: after, complete: after === meta.size };
|
|
4577
|
+
});
|
|
4578
|
+
}
|
|
4579
|
+
use(ids) {
|
|
4580
|
+
return this.#run(async () => {
|
|
4581
|
+
if (ids === undefined || ids.length === 0)
|
|
4582
|
+
return;
|
|
4583
|
+
if (ids.length > MAX_ATTACHMENTS_PER_MESSAGE) {
|
|
4584
|
+
throw new Error(`At most ${MAX_ATTACHMENTS_PER_MESSAGE} attachments per message`);
|
|
4585
|
+
}
|
|
4586
|
+
const checked = [];
|
|
4587
|
+
for (let index = 0;index < ids.length; index++) {
|
|
4588
|
+
const id = ids[index];
|
|
4589
|
+
const meta = await this.#readMeta(id);
|
|
4590
|
+
if (meta === undefined)
|
|
4591
|
+
throw new Error(`Attachment ${index + 1} has expired; attach it again`);
|
|
4592
|
+
if (meta.state === "uploading")
|
|
4593
|
+
throw new Error(`Attachment ${index + 1} is still uploading`);
|
|
4594
|
+
const path = this.#dataPath(meta);
|
|
4595
|
+
let info;
|
|
4596
|
+
try {
|
|
4597
|
+
info = await lstat(path);
|
|
4598
|
+
} catch {
|
|
4599
|
+
throw new Error(`Attachment ${index + 1} is unavailable; attach it again`);
|
|
4600
|
+
}
|
|
4601
|
+
if (!info.isFile() || info.size !== meta.size) {
|
|
4602
|
+
throw new Error(`Attachment ${index + 1} is unavailable; attach it again`);
|
|
4603
|
+
}
|
|
4604
|
+
checked.push({ meta, path });
|
|
4605
|
+
}
|
|
4606
|
+
for (const { meta } of checked) {
|
|
4607
|
+
if (meta.state === "used") {
|
|
4608
|
+
this.#pending.delete(meta.id);
|
|
4609
|
+
continue;
|
|
4610
|
+
}
|
|
4611
|
+
await this.#writeMeta({ ...meta, state: "used" });
|
|
4612
|
+
this.#pending.delete(meta.id);
|
|
4613
|
+
}
|
|
4614
|
+
return checked.map(({ meta, path }) => ({ id: meta.id, name: meta.name, path, size: meta.size }));
|
|
4615
|
+
});
|
|
4616
|
+
}
|
|
4617
|
+
sweep() {
|
|
4618
|
+
return this.#run(async () => {
|
|
4619
|
+
let removed = 0;
|
|
4620
|
+
let entries;
|
|
4621
|
+
try {
|
|
4622
|
+
entries = await readdir3(this.#root, { withFileTypes: true });
|
|
4623
|
+
} catch {
|
|
4624
|
+
return 0;
|
|
4625
|
+
}
|
|
4626
|
+
const now = this.#now();
|
|
4627
|
+
for (const entry of entries) {
|
|
4628
|
+
if (!entry.isDirectory())
|
|
4629
|
+
continue;
|
|
4630
|
+
const directory = this.#directory(entry.name);
|
|
4631
|
+
const meta = await this.#readMeta(entry.name);
|
|
4632
|
+
if (meta !== undefined) {
|
|
4633
|
+
if (meta.state === "used") {
|
|
4634
|
+
this.#pending.delete(meta.id);
|
|
4635
|
+
continue;
|
|
4636
|
+
}
|
|
4637
|
+
if (meta.expiresAt > now) {
|
|
4638
|
+
this.#pending.set(meta.id, meta.size);
|
|
4639
|
+
continue;
|
|
4640
|
+
}
|
|
4641
|
+
} else {
|
|
4642
|
+
this.#pending.delete(entry.name);
|
|
4643
|
+
try {
|
|
4644
|
+
if (now - (await stat6(directory)).mtimeMs <= this.#ttlMs)
|
|
4645
|
+
continue;
|
|
4646
|
+
} catch {
|
|
4647
|
+
continue;
|
|
4648
|
+
}
|
|
4649
|
+
}
|
|
4650
|
+
try {
|
|
4651
|
+
await rm3(directory, { recursive: true, force: true });
|
|
4652
|
+
this.#pending.delete(entry.name);
|
|
4653
|
+
removed += 1;
|
|
4654
|
+
} catch {}
|
|
4655
|
+
}
|
|
4656
|
+
return removed;
|
|
4657
|
+
});
|
|
4658
|
+
}
|
|
4659
|
+
}
|
|
4660
|
+
|
|
3397
4661
|
// apps/host/src/host.ts
|
|
3398
4662
|
var context4 = BACKGROUND_CONTEXT4;
|
|
3399
4663
|
async function hostToken(dataDir) {
|
|
3400
|
-
const path =
|
|
4664
|
+
const path = join10(dataDir, "token");
|
|
3401
4665
|
try {
|
|
3402
|
-
return (await
|
|
4666
|
+
return (await readFile5(path, "utf8")).trim();
|
|
3403
4667
|
} catch {
|
|
3404
|
-
const token =
|
|
3405
|
-
await
|
|
4668
|
+
const token = randomBytes4(24).toString("base64url");
|
|
4669
|
+
await writeFile3(path, `${token}
|
|
3406
4670
|
`, { mode: 384 });
|
|
3407
4671
|
return token;
|
|
3408
4672
|
}
|
|
3409
4673
|
}
|
|
3410
4674
|
async function openHost(options) {
|
|
3411
|
-
await
|
|
4675
|
+
await mkdir4(options.dataDir, { recursive: true });
|
|
3412
4676
|
const stale = Math.max(2000, options.lockStaleMs ?? 1e4);
|
|
3413
4677
|
const release = await lockfile.lock(options.dataDir, {
|
|
3414
4678
|
realpath: false,
|
|
@@ -3420,13 +4684,16 @@ async function openHost(options) {
|
|
|
3420
4684
|
let harness;
|
|
3421
4685
|
let mcp;
|
|
3422
4686
|
let agentRoles;
|
|
4687
|
+
let attachments;
|
|
3423
4688
|
let extensions = [];
|
|
3424
4689
|
try {
|
|
3425
4690
|
const token = await hostToken(options.dataDir);
|
|
4691
|
+
attachments = new AttachmentStore({ dir: join10(options.dataDir, "attachments") });
|
|
4692
|
+
await attachments.open();
|
|
3426
4693
|
const reports = [];
|
|
3427
4694
|
let report = (error) => void reports.push(error);
|
|
3428
4695
|
const registry = createRegistry();
|
|
3429
|
-
agentRoles = await openAgentRoles({ dir:
|
|
4696
|
+
agentRoles = await openAgentRoles({ dir: join10(options.dataDir, "agents") });
|
|
3430
4697
|
extensions = typeof options.extensions === "function" ? options.extensions({
|
|
3431
4698
|
harness: () => {
|
|
3432
4699
|
if (harness === undefined)
|
|
@@ -3449,13 +4716,13 @@ async function openHost(options) {
|
|
|
3449
4716
|
}
|
|
3450
4717
|
});
|
|
3451
4718
|
const hostSettings = await openHostSettings({
|
|
3452
|
-
path:
|
|
4719
|
+
path: join10(options.dataDir, "settings.json"),
|
|
3453
4720
|
fallback: options.settingsFallback,
|
|
3454
4721
|
models: options.modelAuth?.models ?? constantValue(options.modelSummaries())
|
|
3455
4722
|
});
|
|
3456
4723
|
const environments = new Map;
|
|
3457
|
-
const chatsDir =
|
|
3458
|
-
harness = await Harness.open(await (options.openStorage ?? openNodeSqliteStorage)(
|
|
4724
|
+
const chatsDir = join10(options.dataDir, "chats");
|
|
4725
|
+
harness = await Harness.open(await (options.openStorage ?? openNodeSqliteStorage)(join10(options.dataDir, "session.sqlite")), {
|
|
3459
4726
|
models: options.models,
|
|
3460
4727
|
registry,
|
|
3461
4728
|
env: async (target) => {
|
|
@@ -3463,16 +4730,16 @@ async function openHost(options) {
|
|
|
3463
4730
|
if (cwd === undefined)
|
|
3464
4731
|
return;
|
|
3465
4732
|
const worktrees = worktreeRoot(options.dataDir);
|
|
3466
|
-
if (cwd === worktrees || cwd.startsWith(worktrees +
|
|
4733
|
+
if (cwd === worktrees || cwd.startsWith(worktrees + sep6)) {
|
|
3467
4734
|
const index = await target.read.snapshot(IndexDoc, context4);
|
|
3468
4735
|
const record = checkoutAt(index?.checkouts, cwd);
|
|
3469
4736
|
if (record !== undefined) {
|
|
3470
4737
|
await ensureWorktree(record);
|
|
3471
|
-
await
|
|
4738
|
+
await mkdir4(cwd, { recursive: true });
|
|
3472
4739
|
}
|
|
3473
4740
|
}
|
|
3474
4741
|
if (cwd.startsWith(`${chatsDir}/`))
|
|
3475
|
-
await
|
|
4742
|
+
await mkdir4(cwd, { recursive: true });
|
|
3476
4743
|
let env = environments.get(cwd);
|
|
3477
4744
|
if (env === undefined) {
|
|
3478
4745
|
env = new NodeExecutionEnv({ cwd });
|
|
@@ -3496,8 +4763,10 @@ async function openHost(options) {
|
|
|
3496
4763
|
harness,
|
|
3497
4764
|
models: options.models,
|
|
3498
4765
|
modelSummaries: options.modelSummaries,
|
|
3499
|
-
session: { id:
|
|
4766
|
+
session: { id: basename3(options.dataDir), directory: options.dataDir },
|
|
3500
4767
|
dataDir: options.dataDir,
|
|
4768
|
+
attachments,
|
|
4769
|
+
agentsHome: options.agentsHome,
|
|
3501
4770
|
settings: hostSettings,
|
|
3502
4771
|
token,
|
|
3503
4772
|
port: options.port,
|
|
@@ -3562,6 +4831,7 @@ async function openHost(options) {
|
|
|
3562
4831
|
await gateway.close();
|
|
3563
4832
|
await opened.close(context4);
|
|
3564
4833
|
await mcp?.close();
|
|
4834
|
+
attachments?.close();
|
|
3565
4835
|
agentRoles?.close();
|
|
3566
4836
|
for (const each of extensions)
|
|
3567
4837
|
await each.close?.();
|
|
@@ -3576,6 +4846,7 @@ async function openHost(options) {
|
|
|
3576
4846
|
};
|
|
3577
4847
|
} catch (error) {
|
|
3578
4848
|
options.modelAuth?.close();
|
|
4849
|
+
attachments?.close();
|
|
3579
4850
|
agentRoles?.close();
|
|
3580
4851
|
for (const each of extensions)
|
|
3581
4852
|
await each.close?.().catch(() => {});
|
|
@@ -3586,9 +4857,71 @@ async function openHost(options) {
|
|
|
3586
4857
|
}
|
|
3587
4858
|
}
|
|
3588
4859
|
|
|
4860
|
+
// apps/host/src/service/log-rotation.ts
|
|
4861
|
+
import { copyFile, rename as rename3, rm as rm4, stat as stat7, truncate } from "node:fs/promises";
|
|
4862
|
+
import { join as join11 } from "node:path";
|
|
4863
|
+
var HOST_LOG_MAX_BYTES = 10 * 1024 * 1024;
|
|
4864
|
+
var HOST_LOG_HISTORY = 3;
|
|
4865
|
+
var HOST_LOG_CHECK_INTERVAL_MS = 10 * 60 * 1000;
|
|
4866
|
+
var hostLogPath = (dataDir) => join11(dataDir, "logs", "host.log");
|
|
4867
|
+
var rotationTempPath = (log) => `${log}.rotating`;
|
|
4868
|
+
var isMissing = (error) => error.code === "ENOENT";
|
|
4869
|
+
async function rotateHostLog(dataDir, copy = copyFile) {
|
|
4870
|
+
const log = hostLogPath(dataDir);
|
|
4871
|
+
const temp = rotationTempPath(log);
|
|
4872
|
+
await rm4(temp, { force: true });
|
|
4873
|
+
let size;
|
|
4874
|
+
try {
|
|
4875
|
+
size = (await stat7(log)).size;
|
|
4876
|
+
} catch (error) {
|
|
4877
|
+
if (isMissing(error))
|
|
4878
|
+
return false;
|
|
4879
|
+
throw error;
|
|
4880
|
+
}
|
|
4881
|
+
if (size <= HOST_LOG_MAX_BYTES)
|
|
4882
|
+
return false;
|
|
4883
|
+
try {
|
|
4884
|
+
await copy(log, temp);
|
|
4885
|
+
} catch (error) {
|
|
4886
|
+
await rm4(temp, { force: true }).catch(() => {});
|
|
4887
|
+
throw error;
|
|
4888
|
+
}
|
|
4889
|
+
await rm4(`${log}.${HOST_LOG_HISTORY}`, { force: true });
|
|
4890
|
+
for (let index = HOST_LOG_HISTORY - 1;index >= 1; index -= 1) {
|
|
4891
|
+
try {
|
|
4892
|
+
await rename3(`${log}.${index}`, `${log}.${index + 1}`);
|
|
4893
|
+
} catch (error) {
|
|
4894
|
+
if (!isMissing(error))
|
|
4895
|
+
throw error;
|
|
4896
|
+
}
|
|
4897
|
+
}
|
|
4898
|
+
await rename3(temp, `${log}.1`);
|
|
4899
|
+
await truncate(log, 0);
|
|
4900
|
+
return true;
|
|
4901
|
+
}
|
|
4902
|
+
function startHostLogRotation(dataDir) {
|
|
4903
|
+
let running = false;
|
|
4904
|
+
const check = () => {
|
|
4905
|
+
if (running)
|
|
4906
|
+
return;
|
|
4907
|
+
running = true;
|
|
4908
|
+
rotateHostLog(dataDir).catch((error) => {
|
|
4909
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4910
|
+
process.stderr.write(`${JSON.stringify({ event: "log-rotation-failed", error: message })}
|
|
4911
|
+
`);
|
|
4912
|
+
}).finally(() => {
|
|
4913
|
+
running = false;
|
|
4914
|
+
});
|
|
4915
|
+
};
|
|
4916
|
+
check();
|
|
4917
|
+
const timer = setInterval(check, HOST_LOG_CHECK_INTERVAL_MS);
|
|
4918
|
+
timer.unref();
|
|
4919
|
+
return () => clearInterval(timer);
|
|
4920
|
+
}
|
|
4921
|
+
|
|
3589
4922
|
// apps/host/src/model-auth.ts
|
|
3590
|
-
import { existsSync as
|
|
3591
|
-
import { basename as
|
|
4923
|
+
import { existsSync as existsSync3, watch as watch2 } from "node:fs";
|
|
4924
|
+
import { basename as basename4, dirname as dirname3 } from "node:path";
|
|
3592
4925
|
|
|
3593
4926
|
// apps/host/src/pi-setup.ts
|
|
3594
4927
|
import { getSupportedThinkingLevels as getSupportedThinkingLevels2 } from "@earendil-works/pi-ai";
|
|
@@ -3720,7 +5053,7 @@ async function watchModelAuth(modelRuntime, options) {
|
|
|
3720
5053
|
const debounceMs = options.debounceMs ?? 150;
|
|
3721
5054
|
const models = new Published(modelSummaries(modelRuntime), (a, b) => sameList(a, b, sameModel));
|
|
3722
5055
|
const auth = new Published(await storedLogins(modelRuntime), (a, b) => sameList(a.logins, b.logins, sameLogin));
|
|
3723
|
-
const fileName =
|
|
5056
|
+
const fileName = basename4(options.authPath);
|
|
3724
5057
|
let watcher;
|
|
3725
5058
|
let timer;
|
|
3726
5059
|
let reported = false;
|
|
@@ -3737,8 +5070,8 @@ async function watchModelAuth(modelRuntime, options) {
|
|
|
3737
5070
|
reported = true;
|
|
3738
5071
|
}
|
|
3739
5072
|
};
|
|
3740
|
-
const directory =
|
|
3741
|
-
if (
|
|
5073
|
+
const directory = dirname3(options.authPath);
|
|
5074
|
+
if (existsSync3(directory)) {
|
|
3742
5075
|
watcher = watch2(directory, (_event, changed) => {
|
|
3743
5076
|
if (closed || changed !== null && changed !== fileName)
|
|
3744
5077
|
return;
|
|
@@ -3788,7 +5121,7 @@ async function main(argv) {
|
|
|
3788
5121
|
relay: { type: "string" }
|
|
3789
5122
|
}
|
|
3790
5123
|
});
|
|
3791
|
-
const dataDir =
|
|
5124
|
+
const dataDir = resolve7(values["data-dir"] ?? DEFAULT_DATA_DIR);
|
|
3792
5125
|
let relayOrigin;
|
|
3793
5126
|
try {
|
|
3794
5127
|
relayOrigin = resolveRelayOrigin(values.relay, await readServerConfig(dataDir));
|
|
@@ -3796,9 +5129,10 @@ async function main(argv) {
|
|
|
3796
5129
|
console.error(error instanceof Error ? error.message : String(error));
|
|
3797
5130
|
process.exit(1);
|
|
3798
5131
|
}
|
|
5132
|
+
const agentsHome = join12(homedir3(), ".agents");
|
|
3799
5133
|
const extensionsFor = (models, summaries, options2) => (host2) => [
|
|
3800
5134
|
createContext({
|
|
3801
|
-
agentsHome
|
|
5135
|
+
agentsHome,
|
|
3802
5136
|
checkout: (input, context5) => checkoutInfo(host2.harness(), input, context5)
|
|
3803
5137
|
}),
|
|
3804
5138
|
createCoding(options2),
|
|
@@ -3814,9 +5148,10 @@ async function main(argv) {
|
|
|
3814
5148
|
];
|
|
3815
5149
|
const common = {
|
|
3816
5150
|
dataDir,
|
|
5151
|
+
agentsHome,
|
|
3817
5152
|
projects: values.project ?? [],
|
|
3818
5153
|
port: Number(values.port),
|
|
3819
|
-
mcpConfig:
|
|
5154
|
+
mcpConfig: join12(agentsHome, "mcp.json"),
|
|
3820
5155
|
browserOrigins: values["browser-origin"] ?? ["http://127.0.0.1:5199"],
|
|
3821
5156
|
webRoot,
|
|
3822
5157
|
...values["lock-stale-ms"] === undefined ? {} : { lockStaleMs: Number(values["lock-stale-ms"]) },
|
|
@@ -3860,7 +5195,7 @@ async function main(argv) {
|
|
|
3860
5195
|
}
|
|
3861
5196
|
async function piOptions() {
|
|
3862
5197
|
const modelRuntime = await ModelRuntime.create();
|
|
3863
|
-
const modelAuth = await watchModelAuth(modelRuntime, { authPath:
|
|
5198
|
+
const modelAuth = await watchModelAuth(modelRuntime, { authPath: join12(getAgentDir(), "auth.json") });
|
|
3864
5199
|
const settingsManager = SettingsManager.create(process.cwd());
|
|
3865
5200
|
configureHarnessHttp(settingsManager);
|
|
3866
5201
|
const summaries = () => modelAuth.models.value;
|
|
@@ -3886,13 +5221,14 @@ async function main(argv) {
|
|
|
3886
5221
|
}
|
|
3887
5222
|
const options = values.faux !== undefined ? fauxOptions(() => () => fauxAssistantMessage(values.faux)) : await piOptions();
|
|
3888
5223
|
const host = await openHost(options);
|
|
5224
|
+
const stopLogRotation = startHostLogRotation(dataDir);
|
|
3889
5225
|
const { defaultModel: effectiveModel } = host.settings.value;
|
|
3890
5226
|
const model = effectiveModel === null ? null : `${effectiveModel.provider}/${effectiveModel.modelId}`;
|
|
3891
5227
|
const web = packagedVersion === undefined ? "http://127.0.0.1:5199/" : new URL(host.url.replace(/^ws/, "http")).href;
|
|
3892
5228
|
console.log(JSON.stringify({
|
|
3893
5229
|
event: "ready",
|
|
3894
5230
|
url: host.url,
|
|
3895
|
-
tokenFile:
|
|
5231
|
+
tokenFile: join12(dataDir, "token"),
|
|
3896
5232
|
dataDir,
|
|
3897
5233
|
model,
|
|
3898
5234
|
web,
|
|
@@ -3904,6 +5240,7 @@ async function main(argv) {
|
|
|
3904
5240
|
if (stopping)
|
|
3905
5241
|
return;
|
|
3906
5242
|
stopping = true;
|
|
5243
|
+
stopLogRotation();
|
|
3907
5244
|
host.close().finally(() => process.exit(0));
|
|
3908
5245
|
};
|
|
3909
5246
|
process.on("SIGINT", stop);
|
|
@@ -3912,6 +5249,7 @@ async function main(argv) {
|
|
|
3912
5249
|
if (end.reason !== "failed" || stopping)
|
|
3913
5250
|
return;
|
|
3914
5251
|
stopping = true;
|
|
5252
|
+
stopLogRotation();
|
|
3915
5253
|
const error = end.error instanceof Error ? end.error.message : String(end.error);
|
|
3916
5254
|
process.stderr.write(`${JSON.stringify({ event: "fatal", error })}
|
|
3917
5255
|
`);
|