isorite 0.16.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/main.js +1634 -62
- package/package.json +4 -1
- package/web/_app/immutable/assets/0.BGpjfMlq.css +1 -0
- package/web/_app/immutable/assets/4.Dn5sJ8uq.css +1 -0
- package/web/_app/immutable/assets/5.DJhOSn7C.css +1 -0
- package/web/_app/immutable/assets/NoteList.Wxp_AH0Y.css +1 -0
- package/web/_app/immutable/chunks/{BQeONalw.js → -_CDfwKC.js} +1 -1
- package/web/_app/immutable/chunks/{BtcGx-KQ.js → -oRvYXNk.js} +1 -1
- package/web/_app/immutable/chunks/0qd6IUbO.js +1 -0
- package/web/_app/immutable/chunks/{DbtvzL7r.js → 7faei0hL.js} +1 -1
- package/web/_app/immutable/chunks/{zasx25UV.js → BArcFmbE.js} +3 -3
- package/web/_app/immutable/chunks/BF1TezEe.js +1 -0
- package/web/_app/immutable/chunks/{Kem7IXj7.js → BF3mdiSX.js} +1 -1
- package/web/_app/immutable/chunks/BFOLKM4z.js +1 -0
- package/web/_app/immutable/chunks/{BPpUSQ2Y.js → BezGs1Zj.js} +1 -1
- package/web/_app/immutable/chunks/Bg-39frG.js +1 -0
- package/web/_app/immutable/chunks/BleKqNM1.js +1 -0
- package/web/_app/immutable/chunks/{e7GRa0yc.js → BpHE30PI.js} +1 -1
- package/web/_app/immutable/chunks/{DAVMwq6b.js → BtF1frCE.js} +1 -1
- package/web/_app/immutable/chunks/{0B47b24t.js → BuNfZSU6.js} +1 -1
- package/web/_app/immutable/chunks/{BAVnZFPa.js → BubCsN15.js} +1 -1
- package/web/_app/immutable/chunks/{CcE14TDI.js → BvhVny3O.js} +1 -1
- package/web/_app/immutable/chunks/{C9Qdslk3.js → Bvs-JCN-.js} +1 -1
- package/web/_app/immutable/chunks/{DFo4P1vU.js → C-F98t31.js} +1 -1
- package/web/_app/immutable/chunks/{Cpjqsnqr.js → C1OaBy7n.js} +1 -1
- package/web/_app/immutable/chunks/{C8FxuPry.js → C4SqDe8e.js} +1 -1
- package/web/_app/immutable/chunks/{D9uNsgxP.js → C74H8jNb.js} +1 -1
- package/web/_app/immutable/chunks/{CUNetmvQ.js → C7bouMY8.js} +1 -1
- package/web/_app/immutable/chunks/{BiHyHFlW.js → C8IrXs1w.js} +1 -1
- package/web/_app/immutable/chunks/{MrW4zd8l.js → CGGjDDVZ.js} +1 -1
- package/web/_app/immutable/chunks/{Wv-ICQRy.js → CL9QeiGC.js} +85 -85
- package/web/_app/immutable/chunks/{BLzZIf7l.js → CMAnq3x6.js} +1 -1
- package/web/_app/immutable/chunks/{OG9GsKke.js → CMe14KUo.js} +1 -1
- package/web/_app/immutable/chunks/{DW5uy3Jw.js → CXc9Iibq.js} +1 -1
- package/web/_app/immutable/chunks/Cb-KPahQ.js +1 -0
- package/web/_app/immutable/chunks/{DWwu8a1P.js → Cf13Ew0A.js} +1 -1
- package/web/_app/immutable/chunks/CkSjC-iE.js +9 -0
- package/web/_app/immutable/chunks/{DY0M2GMn.js → Cn8Mg7_E.js} +1 -1
- package/web/_app/immutable/chunks/{f7mFVWQS.js → CoeYsgbS.js} +1 -1
- package/web/_app/immutable/chunks/{CZSmkKvQ.js → CrqOdm1D.js} +1 -1
- package/web/_app/immutable/chunks/{CVqwkR8N.js → CyJXJYoE.js} +1 -1
- package/web/_app/immutable/chunks/{AnnzTlE8.js → Cz6kYWmu.js} +1 -1
- package/web/_app/immutable/chunks/{ZTEErEOd.js → CzbFy3_5.js} +1 -1
- package/web/_app/immutable/chunks/{Ih6r6QR2.js → D-9suc4b.js} +1 -1
- package/web/_app/immutable/chunks/{BZX8cNYL.js → D37hxBaz.js} +1 -1
- package/web/_app/immutable/chunks/D7yT4F_8.js +517 -0
- package/web/_app/immutable/chunks/{DxRGLDbV.js → DNs5kWvM.js} +1 -1
- package/web/_app/immutable/chunks/DQp-M3XN.js +1 -0
- package/web/_app/immutable/chunks/{D7jLdCNu.js → DR5d84Z7.js} +1 -1
- package/web/_app/immutable/chunks/{DH_19--P.js → DaEL1AOW.js} +1 -1
- package/web/_app/immutable/chunks/{CSKABQj2.js → Dazeexma.js} +1 -1
- package/web/_app/immutable/chunks/{mhWk5y4o.js → Dc2_04md.js} +1 -1
- package/web/_app/immutable/chunks/{1j--pDqt.js → Dcjgv4h2.js} +1 -1
- package/web/_app/immutable/chunks/{CSTv2fL0.js → DgI3Vj5v.js} +1 -1
- package/web/_app/immutable/chunks/{B7LnLClC.js → Dgko1xMH.js} +1 -1
- package/web/_app/immutable/chunks/{DEQsS4Ez.js → DlgOmQ_G.js} +1 -1
- package/web/_app/immutable/chunks/{BLe29cGh.js → DmS5Qyvl.js} +1 -1
- package/web/_app/immutable/chunks/{CglDFp51.js → DqnDX3G8.js} +1 -1
- package/web/_app/immutable/chunks/{CTuuQ66F.js → DyZdoKBK.js} +1 -1
- package/web/_app/immutable/chunks/{CUB9vnd0.js → DzAOHjhV.js} +1 -1
- package/web/_app/immutable/chunks/{DJT73ARj.js → Iqegk1SI.js} +1 -1
- package/web/_app/immutable/chunks/{Mwr1r9aw.js → ONqdwA3r.js} +1 -1
- package/web/_app/immutable/chunks/{Bytzbt5l.js → Oikibjm2.js} +1 -1
- package/web/_app/immutable/chunks/{C1FDDux-.js → T3u7Skc8.js} +1 -1
- package/web/_app/immutable/chunks/{Ch5Z7cbP.js → cWWhqL1F.js} +1 -1
- package/web/_app/immutable/chunks/{CwHHpacD.js → fRXyV4Ec.js} +1 -1
- package/web/_app/immutable/chunks/{CWc-mNzo.js → jAC4YCqW.js} +1 -1
- package/web/_app/immutable/chunks/{hjSZ041d.js → kFEqmLql.js} +1 -1
- package/web/_app/immutable/chunks/{BtYMl4bD.js → y0C1JQ1v.js} +1 -1
- package/web/_app/immutable/chunks/{DJQ6favH.js → yvD4_bP8.js} +1 -1
- package/web/_app/immutable/entry/app.CyEmP0i2.js +2 -0
- package/web/_app/immutable/entry/start.BxCHvKWL.js +1 -0
- package/web/_app/immutable/nodes/0.Bkbiw_sA.js +11 -0
- package/web/_app/immutable/nodes/1.kdx6h8ts.js +1 -0
- package/web/_app/immutable/nodes/2.CtK7BGB8.js +1 -0
- package/web/_app/immutable/nodes/3.D5PZa2kH.js +1 -0
- package/web/_app/immutable/nodes/4.DzS8oPUH.js +3 -0
- package/web/_app/immutable/nodes/5.B-1BDKsL.js +19 -0
- package/web/_app/immutable/nodes/6.DDT1DtCg.js +1 -0
- package/web/_app/immutable/nodes/7.CCNCaItj.js +1 -0
- package/web/_app/immutable/workers/worker-C_nKscAW.js +1 -0
- package/web/_app/version.json +1 -1
- package/web/fonts/LythMono-LICENSE.md +112 -0
- package/web/fonts/lyth-mono-latin-bold.woff2 +0 -0
- package/web/fonts/lyth-mono-latin-bolditalic.woff2 +0 -0
- package/web/fonts/lyth-mono-latin-italic.woff2 +0 -0
- package/web/fonts/lyth-mono-latin-regular.woff2 +0 -0
- package/web/index.html +19 -17
- package/web/_app/immutable/assets/0.BEFtmzIh.css +0 -1
- package/web/_app/immutable/assets/4.LR47AJKA.css +0 -1
- package/web/_app/immutable/assets/NoteList.C2GYAaLW.css +0 -1
- package/web/_app/immutable/chunks/BHORylQC.js +0 -1
- package/web/_app/immutable/chunks/BMijTvgG.js +0 -1
- package/web/_app/immutable/chunks/C7qBBO-i.js +0 -1
- package/web/_app/immutable/chunks/CA-rzUq8.js +0 -524
- package/web/_app/immutable/chunks/DtEVmiYo.js +0 -1
- package/web/_app/immutable/chunks/Qej0SEux.js +0 -1
- package/web/_app/immutable/chunks/TmxIGCN3.js +0 -1
- package/web/_app/immutable/entry/app.B9TPZNFw.js +0 -2
- package/web/_app/immutable/entry/start.WU9HKeyT.js +0 -1
- package/web/_app/immutable/nodes/0.C9NFEqJd.js +0 -3
- package/web/_app/immutable/nodes/1.B5PJwWOG.js +0 -1
- package/web/_app/immutable/nodes/2.DRtC7zjs.js +0 -1
- package/web/_app/immutable/nodes/3.EEMisvUL.js +0 -1
- package/web/_app/immutable/nodes/4.DthmsFQC.js +0 -19
- package/web/_app/immutable/nodes/5.CEu0DlpQ.js +0 -1
- package/web/_app/immutable/nodes/6.DzvF0ZPt.js +0 -1
- package/web/_app/immutable/workers/worker-CWkpeSVG.js +0 -1
- /package/web/_app/immutable/assets/{6.C00vyVPo.css → 7.C00vyVPo.css} +0 -0
package/dist/main.js
CHANGED
|
@@ -15,7 +15,10 @@ import { serve } from "@hono/node-server";
|
|
|
15
15
|
import { Hono } from "hono";
|
|
16
16
|
import { streamSSE } from "hono/streaming";
|
|
17
17
|
import { z } from "zod";
|
|
18
|
-
import {
|
|
18
|
+
import { createModels } from "@earendil-works/pi-ai";
|
|
19
|
+
import { Agent } from "@earendil-works/pi-agent-core";
|
|
20
|
+
import { DEFAULT_COMPACTION_SETTINGS, createBashTool, createEditTool, createFindTool, createGrepTool, createLsTool, createReadTool, createWriteTool, estimateTokens, generateSummary, shouldCompact } from "@earendil-works/pi-coding-agent";
|
|
21
|
+
import { appendFile, mkdir, readFile, readdir, rename, rm, rmdir, stat, writeFile } from "node:fs/promises";
|
|
19
22
|
import { createInterface } from "node:readline";
|
|
20
23
|
import { PassThrough } from "node:stream";
|
|
21
24
|
import { createServer } from "node:net";
|
|
@@ -141,13 +144,13 @@ const COLUMN_ATTRS = [
|
|
|
141
144
|
"columns",
|
|
142
145
|
"at"
|
|
143
146
|
];
|
|
144
|
-
const TAG = /<(iso-[\w-]+)\b([^>]*)>/gi;
|
|
147
|
+
const TAG$1 = /<(iso-[\w-]+)\b([^>]*)>/gi;
|
|
145
148
|
const attrsOf = (s) => Object.fromEntries([...s.matchAll(/([\w-]+)\s*=\s*"([^"]*)"/g)].map((m) => [m[1].toLowerCase(), m[2]]));
|
|
146
149
|
/** The files an artifact reads, from its markup and script: data="…", an iso-model's src, iso.data/text/bytes("…"). */
|
|
147
150
|
function readsOf(html) {
|
|
148
151
|
const own = html.replace(/<!--[\s\S]*?-->/g, ""), reads = /* @__PURE__ */ new Set();
|
|
149
152
|
for (const r of own.matchAll(/iso\.(?:data|text|bytes)\(\s*["'`]([^"'`$]+)["'`]/g)) reads.add(r[1]);
|
|
150
|
-
for (const t of own.matchAll(TAG)) {
|
|
153
|
+
for (const t of own.matchAll(TAG$1)) {
|
|
151
154
|
const a = attrsOf(t[2]);
|
|
152
155
|
(a.data ?? "").split(/[\s,]+/).filter(Boolean).forEach((f) => reads.add(f));
|
|
153
156
|
if (t[1].toLowerCase() === "iso-model" && a.src) reads.add(a.src);
|
|
@@ -173,7 +176,7 @@ function checkArtifact(html, ctx = {}) {
|
|
|
173
176
|
const reads = /* @__PURE__ */ new Set();
|
|
174
177
|
for (const r of own.matchAll(/iso\.(?:data|text|bytes)\(\s*["'`]([^"'`$]+)["'`]/g)) reads.add(r[1]);
|
|
175
178
|
const files = (a) => (a.data ?? "").split(/[\s,]+/).filter(Boolean);
|
|
176
|
-
for (const t of own.matchAll(TAG)) {
|
|
179
|
+
for (const t of own.matchAll(TAG$1)) {
|
|
177
180
|
const a = attrsOf(t[2]);
|
|
178
181
|
files(a).forEach((f) => reads.add(f));
|
|
179
182
|
if (t[1].toLowerCase() === "iso-model" && a.src) reads.add(a.src);
|
|
@@ -183,7 +186,7 @@ function checkArtifact(html, ctx = {}) {
|
|
|
183
186
|
for (const u of m.uses) if (!ctx.files.includes(u)) err(2, `${u} is in isorite:uses but there's no such file beside it${ctx.files.length ? ` (there are: ${ctx.files.join(", ")})` : ""}`);
|
|
184
187
|
}
|
|
185
188
|
if (ctx.uses && [...ctx.uses].sort().join() !== [...m.uses].sort().join()) err(2, `isorite:uses (${m.uses.join(" ") || "none"}) and the uses given (${ctx.uses.join(" ") || "none"}) differ`);
|
|
186
|
-
if (ctx.columns) for (const t of own.matchAll(TAG)) {
|
|
189
|
+
if (ctx.columns) for (const t of own.matchAll(TAG$1)) {
|
|
187
190
|
const a = attrsOf(t[2]), fs = files(a), known = fs.map((f) => ctx.columns[f]);
|
|
188
191
|
if (!fs.length || known.some((k) => !k)) continue;
|
|
189
192
|
const have = [...new Set(known.flat()), ...fs.length > 1 ? ["file"] : []];
|
|
@@ -4574,6 +4577,50 @@ var Engine = class Engine {
|
|
|
4574
4577
|
this.built = this.index = null;
|
|
4575
4578
|
});
|
|
4576
4579
|
}
|
|
4580
|
+
async deleteFolder(path) {
|
|
4581
|
+
path = path.trim().replace(/^\/+|\/+$/g, "");
|
|
4582
|
+
if (!path || !inBase(path)) throw new EngineError("only a folder of the base is deleted here");
|
|
4583
|
+
checkNoteId(path);
|
|
4584
|
+
await this.dropFolder(path, true);
|
|
4585
|
+
}
|
|
4586
|
+
/** Take away an empty folder, and the empty ones inside it. A folder made by putting a note in it is not among the
|
|
4587
|
+
* listing's entries, so `strict` (a folder asked for by name) is what says it was not there. */
|
|
4588
|
+
async dropFolder(path, strict) {
|
|
4589
|
+
if (!this.opts.notes.rmdir) throw new EngineError("a folder can't be deleted here");
|
|
4590
|
+
await this.one(async () => {
|
|
4591
|
+
const inside = [...this.files.values()].filter((f) => f.path.startsWith(`${path}/`));
|
|
4592
|
+
const held = inside.filter((f) => !("dir" in f && f.dir)).length;
|
|
4593
|
+
if (strict && !this.files.has(path) && !inside.length) throw new EngineError(`there is no folder ${path}`, 404);
|
|
4594
|
+
if (held) throw new EngineError(`${path} still holds ${held} ${held === 1 ? "file" : "files"}; move or delete them first`, 409);
|
|
4595
|
+
for (const d of [...inside.map((f) => f.path), path].sort((a, b) => b.length - a.length)) {
|
|
4596
|
+
await this.opts.notes.rmdir(d);
|
|
4597
|
+
this.files.delete(d);
|
|
4598
|
+
}
|
|
4599
|
+
this.built = this.index = null;
|
|
4600
|
+
});
|
|
4601
|
+
}
|
|
4602
|
+
async moveFolder(from, to) {
|
|
4603
|
+
const clean = (p) => p.trim().replace(/^\/+|\/+$/g, "");
|
|
4604
|
+
from = clean(from);
|
|
4605
|
+
to = clean(to);
|
|
4606
|
+
if (!from || !to || !inBase(from) || !inBase(to)) throw new EngineError("only a folder of the base is moved, and only within it");
|
|
4607
|
+
checkNoteId(from);
|
|
4608
|
+
checkNoteId(to);
|
|
4609
|
+
if (to === from) throw new EngineError("the folder is there already");
|
|
4610
|
+
if (to.startsWith(`${from}/`)) throw new EngineError("a folder can't be moved into itself");
|
|
4611
|
+
const inside = [...this.files.values()].filter((f) => f.path.startsWith(`${from}/`));
|
|
4612
|
+
const dirs = inside.filter((f) => "dir" in f && f.dir).map((f) => f.path);
|
|
4613
|
+
const notes = inside.filter((f) => !("dir" in f && f.dir) && f.path.endsWith(".md")).map((f) => f.path.slice(0, -3));
|
|
4614
|
+
const others = inside.filter((f) => !("dir" in f && f.dir) && !f.path.endsWith(".md")).length;
|
|
4615
|
+
if (!this.files.has(from) && !inside.length) throw new EngineError(`there is no folder ${from}`, 404);
|
|
4616
|
+
if (others) throw new EngineError(`${from} holds ${others} ${others === 1 ? "file" : "files"} that ${others === 1 ? "isn't a note" : "aren't notes"} (pictures, say); move those by hand first`, 409);
|
|
4617
|
+
const at = (p) => `${to}${p.slice(from.length)}`;
|
|
4618
|
+
if (this.files.has(`${to}.md`)) throw new EngineError(`there is a note at ${to} already`, 409);
|
|
4619
|
+
for (const id of notes) if (this.files.has(`${at(id)}.md`)) throw new EngineError(`there is already a note at ${at(id)}`, 409);
|
|
4620
|
+
for (const d of [from, ...dirs].sort()) await this.makeFolder(at(d));
|
|
4621
|
+
for (const id of notes) await this.moveNote(id, at(id), (await this.readNote(id)).version);
|
|
4622
|
+
await this.dropFolder(from, false);
|
|
4623
|
+
}
|
|
4577
4624
|
/** `who` is you unless Iso moves it, in Write mode. */
|
|
4578
4625
|
async moveNote(id, to, base, who = "you") {
|
|
4579
4626
|
to = to.trim().replace(/^\/+|\/+$/g, "").replace(/\.md$/, "");
|
|
@@ -4720,7 +4767,7 @@ var Engine = class Engine {
|
|
|
4720
4767
|
}
|
|
4721
4768
|
/** The code a code link names (M29): its symbol's lines, or the lines it gives, or the file's start. A symbol is
|
|
4722
4769
|
* found by its name in the file as it is now, so the link holds where the code has moved. */
|
|
4723
|
-
async code(href) {
|
|
4770
|
+
async code(href, full = false) {
|
|
4724
4771
|
const target = codeTarget(href);
|
|
4725
4772
|
if (!target) throw new EngineError("not a code link", 400);
|
|
4726
4773
|
const { path, symbol } = target, where = await this.repositoryOfBase();
|
|
@@ -4743,6 +4790,23 @@ var Engine = class Engine {
|
|
|
4743
4790
|
const lines = new TextDecoder().decode(bytes).split(/\r?\n/);
|
|
4744
4791
|
if (lines.at(-1) === "") lines.pop();
|
|
4745
4792
|
let start = target.line ?? 1, end = target.end ?? target.line ?? lines.length, count;
|
|
4793
|
+
if (full && symbol) {
|
|
4794
|
+
const found = hasSymbols(path) ? findSymbol(lines.join("\n"), path, symbol) : null;
|
|
4795
|
+
return {
|
|
4796
|
+
state: "ok",
|
|
4797
|
+
path,
|
|
4798
|
+
symbol,
|
|
4799
|
+
start: 1,
|
|
4800
|
+
end: lines.length,
|
|
4801
|
+
total: lines.length,
|
|
4802
|
+
text: lines.join("\n"),
|
|
4803
|
+
...found ? { mark: {
|
|
4804
|
+
start: found.start,
|
|
4805
|
+
end: found.end
|
|
4806
|
+
} } : {},
|
|
4807
|
+
...found?.count && found.count > 1 ? { count: found.count } : {}
|
|
4808
|
+
};
|
|
4809
|
+
}
|
|
4746
4810
|
if (symbol) {
|
|
4747
4811
|
if (!hasSymbols(path)) return {
|
|
4748
4812
|
state: "unsupported",
|
|
@@ -4762,6 +4826,20 @@ var Engine = class Engine {
|
|
|
4762
4826
|
state: "no-symbol",
|
|
4763
4827
|
path
|
|
4764
4828
|
};
|
|
4829
|
+
if (full) return {
|
|
4830
|
+
state: "ok",
|
|
4831
|
+
path,
|
|
4832
|
+
...symbol ? { symbol } : {},
|
|
4833
|
+
start: 1,
|
|
4834
|
+
end: lines.length,
|
|
4835
|
+
total: lines.length,
|
|
4836
|
+
text: lines.join("\n"),
|
|
4837
|
+
...symbol || target.line ? { mark: {
|
|
4838
|
+
start,
|
|
4839
|
+
end
|
|
4840
|
+
} } : {},
|
|
4841
|
+
...count && count > 1 ? { count } : {}
|
|
4842
|
+
};
|
|
4765
4843
|
const shown = Math.min(end, start + 120 - 1);
|
|
4766
4844
|
return {
|
|
4767
4845
|
state: "ok",
|
|
@@ -5301,6 +5379,9 @@ var MemoryFiles = class {
|
|
|
5301
5379
|
async mkdir(dir) {
|
|
5302
5380
|
this.folders.add(dir);
|
|
5303
5381
|
}
|
|
5382
|
+
async rmdir(dir) {
|
|
5383
|
+
this.folders.delete(dir);
|
|
5384
|
+
}
|
|
5304
5385
|
async list(dir) {
|
|
5305
5386
|
const prefix = dir ? dir + "/" : "";
|
|
5306
5387
|
const found = /* @__PURE__ */ new Map();
|
|
@@ -5627,7 +5708,23 @@ function given(asked, parts, title) {
|
|
|
5627
5708
|
if (passages.length) out.push(passages.join("\n\n"));
|
|
5628
5709
|
return out.join("\n\n");
|
|
5629
5710
|
}
|
|
5630
|
-
|
|
5711
|
+
/** The same for an agent that takes embedded context: notes and folders as text for the agent to read with isorite's
|
|
5712
|
+
* tools, and each passage as a resource block (the note's address, the passage as its text) that the text points to. */
|
|
5713
|
+
function givenEmbedded(parts, title, base) {
|
|
5714
|
+
const passages = parts.flatMap((p) => p.kind === "passage" ? [p] : []);
|
|
5715
|
+
return {
|
|
5716
|
+
text: [given("", parts.filter((p) => p.kind !== "passage"), title).trim(), passages.length ? `${passages.length === 1 ? "A passage I selected is" : `${passages.length} passages I selected are`} attached as ${passages.length === 1 ? "a resource" : "resources"}, named for the note ${passages.length === 1 ? "it is" : "each is"} from (the note may have changed since: read it for the current text):\n${passages.map((p) => `- from ${title(p.id)} (${p.id})`).join("\n")}` : ""].filter(Boolean).join("\n\n"),
|
|
5717
|
+
blocks: passages.map((p) => ({
|
|
5718
|
+
type: "resource",
|
|
5719
|
+
resource: {
|
|
5720
|
+
uri: `isorite://${base}/${p.id}`,
|
|
5721
|
+
mimeType: "text/markdown",
|
|
5722
|
+
text: p.text
|
|
5723
|
+
}
|
|
5724
|
+
}))
|
|
5725
|
+
};
|
|
5726
|
+
}
|
|
5727
|
+
const OUTPUT$1 = 4e3;
|
|
5631
5728
|
const NOTE_TOOLS = /(^|[_:./-])(read_note|outline_note|edit_note|write_note)$/;
|
|
5632
5729
|
const CHANGES = /* @__PURE__ */ new Set([
|
|
5633
5730
|
"edit",
|
|
@@ -5643,7 +5740,7 @@ const ACTIONS = {
|
|
|
5643
5740
|
read: "read",
|
|
5644
5741
|
fetch: "webfetch"
|
|
5645
5742
|
};
|
|
5646
|
-
const EMPTY = {
|
|
5743
|
+
const EMPTY$1 = {
|
|
5647
5744
|
connected: false,
|
|
5648
5745
|
agent: "",
|
|
5649
5746
|
session: null,
|
|
@@ -5659,7 +5756,7 @@ const EMPTY = {
|
|
|
5659
5756
|
model: "",
|
|
5660
5757
|
sessions: []
|
|
5661
5758
|
};
|
|
5662
|
-
const textOf = (blocks) => (Array.isArray(blocks) ? blocks : [blocks]).map((b) => b?.type === "text" ? b.text ?? "" : b?.type === "resource_link" ? `@${b.name ?? b.uri ?? ""}` : "").join("");
|
|
5759
|
+
const textOf$1 = (blocks) => (Array.isArray(blocks) ? blocks : [blocks]).map((b) => b?.type === "text" ? b.text ?? "" : b?.type === "resource_link" ? `@${b.name ?? b.uri ?? ""}` : "").join("");
|
|
5663
5760
|
const lines = (v) => v ? v.replace(/\n$/, "").split("\n") : [];
|
|
5664
5761
|
const keyOf = (agent, session) => `${agent}\u0000${session}`;
|
|
5665
5762
|
const str = (v) => typeof v === "string" ? v : "";
|
|
@@ -5712,7 +5809,7 @@ var AcpFollower = class {
|
|
|
5712
5809
|
#shown = null;
|
|
5713
5810
|
#chosen = false;
|
|
5714
5811
|
#reading = null;
|
|
5715
|
-
#view = EMPTY;
|
|
5812
|
+
#view = EMPTY$1;
|
|
5716
5813
|
#ours = 0;
|
|
5717
5814
|
/** Why what the pane last asked of an agent was refused (signing in, a session it does not have). */
|
|
5718
5815
|
#problem = "";
|
|
@@ -5785,12 +5882,13 @@ var AcpFollower = class {
|
|
|
5785
5882
|
size: live.size,
|
|
5786
5883
|
ours: this.#ours,
|
|
5787
5884
|
sessions: this.#sessions(),
|
|
5885
|
+
...live.link.steer ? { steer: true } : {},
|
|
5788
5886
|
commands: live.commands,
|
|
5789
5887
|
settings: [...live.modes ? [live.modes] : [], ...live.configs],
|
|
5790
5888
|
...problem
|
|
5791
5889
|
};
|
|
5792
5890
|
else if (read) this.#view = {
|
|
5793
|
-
...EMPTY,
|
|
5891
|
+
...EMPTY$1,
|
|
5794
5892
|
connected,
|
|
5795
5893
|
reading: true,
|
|
5796
5894
|
agent: read.name,
|
|
@@ -5804,7 +5902,7 @@ var AcpFollower = class {
|
|
|
5804
5902
|
else {
|
|
5805
5903
|
const link = [...this.#links.values()].at(-1);
|
|
5806
5904
|
this.#view = {
|
|
5807
|
-
...EMPTY,
|
|
5905
|
+
...EMPTY$1,
|
|
5808
5906
|
connected,
|
|
5809
5907
|
agent: link?.name ?? "",
|
|
5810
5908
|
agentId: link?.agent ?? "",
|
|
@@ -5885,6 +5983,8 @@ var AcpFollower = class {
|
|
|
5885
5983
|
resume: false,
|
|
5886
5984
|
list: false
|
|
5887
5985
|
},
|
|
5986
|
+
embedded: false,
|
|
5987
|
+
steer: false,
|
|
5888
5988
|
toAgent: /* @__PURE__ */ new Map(),
|
|
5889
5989
|
fromAgent: /* @__PURE__ */ new Map(),
|
|
5890
5990
|
listed: [],
|
|
@@ -5939,11 +6039,36 @@ var AcpFollower = class {
|
|
|
5939
6039
|
type: "open",
|
|
5940
6040
|
...this.#reading ? { session: this.#reading.session } : {},
|
|
5941
6041
|
then: text,
|
|
5942
|
-
...this.#unopened ? {
|
|
6042
|
+
...this.#unopened ? {
|
|
6043
|
+
give: this.#unopened.text,
|
|
6044
|
+
...this.#unopened.blocks?.length ? { giveBlocks: this.#unopened.blocks } : {}
|
|
6045
|
+
} : {}
|
|
5943
6046
|
});
|
|
5944
6047
|
}
|
|
6048
|
+
/** Subagents the agent announced (`subagent_update`), by their session: each is a session of its own that the pane
|
|
6049
|
+
* does not list, and what it reads and changes in the base is told under its own name, `<agent>-<title>-<n>`. */
|
|
6050
|
+
#children = /* @__PURE__ */ new Map();
|
|
6051
|
+
#childCount = /* @__PURE__ */ new Map();
|
|
5945
6052
|
/** Given before a session was open: for the one the next prompt opens. */
|
|
5946
6053
|
#unopened = null;
|
|
6054
|
+
async steer(text) {
|
|
6055
|
+
const live = this.#need();
|
|
6056
|
+
if (!live.link.steer) throw new Error(`${live.link.name || "this agent"} cannot be steered while it works: stop it and ask again`);
|
|
6057
|
+
if (!live.busy) throw new Error("the agent is not working: ask it instead");
|
|
6058
|
+
live.link.send({
|
|
6059
|
+
type: "steer",
|
|
6060
|
+
session: live.session,
|
|
6061
|
+
text
|
|
6062
|
+
});
|
|
6063
|
+
this.#edit(live, (items) => [...items, {
|
|
6064
|
+
kind: "message",
|
|
6065
|
+
id: `steer-${live.turns.length}-${items.length}`,
|
|
6066
|
+
text,
|
|
6067
|
+
mode: "steer",
|
|
6068
|
+
state: "sent"
|
|
6069
|
+
}]);
|
|
6070
|
+
this.#tell();
|
|
6071
|
+
}
|
|
5947
6072
|
async interrupt() {
|
|
5948
6073
|
const live = this.#need();
|
|
5949
6074
|
live.link.send({
|
|
@@ -5952,24 +6077,31 @@ var AcpFollower = class {
|
|
|
5952
6077
|
});
|
|
5953
6078
|
}
|
|
5954
6079
|
/** Notes and passages given from the pane: those not given in the session yet go before its next prompt. */
|
|
5955
|
-
async give(parts, render) {
|
|
6080
|
+
async give(parts, render, embed) {
|
|
5956
6081
|
const live = this.#shownLive();
|
|
6082
|
+
const made = (p, embedded) => {
|
|
6083
|
+
const e = embedded && embed ? embed(p) : null;
|
|
6084
|
+
return e ? {
|
|
6085
|
+
parts: p,
|
|
6086
|
+
text: e.text,
|
|
6087
|
+
...e.blocks.length ? { blocks: e.blocks } : {}
|
|
6088
|
+
} : {
|
|
6089
|
+
parts: p,
|
|
6090
|
+
text: render(p)
|
|
6091
|
+
};
|
|
6092
|
+
};
|
|
5957
6093
|
if (!live) {
|
|
5958
|
-
|
|
5959
|
-
|
|
5960
|
-
text: render(parts)
|
|
5961
|
-
} : null;
|
|
6094
|
+
const link = [...this.#links.values()].at(-1);
|
|
6095
|
+
this.#unopened = parts.length ? made(parts, link?.embedded === true) : null;
|
|
5962
6096
|
return;
|
|
5963
6097
|
}
|
|
5964
6098
|
const fresh = parts.filter((g) => !live.given.has(givenKey(g)));
|
|
5965
|
-
live.pending = fresh.length ?
|
|
5966
|
-
parts: fresh,
|
|
5967
|
-
text: render(fresh)
|
|
5968
|
-
} : null;
|
|
6099
|
+
live.pending = fresh.length ? made(fresh, live.link.embedded) : null;
|
|
5969
6100
|
live.link.send({
|
|
5970
6101
|
type: "give",
|
|
5971
6102
|
session: live.session,
|
|
5972
|
-
text: live.pending?.text ?? ""
|
|
6103
|
+
text: live.pending?.text ?? "",
|
|
6104
|
+
...live.pending?.blocks ? { blocks: live.pending.blocks } : {}
|
|
5973
6105
|
});
|
|
5974
6106
|
}
|
|
5975
6107
|
async reply(id, decision) {
|
|
@@ -6122,6 +6254,8 @@ var AcpFollower = class {
|
|
|
6122
6254
|
case "initialize": {
|
|
6123
6255
|
const info = m.result?.agentInfo;
|
|
6124
6256
|
const caps = m.result?.agentCapabilities ?? {};
|
|
6257
|
+
link.embedded = caps.promptCapabilities?.embeddedContext === true;
|
|
6258
|
+
link.steer = (m.result?._meta)?.steering?.supported === true;
|
|
6125
6259
|
link.agent = agentName(info?.name ?? "agent");
|
|
6126
6260
|
link.name = info?.title ?? info?.name ?? "The agent";
|
|
6127
6261
|
link.can = {
|
|
@@ -6200,7 +6334,7 @@ var AcpFollower = class {
|
|
|
6200
6334
|
return;
|
|
6201
6335
|
}
|
|
6202
6336
|
if (m.method === "session/update") {
|
|
6203
|
-
const live = this.#lives.get(keyOf(link.agent, sessionId));
|
|
6337
|
+
const live = this.#lives.get(keyOf(link.agent, sessionId)) ?? this.#children.get(keyOf(link.agent, sessionId));
|
|
6204
6338
|
if (live) this.#update(live, m.params?.update ?? {});
|
|
6205
6339
|
}
|
|
6206
6340
|
}
|
|
@@ -6276,7 +6410,7 @@ var AcpFollower = class {
|
|
|
6276
6410
|
live.busy = true;
|
|
6277
6411
|
live.turns = [...live.turns, {
|
|
6278
6412
|
id: key,
|
|
6279
|
-
asked: textOf(given ? blocks.slice(1) : blocks),
|
|
6413
|
+
asked: textOf$1(given ? blocks.slice(1) : blocks),
|
|
6280
6414
|
from: way === "ours" ? "here" : "terminal",
|
|
6281
6415
|
items: [],
|
|
6282
6416
|
state: "running",
|
|
@@ -6319,7 +6453,7 @@ var AcpFollower = class {
|
|
|
6319
6453
|
switch (u.sessionUpdate) {
|
|
6320
6454
|
case "user_message_chunk": {
|
|
6321
6455
|
if (!live.loading) break;
|
|
6322
|
-
const text = textOf(u.content), t = live.turns.at(-1);
|
|
6456
|
+
const text = textOf$1(u.content), t = live.turns.at(-1);
|
|
6323
6457
|
if (live.replaying && t && !t.items.length) live.turns = [...live.turns.slice(0, -1), {
|
|
6324
6458
|
...t,
|
|
6325
6459
|
asked: t.asked + text
|
|
@@ -6336,7 +6470,7 @@ var AcpFollower = class {
|
|
|
6336
6470
|
}
|
|
6337
6471
|
case "agent_message_chunk": {
|
|
6338
6472
|
live.replaying = false;
|
|
6339
|
-
const text = textOf(u.content);
|
|
6473
|
+
const text = textOf$1(u.content);
|
|
6340
6474
|
this.#edit(live, (items) => {
|
|
6341
6475
|
const end = items.at(-1);
|
|
6342
6476
|
if (end?.kind === "text") items[items.length - 1] = {
|
|
@@ -6351,6 +6485,24 @@ var AcpFollower = class {
|
|
|
6351
6485
|
});
|
|
6352
6486
|
break;
|
|
6353
6487
|
}
|
|
6488
|
+
case "agent_thought_chunk": {
|
|
6489
|
+
live.replaying = false;
|
|
6490
|
+
const text = textOf$1(u.content);
|
|
6491
|
+
if (!text) break;
|
|
6492
|
+
this.#edit(live, (items) => {
|
|
6493
|
+
const end = items.at(-1);
|
|
6494
|
+
if (end?.kind === "thinking") items[items.length - 1] = {
|
|
6495
|
+
kind: "thinking",
|
|
6496
|
+
text: end.text + text
|
|
6497
|
+
};
|
|
6498
|
+
else items.push({
|
|
6499
|
+
kind: "thinking",
|
|
6500
|
+
text
|
|
6501
|
+
});
|
|
6502
|
+
return items;
|
|
6503
|
+
});
|
|
6504
|
+
break;
|
|
6505
|
+
}
|
|
6354
6506
|
case "tool_call":
|
|
6355
6507
|
case "tool_call_update": {
|
|
6356
6508
|
live.replaying = false;
|
|
@@ -6368,7 +6520,7 @@ var AcpFollower = class {
|
|
|
6368
6520
|
} : items[at];
|
|
6369
6521
|
const content = Array.isArray(u.content) ? u.content : null;
|
|
6370
6522
|
const diff = content?.find((c) => c.type === "diff");
|
|
6371
|
-
const output = content ? content.filter((c) => c.type === "content").map((c) => textOf(c.content)).join("\n").slice(0, OUTPUT) : was.output;
|
|
6523
|
+
const output = content ? content.filter((c) => c.type === "content").map((c) => textOf$1(c.content)).join("\n").slice(0, OUTPUT$1) : was.output;
|
|
6372
6524
|
const input = u.rawInput ?? {};
|
|
6373
6525
|
kind = typeof u.kind === "string" ? u.kind : diff ? "edit" : "";
|
|
6374
6526
|
const name = typeof u.name === "string" ? u.name : kind && at < 0 ? kind : was.name;
|
|
@@ -6420,6 +6572,58 @@ var AcpFollower = class {
|
|
|
6420
6572
|
case "session_info_update":
|
|
6421
6573
|
if (typeof u.title === "string") live.title = u.title;
|
|
6422
6574
|
break;
|
|
6575
|
+
case "subagent_update": {
|
|
6576
|
+
const id = str(u.sessionId);
|
|
6577
|
+
if (!id || live.link.agent === "") break;
|
|
6578
|
+
const key = keyOf(live.link.agent, id), title = str(u.title) || str(u.description) || "subagent";
|
|
6579
|
+
let child = this.#children.get(key);
|
|
6580
|
+
if (!child) {
|
|
6581
|
+
const n = (this.#childCount.get(live.link.agent) ?? 0) + 1;
|
|
6582
|
+
this.#childCount.set(live.link.agent, n);
|
|
6583
|
+
const agent = agentName(`${live.link.agent}-${title.split(/\s+/).slice(0, 3).join("-")}-${n}`);
|
|
6584
|
+
child = {
|
|
6585
|
+
...live,
|
|
6586
|
+
link: {
|
|
6587
|
+
...live.link,
|
|
6588
|
+
agent
|
|
6589
|
+
},
|
|
6590
|
+
session: id,
|
|
6591
|
+
title,
|
|
6592
|
+
turns: [{
|
|
6593
|
+
id,
|
|
6594
|
+
asked: title,
|
|
6595
|
+
from: live.from,
|
|
6596
|
+
items: [],
|
|
6597
|
+
state: "running"
|
|
6598
|
+
}],
|
|
6599
|
+
seen: /* @__PURE__ */ new Set(),
|
|
6600
|
+
asking: /* @__PURE__ */ new Map(),
|
|
6601
|
+
pending: null,
|
|
6602
|
+
given: /* @__PURE__ */ new Set(),
|
|
6603
|
+
loading: false,
|
|
6604
|
+
replaying: false
|
|
6605
|
+
};
|
|
6606
|
+
this.#children.set(key, child);
|
|
6607
|
+
}
|
|
6608
|
+
const state = u.state ?? {};
|
|
6609
|
+
const status = state.state === "idle" ? state.error || state.stopReason === "error" ? "failed" : "done" : "running";
|
|
6610
|
+
this.#edit(live, (items) => {
|
|
6611
|
+
const at = items.findIndex((i) => i.kind === "tool" && i.id === `subagent-${id}`);
|
|
6612
|
+
const was = at < 0 ? null : items[at];
|
|
6613
|
+
const now = {
|
|
6614
|
+
kind: "tool",
|
|
6615
|
+
id: `subagent-${id}`,
|
|
6616
|
+
name: "subagent",
|
|
6617
|
+
title: was?.title || title,
|
|
6618
|
+
status,
|
|
6619
|
+
output: str(u.description) || was?.output || ""
|
|
6620
|
+
};
|
|
6621
|
+
if (at < 0) items.push(now);
|
|
6622
|
+
else items[at] = now;
|
|
6623
|
+
return items;
|
|
6624
|
+
});
|
|
6625
|
+
break;
|
|
6626
|
+
}
|
|
6423
6627
|
case "available_commands_update":
|
|
6424
6628
|
live.commands = commandsOf(u.availableCommands);
|
|
6425
6629
|
break;
|
|
@@ -6489,7 +6693,7 @@ var AcpFollower = class {
|
|
|
6489
6693
|
};
|
|
6490
6694
|
//#endregion
|
|
6491
6695
|
//#region packages/server/src/version.ts
|
|
6492
|
-
const VERSION = "0.
|
|
6696
|
+
const VERSION = "0.18.0";
|
|
6493
6697
|
//#endregion
|
|
6494
6698
|
//#region packages/server/src/connect.ts
|
|
6495
6699
|
const HARNESSES = [{
|
|
@@ -6792,8 +6996,8 @@ function missingKey(command, env = process.env) {
|
|
|
6792
6996
|
//#region packages/server/src/agents.ts
|
|
6793
6997
|
init_self();
|
|
6794
6998
|
const FRESH = 36e5;
|
|
6795
|
-
const ID = /^[a-z0-9][a-z0-9._-]{0,63}$/;
|
|
6796
|
-
const dataDir = (env) => join(env.XDG_DATA_HOME || join(homedir(), ".local", "share"), "isorite", "agents");
|
|
6999
|
+
const ID$2 = /^[a-z0-9][a-z0-9._-]{0,63}$/;
|
|
7000
|
+
const dataDir$1 = (env) => join(env.XDG_DATA_HOME || join(homedir(), ".local", "share"), "isorite", "agents");
|
|
6797
7001
|
/** The registry's name for this platform. */
|
|
6798
7002
|
const platform$1 = () => `${process.platform === "win32" ? "windows" : process.platform}-${process.arch === "arm64" ? "aarch64" : process.arch === "x64" ? "x86_64" : process.arch}`;
|
|
6799
7003
|
/** A word a shell takes as it is. */
|
|
@@ -6805,7 +7009,7 @@ async function readRegistry(env = process.env) {
|
|
|
6805
7009
|
const from = env.ISORITE_ACP_REGISTRY || "https://cdn.agentclientprotocol.com/registry/v1/latest/registry.json";
|
|
6806
7010
|
const parse = (text) => {
|
|
6807
7011
|
const agents = JSON.parse(text).agents;
|
|
6808
|
-
return Array.isArray(agents) ? agents.filter((a) => typeof a?.id === "string" && ID.test(a.id) && typeof a.name === "string") : [];
|
|
7012
|
+
return Array.isArray(agents) ? agents.filter((a) => typeof a?.id === "string" && ID$2.test(a.id) && typeof a.name === "string") : [];
|
|
6809
7013
|
};
|
|
6810
7014
|
if (!/^https?:/.test(from)) return parse(readFileSync(from, "utf8"));
|
|
6811
7015
|
const kept = join(cacheDir(env), "acp-registry.json");
|
|
@@ -6848,7 +7052,7 @@ async function commandOf(e, env = process.env) {
|
|
|
6848
7052
|
const d = e.distribution ?? {};
|
|
6849
7053
|
const bin = d.binary?.[platform$1()];
|
|
6850
7054
|
if (bin) {
|
|
6851
|
-
const dir = join(dataDir(env), e.id, (e.version || "latest").replace(/[^\w.-]/g, "_"));
|
|
7055
|
+
const dir = join(dataDir$1(env), e.id, (e.version || "latest").replace(/[^\w.-]/g, "_"));
|
|
6852
7056
|
const cmd = resolve(dir, bin.cmd);
|
|
6853
7057
|
if (!cmd.startsWith(dir)) throw new Error(`${e.name}: its program is outside its folder`);
|
|
6854
7058
|
if (!existsSync(cmd)) await fetchProgram(bin, dir);
|
|
@@ -6923,7 +7127,7 @@ async function fetchProgram(bin, dir) {
|
|
|
6923
7127
|
/** What bases.toml says this base has chosen. */
|
|
6924
7128
|
function chosenAgent(base, config) {
|
|
6925
7129
|
const row = table(readConfig(basesPath(config))[base]);
|
|
6926
|
-
if (typeof row.agent === "string" && ID.test(row.agent)) return { id: row.agent };
|
|
7130
|
+
if (typeof row.agent === "string" && ID$2.test(row.agent)) return { id: row.agent };
|
|
6927
7131
|
if (typeof row.agent_command === "string" && row.agent_command.trim()) return { command: row.agent_command.trim() };
|
|
6928
7132
|
return null;
|
|
6929
7133
|
}
|
|
@@ -7213,7 +7417,8 @@ const AgentStatus = z.object({
|
|
|
7213
7417
|
session: z.string().min(1).max(200),
|
|
7214
7418
|
busy: z.boolean()
|
|
7215
7419
|
}).strict();
|
|
7216
|
-
function createApp({ api, token, loopback, web, allowHosts = [], notes, acp, bases, base, agents, config }) {
|
|
7420
|
+
function createApp({ api, token, loopback, web, allowHosts = [], notes, acp, terminal, bases, base, agents, config, pi }) {
|
|
7421
|
+
const pane = terminal ?? acp;
|
|
7217
7422
|
const app = new Hono();
|
|
7218
7423
|
const allowed = new Set([...allowHosts].map((h) => h.toLowerCase()));
|
|
7219
7424
|
const reply = (c, status, value) => c.body(JSON.stringify(value), status, {
|
|
@@ -7325,7 +7530,7 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes, acp, bas
|
|
|
7325
7530
|
});
|
|
7326
7531
|
app.get("/api/code", async (c) => {
|
|
7327
7532
|
const href = c.req.query("href") ?? "";
|
|
7328
|
-
return answer(c, () => api.code(href));
|
|
7533
|
+
return answer(c, () => api.code(href, c.req.query("full") === "1"));
|
|
7329
7534
|
});
|
|
7330
7535
|
app.put("/api/note", async (c) => {
|
|
7331
7536
|
const refused = writeRefused(c);
|
|
@@ -7365,6 +7570,11 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes, acp, bas
|
|
|
7365
7570
|
base: z.string().max(64)
|
|
7366
7571
|
}).strict(), (c, b) => api.moveNote(id(c), b.to, b.base));
|
|
7367
7572
|
post("/api/folder", z.object({ path: z.string().max(400) }).strict(), (_c, b) => api.makeFolder(b.path));
|
|
7573
|
+
post("/api/folder/delete", z.object({ path: z.string().max(400) }).strict(), (_c, b) => api.deleteFolder(b.path));
|
|
7574
|
+
post("/api/folder/move", z.object({
|
|
7575
|
+
from: z.string().max(400),
|
|
7576
|
+
to: z.string().max(400)
|
|
7577
|
+
}).strict(), (_c, b) => api.moveFolder(b.from, b.to));
|
|
7368
7578
|
post("/api/note/delete", z.object({ base: z.string().max(64) }).strict(), (c, b) => api.deleteNote(id(c), b.base));
|
|
7369
7579
|
post("/api/note/done", z.object({
|
|
7370
7580
|
done: z.boolean(),
|
|
@@ -7467,8 +7677,54 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes, acp, bas
|
|
|
7467
7677
|
return agents.choose(b.choice);
|
|
7468
7678
|
});
|
|
7469
7679
|
}
|
|
7470
|
-
if (
|
|
7471
|
-
|
|
7680
|
+
if (pi) {
|
|
7681
|
+
const mine = (c) => {
|
|
7682
|
+
if (!local(c)) throw new EngineError("the model and keys are set only from this machine", 403);
|
|
7683
|
+
};
|
|
7684
|
+
app.get("/api/pi", (c) => answer(c, async () => {
|
|
7685
|
+
mine(c);
|
|
7686
|
+
return pi.state();
|
|
7687
|
+
}));
|
|
7688
|
+
const Provider = z.string().regex(/^[a-z0-9-]{1,40}$/);
|
|
7689
|
+
const invalid = async (fn) => {
|
|
7690
|
+
try {
|
|
7691
|
+
await fn();
|
|
7692
|
+
} catch (e) {
|
|
7693
|
+
throw new EngineError(e.message, 400);
|
|
7694
|
+
}
|
|
7695
|
+
};
|
|
7696
|
+
post("/api/pi/key", z.object({
|
|
7697
|
+
provider: Provider,
|
|
7698
|
+
key: z.string().max(2e3)
|
|
7699
|
+
}).strict(), async (c, b) => {
|
|
7700
|
+
mine(c);
|
|
7701
|
+
await invalid(() => pi.creds.set(b.provider, b.key));
|
|
7702
|
+
});
|
|
7703
|
+
post("/api/pi/key/remove", z.object({ provider: Provider }).strict(), async (c, b) => {
|
|
7704
|
+
mine(c);
|
|
7705
|
+
await invalid(() => pi.creds.remove(b.provider));
|
|
7706
|
+
});
|
|
7707
|
+
post("/api/pi/reach", z.object({
|
|
7708
|
+
files: z.enum([
|
|
7709
|
+
"off",
|
|
7710
|
+
"read",
|
|
7711
|
+
"write"
|
|
7712
|
+
]),
|
|
7713
|
+
shell: z.boolean()
|
|
7714
|
+
}).strict(), async (c, b) => {
|
|
7715
|
+
mine(c);
|
|
7716
|
+
await invalid(() => pi.setReach(b));
|
|
7717
|
+
});
|
|
7718
|
+
post("/api/pi/choose", z.object({
|
|
7719
|
+
provider: Provider,
|
|
7720
|
+
model: z.string().min(1).max(200)
|
|
7721
|
+
}).strict(), async (c, b) => {
|
|
7722
|
+
mine(c);
|
|
7723
|
+
await invalid(() => pi.choose(b.provider, b.model));
|
|
7724
|
+
});
|
|
7725
|
+
}
|
|
7726
|
+
if (pane) {
|
|
7727
|
+
app.get("/api/terminal", (c) => answer(c, async () => pane.view()));
|
|
7472
7728
|
const failed = (e) => {
|
|
7473
7729
|
throw new EngineError(e.message, 409);
|
|
7474
7730
|
};
|
|
@@ -7480,11 +7736,22 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes, acp, bas
|
|
|
7480
7736
|
const broken = await api.brokenNews();
|
|
7481
7737
|
const news = broken.length ? `\n\n(isorite: code links in the notes that no longer resolve: ${broken.map((x) => `${x.href} in /${x.id}.md`).join("; ")}.)` : "";
|
|
7482
7738
|
const title = (id) => titles.get(id) ?? id;
|
|
7483
|
-
await
|
|
7484
|
-
await
|
|
7739
|
+
await pane.give(b.given, (parts) => given("", parts, title).trim(), (parts) => givenEmbedded(parts, title, base ?? "notes")).catch(failed);
|
|
7740
|
+
await pane.prompt(b.asked.trim() + news).catch(failed);
|
|
7485
7741
|
});
|
|
7742
|
+
post("/api/terminal/steer", z.object({
|
|
7743
|
+
asked: z.string().min(1).max(8e3),
|
|
7744
|
+
given: z.array(Given).max(40)
|
|
7745
|
+
}).strict(), async (_c, b) => {
|
|
7746
|
+
if (!pane.steer) throw new EngineError("this agent cannot be steered while it works: stop it and ask again", 409);
|
|
7747
|
+
const titles = new Map((await api.notes()).notes.map((n) => [n.id, n.title]));
|
|
7748
|
+
const title = (id) => titles.get(id) ?? id;
|
|
7749
|
+
await pane.give(b.given, (parts) => given("", parts, title).trim(), (parts) => givenEmbedded(parts, title, base ?? "notes")).catch(failed);
|
|
7750
|
+
await pane.steer(b.asked.trim()).catch(failed);
|
|
7751
|
+
});
|
|
7752
|
+
post("/api/terminal/withdraw", z.object({ given: Given }).strict(), async (_c, b) => ({ withdrawn: pane.withdraw ? await pane.withdraw(b.given) : false }));
|
|
7486
7753
|
post("/api/terminal/stop", z.object({}).strict(), async () => {
|
|
7487
|
-
await
|
|
7754
|
+
await pane.interrupt().catch(failed);
|
|
7488
7755
|
});
|
|
7489
7756
|
post("/api/terminal/permission", z.object({
|
|
7490
7757
|
id: z.string().max(200),
|
|
@@ -7494,7 +7761,7 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes, acp, bas
|
|
|
7494
7761
|
"reject"
|
|
7495
7762
|
])
|
|
7496
7763
|
}).strict(), async (_c, b) => {
|
|
7497
|
-
await
|
|
7764
|
+
await pane.reply(b.id, b.decision).catch(failed);
|
|
7498
7765
|
});
|
|
7499
7766
|
}
|
|
7500
7767
|
const endpoint = new McpEndpoint(api, {
|
|
@@ -7527,7 +7794,7 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes, acp, bas
|
|
|
7527
7794
|
const quiet = Number(c.req.query("quiet"));
|
|
7528
7795
|
const who = (rpc) => {
|
|
7529
7796
|
const agent = named && /^[a-z0-9][a-z0-9-]{0,39}$/.test(named) ? named : agentName(clientName(rpc) ?? "mcp");
|
|
7530
|
-
const asked =
|
|
7797
|
+
const asked = pane?.asked(agent) ?? "";
|
|
7531
7798
|
return {
|
|
7532
7799
|
agent,
|
|
7533
7800
|
session: (c.req.query("session") ?? "http").slice(0, 200),
|
|
@@ -7543,25 +7810,38 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes, acp, bas
|
|
|
7543
7810
|
if (acp) {
|
|
7544
7811
|
const render = async () => {
|
|
7545
7812
|
const titles = new Map((await api.notes()).notes.map((n) => [n.id, n.title]));
|
|
7546
|
-
|
|
7813
|
+
const title = (id) => titles.get(id) ?? id;
|
|
7814
|
+
return {
|
|
7815
|
+
text: (parts) => given("", parts, title).trim(),
|
|
7816
|
+
embed: (parts) => givenEmbedded(parts, title, base ?? "notes")
|
|
7817
|
+
};
|
|
7547
7818
|
};
|
|
7819
|
+
const host = pane ?? acp;
|
|
7548
7820
|
const refused = (e) => {
|
|
7549
7821
|
throw new EngineError(e.message, 409);
|
|
7550
7822
|
};
|
|
7551
7823
|
post("/api/acp/give", z.object({ given: z.array(Given).max(40) }).strict(), async (_c, b) => {
|
|
7552
|
-
|
|
7824
|
+
const r = await render();
|
|
7825
|
+
await host.give(b.given, r.text, r.embed).catch(refused);
|
|
7553
7826
|
});
|
|
7554
7827
|
post("/api/acp/follow", z.object({
|
|
7555
7828
|
agent: z.string().max(40),
|
|
7556
7829
|
session: z.string().min(1).max(200)
|
|
7557
7830
|
}).strict(), async (_c, b) => {
|
|
7558
|
-
await
|
|
7831
|
+
await host.follow(b.agent, b.session).catch(refused);
|
|
7832
|
+
});
|
|
7833
|
+
post("/api/acp/fork", z.object({
|
|
7834
|
+
agent: z.string().max(40),
|
|
7835
|
+
session: z.string().min(1).max(200)
|
|
7836
|
+
}).strict(), async (_c, b) => {
|
|
7837
|
+
if (!host.fork) throw new EngineError("this agent's sessions cannot be forked here", 409);
|
|
7838
|
+
await host.fork(b.agent, b.session).catch(refused);
|
|
7559
7839
|
});
|
|
7560
7840
|
post("/api/acp/forget", z.object({
|
|
7561
7841
|
agent: z.string().max(40),
|
|
7562
7842
|
session: z.string().min(1).max(200)
|
|
7563
7843
|
}).strict(), async (_c, b) => {
|
|
7564
|
-
await
|
|
7844
|
+
await host.forget(b.agent, b.session).catch(refused);
|
|
7565
7845
|
});
|
|
7566
7846
|
post("/api/acp/set", z.object({
|
|
7567
7847
|
kind: z.enum(["mode", "config"]),
|
|
@@ -7569,16 +7849,16 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes, acp, bas
|
|
|
7569
7849
|
value: z.string().max(200)
|
|
7570
7850
|
}).strict(), async (_c, b) => {
|
|
7571
7851
|
try {
|
|
7572
|
-
|
|
7852
|
+
host.set(b.kind, b.id, b.value);
|
|
7573
7853
|
} catch (e) {
|
|
7574
7854
|
throw new EngineError(e.message, 409);
|
|
7575
7855
|
}
|
|
7576
7856
|
});
|
|
7577
7857
|
post("/api/acp/new", z.object({ agent: z.string().max(40) }).strict(), async (_c, b) => {
|
|
7578
|
-
await
|
|
7858
|
+
await host.fresh(b.agent).catch(refused);
|
|
7579
7859
|
});
|
|
7580
7860
|
post("/api/acp/list", z.object({}).strict(), async () => {
|
|
7581
|
-
|
|
7861
|
+
host.refresh();
|
|
7582
7862
|
});
|
|
7583
7863
|
post("/api/acp/tap", z.object({
|
|
7584
7864
|
link: z.string().min(1).max(100),
|
|
@@ -7650,7 +7930,7 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes, acp, bas
|
|
|
7650
7930
|
})
|
|
7651
7931
|
}).catch(() => void 0);
|
|
7652
7932
|
};
|
|
7653
|
-
const unfollow =
|
|
7933
|
+
const unfollow = pane?.listen(send);
|
|
7654
7934
|
const beat = setInterval(() => {
|
|
7655
7935
|
stream.write(": still here\n\n");
|
|
7656
7936
|
}, 25e3);
|
|
@@ -7658,7 +7938,7 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes, acp, bas
|
|
|
7658
7938
|
event: "open",
|
|
7659
7939
|
data: "{}"
|
|
7660
7940
|
});
|
|
7661
|
-
if (
|
|
7941
|
+
if (pane?.view().connected) send(pane.view());
|
|
7662
7942
|
streams.set(stream, c.req.query("agent") === void 0);
|
|
7663
7943
|
watchers();
|
|
7664
7944
|
await new Promise((done) => stream.onAbort(done));
|
|
@@ -7692,6 +7972,1268 @@ function staticFile(c, web) {
|
|
|
7692
7972
|
});
|
|
7693
7973
|
}
|
|
7694
7974
|
//#endregion
|
|
7975
|
+
//#region packages/server/src/pi/store.ts
|
|
7976
|
+
/** Said when a saved session cannot be read: names the file, so the person can look at it or move it away. */
|
|
7977
|
+
var StoreError = class extends Error {
|
|
7978
|
+
file;
|
|
7979
|
+
constructor(file, why) {
|
|
7980
|
+
super(`The saved pi session ${file} could not be read (${why}). It was left where it is.`);
|
|
7981
|
+
this.file = file;
|
|
7982
|
+
}
|
|
7983
|
+
};
|
|
7984
|
+
const ID$1 = /^[0-9a-f-]{8,64}$/i;
|
|
7985
|
+
var FileStore = class {
|
|
7986
|
+
#dir;
|
|
7987
|
+
constructor(dir) {
|
|
7988
|
+
this.#dir = dir;
|
|
7989
|
+
}
|
|
7990
|
+
async save(r) {
|
|
7991
|
+
if (!ID$1.test(r.session)) throw new Error("not a session id");
|
|
7992
|
+
await mkdir(this.#dir, {
|
|
7993
|
+
recursive: true,
|
|
7994
|
+
mode: 448
|
|
7995
|
+
});
|
|
7996
|
+
const file = join(this.#dir, `${r.session}.json`), tmp = `${file}.${process.pid}.tmp`;
|
|
7997
|
+
await writeFile(tmp, JSON.stringify(r), { mode: 384 });
|
|
7998
|
+
await rename(tmp, file);
|
|
7999
|
+
}
|
|
8000
|
+
async latest() {
|
|
8001
|
+
let names;
|
|
8002
|
+
try {
|
|
8003
|
+
names = (await readdir(this.#dir)).filter((n) => n.endsWith(".json"));
|
|
8004
|
+
} catch {
|
|
8005
|
+
return null;
|
|
8006
|
+
}
|
|
8007
|
+
const newest = (await Promise.all(names.map(async (n) => ({
|
|
8008
|
+
n,
|
|
8009
|
+
t: (await stat(join(this.#dir, n))).mtimeMs
|
|
8010
|
+
})))).sort((a, b) => b.t - a.t)[0];
|
|
8011
|
+
if (!newest) return null;
|
|
8012
|
+
return this.#read(join(this.#dir, newest.n));
|
|
8013
|
+
}
|
|
8014
|
+
async #read(file) {
|
|
8015
|
+
let record;
|
|
8016
|
+
try {
|
|
8017
|
+
record = JSON.parse(await readFile(file, "utf8"));
|
|
8018
|
+
} catch (e) {
|
|
8019
|
+
throw new StoreError(file, e.message);
|
|
8020
|
+
}
|
|
8021
|
+
const r = record;
|
|
8022
|
+
if (!r || r.version !== 1 || typeof r.session !== "string" || !Array.isArray(r.turns) || !Array.isArray(r.messages)) throw new StoreError(file, "it is not a session this version of isorite wrote");
|
|
8023
|
+
return r;
|
|
8024
|
+
}
|
|
8025
|
+
async list() {
|
|
8026
|
+
let names;
|
|
8027
|
+
try {
|
|
8028
|
+
names = (await readdir(this.#dir)).filter((n) => n.endsWith(".json"));
|
|
8029
|
+
} catch {
|
|
8030
|
+
return {
|
|
8031
|
+
records: [],
|
|
8032
|
+
unreadable: []
|
|
8033
|
+
};
|
|
8034
|
+
}
|
|
8035
|
+
const records = [], unreadable = [];
|
|
8036
|
+
for (const n of names) try {
|
|
8037
|
+
records.push(await this.#read(join(this.#dir, n)));
|
|
8038
|
+
} catch (e) {
|
|
8039
|
+
unreadable.push(e instanceof StoreError ? e.file : join(this.#dir, n));
|
|
8040
|
+
}
|
|
8041
|
+
return {
|
|
8042
|
+
records: records.sort((a, b) => a.last < b.last ? 1 : -1),
|
|
8043
|
+
unreadable
|
|
8044
|
+
};
|
|
8045
|
+
}
|
|
8046
|
+
async load(session) {
|
|
8047
|
+
if (!ID$1.test(session)) return null;
|
|
8048
|
+
const file = join(this.#dir, `${session}.json`);
|
|
8049
|
+
try {
|
|
8050
|
+
await stat(file);
|
|
8051
|
+
} catch {
|
|
8052
|
+
return null;
|
|
8053
|
+
}
|
|
8054
|
+
return this.#read(file);
|
|
8055
|
+
}
|
|
8056
|
+
async remove(session) {
|
|
8057
|
+
if (!ID$1.test(session)) throw new Error("not a session id");
|
|
8058
|
+
await rm(join(this.#dir, `${session}.json`), { force: true });
|
|
8059
|
+
}
|
|
8060
|
+
};
|
|
8061
|
+
/** Where a base's pi sessions are kept: isorite's data folder, apart from the notes and the repository. */
|
|
8062
|
+
function storeDir(notes, env = process.env) {
|
|
8063
|
+
const key = createHash("sha1").update(notes).digest("hex").slice(0, 12);
|
|
8064
|
+
return join(env.XDG_DATA_HOME || join(homedir(), ".local", "share"), "isorite", "pi", key);
|
|
8065
|
+
}
|
|
8066
|
+
//#endregion
|
|
8067
|
+
//#region packages/server/src/pi/tools.ts
|
|
8068
|
+
/** `who` names the agent and session as the base knows them; `asked` is what is being asked now, for a change the agent makes. */
|
|
8069
|
+
async function isoriteTools(api, who, asked) {
|
|
8070
|
+
return (await api.agentTools()).map(({ function: f }) => ({
|
|
8071
|
+
name: f.name,
|
|
8072
|
+
label: f.name,
|
|
8073
|
+
description: f.description,
|
|
8074
|
+
parameters: f.parameters,
|
|
8075
|
+
execute: async (_id, params) => {
|
|
8076
|
+
const now = asked();
|
|
8077
|
+
const { text } = await api.agentLook({
|
|
8078
|
+
...who,
|
|
8079
|
+
tool: f.name,
|
|
8080
|
+
args: params,
|
|
8081
|
+
...now ? { asked: now } : {}
|
|
8082
|
+
});
|
|
8083
|
+
return {
|
|
8084
|
+
content: [{
|
|
8085
|
+
type: "text",
|
|
8086
|
+
text
|
|
8087
|
+
}],
|
|
8088
|
+
details: void 0
|
|
8089
|
+
};
|
|
8090
|
+
}
|
|
8091
|
+
}));
|
|
8092
|
+
}
|
|
8093
|
+
/** What a call acted on, in a line: a query, a note, a path, a command. */
|
|
8094
|
+
function titleOf(args) {
|
|
8095
|
+
if (!args || typeof args !== "object") return "";
|
|
8096
|
+
const a = args;
|
|
8097
|
+
for (const k of [
|
|
8098
|
+
"query",
|
|
8099
|
+
"note",
|
|
8100
|
+
"id",
|
|
8101
|
+
"path",
|
|
8102
|
+
"command",
|
|
8103
|
+
"question",
|
|
8104
|
+
"text"
|
|
8105
|
+
]) if (typeof a[k] === "string" && a[k]) return String(a[k]).slice(0, 200);
|
|
8106
|
+
const first = Object.values(a).find((v) => typeof v === "string" && v);
|
|
8107
|
+
return typeof first === "string" ? first.slice(0, 200) : "";
|
|
8108
|
+
}
|
|
8109
|
+
//#endregion
|
|
8110
|
+
//#region packages/server/src/pi/host.ts
|
|
8111
|
+
const OUTPUT = 4e3;
|
|
8112
|
+
/** What the summary of an earlier conversation begins with, in the message that stands for it. */
|
|
8113
|
+
const SUMMARY = "[The earlier part of this conversation, summarised to make room]\n";
|
|
8114
|
+
const EMPTY = {
|
|
8115
|
+
connected: true,
|
|
8116
|
+
agent: "",
|
|
8117
|
+
session: null,
|
|
8118
|
+
title: "",
|
|
8119
|
+
busy: false,
|
|
8120
|
+
turns: [],
|
|
8121
|
+
asking: [],
|
|
8122
|
+
cost: 0,
|
|
8123
|
+
tokens: {
|
|
8124
|
+
input: 0,
|
|
8125
|
+
output: 0
|
|
8126
|
+
},
|
|
8127
|
+
model: "",
|
|
8128
|
+
sessions: []
|
|
8129
|
+
};
|
|
8130
|
+
/** A block of context given from the tray is tagged with the chip it came from and the turn it came in with, so it can be
|
|
8131
|
+
* taken back out of what the model is sent when the chip is removed (M36, step 10). */
|
|
8132
|
+
const TAG = /^\[context (\S+) @(\S+)\]\n/;
|
|
8133
|
+
const WITHDRAWN = "[Context the person gave earlier here was withdrawn: they took it out of the conversation. Do not rely on it; read the notes again if you need them.]";
|
|
8134
|
+
const tagged = (key, turn, block) => `[context ${encodeURIComponent(key)} @${turn}]\n${block}\n[/context]\n\n`;
|
|
8135
|
+
const textOf = (content) => (Array.isArray(content) ? content : []).map((b) => b?.type === "text" ? b.text ?? "" : "").join("");
|
|
8136
|
+
const userText = (content) => typeof content === "string" ? content : textOf(content);
|
|
8137
|
+
var PiHost = class {
|
|
8138
|
+
#api;
|
|
8139
|
+
#model;
|
|
8140
|
+
#streamFn;
|
|
8141
|
+
#getApiKey;
|
|
8142
|
+
#id;
|
|
8143
|
+
#name;
|
|
8144
|
+
#ask;
|
|
8145
|
+
#more;
|
|
8146
|
+
#guard;
|
|
8147
|
+
#now;
|
|
8148
|
+
#store;
|
|
8149
|
+
#compaction;
|
|
8150
|
+
/** The thinking item the assistant message now streaming writes into. */
|
|
8151
|
+
#thinkingAt = -1;
|
|
8152
|
+
/** Cancels a summary being written. */
|
|
8153
|
+
#summarising = null;
|
|
8154
|
+
/** Why the last thing asked of the agent could not be done, shown in the pane. */
|
|
8155
|
+
#problem = "";
|
|
8156
|
+
/** The other sessions kept, by id (the one shown is in the fields below); and the last write, waited for before one is left. */
|
|
8157
|
+
#kept = /* @__PURE__ */ new Map();
|
|
8158
|
+
#saving = Promise.resolve();
|
|
8159
|
+
#agent = null;
|
|
8160
|
+
#session = "";
|
|
8161
|
+
#at = "";
|
|
8162
|
+
#title = "";
|
|
8163
|
+
#turns = [];
|
|
8164
|
+
#busy = false;
|
|
8165
|
+
/** The person stopped it: an aborted run is then a stop, not a failure. */
|
|
8166
|
+
#stopping = false;
|
|
8167
|
+
#cost = 0;
|
|
8168
|
+
#used = 0;
|
|
8169
|
+
#asking = /* @__PURE__ */ new Map();
|
|
8170
|
+
#allowed = /* @__PURE__ */ new Set();
|
|
8171
|
+
#pending = null;
|
|
8172
|
+
/** The blocks taken back out: `<key>@<turn>` of each, kept with the session. */
|
|
8173
|
+
#withdrawn = /* @__PURE__ */ new Set();
|
|
8174
|
+
#given = /* @__PURE__ */ new Set();
|
|
8175
|
+
/** What was said while it worked and has not reached the model: the item's id, and the text as the model will have it. */
|
|
8176
|
+
#queued = [];
|
|
8177
|
+
/** The first user message of a run is the prompt itself: it is not a queued message landing. */
|
|
8178
|
+
#primed = false;
|
|
8179
|
+
/** The text item the assistant message now streaming writes into. */
|
|
8180
|
+
#writing = -1;
|
|
8181
|
+
#view = EMPTY;
|
|
8182
|
+
#listeners = /* @__PURE__ */ new Set();
|
|
8183
|
+
constructor({ api, model, streamFn, id = "pi", name = "pi", ask = () => false, more = () => [], guard = () => void 0, clock = Date.now, store, compaction, getApiKey }) {
|
|
8184
|
+
this.#getApiKey = getApiKey;
|
|
8185
|
+
this.#api = api;
|
|
8186
|
+
this.#model = model;
|
|
8187
|
+
this.#streamFn = streamFn;
|
|
8188
|
+
this.#id = id;
|
|
8189
|
+
this.#name = name;
|
|
8190
|
+
this.#ask = ask;
|
|
8191
|
+
this.#more = more;
|
|
8192
|
+
this.#guard = guard;
|
|
8193
|
+
this.#now = clock;
|
|
8194
|
+
this.#store = store ?? null;
|
|
8195
|
+
this.#compaction = {
|
|
8196
|
+
enabled: compaction?.enabled ?? DEFAULT_COMPACTION_SETTINGS.enabled ?? true,
|
|
8197
|
+
reserveTokens: compaction?.reserveTokens ?? DEFAULT_COMPACTION_SETTINGS.reserveTokens ?? 16384,
|
|
8198
|
+
keepRecentTokens: compaction?.keepRecentTokens ?? DEFAULT_COMPACTION_SETTINGS.keepRecentTokens ?? 2e4
|
|
8199
|
+
};
|
|
8200
|
+
api.events?.((e) => {
|
|
8201
|
+
if (e.type !== "agent-item" || e.agent !== this.#id || !this.#turns.length) return;
|
|
8202
|
+
this.#edit((items) => [...items, e.item]);
|
|
8203
|
+
this.#tell();
|
|
8204
|
+
});
|
|
8205
|
+
this.#tell();
|
|
8206
|
+
}
|
|
8207
|
+
/** Carry on with another model (and the way to reach it): the session stays, and the next request goes to it. */
|
|
8208
|
+
use(model, streamFn, name) {
|
|
8209
|
+
this.#model = model;
|
|
8210
|
+
this.#streamFn = streamFn;
|
|
8211
|
+
if (name) this.#name = name;
|
|
8212
|
+
if (this.#agent) this.#agent.state.model = model;
|
|
8213
|
+
this.#tell();
|
|
8214
|
+
}
|
|
8215
|
+
view() {
|
|
8216
|
+
return this.#view;
|
|
8217
|
+
}
|
|
8218
|
+
listen(fn) {
|
|
8219
|
+
this.#listeners.add(fn);
|
|
8220
|
+
return () => {
|
|
8221
|
+
this.#listeners.delete(fn);
|
|
8222
|
+
};
|
|
8223
|
+
}
|
|
8224
|
+
asked(agent) {
|
|
8225
|
+
const t = this.#turns.at(-1);
|
|
8226
|
+
return agent === this.#id && t?.state === "running" ? t.asked : "";
|
|
8227
|
+
}
|
|
8228
|
+
async give(parts, render) {
|
|
8229
|
+
const fresh = parts.filter((g) => !this.#given.has(givenKey(g)));
|
|
8230
|
+
this.#pending = fresh.length ? {
|
|
8231
|
+
parts: fresh,
|
|
8232
|
+
blocks: fresh.map((g) => render([g]))
|
|
8233
|
+
} : null;
|
|
8234
|
+
}
|
|
8235
|
+
async prompt(text) {
|
|
8236
|
+
if (this.#busy) {
|
|
8237
|
+
await this.#say(text, "followUp");
|
|
8238
|
+
return;
|
|
8239
|
+
}
|
|
8240
|
+
this.#problem = "";
|
|
8241
|
+
if (!this.#agent) this.#open();
|
|
8242
|
+
const agent = this.#agent;
|
|
8243
|
+
const pending = this.#pending;
|
|
8244
|
+
this.#pending = null;
|
|
8245
|
+
if (pending) for (const g of pending.parts) this.#given.add(givenKey(g));
|
|
8246
|
+
agent.state.tools = [...await isoriteTools(this.#api, {
|
|
8247
|
+
agent: this.#id,
|
|
8248
|
+
session: this.#session
|
|
8249
|
+
}, () => this.asked(this.#id)), ...this.#more()];
|
|
8250
|
+
const turn = {
|
|
8251
|
+
id: randomUUID(),
|
|
8252
|
+
asked: text,
|
|
8253
|
+
from: "here",
|
|
8254
|
+
items: [],
|
|
8255
|
+
state: "running",
|
|
8256
|
+
...pending ? { given: pending.parts } : {},
|
|
8257
|
+
at: new Date(this.#now()).toISOString()
|
|
8258
|
+
};
|
|
8259
|
+
this.#turns = [...this.#turns, turn];
|
|
8260
|
+
if (!this.#title) this.#title = text.slice(0, 80);
|
|
8261
|
+
this.#busy = true;
|
|
8262
|
+
this.#primed = false;
|
|
8263
|
+
this.#queued = [];
|
|
8264
|
+
this.#writing = -1;
|
|
8265
|
+
this.#thinkingAt = -1;
|
|
8266
|
+
this.#api.agentStatus(this.#id, this.#session, true).catch(() => void 0);
|
|
8267
|
+
this.#tell();
|
|
8268
|
+
const message = this.#message(pending, turn.id.slice(0, 8), text);
|
|
8269
|
+
(async () => {
|
|
8270
|
+
await this.#compactIfNeeded(agent);
|
|
8271
|
+
if (this.#stopping) throw new Error("stopped");
|
|
8272
|
+
await agent.prompt(message);
|
|
8273
|
+
})().then(() => void 0, (e) => {
|
|
8274
|
+
if (this.#stopping) this.#set("stopped");
|
|
8275
|
+
else this.#fail(e.message);
|
|
8276
|
+
}).then(() => this.#end());
|
|
8277
|
+
}
|
|
8278
|
+
/** The person's message for the model: each block given with it, tagged, then what they asked. */
|
|
8279
|
+
#message(pending, turn, text) {
|
|
8280
|
+
return {
|
|
8281
|
+
role: "user",
|
|
8282
|
+
content: [...(pending?.parts ?? []).map((g, i) => tagged(givenKey(g), turn, pending.blocks[i] ?? "")), text].map((t) => ({
|
|
8283
|
+
type: "text",
|
|
8284
|
+
text: t
|
|
8285
|
+
})),
|
|
8286
|
+
timestamp: this.#now()
|
|
8287
|
+
};
|
|
8288
|
+
}
|
|
8289
|
+
/** What the model is sent: the messages, with each withdrawn block replaced by a line saying so. */
|
|
8290
|
+
#redact(messages) {
|
|
8291
|
+
if (!this.#withdrawn.size) return messages;
|
|
8292
|
+
return messages.map((m) => {
|
|
8293
|
+
const msg = m;
|
|
8294
|
+
if (msg.role !== "user" || !Array.isArray(msg.content)) return m;
|
|
8295
|
+
let changed = false;
|
|
8296
|
+
const content = msg.content.map((c) => {
|
|
8297
|
+
const tag = c?.type === "text" && c.text ? TAG.exec(c.text) : null;
|
|
8298
|
+
if (!tag || !this.#withdrawn.has(`${decodeURIComponent(tag[1])}@${tag[2]}`)) return c;
|
|
8299
|
+
changed = true;
|
|
8300
|
+
return {
|
|
8301
|
+
type: "text",
|
|
8302
|
+
text: WITHDRAWN
|
|
8303
|
+
};
|
|
8304
|
+
});
|
|
8305
|
+
return changed ? {
|
|
8306
|
+
...m,
|
|
8307
|
+
content
|
|
8308
|
+
} : m;
|
|
8309
|
+
});
|
|
8310
|
+
}
|
|
8311
|
+
/** The person took a chip out of the tray: the block given with it comes out of everything the model is sent from now
|
|
8312
|
+
* on. The turns it came in stay as they were, marked. Says whether there was anything to take out. */
|
|
8313
|
+
async withdraw(g) {
|
|
8314
|
+
const key = givenKey(g);
|
|
8315
|
+
if (this.#pending) {
|
|
8316
|
+
const keep = this.#pending.parts.map((p, i) => ({
|
|
8317
|
+
p,
|
|
8318
|
+
b: this.#pending.blocks[i]
|
|
8319
|
+
})).filter((x) => givenKey(x.p) !== key);
|
|
8320
|
+
this.#pending = keep.length ? {
|
|
8321
|
+
parts: keep.map((x) => x.p),
|
|
8322
|
+
blocks: keep.map((x) => x.b)
|
|
8323
|
+
} : null;
|
|
8324
|
+
}
|
|
8325
|
+
if (!this.#given.has(key)) return false;
|
|
8326
|
+
this.#given.delete(key);
|
|
8327
|
+
for (const m of this.#agent?.state.messages ?? []) {
|
|
8328
|
+
const msg = m;
|
|
8329
|
+
if (msg.role !== "user" || !Array.isArray(msg.content)) continue;
|
|
8330
|
+
for (const c of msg.content) {
|
|
8331
|
+
const tag = c?.type === "text" && c.text ? TAG.exec(c.text) : null;
|
|
8332
|
+
if (tag && decodeURIComponent(tag[1]) === key) this.#withdrawn.add(`${key}@${tag[2]}`);
|
|
8333
|
+
}
|
|
8334
|
+
}
|
|
8335
|
+
this.#turns = this.#turns.map((t) => {
|
|
8336
|
+
const at = (t.given ?? []).flatMap((x, i) => givenKey(x) === key && !t.withdrawn?.includes(i) ? [i] : []);
|
|
8337
|
+
return at.length ? {
|
|
8338
|
+
...t,
|
|
8339
|
+
withdrawn: [...t.withdrawn ?? [], ...at]
|
|
8340
|
+
} : t;
|
|
8341
|
+
});
|
|
8342
|
+
this.#save();
|
|
8343
|
+
this.#keep();
|
|
8344
|
+
this.#tell();
|
|
8345
|
+
return true;
|
|
8346
|
+
}
|
|
8347
|
+
async steer(text) {
|
|
8348
|
+
if (!this.#busy) throw new Error("the agent is not working: ask it instead");
|
|
8349
|
+
await this.#say(text, "steer");
|
|
8350
|
+
}
|
|
8351
|
+
/** Said while it works: a steer comes in after the step now running, a follow-up when it would otherwise stop. */
|
|
8352
|
+
async #say(text, mode) {
|
|
8353
|
+
const agent = this.#agent;
|
|
8354
|
+
if (!agent) return;
|
|
8355
|
+
const pending = this.#pending;
|
|
8356
|
+
this.#pending = null;
|
|
8357
|
+
if (pending) for (const g of pending.parts) this.#given.add(givenKey(g));
|
|
8358
|
+
const id = randomUUID();
|
|
8359
|
+
const message = this.#message(pending, (this.#turns.at(-1)?.id ?? id).slice(0, 8), text);
|
|
8360
|
+
this.#queued.push({
|
|
8361
|
+
id,
|
|
8362
|
+
body: userText(message.content)
|
|
8363
|
+
});
|
|
8364
|
+
this.#edit((items) => [...items, {
|
|
8365
|
+
kind: "message",
|
|
8366
|
+
id,
|
|
8367
|
+
text,
|
|
8368
|
+
mode,
|
|
8369
|
+
state: "queued"
|
|
8370
|
+
}]);
|
|
8371
|
+
if (mode === "steer") agent.steer(message);
|
|
8372
|
+
else agent.followUp(message);
|
|
8373
|
+
this.#tell();
|
|
8374
|
+
}
|
|
8375
|
+
async interrupt() {
|
|
8376
|
+
this.#stopping = true;
|
|
8377
|
+
this.#summarising?.abort();
|
|
8378
|
+
this.#agent?.clearAllQueues();
|
|
8379
|
+
for (const a of this.#asking.values()) a.wait("reject");
|
|
8380
|
+
this.#agent?.abort();
|
|
8381
|
+
}
|
|
8382
|
+
async reply(id, decision) {
|
|
8383
|
+
const asked = this.#asking.get(id);
|
|
8384
|
+
if (!asked) throw new Error("the agent is not asking that");
|
|
8385
|
+
asked.wait(decision);
|
|
8386
|
+
}
|
|
8387
|
+
/** Show another kept session, and carry on in it. The one left is already kept (it is written after each turn). */
|
|
8388
|
+
async follow(_agent, session) {
|
|
8389
|
+
if (session === this.#session) return;
|
|
8390
|
+
if (this.#busy) throw new Error("the agent is working: stop it first");
|
|
8391
|
+
const kept = await this.#fetch(session);
|
|
8392
|
+
this.#restore(kept);
|
|
8393
|
+
}
|
|
8394
|
+
/** A new session from the pane; the one shown stays kept and can be shown again. */
|
|
8395
|
+
async fresh(_agent) {
|
|
8396
|
+
if (this.#busy) throw new Error("the agent is working: stop it first");
|
|
8397
|
+
await this.#saving;
|
|
8398
|
+
this.#blank();
|
|
8399
|
+
this.#tell();
|
|
8400
|
+
}
|
|
8401
|
+
/** A new session that begins where `session` is (its turns and the model's messages), going its own way from there;
|
|
8402
|
+
* its cost starts again at nothing. The original is not changed. */
|
|
8403
|
+
async fork(_agent, session) {
|
|
8404
|
+
if (this.#busy) throw new Error("the agent is working: stop it first");
|
|
8405
|
+
const from = session === this.#session && this.#agent ? this.#record() : await this.#fetch(session);
|
|
8406
|
+
if (!from || !this.#store) throw new Error("that session is not kept, so there is nothing to fork");
|
|
8407
|
+
const now = new Date(this.#now()).toISOString();
|
|
8408
|
+
const copy = {
|
|
8409
|
+
...JSON.parse(JSON.stringify(from)),
|
|
8410
|
+
session: randomUUID(),
|
|
8411
|
+
title: `${from.title || "A session"} (fork)`.slice(0, 90),
|
|
8412
|
+
at: now,
|
|
8413
|
+
last: now,
|
|
8414
|
+
cost: 0
|
|
8415
|
+
};
|
|
8416
|
+
await this.#store.save(copy);
|
|
8417
|
+
this.#kept.set(copy.session, this.#summary(copy));
|
|
8418
|
+
this.#restore(copy);
|
|
8419
|
+
}
|
|
8420
|
+
set() {
|
|
8421
|
+
throw new Error(`${this.#name} offers no modes or options yet`);
|
|
8422
|
+
}
|
|
8423
|
+
/** Remove what is kept of a session. The one shown is left first, for a new one. */
|
|
8424
|
+
async forget(_agent, session) {
|
|
8425
|
+
if (!this.#store) throw new Error("nothing is kept apart from the session shown");
|
|
8426
|
+
if (session === this.#session) {
|
|
8427
|
+
if (this.#busy) throw new Error("the agent is working: stop it first");
|
|
8428
|
+
await this.#saving;
|
|
8429
|
+
this.#blank();
|
|
8430
|
+
}
|
|
8431
|
+
await this.#store.remove(session);
|
|
8432
|
+
this.#kept.delete(session);
|
|
8433
|
+
this.#tell();
|
|
8434
|
+
}
|
|
8435
|
+
/** Read the list of kept sessions again. */
|
|
8436
|
+
refresh() {
|
|
8437
|
+
this.#index().then(() => this.#tell());
|
|
8438
|
+
}
|
|
8439
|
+
async #index() {
|
|
8440
|
+
if (!this.#store) return;
|
|
8441
|
+
const { records, unreadable } = await this.#store.list();
|
|
8442
|
+
this.#kept = new Map(records.map((r) => [r.session, this.#summary(r)]));
|
|
8443
|
+
if (unreadable.length) this.#problem = `${unreadable.length === 1 ? "A saved session" : `${unreadable.length} saved sessions`} could not be read (${unreadable.join(", ")}). ${unreadable.length === 1 ? "It was" : "They were"} left where ${unreadable.length === 1 ? "it is" : "they are"}.`;
|
|
8444
|
+
}
|
|
8445
|
+
#summary(r) {
|
|
8446
|
+
return {
|
|
8447
|
+
session: r.session,
|
|
8448
|
+
title: r.title,
|
|
8449
|
+
asked: r.turns[0]?.asked ?? "",
|
|
8450
|
+
last: r.last,
|
|
8451
|
+
turns: r.turns.length,
|
|
8452
|
+
notes: 0,
|
|
8453
|
+
live: false,
|
|
8454
|
+
busy: false,
|
|
8455
|
+
kept: true,
|
|
8456
|
+
open: true
|
|
8457
|
+
};
|
|
8458
|
+
}
|
|
8459
|
+
async #fetch(session) {
|
|
8460
|
+
if (!this.#store) throw new Error("that session is not here to carry on");
|
|
8461
|
+
await this.#saving;
|
|
8462
|
+
const kept = await this.#store.load(session);
|
|
8463
|
+
if (!kept) throw new Error("that session is not kept");
|
|
8464
|
+
return kept;
|
|
8465
|
+
}
|
|
8466
|
+
/** No session shown: the next question begins a new one. Everything of the last is let go (it stays kept). */
|
|
8467
|
+
#blank() {
|
|
8468
|
+
this.#agent = null;
|
|
8469
|
+
this.#session = "";
|
|
8470
|
+
this.#at = "";
|
|
8471
|
+
this.#turns = [];
|
|
8472
|
+
this.#title = "";
|
|
8473
|
+
this.#cost = 0;
|
|
8474
|
+
this.#used = 0;
|
|
8475
|
+
this.#given = /* @__PURE__ */ new Set();
|
|
8476
|
+
this.#withdrawn = /* @__PURE__ */ new Set();
|
|
8477
|
+
this.#pending = null;
|
|
8478
|
+
this.#queued = [];
|
|
8479
|
+
this.#problem = "";
|
|
8480
|
+
this.#writing = -1;
|
|
8481
|
+
this.#thinkingAt = -1;
|
|
8482
|
+
this.#primed = false;
|
|
8483
|
+
this.#stopping = false;
|
|
8484
|
+
}
|
|
8485
|
+
/** Show `kept` and carry on from it: its turns for the pane, its messages for the model. A turn that was running when
|
|
8486
|
+
* the server went down is shown as stopped: it never finished. */
|
|
8487
|
+
#restore(kept) {
|
|
8488
|
+
this.#blank();
|
|
8489
|
+
this.#open(kept);
|
|
8490
|
+
this.#turns = kept.turns.map((t) => t.state === "running" ? {
|
|
8491
|
+
...t,
|
|
8492
|
+
state: "stopped"
|
|
8493
|
+
} : t);
|
|
8494
|
+
this.#title = kept.title;
|
|
8495
|
+
this.#cost = kept.cost;
|
|
8496
|
+
this.#used = kept.used;
|
|
8497
|
+
this.#given = new Set(kept.given);
|
|
8498
|
+
this.#withdrawn = new Set(kept.withdrawn ?? []);
|
|
8499
|
+
this.#tell();
|
|
8500
|
+
}
|
|
8501
|
+
/** Take up the session last kept, if there is one. An unreadable one is reported in the pane and left alone, and the
|
|
8502
|
+
* agent starts fresh. */
|
|
8503
|
+
async resume() {
|
|
8504
|
+
if (!this.#store || this.#agent) return;
|
|
8505
|
+
let kept;
|
|
8506
|
+
try {
|
|
8507
|
+
kept = await this.#store.latest();
|
|
8508
|
+
} catch (e) {
|
|
8509
|
+
this.#problem = e.message;
|
|
8510
|
+
this.#tell();
|
|
8511
|
+
return;
|
|
8512
|
+
}
|
|
8513
|
+
try {
|
|
8514
|
+
await this.#index();
|
|
8515
|
+
} catch {}
|
|
8516
|
+
if (!kept) {
|
|
8517
|
+
this.#tell();
|
|
8518
|
+
return;
|
|
8519
|
+
}
|
|
8520
|
+
this.#restore(kept);
|
|
8521
|
+
}
|
|
8522
|
+
#record() {
|
|
8523
|
+
return {
|
|
8524
|
+
version: 1,
|
|
8525
|
+
session: this.#session,
|
|
8526
|
+
title: this.#title,
|
|
8527
|
+
at: this.#at,
|
|
8528
|
+
last: this.#turns.at(-1)?.at ?? this.#at,
|
|
8529
|
+
cost: this.#cost,
|
|
8530
|
+
used: this.#used,
|
|
8531
|
+
given: [...this.#given],
|
|
8532
|
+
withdrawn: [...this.#withdrawn],
|
|
8533
|
+
turns: this.#turns,
|
|
8534
|
+
messages: JSON.parse(JSON.stringify(this.#agent.state.messages)).filter((m) => m.role !== "system")
|
|
8535
|
+
};
|
|
8536
|
+
}
|
|
8537
|
+
#save() {
|
|
8538
|
+
if (!this.#store || !this.#agent || !this.#turns.length) return;
|
|
8539
|
+
const record = this.#record();
|
|
8540
|
+
this.#kept.set(record.session, this.#summary(record));
|
|
8541
|
+
const store = this.#store;
|
|
8542
|
+
this.#saving = this.#saving.then(() => store.save(record)).catch((e) => {
|
|
8543
|
+
this.#problem = `Could not keep this session: ${e.message}`;
|
|
8544
|
+
this.#tell();
|
|
8545
|
+
});
|
|
8546
|
+
}
|
|
8547
|
+
#open(kept) {
|
|
8548
|
+
this.#session = kept?.session ?? randomUUID();
|
|
8549
|
+
this.#at = kept?.at ?? new Date(this.#now()).toISOString();
|
|
8550
|
+
this.#agent = new Agent({
|
|
8551
|
+
initialState: {
|
|
8552
|
+
systemPrompt: INSTRUCTIONS,
|
|
8553
|
+
model: this.#model,
|
|
8554
|
+
...kept ? { messages: kept.messages } : {}
|
|
8555
|
+
},
|
|
8556
|
+
transformContext: async (messages) => this.#redact(messages),
|
|
8557
|
+
streamFn: ((...args) => this.#streamFn(...args)),
|
|
8558
|
+
...this.#getApiKey ? { getApiKey: this.#getApiKey } : {},
|
|
8559
|
+
sessionId: this.#session,
|
|
8560
|
+
beforeToolCall: async ({ toolCall, args }) => {
|
|
8561
|
+
let refused;
|
|
8562
|
+
try {
|
|
8563
|
+
refused = this.#guard({
|
|
8564
|
+
tool: toolCall.name,
|
|
8565
|
+
args
|
|
8566
|
+
});
|
|
8567
|
+
} catch {
|
|
8568
|
+
refused = "that could not be checked";
|
|
8569
|
+
}
|
|
8570
|
+
if (refused) return {
|
|
8571
|
+
block: true,
|
|
8572
|
+
reason: refused
|
|
8573
|
+
};
|
|
8574
|
+
if (!this.#ask(toolCall.name) || this.#allowed.has(toolCall.name)) return void 0;
|
|
8575
|
+
const id = randomUUID();
|
|
8576
|
+
const decision = await new Promise((wait) => {
|
|
8577
|
+
this.#asking.set(id, {
|
|
8578
|
+
id,
|
|
8579
|
+
action: toolCall.name,
|
|
8580
|
+
resources: [titleOf(args)],
|
|
8581
|
+
tool: toolCall.name,
|
|
8582
|
+
wait
|
|
8583
|
+
});
|
|
8584
|
+
this.#tell();
|
|
8585
|
+
});
|
|
8586
|
+
this.#asking.delete(id);
|
|
8587
|
+
this.#tell();
|
|
8588
|
+
if (decision === "reject") return {
|
|
8589
|
+
block: true,
|
|
8590
|
+
reason: "The person declined this."
|
|
8591
|
+
};
|
|
8592
|
+
if (decision === "always") this.#allowed.add(toolCall.name);
|
|
8593
|
+
}
|
|
8594
|
+
});
|
|
8595
|
+
this.#agent.subscribe((e) => this.#on(e));
|
|
8596
|
+
}
|
|
8597
|
+
#on(e) {
|
|
8598
|
+
if (e.type === "message_start" && e.message.role === "user") {
|
|
8599
|
+
if (!this.#primed) {
|
|
8600
|
+
this.#primed = true;
|
|
8601
|
+
return;
|
|
8602
|
+
}
|
|
8603
|
+
const said = userText(e.message.content);
|
|
8604
|
+
const at = this.#queued.findIndex((q) => q.body === said);
|
|
8605
|
+
if (at < 0) return;
|
|
8606
|
+
const [q] = this.#queued.splice(at, 1);
|
|
8607
|
+
this.#edit((items) => {
|
|
8608
|
+
const was = items.find((i) => i.kind === "message" && i.id === q.id);
|
|
8609
|
+
return was ? [...items.filter((i) => i !== was), {
|
|
8610
|
+
...was,
|
|
8611
|
+
state: "landed"
|
|
8612
|
+
}] : items;
|
|
8613
|
+
});
|
|
8614
|
+
} else if (e.type === "message_start" && e.message.role === "assistant") {
|
|
8615
|
+
this.#edit((items) => [...items, {
|
|
8616
|
+
kind: "text",
|
|
8617
|
+
text: ""
|
|
8618
|
+
}]);
|
|
8619
|
+
this.#writing = (this.#turns.at(-1)?.items.length ?? 1) - 1;
|
|
8620
|
+
} else if (e.type === "message_update" && e.assistantMessageEvent.type === "thinking_delta") {
|
|
8621
|
+
const delta = e.assistantMessageEvent.delta;
|
|
8622
|
+
if (this.#thinkingAt < 0) {
|
|
8623
|
+
const at = Math.max(this.#writing, 0);
|
|
8624
|
+
this.#edit((items) => [
|
|
8625
|
+
...items.slice(0, at),
|
|
8626
|
+
{
|
|
8627
|
+
kind: "thinking",
|
|
8628
|
+
text: ""
|
|
8629
|
+
},
|
|
8630
|
+
...items.slice(at)
|
|
8631
|
+
]);
|
|
8632
|
+
this.#thinkingAt = at;
|
|
8633
|
+
this.#writing = at + 1;
|
|
8634
|
+
}
|
|
8635
|
+
const t = this.#thinkingAt;
|
|
8636
|
+
this.#edit((items) => items.map((i, k) => k === t && i.kind === "thinking" ? {
|
|
8637
|
+
...i,
|
|
8638
|
+
text: i.text + delta
|
|
8639
|
+
} : i));
|
|
8640
|
+
} else if (e.type === "message_update" && e.assistantMessageEvent.type === "text_delta") {
|
|
8641
|
+
const at = this.#writing, delta = e.assistantMessageEvent.delta;
|
|
8642
|
+
this.#edit((items) => items.map((i, k) => k === at && i.kind === "text" ? {
|
|
8643
|
+
...i,
|
|
8644
|
+
text: i.text + delta
|
|
8645
|
+
} : i));
|
|
8646
|
+
} else if (e.type === "message_end" && e.message.role === "assistant") {
|
|
8647
|
+
const m = e.message;
|
|
8648
|
+
const at = this.#writing, think = this.#thinkingAt, whole = textOf(m.content);
|
|
8649
|
+
const thought = (Array.isArray(m.content) ? m.content : []).map((b) => b?.type === "thinking" ? b.thinking ?? "" : "").join("");
|
|
8650
|
+
this.#edit((items) => items.flatMap((i, k) => k === think ? thought.trim() ? [{
|
|
8651
|
+
kind: "thinking",
|
|
8652
|
+
text: thought
|
|
8653
|
+
}] : [] : k !== at ? [i] : whole ? [{
|
|
8654
|
+
kind: "text",
|
|
8655
|
+
text: whole
|
|
8656
|
+
}] : []));
|
|
8657
|
+
this.#writing = -1;
|
|
8658
|
+
this.#thinkingAt = -1;
|
|
8659
|
+
this.#cost += m.usage?.cost?.total ?? 0;
|
|
8660
|
+
this.#used = m.usage?.totalTokens ?? this.#used;
|
|
8661
|
+
} else if (e.type === "tool_execution_start") this.#edit((items) => [...items, {
|
|
8662
|
+
kind: "tool",
|
|
8663
|
+
id: e.toolCallId,
|
|
8664
|
+
name: e.toolName,
|
|
8665
|
+
title: titleOf(e.args),
|
|
8666
|
+
status: "running",
|
|
8667
|
+
output: ""
|
|
8668
|
+
}]);
|
|
8669
|
+
else if (e.type === "tool_execution_end") {
|
|
8670
|
+
const out = textOf(e.result?.content).slice(0, OUTPUT);
|
|
8671
|
+
this.#edit((items) => items.map((i) => i.kind === "tool" && i.id === e.toolCallId ? {
|
|
8672
|
+
...i,
|
|
8673
|
+
status: e.isError ? "failed" : "done",
|
|
8674
|
+
output: out
|
|
8675
|
+
} : i));
|
|
8676
|
+
} else return;
|
|
8677
|
+
this.#tell();
|
|
8678
|
+
}
|
|
8679
|
+
#fail(message) {
|
|
8680
|
+
this.#edit((items) => [...items, {
|
|
8681
|
+
kind: "text",
|
|
8682
|
+
text: `(${message})`
|
|
8683
|
+
}]);
|
|
8684
|
+
this.#set("failed");
|
|
8685
|
+
}
|
|
8686
|
+
#end() {
|
|
8687
|
+
this.#dropQueued();
|
|
8688
|
+
const last = [...this.#agent?.state.messages ?? []].reverse().find((m) => m.role === "assistant");
|
|
8689
|
+
if (this.#turns.at(-1)?.state === "running") {
|
|
8690
|
+
if (this.#stopping || last?.stopReason === "aborted") this.#set("stopped");
|
|
8691
|
+
else if (last?.stopReason === "error") this.#fail(last.errorMessage ?? "the model failed");
|
|
8692
|
+
else this.#set("done");
|
|
8693
|
+
}
|
|
8694
|
+
this.#busy = false;
|
|
8695
|
+
this.#stopping = false;
|
|
8696
|
+
this.#api.agentStatus(this.#id, this.#session, false).catch(() => void 0);
|
|
8697
|
+
this.#keep();
|
|
8698
|
+
this.#save();
|
|
8699
|
+
this.#tell();
|
|
8700
|
+
}
|
|
8701
|
+
/** What the model would be sent now: the last answer's own count, and an estimate for what came after it. */
|
|
8702
|
+
#contextTokens(messages) {
|
|
8703
|
+
let last = -1;
|
|
8704
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
8705
|
+
const m = messages[i];
|
|
8706
|
+
if (m.role === "assistant" && m.usage?.totalTokens && m.stopReason !== "aborted" && m.stopReason !== "error") {
|
|
8707
|
+
last = i;
|
|
8708
|
+
break;
|
|
8709
|
+
}
|
|
8710
|
+
}
|
|
8711
|
+
return (last >= 0 ? messages[last].usage.totalTokens : 0) + messages.slice(last + 1).reduce((n, m) => n + estimateTokens(m), 0);
|
|
8712
|
+
}
|
|
8713
|
+
/** Before a prompt: if what the model would be sent nears its window, summarise the earlier part of the conversation
|
|
8714
|
+
* and keep the recent part as it was (cut at a message of the person's, so a tool call and its result stay together).
|
|
8715
|
+
* The turn shows it. If the summary cannot be made the conversation goes on as it was and the turn says so. */
|
|
8716
|
+
async #compactIfNeeded(agent) {
|
|
8717
|
+
const window = this.#model.contextWindow, settings = this.#compaction;
|
|
8718
|
+
if (!settings.enabled || !window) return;
|
|
8719
|
+
const messages = [...agent.state.messages];
|
|
8720
|
+
const before = this.#contextTokens(messages);
|
|
8721
|
+
if (!shouldCompact(before, window, settings)) return;
|
|
8722
|
+
const roleOf = (m) => m?.role;
|
|
8723
|
+
let head = 0;
|
|
8724
|
+
while (roleOf(messages[head]) === "system") head++;
|
|
8725
|
+
const earlier = messages[head]?.content;
|
|
8726
|
+
const previous = userText(earlier).startsWith(SUMMARY) ? userText(earlier).slice(65) : void 0;
|
|
8727
|
+
const from = head + (previous === void 0 ? 0 : 1);
|
|
8728
|
+
let acc = 0, cut = from;
|
|
8729
|
+
for (let i = messages.length - 1; i >= from; i--) {
|
|
8730
|
+
acc += estimateTokens(messages[i]);
|
|
8731
|
+
if (acc >= settings.keepRecentTokens) {
|
|
8732
|
+
cut = i;
|
|
8733
|
+
break;
|
|
8734
|
+
}
|
|
8735
|
+
}
|
|
8736
|
+
while (cut > from && roleOf(messages[cut]) !== "user") cut--;
|
|
8737
|
+
if (cut <= from) return;
|
|
8738
|
+
const id = this.#turns.at(-1)?.items.length ?? 0;
|
|
8739
|
+
this.#edit((items) => [...items, {
|
|
8740
|
+
kind: "compaction",
|
|
8741
|
+
state: "running",
|
|
8742
|
+
before
|
|
8743
|
+
}]);
|
|
8744
|
+
this.#tell();
|
|
8745
|
+
this.#summarising = new AbortController();
|
|
8746
|
+
try {
|
|
8747
|
+
const summary = await generateSummary(this.#redact(messages.slice(from, cut)), this.#model, settings.reserveTokens, void 0, void 0, this.#summarising.signal, void 0, previous, void 0, this.#streamFn);
|
|
8748
|
+
const stand = {
|
|
8749
|
+
role: "user",
|
|
8750
|
+
content: [{
|
|
8751
|
+
type: "text",
|
|
8752
|
+
text: SUMMARY + summary
|
|
8753
|
+
}],
|
|
8754
|
+
timestamp: this.#now()
|
|
8755
|
+
};
|
|
8756
|
+
const recent = messages.slice(cut).map((m) => m.role === "assistant" ? {
|
|
8757
|
+
...m,
|
|
8758
|
+
usage: {
|
|
8759
|
+
...m.usage,
|
|
8760
|
+
totalTokens: 0
|
|
8761
|
+
}
|
|
8762
|
+
} : m);
|
|
8763
|
+
const kept = [
|
|
8764
|
+
...messages.slice(0, head),
|
|
8765
|
+
stand,
|
|
8766
|
+
...recent
|
|
8767
|
+
];
|
|
8768
|
+
agent.state.messages = kept;
|
|
8769
|
+
const after = this.#contextTokens(kept);
|
|
8770
|
+
this.#used = after;
|
|
8771
|
+
this.#edit((items) => items.map((i, k) => k === id && i.kind === "compaction" ? {
|
|
8772
|
+
...i,
|
|
8773
|
+
state: "done",
|
|
8774
|
+
after,
|
|
8775
|
+
summary
|
|
8776
|
+
} : i));
|
|
8777
|
+
} catch (e) {
|
|
8778
|
+
const error = this.#stopping ? "stopped" : e.message;
|
|
8779
|
+
this.#edit((items) => items.map((i, k) => k === id && i.kind === "compaction" ? {
|
|
8780
|
+
...i,
|
|
8781
|
+
state: "failed",
|
|
8782
|
+
error
|
|
8783
|
+
} : i));
|
|
8784
|
+
} finally {
|
|
8785
|
+
this.#summarising = null;
|
|
8786
|
+
this.#tell();
|
|
8787
|
+
}
|
|
8788
|
+
}
|
|
8789
|
+
/** Said but never taken up (the run ended or was stopped first). */
|
|
8790
|
+
#dropQueued() {
|
|
8791
|
+
if (!this.#queued.length) return;
|
|
8792
|
+
const ids = new Set(this.#queued.map((q) => q.id));
|
|
8793
|
+
this.#queued = [];
|
|
8794
|
+
this.#agent?.clearAllQueues();
|
|
8795
|
+
this.#edit((items) => items.map((i) => i.kind === "message" && ids.has(i.id) ? {
|
|
8796
|
+
...i,
|
|
8797
|
+
state: "dropped"
|
|
8798
|
+
} : i));
|
|
8799
|
+
}
|
|
8800
|
+
#set(state) {
|
|
8801
|
+
const turns = [...this.#turns], t = turns.at(-1);
|
|
8802
|
+
if (t) turns[turns.length - 1] = {
|
|
8803
|
+
...t,
|
|
8804
|
+
state
|
|
8805
|
+
};
|
|
8806
|
+
this.#turns = turns;
|
|
8807
|
+
}
|
|
8808
|
+
#edit(fn) {
|
|
8809
|
+
const turns = [...this.#turns], t = turns.at(-1);
|
|
8810
|
+
if (!t) return;
|
|
8811
|
+
turns[turns.length - 1] = {
|
|
8812
|
+
...t,
|
|
8813
|
+
items: fn([...t.items])
|
|
8814
|
+
};
|
|
8815
|
+
this.#turns = turns;
|
|
8816
|
+
}
|
|
8817
|
+
#keep() {
|
|
8818
|
+
if (!this.#api.keepAgentRecord || !this.#turns.length) return;
|
|
8819
|
+
const last = this.#turns.at(-1)?.at ?? this.#at;
|
|
8820
|
+
this.#api.keepAgentRecord({
|
|
8821
|
+
agent: this.#id,
|
|
8822
|
+
name: this.#name,
|
|
8823
|
+
session: this.#session,
|
|
8824
|
+
title: this.#title,
|
|
8825
|
+
at: this.#at,
|
|
8826
|
+
last,
|
|
8827
|
+
turns: this.#turns
|
|
8828
|
+
}).catch(() => void 0);
|
|
8829
|
+
}
|
|
8830
|
+
/** The session shown, with the others kept, newest first. */
|
|
8831
|
+
#sessions(entry) {
|
|
8832
|
+
const others = [...this.#kept.values()].filter((k) => k.session !== this.#session).map((k) => ({
|
|
8833
|
+
agent: this.#id,
|
|
8834
|
+
name: this.#name,
|
|
8835
|
+
...k
|
|
8836
|
+
}));
|
|
8837
|
+
return [...entry ? [entry] : [], ...others].sort((a, b) => a.last < b.last ? 1 : a.last > b.last ? -1 : 0);
|
|
8838
|
+
}
|
|
8839
|
+
#tell() {
|
|
8840
|
+
const last = this.#turns.at(-1)?.at ?? this.#at;
|
|
8841
|
+
const entry = this.#session ? {
|
|
8842
|
+
agent: this.#id,
|
|
8843
|
+
name: this.#name,
|
|
8844
|
+
session: this.#session,
|
|
8845
|
+
title: this.#title,
|
|
8846
|
+
asked: this.#turns[0]?.asked ?? "",
|
|
8847
|
+
last,
|
|
8848
|
+
turns: this.#turns.length,
|
|
8849
|
+
notes: 0,
|
|
8850
|
+
live: true,
|
|
8851
|
+
busy: this.#busy,
|
|
8852
|
+
kept: Boolean(this.#store),
|
|
8853
|
+
open: true
|
|
8854
|
+
} : null;
|
|
8855
|
+
this.#view = {
|
|
8856
|
+
connected: true,
|
|
8857
|
+
agent: this.#name,
|
|
8858
|
+
agentId: this.#id,
|
|
8859
|
+
session: this.#session || null,
|
|
8860
|
+
title: this.#title,
|
|
8861
|
+
busy: this.#busy,
|
|
8862
|
+
turns: this.#turns,
|
|
8863
|
+
asking: [...this.#asking.values()].map(({ wait: _, ...p }) => p),
|
|
8864
|
+
cost: this.#cost,
|
|
8865
|
+
tokens: {
|
|
8866
|
+
input: this.#used,
|
|
8867
|
+
output: 0
|
|
8868
|
+
},
|
|
8869
|
+
model: this.#model.id ?? "",
|
|
8870
|
+
...this.#problem ? { problem: this.#problem } : {},
|
|
8871
|
+
steer: true,
|
|
8872
|
+
...this.#model.contextWindow ? { size: this.#model.contextWindow } : {},
|
|
8873
|
+
fork: Boolean(this.#store),
|
|
8874
|
+
sessions: this.#sessions(entry)
|
|
8875
|
+
};
|
|
8876
|
+
for (const fn of this.#listeners) fn(this.#view);
|
|
8877
|
+
}
|
|
8878
|
+
};
|
|
8879
|
+
//#endregion
|
|
8880
|
+
//#region packages/server/src/pi/files.ts
|
|
8881
|
+
const READS = [
|
|
8882
|
+
"read",
|
|
8883
|
+
"grep",
|
|
8884
|
+
"find",
|
|
8885
|
+
"ls"
|
|
8886
|
+
];
|
|
8887
|
+
const WRITES = ["edit", "write"];
|
|
8888
|
+
function fileTools(cwd, { files, shell }) {
|
|
8889
|
+
const tools = [];
|
|
8890
|
+
if (files !== "off") tools.push(createReadTool(cwd), createGrepTool(cwd), createFindTool(cwd), createLsTool(cwd));
|
|
8891
|
+
if (files === "write") tools.push(createEditTool(cwd), createWriteTool(cwd));
|
|
8892
|
+
if (shell) tools.push(createBashTool(cwd));
|
|
8893
|
+
return tools;
|
|
8894
|
+
}
|
|
8895
|
+
/** A reason to refuse the call, or undefined to let it through. Anything that cannot be checked is refused. */
|
|
8896
|
+
function pathGuard(cwd) {
|
|
8897
|
+
const root = resolve(cwd);
|
|
8898
|
+
return ({ tool, args }) => {
|
|
8899
|
+
if (![...READS, ...WRITES].includes(tool)) return void 0;
|
|
8900
|
+
const given = args?.path;
|
|
8901
|
+
if (given === void 0 && (tool === "ls" || tool === "grep" || tool === "find")) return void 0;
|
|
8902
|
+
if (typeof given !== "string" || !given) return `${tool} needs a path inside the project`;
|
|
8903
|
+
const target = isAbsolute(given) ? resolve(given) : resolve(root, given);
|
|
8904
|
+
const inside = relative(root, target);
|
|
8905
|
+
if (inside.startsWith("..") || isAbsolute(inside)) return `${given} is outside the project folder`;
|
|
8906
|
+
if (WRITES.includes(tool) && inside.split(sep).includes(".git")) return `${tool} does not reach .git`;
|
|
8907
|
+
};
|
|
8908
|
+
}
|
|
8909
|
+
//#endregion
|
|
8910
|
+
//#region packages/server/src/pi/credentials.ts
|
|
8911
|
+
/** The providers the settings offer: those a person signs up to with a key of their own. Pi's subscription sign-ins
|
|
8912
|
+
* (for a ChatGPT or Copilot plan) are not offered: their terms are the provider's, not isorite's to rely on. */
|
|
8913
|
+
const PROVIDERS = [
|
|
8914
|
+
{
|
|
8915
|
+
id: "openrouter",
|
|
8916
|
+
name: "OpenRouter",
|
|
8917
|
+
env: "OPENROUTER_API_KEY"
|
|
8918
|
+
},
|
|
8919
|
+
{
|
|
8920
|
+
id: "anthropic",
|
|
8921
|
+
name: "Anthropic (API key)",
|
|
8922
|
+
env: "ANTHROPIC_API_KEY"
|
|
8923
|
+
},
|
|
8924
|
+
{
|
|
8925
|
+
id: "openai",
|
|
8926
|
+
name: "OpenAI (API key)",
|
|
8927
|
+
env: "OPENAI_API_KEY"
|
|
8928
|
+
},
|
|
8929
|
+
{
|
|
8930
|
+
id: "google",
|
|
8931
|
+
name: "Google Gemini",
|
|
8932
|
+
env: "GEMINI_API_KEY"
|
|
8933
|
+
},
|
|
8934
|
+
{
|
|
8935
|
+
id: "mistral",
|
|
8936
|
+
name: "Mistral",
|
|
8937
|
+
env: "MISTRAL_API_KEY"
|
|
8938
|
+
},
|
|
8939
|
+
{
|
|
8940
|
+
id: "groq",
|
|
8941
|
+
name: "Groq",
|
|
8942
|
+
env: "GROQ_API_KEY"
|
|
8943
|
+
},
|
|
8944
|
+
{
|
|
8945
|
+
id: "xai",
|
|
8946
|
+
name: "xAI",
|
|
8947
|
+
env: "XAI_API_KEY"
|
|
8948
|
+
},
|
|
8949
|
+
{
|
|
8950
|
+
id: "deepseek",
|
|
8951
|
+
name: "DeepSeek",
|
|
8952
|
+
env: "DEEPSEEK_API_KEY"
|
|
8953
|
+
},
|
|
8954
|
+
{
|
|
8955
|
+
id: "together",
|
|
8956
|
+
name: "Together",
|
|
8957
|
+
env: "TOGETHER_API_KEY"
|
|
8958
|
+
},
|
|
8959
|
+
{
|
|
8960
|
+
id: "fireworks",
|
|
8961
|
+
name: "Fireworks",
|
|
8962
|
+
env: "FIREWORKS_API_KEY"
|
|
8963
|
+
},
|
|
8964
|
+
{
|
|
8965
|
+
id: "cerebras",
|
|
8966
|
+
name: "Cerebras",
|
|
8967
|
+
env: "CEREBRAS_API_KEY"
|
|
8968
|
+
}
|
|
8969
|
+
];
|
|
8970
|
+
/** The default: the project's files to read, nothing to change them with, no shell. */
|
|
8971
|
+
const DEFAULT_REACH = {
|
|
8972
|
+
files: "read",
|
|
8973
|
+
shell: false
|
|
8974
|
+
};
|
|
8975
|
+
const ID = /^[a-z0-9-]{1,40}$/;
|
|
8976
|
+
/** Where isorite keeps what is its own, not the base's. */
|
|
8977
|
+
function dataDir(env = process.env) {
|
|
8978
|
+
return join(env.XDG_DATA_HOME || join(homedir(), ".local", "share"), "isorite");
|
|
8979
|
+
}
|
|
8980
|
+
var Credentials = class {
|
|
8981
|
+
#file;
|
|
8982
|
+
#dir;
|
|
8983
|
+
#env;
|
|
8984
|
+
#kept = null;
|
|
8985
|
+
#writing = Promise.resolve();
|
|
8986
|
+
constructor(env = process.env) {
|
|
8987
|
+
this.#env = env;
|
|
8988
|
+
this.#dir = dataDir(env);
|
|
8989
|
+
this.#file = join(this.#dir, "credentials.json");
|
|
8990
|
+
}
|
|
8991
|
+
async #load() {
|
|
8992
|
+
if (this.#kept) return this.#kept;
|
|
8993
|
+
let text;
|
|
8994
|
+
try {
|
|
8995
|
+
text = await readFile(this.#file, "utf8");
|
|
8996
|
+
} catch (e) {
|
|
8997
|
+
if (e.code === "ENOENT") return this.#kept = {
|
|
8998
|
+
version: 1,
|
|
8999
|
+
keys: {}
|
|
9000
|
+
};
|
|
9001
|
+
throw new Error(`The keys file ${this.#file} could not be read (${e.code ?? "unreadable"}). It was left where it is.`);
|
|
9002
|
+
}
|
|
9003
|
+
let parsed;
|
|
9004
|
+
try {
|
|
9005
|
+
parsed = JSON.parse(text);
|
|
9006
|
+
} catch {
|
|
9007
|
+
throw new Error(`The keys file ${this.#file} is not valid JSON. It was left where it is.`);
|
|
9008
|
+
}
|
|
9009
|
+
if (!parsed || parsed.version !== 1 || typeof parsed.keys !== "object" || parsed.keys === null) throw new Error(`The keys file ${this.#file} is not one this version of isorite wrote. It was left where it is.`);
|
|
9010
|
+
return this.#kept = {
|
|
9011
|
+
version: 1,
|
|
9012
|
+
keys: parsed.keys,
|
|
9013
|
+
...parsed.choice ? { choice: parsed.choice } : {},
|
|
9014
|
+
...parsed.reach ? { reach: parsed.reach } : {}
|
|
9015
|
+
};
|
|
9016
|
+
}
|
|
9017
|
+
async #save(next) {
|
|
9018
|
+
const run = async () => {
|
|
9019
|
+
await mkdir(this.#dir, {
|
|
9020
|
+
recursive: true,
|
|
9021
|
+
mode: 448
|
|
9022
|
+
});
|
|
9023
|
+
const tmp = `${this.#file}.${process.pid}.tmp`;
|
|
9024
|
+
await writeFile(tmp, JSON.stringify(next), { mode: 384 });
|
|
9025
|
+
await rename(tmp, this.#file);
|
|
9026
|
+
this.#kept = next;
|
|
9027
|
+
};
|
|
9028
|
+
const turn = this.#writing.then(run, run);
|
|
9029
|
+
this.#writing = turn.catch(() => void 0);
|
|
9030
|
+
await turn;
|
|
9031
|
+
}
|
|
9032
|
+
/** The key to use for `provider`: the one saved here, else the provider's own variable in the environment. */
|
|
9033
|
+
async key(provider) {
|
|
9034
|
+
const kept = (await this.#load()).keys[provider];
|
|
9035
|
+
if (kept) return kept;
|
|
9036
|
+
const env = PROVIDERS.find((p) => p.id === provider)?.env;
|
|
9037
|
+
return env && this.#env[env] || void 0;
|
|
9038
|
+
}
|
|
9039
|
+
/** Where each provider's key comes from, never the key. */
|
|
9040
|
+
async sources() {
|
|
9041
|
+
const kept = (await this.#load()).keys;
|
|
9042
|
+
return Object.fromEntries(PROVIDERS.map((p) => [p.id, kept[p.id] ? "saved" : this.#env[p.env] ? "environment" : null]));
|
|
9043
|
+
}
|
|
9044
|
+
async set(provider, key) {
|
|
9045
|
+
if (!PROVIDERS.some((p) => p.id === provider)) throw new Error(`"${provider}" is not a provider isorite offers`);
|
|
9046
|
+
const k = key.trim();
|
|
9047
|
+
if (!k || k.length > 1e3 || /\s/.test(k)) throw new Error("that does not look like a key (it is empty, has spaces, or is very long)");
|
|
9048
|
+
const now = await this.#load();
|
|
9049
|
+
await this.#save({
|
|
9050
|
+
...now,
|
|
9051
|
+
keys: {
|
|
9052
|
+
...now.keys,
|
|
9053
|
+
[provider]: k
|
|
9054
|
+
}
|
|
9055
|
+
});
|
|
9056
|
+
}
|
|
9057
|
+
async remove(provider) {
|
|
9058
|
+
const now = await this.#load();
|
|
9059
|
+
if (!(provider in now.keys)) return;
|
|
9060
|
+
const { [provider]: _gone, ...keys } = now.keys;
|
|
9061
|
+
await this.#save({
|
|
9062
|
+
...now,
|
|
9063
|
+
keys
|
|
9064
|
+
});
|
|
9065
|
+
}
|
|
9066
|
+
/** What was chosen in the settings, else the default. Anything else in the file is not believed. */
|
|
9067
|
+
async reach() {
|
|
9068
|
+
const r = (await this.#load()).reach;
|
|
9069
|
+
return r && [
|
|
9070
|
+
"off",
|
|
9071
|
+
"read",
|
|
9072
|
+
"write"
|
|
9073
|
+
].includes(r.files) && typeof r.shell === "boolean" ? {
|
|
9074
|
+
files: r.files,
|
|
9075
|
+
shell: r.shell
|
|
9076
|
+
} : { ...DEFAULT_REACH };
|
|
9077
|
+
}
|
|
9078
|
+
async setReach(reach) {
|
|
9079
|
+
if (![
|
|
9080
|
+
"off",
|
|
9081
|
+
"read",
|
|
9082
|
+
"write"
|
|
9083
|
+
].includes(reach.files) || typeof reach.shell !== "boolean") throw new Error("files is off, read or write; shell is on or off");
|
|
9084
|
+
const now = await this.#load();
|
|
9085
|
+
await this.#save({
|
|
9086
|
+
...now,
|
|
9087
|
+
reach: {
|
|
9088
|
+
files: reach.files,
|
|
9089
|
+
shell: reach.shell
|
|
9090
|
+
}
|
|
9091
|
+
});
|
|
9092
|
+
}
|
|
9093
|
+
async choice() {
|
|
9094
|
+
return (await this.#load()).choice ?? null;
|
|
9095
|
+
}
|
|
9096
|
+
async choose(provider, model) {
|
|
9097
|
+
if (!PROVIDERS.some((p) => p.id === provider) || !ID.test(provider)) throw new Error(`"${provider}" is not a provider isorite offers`);
|
|
9098
|
+
if (!model.trim() || model.length > 200) throw new Error("a model is needed");
|
|
9099
|
+
const now = await this.#load();
|
|
9100
|
+
await this.#save({
|
|
9101
|
+
...now,
|
|
9102
|
+
choice: {
|
|
9103
|
+
provider,
|
|
9104
|
+
model: model.trim()
|
|
9105
|
+
}
|
|
9106
|
+
});
|
|
9107
|
+
}
|
|
9108
|
+
};
|
|
9109
|
+
//#endregion
|
|
9110
|
+
//#region packages/server/src/pi/index.ts
|
|
9111
|
+
const camel = (s) => s.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
|
|
9112
|
+
/** The model `provider/model` and the way to reach it. Throws (naming what is wrong) where pi has no such provider or model. */
|
|
9113
|
+
async function modelFor(provider, model) {
|
|
9114
|
+
if (!provider || !model || !/^[a-z0-9-]+$/.test(provider)) throw new Error(`a model is <provider>/<model>, as anthropic/claude-sonnet-4-6; it was "${provider}/${model}"`);
|
|
9115
|
+
let make;
|
|
9116
|
+
try {
|
|
9117
|
+
make = (await import(`@earendil-works/pi-ai/providers/${provider}`))[`${camel(provider)}Provider`];
|
|
9118
|
+
} catch {}
|
|
9119
|
+
if (typeof make !== "function") throw new Error(`pi has no provider "${provider}"`);
|
|
9120
|
+
const models = createModels();
|
|
9121
|
+
models.setProvider(make());
|
|
9122
|
+
const chosen = models.getModel(provider, model);
|
|
9123
|
+
if (!chosen) throw new Error(`pi's ${provider} has no model "${model}"`);
|
|
9124
|
+
return {
|
|
9125
|
+
chosen,
|
|
9126
|
+
streamFn: models.streamSimple.bind(models)
|
|
9127
|
+
};
|
|
9128
|
+
}
|
|
9129
|
+
/** The model named by ISORITE_PI, else the one chosen in the settings; null where there is none. */
|
|
9130
|
+
async function wanted(env, creds) {
|
|
9131
|
+
const named = env.ISORITE_PI?.trim();
|
|
9132
|
+
if (named) {
|
|
9133
|
+
const [provider, ...rest] = named.split("/");
|
|
9134
|
+
if (!provider || !rest.length) throw new Error(`ISORITE_PI is <provider>/<model>, as anthropic/claude-sonnet-4-6; it was "${named}"`);
|
|
9135
|
+
return {
|
|
9136
|
+
choice: {
|
|
9137
|
+
provider,
|
|
9138
|
+
model: rest.join("/")
|
|
9139
|
+
},
|
|
9140
|
+
source: "environment"
|
|
9141
|
+
};
|
|
9142
|
+
}
|
|
9143
|
+
const saved = await creds.choice();
|
|
9144
|
+
return saved ? {
|
|
9145
|
+
choice: saved,
|
|
9146
|
+
source: "settings"
|
|
9147
|
+
} : null;
|
|
9148
|
+
}
|
|
9149
|
+
/** What the settings read and change: which model the native agent uses and where its key comes from, for the
|
|
9150
|
+
* hosts running (one for each base). It never hands out a key. */
|
|
9151
|
+
var PiControl = class {
|
|
9152
|
+
creds;
|
|
9153
|
+
#env;
|
|
9154
|
+
#hosts = /* @__PURE__ */ new Set();
|
|
9155
|
+
constructor(env = process.env, creds = new Credentials(env)) {
|
|
9156
|
+
this.#env = env;
|
|
9157
|
+
this.creds = creds;
|
|
9158
|
+
}
|
|
9159
|
+
/** What the agent may reach now, read synchronously where a prompt starts. Loaded by `load`. */
|
|
9160
|
+
reach = { ...DEFAULT_REACH };
|
|
9161
|
+
/** True where the environment (ISORITE_PI_FILES, ISORITE_PI_SHELL) sets it, and so wins. */
|
|
9162
|
+
reachFromEnvironment = false;
|
|
9163
|
+
async load() {
|
|
9164
|
+
const files = this.#env.ISORITE_PI_FILES?.trim(), shell = this.#env.ISORITE_PI_SHELL?.trim();
|
|
9165
|
+
if (files !== void 0 && files !== "" && ![
|
|
9166
|
+
"off",
|
|
9167
|
+
"read",
|
|
9168
|
+
"write"
|
|
9169
|
+
].includes(files)) throw new Error(`ISORITE_PI_FILES is off, read or write; it was "${files}"`);
|
|
9170
|
+
this.reachFromEnvironment = Boolean(files) || shell !== void 0 && shell !== "";
|
|
9171
|
+
const saved = await this.creds.reach();
|
|
9172
|
+
this.reach = {
|
|
9173
|
+
files: files || saved.files,
|
|
9174
|
+
shell: shell ? [
|
|
9175
|
+
"1",
|
|
9176
|
+
"true",
|
|
9177
|
+
"on"
|
|
9178
|
+
].includes(shell) : saved.shell
|
|
9179
|
+
};
|
|
9180
|
+
}
|
|
9181
|
+
async setReach(reach) {
|
|
9182
|
+
await this.creds.setReach(reach);
|
|
9183
|
+
if (!this.reachFromEnvironment) this.reach = { ...reach };
|
|
9184
|
+
}
|
|
9185
|
+
attach(host) {
|
|
9186
|
+
this.#hosts.add(host);
|
|
9187
|
+
}
|
|
9188
|
+
async state() {
|
|
9189
|
+
await this.load().catch(() => void 0);
|
|
9190
|
+
const want = await wanted(this.#env, this.creds).catch(() => null);
|
|
9191
|
+
return {
|
|
9192
|
+
/** True where the pane of this base is the native agent's now. */
|
|
9193
|
+
active: this.#hosts.size > 0,
|
|
9194
|
+
choice: want?.choice ?? null,
|
|
9195
|
+
/** Where the choice is made: the environment wins over the settings while it is set. */
|
|
9196
|
+
source: want?.source ?? null,
|
|
9197
|
+
providers: PROVIDERS.map((p) => ({
|
|
9198
|
+
id: p.id,
|
|
9199
|
+
name: p.name,
|
|
9200
|
+
env: p.env
|
|
9201
|
+
})),
|
|
9202
|
+
keys: await this.creds.sources(),
|
|
9203
|
+
reach: this.reach,
|
|
9204
|
+
reachFromEnvironment: this.reachFromEnvironment
|
|
9205
|
+
};
|
|
9206
|
+
}
|
|
9207
|
+
async choose(provider, model) {
|
|
9208
|
+
const { chosen, streamFn } = await modelFor(provider, model.trim());
|
|
9209
|
+
await this.creds.choose(provider, chosen.id);
|
|
9210
|
+
if (!this.#env.ISORITE_PI?.trim()) for (const h of this.#hosts) h.use(chosen, streamFn, `pi (${chosen.id})`);
|
|
9211
|
+
}
|
|
9212
|
+
};
|
|
9213
|
+
/** The pi host for `api`, or null where pi is not chosen. Throws (naming what is wrong) where it is chosen and cannot be had. */
|
|
9214
|
+
async function piHostFor(api, env = process.env, project = process.cwd(), notes = project, control = new PiControl(env)) {
|
|
9215
|
+
const want = await wanted(env, control.creds);
|
|
9216
|
+
if (!want) return null;
|
|
9217
|
+
const { provider, model } = want.choice;
|
|
9218
|
+
const { chosen, streamFn } = await modelFor(provider, model);
|
|
9219
|
+
await control.load();
|
|
9220
|
+
const asked = new Set((env.ISORITE_PI_ASK ?? "").split(",").map((t) => t.trim()).filter(Boolean));
|
|
9221
|
+
const host = new PiHost({
|
|
9222
|
+
api,
|
|
9223
|
+
store: new FileStore(storeDir(notes, env)),
|
|
9224
|
+
model: chosen,
|
|
9225
|
+
streamFn,
|
|
9226
|
+
name: `pi (${model})`,
|
|
9227
|
+
getApiKey: (p) => control.creds.key(p),
|
|
9228
|
+
...asked.size ? { ask: (tool) => asked.has(tool) } : {},
|
|
9229
|
+
more: () => fileTools(project, control.reach),
|
|
9230
|
+
guard: pathGuard(project)
|
|
9231
|
+
});
|
|
9232
|
+
await host.resume();
|
|
9233
|
+
control.attach(host);
|
|
9234
|
+
return host;
|
|
9235
|
+
}
|
|
9236
|
+
//#endregion
|
|
7695
9237
|
//#region packages/server/src/nodefiles.ts
|
|
7696
9238
|
const gone = (e) => ["ENOENT", "ENOTDIR"].includes(e.code ?? "");
|
|
7697
9239
|
var NodeFiles = class {
|
|
@@ -7743,6 +9285,13 @@ var NodeFiles = class {
|
|
|
7743
9285
|
async mkdir(dir) {
|
|
7744
9286
|
await mkdir(this.at(dir), { recursive: true });
|
|
7745
9287
|
}
|
|
9288
|
+
async rmdir(dir) {
|
|
9289
|
+
try {
|
|
9290
|
+
await rmdir(this.at(dir));
|
|
9291
|
+
} catch (e) {
|
|
9292
|
+
if (e.code !== "ENOENT") throw e;
|
|
9293
|
+
}
|
|
9294
|
+
}
|
|
7746
9295
|
async append(path, text) {
|
|
7747
9296
|
const full = this.at(path);
|
|
7748
9297
|
await mkdir(dirname(full), { recursive: true });
|
|
@@ -8034,6 +9583,7 @@ async function openServer(path, { host = "127.0.0.1", port = 4767, allowHosts =
|
|
|
8034
9583
|
const embedder = model ? nodeEmbedder(model) : null;
|
|
8035
9584
|
const token = randomBytes(24).toString("base64url"), loopback = LOOPBACK.has(host);
|
|
8036
9585
|
const web = existsSync(join(WEB_DIR, "index.html")) ? WEB_DIR : null;
|
|
9586
|
+
const pi = new PiControl();
|
|
8037
9587
|
const bases = new Bases({
|
|
8038
9588
|
config,
|
|
8039
9589
|
first,
|
|
@@ -8048,6 +9598,7 @@ async function openServer(path, { host = "127.0.0.1", port = 4767, allowHosts =
|
|
|
8048
9598
|
api,
|
|
8049
9599
|
notes: known.path
|
|
8050
9600
|
});
|
|
9601
|
+
const terminal = await piHostFor(api, process.env, projectOf(known.path).repo, known.path, pi);
|
|
8051
9602
|
const agents = new AgentRunner({
|
|
8052
9603
|
base: known.name,
|
|
8053
9604
|
url: `http://127.0.0.1:${port}${known.name === first ? "" : `/b/${known.name}`}`,
|
|
@@ -8065,6 +9616,8 @@ async function openServer(path, { host = "127.0.0.1", port = 4767, allowHosts =
|
|
|
8065
9616
|
allowHosts,
|
|
8066
9617
|
notes: known.path,
|
|
8067
9618
|
acp,
|
|
9619
|
+
...terminal ? { terminal } : {},
|
|
9620
|
+
...loopback ? { pi } : {},
|
|
8068
9621
|
bases,
|
|
8069
9622
|
base: known.name,
|
|
8070
9623
|
...loopback ? { agents } : {},
|
|
@@ -8556,13 +10109,17 @@ async function acp(path, { agent, port = 4767, input, output, env = process.env,
|
|
|
8556
10109
|
const given = /* @__PURE__ */ new Map();
|
|
8557
10110
|
const withGiven = (session, prompt) => {
|
|
8558
10111
|
const blocks = Array.isArray(prompt) ? prompt : [];
|
|
8559
|
-
const
|
|
8560
|
-
if (!
|
|
10112
|
+
const g = given.get(session);
|
|
10113
|
+
if (!g) return blocks;
|
|
8561
10114
|
given.delete(session);
|
|
8562
|
-
return [
|
|
8563
|
-
|
|
8564
|
-
|
|
8565
|
-
|
|
10115
|
+
return [
|
|
10116
|
+
{
|
|
10117
|
+
type: "text",
|
|
10118
|
+
text: g.text
|
|
10119
|
+
},
|
|
10120
|
+
...g.blocks ?? [],
|
|
10121
|
+
...blocks
|
|
10122
|
+
];
|
|
8566
10123
|
};
|
|
8567
10124
|
let batch = [];
|
|
8568
10125
|
let flushing = null;
|
|
@@ -8852,8 +10409,18 @@ async function acp(path, { agent, port = 4767, input, output, env = process.env,
|
|
|
8852
10409
|
});
|
|
8853
10410
|
sendWaiting();
|
|
8854
10411
|
}
|
|
10412
|
+
if (c.type === "steer") ask("_session/steering", {
|
|
10413
|
+
sessionId: c.session,
|
|
10414
|
+
prompt: [{
|
|
10415
|
+
type: "text",
|
|
10416
|
+
text: c.text
|
|
10417
|
+
}]
|
|
10418
|
+
});
|
|
8855
10419
|
if (c.type === "give") {
|
|
8856
|
-
if (c.text) given.set(c.session,
|
|
10420
|
+
if (c.text) given.set(c.session, {
|
|
10421
|
+
text: c.text,
|
|
10422
|
+
...c.blocks?.length ? { blocks: c.blocks } : {}
|
|
10423
|
+
});
|
|
8857
10424
|
else given.delete(c.session);
|
|
8858
10425
|
}
|
|
8859
10426
|
if (c.type === "cancel") {
|
|
@@ -8878,11 +10445,15 @@ async function acp(path, { agent, port = 4767, input, output, env = process.env,
|
|
|
8878
10445
|
cwd,
|
|
8879
10446
|
mcpServers: [isoriteMcp()]
|
|
8880
10447
|
};
|
|
8881
|
-
|
|
10448
|
+
const give = c.give ? {
|
|
10449
|
+
text: c.give,
|
|
10450
|
+
...c.giveBlocks?.length ? { blocks: c.giveBlocks } : {}
|
|
10451
|
+
} : void 0;
|
|
10452
|
+
if (!c.session) ask("session/new", params, c.then, give);
|
|
8882
10453
|
else ask(can.load ? "session/load" : "session/resume", {
|
|
8883
10454
|
...params,
|
|
8884
10455
|
sessionId: c.session
|
|
8885
|
-
}, c.then,
|
|
10456
|
+
}, c.then, give);
|
|
8886
10457
|
}
|
|
8887
10458
|
if (c.type === "permission" && agentAsked.has(c.id)) {
|
|
8888
10459
|
const id = agentAsked.get(c.id);
|
|
@@ -8911,7 +10482,8 @@ async function acp(path, { agent, port = 4767, input, output, env = process.env,
|
|
|
8911
10482
|
readTextFile: false,
|
|
8912
10483
|
writeTextFile: false
|
|
8913
10484
|
},
|
|
8914
|
-
terminal: false
|
|
10485
|
+
terminal: false,
|
|
10486
|
+
subagents: {}
|
|
8915
10487
|
},
|
|
8916
10488
|
clientInfo: {
|
|
8917
10489
|
name: "isorite",
|