isorite 0.17.0 → 0.18.1
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 +1577 -65
- package/package.json +4 -1
- package/web/_app/immutable/assets/0.i592VXJM.css +1 -0
- package/web/_app/immutable/chunks/{2BcU70Iq.js → -_CDfwKC.js} +1 -1
- package/web/_app/immutable/chunks/{DrPF-vh8.js → -oRvYXNk.js} +1 -1
- package/web/_app/immutable/chunks/0qd6IUbO.js +1 -0
- package/web/_app/immutable/chunks/{yy6sD_us.js → 7faei0hL.js} +1 -1
- package/web/_app/immutable/chunks/{BNlziJ_1.js → BArcFmbE.js} +3 -3
- package/web/_app/immutable/chunks/{NgkOM4d0.js → BF1TezEe.js} +1 -1
- package/web/_app/immutable/chunks/{DK9wjWIk.js → BF3mdiSX.js} +1 -1
- package/web/_app/immutable/chunks/{BbvUCVY_.js → BezGs1Zj.js} +1 -1
- package/web/_app/immutable/chunks/{BjLgp18-.js → BpHE30PI.js} +1 -1
- package/web/_app/immutable/chunks/{Ds9L6FPB.js → BtF1frCE.js} +1 -1
- package/web/_app/immutable/chunks/{D_xpsaTJ.js → BuNfZSU6.js} +1 -1
- package/web/_app/immutable/chunks/{0KrpTYPM.js → BubCsN15.js} +1 -1
- package/web/_app/immutable/chunks/{_zY4mrA0.js → BvhVny3O.js} +1 -1
- package/web/_app/immutable/chunks/{CcRcjPTM.js → Bvs-JCN-.js} +1 -1
- package/web/_app/immutable/chunks/{1W6iRjg5.js → C-F98t31.js} +1 -1
- package/web/_app/immutable/chunks/{rv_cF9pp.js → C1OaBy7n.js} +1 -1
- package/web/_app/immutable/chunks/{CMN7rSZK.js → C4SqDe8e.js} +1 -1
- package/web/_app/immutable/chunks/{BBU5FdEo.js → C74H8jNb.js} +1 -1
- package/web/_app/immutable/chunks/{Cn7j5NKi.js → C7bouMY8.js} +1 -1
- package/web/_app/immutable/chunks/{vGOSIW7X.js → C8IrXs1w.js} +1 -1
- package/web/_app/immutable/chunks/{CwcA6hab.js → CGGjDDVZ.js} +1 -1
- package/web/_app/immutable/chunks/{CabmfniL.js → CMAnq3x6.js} +1 -1
- package/web/_app/immutable/chunks/{Bx_Vz2qr.js → CMe14KUo.js} +1 -1
- package/web/_app/immutable/chunks/{DQJRaTNz.js → CXc9Iibq.js} +1 -1
- package/web/_app/immutable/chunks/Cb-KPahQ.js +1 -0
- package/web/_app/immutable/chunks/{CpJ4vfn-.js → Cf13Ew0A.js} +1 -1
- package/web/_app/immutable/chunks/{CQhfj5-f.js → CkSjC-iE.js} +3 -3
- package/web/_app/immutable/chunks/{sBy91tGs.js → Cn8Mg7_E.js} +1 -1
- package/web/_app/immutable/chunks/Co7i-dcu.js +1 -0
- package/web/_app/immutable/chunks/{1MewxdCy.js → CoeYsgbS.js} +1 -1
- package/web/_app/immutable/chunks/{CccIbhmH.js → CqmdC7Q3.js} +2 -2
- package/web/_app/immutable/chunks/{DJHLXLij.js → CrqOdm1D.js} +1 -1
- package/web/_app/immutable/chunks/{B-04K_Pl.js → CyJXJYoE.js} +1 -1
- package/web/_app/immutable/chunks/{CxEu-Di8.js → Cz6kYWmu.js} +1 -1
- package/web/_app/immutable/chunks/{ZDjPMhAb.js → CzbFy3_5.js} +1 -1
- package/web/_app/immutable/chunks/{yYKJ4mbq.js → D-9suc4b.js} +1 -1
- package/web/_app/immutable/chunks/{qfBACIi9.js → D37hxBaz.js} +1 -1
- package/web/_app/immutable/chunks/D4nFtMQC.js +1 -0
- package/web/_app/immutable/chunks/{CmFzeyfe.js → DNs5kWvM.js} +1 -1
- package/web/_app/immutable/chunks/{C2EzU0qX.js → DR5d84Z7.js} +1 -1
- package/web/_app/immutable/chunks/{C2QvvHIN.js → DaEL1AOW.js} +1 -1
- package/web/_app/immutable/chunks/{BCsFs14H.js → Dazeexma.js} +1 -1
- package/web/_app/immutable/chunks/{DqZ4UzUH.js → Dc2_04md.js} +1 -1
- package/web/_app/immutable/chunks/{DfLI-CvW.js → Dcjgv4h2.js} +1 -1
- package/web/_app/immutable/chunks/{DMpFmiFg.js → DdegnILL.js} +1 -1
- package/web/_app/immutable/chunks/{CRbUrapz.js → DgI3Vj5v.js} +1 -1
- package/web/_app/immutable/chunks/{fpNFwizw.js → Dgko1xMH.js} +1 -1
- package/web/_app/immutable/chunks/DjN194Ef.js +1 -0
- package/web/_app/immutable/chunks/{DS7XcdHs.js → DlgOmQ_G.js} +1 -1
- package/web/_app/immutable/chunks/{DMTkA0aK.js → DmS5Qyvl.js} +1 -1
- package/web/_app/immutable/chunks/{DeazQJkV.js → DqnDX3G8.js} +1 -1
- package/web/_app/immutable/chunks/{DEwfz-Il.js → DzAOHjhV.js} +1 -1
- package/web/_app/immutable/chunks/{DRKiKCov.js → Iqegk1SI.js} +1 -1
- package/web/_app/immutable/chunks/{Cl-Xmm9N.js → ONqdwA3r.js} +1 -1
- package/web/_app/immutable/chunks/{Dns-yz2Q.js → Oikibjm2.js} +1 -1
- package/web/_app/immutable/chunks/{CdD6waB-.js → T3u7Skc8.js} +1 -1
- package/web/_app/immutable/chunks/{Df3spPXn.js → cWWhqL1F.js} +1 -1
- package/web/_app/immutable/chunks/{CfTuc06U.js → fRXyV4Ec.js} +1 -1
- package/web/_app/immutable/chunks/{Dzu8v7nB.js → jAC4YCqW.js} +1 -1
- package/web/_app/immutable/chunks/{CB6ztdCQ.js → kFEqmLql.js} +1 -1
- package/web/_app/immutable/chunks/lSTs4h6V.js +1 -0
- package/web/_app/immutable/chunks/ufOJrCuz.js +517 -0
- package/web/_app/immutable/chunks/{BBhr3bLI.js → y0C1JQ1v.js} +1 -1
- package/web/_app/immutable/chunks/{CAuYgj5q.js → yvD4_bP8.js} +1 -1
- package/web/_app/immutable/entry/app.BRzQKhKX.js +2 -0
- package/web/_app/immutable/entry/start.-f1rSSM0.js +1 -0
- package/web/_app/immutable/nodes/0.i_AjenOL.js +11 -0
- package/web/_app/immutable/nodes/1.D6qHWJUF.js +1 -0
- package/web/_app/immutable/nodes/2.BPWtlg8V.js +1 -0
- package/web/_app/immutable/nodes/3.DaRPyiq0.js +1 -0
- package/web/_app/immutable/nodes/4.CkZ28L5K.js +3 -0
- package/web/_app/immutable/nodes/5.6ZCfORwJ.js +22 -0
- package/web/_app/immutable/nodes/6.B9EIxq5B.js +1 -0
- package/web/_app/immutable/nodes/7.DGJflKit.js +1 -0
- package/web/_app/version.json +1 -1
- package/web/index.html +17 -17
- package/web/_app/immutable/assets/0.DVQOzs4l.css +0 -1
- package/web/_app/immutable/chunks/BJOSVfg-.js +0 -1
- package/web/_app/immutable/chunks/CHjmZsHD.js +0 -1
- package/web/_app/immutable/chunks/CM4EA3Wo.js +0 -1
- package/web/_app/immutable/chunks/DH2bqH7t.js +0 -517
- package/web/_app/immutable/chunks/Ddw9FbDu.js +0 -1
- package/web/_app/immutable/chunks/IECP4ppk.js +0 -1
- package/web/_app/immutable/chunks/ZDkogIhD.js +0 -1
- package/web/_app/immutable/entry/app.CX19wIuG.js +0 -2
- package/web/_app/immutable/entry/start.DaiosA4i.js +0 -1
- package/web/_app/immutable/nodes/0.B6_Ycc14.js +0 -3
- package/web/_app/immutable/nodes/1.JA1sPybf.js +0 -1
- package/web/_app/immutable/nodes/2.DN2phYmE.js +0 -1
- package/web/_app/immutable/nodes/3.CbnLNp2F.js +0 -1
- package/web/_app/immutable/nodes/4.CIsG7Kdu.js +0 -3
- package/web/_app/immutable/nodes/5.D__u9ji8.js +0 -19
- package/web/_app/immutable/nodes/6.DBP0fpLt.js +0 -1
- package/web/_app/immutable/nodes/7.DuaH9aUr.js +0 -1
package/dist/main.js
CHANGED
|
@@ -15,6 +15,9 @@ 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 { 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";
|
|
18
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";
|
|
@@ -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"] : []];
|
|
@@ -2516,7 +2519,8 @@ function knownBases(config = configPath()) {
|
|
|
2516
2519
|
name,
|
|
2517
2520
|
path: expand(row.path),
|
|
2518
2521
|
lend: row.lend === true,
|
|
2519
|
-
...typeof row.used === "string" ? { used: row.used } : {}
|
|
2522
|
+
...typeof row.used === "string" ? { used: row.used } : {},
|
|
2523
|
+
...typeof row.title === "string" && row.title ? { title: row.title } : {}
|
|
2520
2524
|
}] : [];
|
|
2521
2525
|
});
|
|
2522
2526
|
}
|
|
@@ -2554,6 +2558,19 @@ function rememberBase(folder, { path = configPath(), as, lend } = {}) {
|
|
|
2554
2558
|
writeConfig(bases, basesPath(path));
|
|
2555
2559
|
return name;
|
|
2556
2560
|
}
|
|
2561
|
+
/** Give a known base a title to be shown by (M37), or take it back with an empty one: its name stays, as it is the base's
|
|
2562
|
+
* address and what the agent's sessions know it by. False when there is no base of that name. */
|
|
2563
|
+
function titleBase(name, title, path = configPath()) {
|
|
2564
|
+
const bases = readConfig(basesPath(path));
|
|
2565
|
+
if (!BASE_NAME.test(name) || !bases[name]) return false;
|
|
2566
|
+
const { title: _was, ...rest } = table(bases[name]);
|
|
2567
|
+
bases[name] = {
|
|
2568
|
+
...rest,
|
|
2569
|
+
...title.trim() ? { title: title.trim() } : {}
|
|
2570
|
+
};
|
|
2571
|
+
writeConfig(bases, basesPath(path));
|
|
2572
|
+
return true;
|
|
2573
|
+
}
|
|
2557
2574
|
/** Lend a known base to every session, or stop lending it. False when there is no base of that name. */
|
|
2558
2575
|
function lendBase(name, lend, path = configPath()) {
|
|
2559
2576
|
const bases = readConfig(basesPath(path));
|
|
@@ -5469,7 +5486,8 @@ var Bases = class {
|
|
|
5469
5486
|
name: b.name,
|
|
5470
5487
|
path: b.path,
|
|
5471
5488
|
lend: b.lend,
|
|
5472
|
-
open: this.#ready.has(b.name)
|
|
5489
|
+
open: this.#ready.has(b.name),
|
|
5490
|
+
title: b.title ?? b.name
|
|
5473
5491
|
}));
|
|
5474
5492
|
}
|
|
5475
5493
|
async lend(name, on) {
|
|
@@ -5477,6 +5495,9 @@ var Bases = class {
|
|
|
5477
5495
|
if (on) await this.get(name);
|
|
5478
5496
|
return true;
|
|
5479
5497
|
}
|
|
5498
|
+
title(name, title) {
|
|
5499
|
+
return titleBase(name, title, this.#config);
|
|
5500
|
+
}
|
|
5480
5501
|
async open(path) {
|
|
5481
5502
|
const name = rememberBase(resolve(path), { path: this.#config });
|
|
5482
5503
|
await this.get(name);
|
|
@@ -5705,7 +5726,23 @@ function given(asked, parts, title) {
|
|
|
5705
5726
|
if (passages.length) out.push(passages.join("\n\n"));
|
|
5706
5727
|
return out.join("\n\n");
|
|
5707
5728
|
}
|
|
5708
|
-
|
|
5729
|
+
/** The same for an agent that takes embedded context: notes and folders as text for the agent to read with isorite's
|
|
5730
|
+
* tools, and each passage as a resource block (the note's address, the passage as its text) that the text points to. */
|
|
5731
|
+
function givenEmbedded(parts, title, base) {
|
|
5732
|
+
const passages = parts.flatMap((p) => p.kind === "passage" ? [p] : []);
|
|
5733
|
+
return {
|
|
5734
|
+
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"),
|
|
5735
|
+
blocks: passages.map((p) => ({
|
|
5736
|
+
type: "resource",
|
|
5737
|
+
resource: {
|
|
5738
|
+
uri: `isorite://${base}/${p.id}`,
|
|
5739
|
+
mimeType: "text/markdown",
|
|
5740
|
+
text: p.text
|
|
5741
|
+
}
|
|
5742
|
+
}))
|
|
5743
|
+
};
|
|
5744
|
+
}
|
|
5745
|
+
const OUTPUT$1 = 4e3;
|
|
5709
5746
|
const NOTE_TOOLS = /(^|[_:./-])(read_note|outline_note|edit_note|write_note)$/;
|
|
5710
5747
|
const CHANGES = /* @__PURE__ */ new Set([
|
|
5711
5748
|
"edit",
|
|
@@ -5721,7 +5758,7 @@ const ACTIONS = {
|
|
|
5721
5758
|
read: "read",
|
|
5722
5759
|
fetch: "webfetch"
|
|
5723
5760
|
};
|
|
5724
|
-
const EMPTY = {
|
|
5761
|
+
const EMPTY$1 = {
|
|
5725
5762
|
connected: false,
|
|
5726
5763
|
agent: "",
|
|
5727
5764
|
session: null,
|
|
@@ -5737,7 +5774,7 @@ const EMPTY = {
|
|
|
5737
5774
|
model: "",
|
|
5738
5775
|
sessions: []
|
|
5739
5776
|
};
|
|
5740
|
-
const textOf = (blocks) => (Array.isArray(blocks) ? blocks : [blocks]).map((b) => b?.type === "text" ? b.text ?? "" : b?.type === "resource_link" ? `@${b.name ?? b.uri ?? ""}` : "").join("");
|
|
5777
|
+
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("");
|
|
5741
5778
|
const lines = (v) => v ? v.replace(/\n$/, "").split("\n") : [];
|
|
5742
5779
|
const keyOf = (agent, session) => `${agent}\u0000${session}`;
|
|
5743
5780
|
const str = (v) => typeof v === "string" ? v : "";
|
|
@@ -5790,7 +5827,7 @@ var AcpFollower = class {
|
|
|
5790
5827
|
#shown = null;
|
|
5791
5828
|
#chosen = false;
|
|
5792
5829
|
#reading = null;
|
|
5793
|
-
#view = EMPTY;
|
|
5830
|
+
#view = EMPTY$1;
|
|
5794
5831
|
#ours = 0;
|
|
5795
5832
|
/** Why what the pane last asked of an agent was refused (signing in, a session it does not have). */
|
|
5796
5833
|
#problem = "";
|
|
@@ -5863,12 +5900,13 @@ var AcpFollower = class {
|
|
|
5863
5900
|
size: live.size,
|
|
5864
5901
|
ours: this.#ours,
|
|
5865
5902
|
sessions: this.#sessions(),
|
|
5903
|
+
...live.link.steer ? { steer: true } : {},
|
|
5866
5904
|
commands: live.commands,
|
|
5867
5905
|
settings: [...live.modes ? [live.modes] : [], ...live.configs],
|
|
5868
5906
|
...problem
|
|
5869
5907
|
};
|
|
5870
5908
|
else if (read) this.#view = {
|
|
5871
|
-
...EMPTY,
|
|
5909
|
+
...EMPTY$1,
|
|
5872
5910
|
connected,
|
|
5873
5911
|
reading: true,
|
|
5874
5912
|
agent: read.name,
|
|
@@ -5882,7 +5920,7 @@ var AcpFollower = class {
|
|
|
5882
5920
|
else {
|
|
5883
5921
|
const link = [...this.#links.values()].at(-1);
|
|
5884
5922
|
this.#view = {
|
|
5885
|
-
...EMPTY,
|
|
5923
|
+
...EMPTY$1,
|
|
5886
5924
|
connected,
|
|
5887
5925
|
agent: link?.name ?? "",
|
|
5888
5926
|
agentId: link?.agent ?? "",
|
|
@@ -5963,6 +6001,8 @@ var AcpFollower = class {
|
|
|
5963
6001
|
resume: false,
|
|
5964
6002
|
list: false
|
|
5965
6003
|
},
|
|
6004
|
+
embedded: false,
|
|
6005
|
+
steer: false,
|
|
5966
6006
|
toAgent: /* @__PURE__ */ new Map(),
|
|
5967
6007
|
fromAgent: /* @__PURE__ */ new Map(),
|
|
5968
6008
|
listed: [],
|
|
@@ -6017,11 +6057,36 @@ var AcpFollower = class {
|
|
|
6017
6057
|
type: "open",
|
|
6018
6058
|
...this.#reading ? { session: this.#reading.session } : {},
|
|
6019
6059
|
then: text,
|
|
6020
|
-
...this.#unopened ? {
|
|
6060
|
+
...this.#unopened ? {
|
|
6061
|
+
give: this.#unopened.text,
|
|
6062
|
+
...this.#unopened.blocks?.length ? { giveBlocks: this.#unopened.blocks } : {}
|
|
6063
|
+
} : {}
|
|
6021
6064
|
});
|
|
6022
6065
|
}
|
|
6066
|
+
/** Subagents the agent announced (`subagent_update`), by their session: each is a session of its own that the pane
|
|
6067
|
+
* does not list, and what it reads and changes in the base is told under its own name, `<agent>-<title>-<n>`. */
|
|
6068
|
+
#children = /* @__PURE__ */ new Map();
|
|
6069
|
+
#childCount = /* @__PURE__ */ new Map();
|
|
6023
6070
|
/** Given before a session was open: for the one the next prompt opens. */
|
|
6024
6071
|
#unopened = null;
|
|
6072
|
+
async steer(text) {
|
|
6073
|
+
const live = this.#need();
|
|
6074
|
+
if (!live.link.steer) throw new Error(`${live.link.name || "this agent"} cannot be steered while it works: stop it and ask again`);
|
|
6075
|
+
if (!live.busy) throw new Error("the agent is not working: ask it instead");
|
|
6076
|
+
live.link.send({
|
|
6077
|
+
type: "steer",
|
|
6078
|
+
session: live.session,
|
|
6079
|
+
text
|
|
6080
|
+
});
|
|
6081
|
+
this.#edit(live, (items) => [...items, {
|
|
6082
|
+
kind: "message",
|
|
6083
|
+
id: `steer-${live.turns.length}-${items.length}`,
|
|
6084
|
+
text,
|
|
6085
|
+
mode: "steer",
|
|
6086
|
+
state: "sent"
|
|
6087
|
+
}]);
|
|
6088
|
+
this.#tell();
|
|
6089
|
+
}
|
|
6025
6090
|
async interrupt() {
|
|
6026
6091
|
const live = this.#need();
|
|
6027
6092
|
live.link.send({
|
|
@@ -6030,24 +6095,31 @@ var AcpFollower = class {
|
|
|
6030
6095
|
});
|
|
6031
6096
|
}
|
|
6032
6097
|
/** Notes and passages given from the pane: those not given in the session yet go before its next prompt. */
|
|
6033
|
-
async give(parts, render) {
|
|
6098
|
+
async give(parts, render, embed) {
|
|
6034
6099
|
const live = this.#shownLive();
|
|
6100
|
+
const made = (p, embedded) => {
|
|
6101
|
+
const e = embedded && embed ? embed(p) : null;
|
|
6102
|
+
return e ? {
|
|
6103
|
+
parts: p,
|
|
6104
|
+
text: e.text,
|
|
6105
|
+
...e.blocks.length ? { blocks: e.blocks } : {}
|
|
6106
|
+
} : {
|
|
6107
|
+
parts: p,
|
|
6108
|
+
text: render(p)
|
|
6109
|
+
};
|
|
6110
|
+
};
|
|
6035
6111
|
if (!live) {
|
|
6036
|
-
|
|
6037
|
-
|
|
6038
|
-
text: render(parts)
|
|
6039
|
-
} : null;
|
|
6112
|
+
const link = [...this.#links.values()].at(-1);
|
|
6113
|
+
this.#unopened = parts.length ? made(parts, link?.embedded === true) : null;
|
|
6040
6114
|
return;
|
|
6041
6115
|
}
|
|
6042
6116
|
const fresh = parts.filter((g) => !live.given.has(givenKey(g)));
|
|
6043
|
-
live.pending = fresh.length ?
|
|
6044
|
-
parts: fresh,
|
|
6045
|
-
text: render(fresh)
|
|
6046
|
-
} : null;
|
|
6117
|
+
live.pending = fresh.length ? made(fresh, live.link.embedded) : null;
|
|
6047
6118
|
live.link.send({
|
|
6048
6119
|
type: "give",
|
|
6049
6120
|
session: live.session,
|
|
6050
|
-
text: live.pending?.text ?? ""
|
|
6121
|
+
text: live.pending?.text ?? "",
|
|
6122
|
+
...live.pending?.blocks ? { blocks: live.pending.blocks } : {}
|
|
6051
6123
|
});
|
|
6052
6124
|
}
|
|
6053
6125
|
async reply(id, decision) {
|
|
@@ -6200,6 +6272,8 @@ var AcpFollower = class {
|
|
|
6200
6272
|
case "initialize": {
|
|
6201
6273
|
const info = m.result?.agentInfo;
|
|
6202
6274
|
const caps = m.result?.agentCapabilities ?? {};
|
|
6275
|
+
link.embedded = caps.promptCapabilities?.embeddedContext === true;
|
|
6276
|
+
link.steer = (m.result?._meta)?.steering?.supported === true;
|
|
6203
6277
|
link.agent = agentName(info?.name ?? "agent");
|
|
6204
6278
|
link.name = info?.title ?? info?.name ?? "The agent";
|
|
6205
6279
|
link.can = {
|
|
@@ -6278,7 +6352,7 @@ var AcpFollower = class {
|
|
|
6278
6352
|
return;
|
|
6279
6353
|
}
|
|
6280
6354
|
if (m.method === "session/update") {
|
|
6281
|
-
const live = this.#lives.get(keyOf(link.agent, sessionId));
|
|
6355
|
+
const live = this.#lives.get(keyOf(link.agent, sessionId)) ?? this.#children.get(keyOf(link.agent, sessionId));
|
|
6282
6356
|
if (live) this.#update(live, m.params?.update ?? {});
|
|
6283
6357
|
}
|
|
6284
6358
|
}
|
|
@@ -6354,7 +6428,7 @@ var AcpFollower = class {
|
|
|
6354
6428
|
live.busy = true;
|
|
6355
6429
|
live.turns = [...live.turns, {
|
|
6356
6430
|
id: key,
|
|
6357
|
-
asked: textOf(given ? blocks.slice(1) : blocks),
|
|
6431
|
+
asked: textOf$1(given ? blocks.slice(1) : blocks),
|
|
6358
6432
|
from: way === "ours" ? "here" : "terminal",
|
|
6359
6433
|
items: [],
|
|
6360
6434
|
state: "running",
|
|
@@ -6397,7 +6471,7 @@ var AcpFollower = class {
|
|
|
6397
6471
|
switch (u.sessionUpdate) {
|
|
6398
6472
|
case "user_message_chunk": {
|
|
6399
6473
|
if (!live.loading) break;
|
|
6400
|
-
const text = textOf(u.content), t = live.turns.at(-1);
|
|
6474
|
+
const text = textOf$1(u.content), t = live.turns.at(-1);
|
|
6401
6475
|
if (live.replaying && t && !t.items.length) live.turns = [...live.turns.slice(0, -1), {
|
|
6402
6476
|
...t,
|
|
6403
6477
|
asked: t.asked + text
|
|
@@ -6414,7 +6488,7 @@ var AcpFollower = class {
|
|
|
6414
6488
|
}
|
|
6415
6489
|
case "agent_message_chunk": {
|
|
6416
6490
|
live.replaying = false;
|
|
6417
|
-
const text = textOf(u.content);
|
|
6491
|
+
const text = textOf$1(u.content);
|
|
6418
6492
|
this.#edit(live, (items) => {
|
|
6419
6493
|
const end = items.at(-1);
|
|
6420
6494
|
if (end?.kind === "text") items[items.length - 1] = {
|
|
@@ -6429,6 +6503,24 @@ var AcpFollower = class {
|
|
|
6429
6503
|
});
|
|
6430
6504
|
break;
|
|
6431
6505
|
}
|
|
6506
|
+
case "agent_thought_chunk": {
|
|
6507
|
+
live.replaying = false;
|
|
6508
|
+
const text = textOf$1(u.content);
|
|
6509
|
+
if (!text) break;
|
|
6510
|
+
this.#edit(live, (items) => {
|
|
6511
|
+
const end = items.at(-1);
|
|
6512
|
+
if (end?.kind === "thinking") items[items.length - 1] = {
|
|
6513
|
+
kind: "thinking",
|
|
6514
|
+
text: end.text + text
|
|
6515
|
+
};
|
|
6516
|
+
else items.push({
|
|
6517
|
+
kind: "thinking",
|
|
6518
|
+
text
|
|
6519
|
+
});
|
|
6520
|
+
return items;
|
|
6521
|
+
});
|
|
6522
|
+
break;
|
|
6523
|
+
}
|
|
6432
6524
|
case "tool_call":
|
|
6433
6525
|
case "tool_call_update": {
|
|
6434
6526
|
live.replaying = false;
|
|
@@ -6446,7 +6538,7 @@ var AcpFollower = class {
|
|
|
6446
6538
|
} : items[at];
|
|
6447
6539
|
const content = Array.isArray(u.content) ? u.content : null;
|
|
6448
6540
|
const diff = content?.find((c) => c.type === "diff");
|
|
6449
|
-
const output = content ? content.filter((c) => c.type === "content").map((c) => textOf(c.content)).join("\n").slice(0, OUTPUT) : was.output;
|
|
6541
|
+
const output = content ? content.filter((c) => c.type === "content").map((c) => textOf$1(c.content)).join("\n").slice(0, OUTPUT$1) : was.output;
|
|
6450
6542
|
const input = u.rawInput ?? {};
|
|
6451
6543
|
kind = typeof u.kind === "string" ? u.kind : diff ? "edit" : "";
|
|
6452
6544
|
const name = typeof u.name === "string" ? u.name : kind && at < 0 ? kind : was.name;
|
|
@@ -6498,6 +6590,58 @@ var AcpFollower = class {
|
|
|
6498
6590
|
case "session_info_update":
|
|
6499
6591
|
if (typeof u.title === "string") live.title = u.title;
|
|
6500
6592
|
break;
|
|
6593
|
+
case "subagent_update": {
|
|
6594
|
+
const id = str(u.sessionId);
|
|
6595
|
+
if (!id || live.link.agent === "") break;
|
|
6596
|
+
const key = keyOf(live.link.agent, id), title = str(u.title) || str(u.description) || "subagent";
|
|
6597
|
+
let child = this.#children.get(key);
|
|
6598
|
+
if (!child) {
|
|
6599
|
+
const n = (this.#childCount.get(live.link.agent) ?? 0) + 1;
|
|
6600
|
+
this.#childCount.set(live.link.agent, n);
|
|
6601
|
+
const agent = agentName(`${live.link.agent}-${title.split(/\s+/).slice(0, 3).join("-")}-${n}`);
|
|
6602
|
+
child = {
|
|
6603
|
+
...live,
|
|
6604
|
+
link: {
|
|
6605
|
+
...live.link,
|
|
6606
|
+
agent
|
|
6607
|
+
},
|
|
6608
|
+
session: id,
|
|
6609
|
+
title,
|
|
6610
|
+
turns: [{
|
|
6611
|
+
id,
|
|
6612
|
+
asked: title,
|
|
6613
|
+
from: live.from,
|
|
6614
|
+
items: [],
|
|
6615
|
+
state: "running"
|
|
6616
|
+
}],
|
|
6617
|
+
seen: /* @__PURE__ */ new Set(),
|
|
6618
|
+
asking: /* @__PURE__ */ new Map(),
|
|
6619
|
+
pending: null,
|
|
6620
|
+
given: /* @__PURE__ */ new Set(),
|
|
6621
|
+
loading: false,
|
|
6622
|
+
replaying: false
|
|
6623
|
+
};
|
|
6624
|
+
this.#children.set(key, child);
|
|
6625
|
+
}
|
|
6626
|
+
const state = u.state ?? {};
|
|
6627
|
+
const status = state.state === "idle" ? state.error || state.stopReason === "error" ? "failed" : "done" : "running";
|
|
6628
|
+
this.#edit(live, (items) => {
|
|
6629
|
+
const at = items.findIndex((i) => i.kind === "tool" && i.id === `subagent-${id}`);
|
|
6630
|
+
const was = at < 0 ? null : items[at];
|
|
6631
|
+
const now = {
|
|
6632
|
+
kind: "tool",
|
|
6633
|
+
id: `subagent-${id}`,
|
|
6634
|
+
name: "subagent",
|
|
6635
|
+
title: was?.title || title,
|
|
6636
|
+
status,
|
|
6637
|
+
output: str(u.description) || was?.output || ""
|
|
6638
|
+
};
|
|
6639
|
+
if (at < 0) items.push(now);
|
|
6640
|
+
else items[at] = now;
|
|
6641
|
+
return items;
|
|
6642
|
+
});
|
|
6643
|
+
break;
|
|
6644
|
+
}
|
|
6501
6645
|
case "available_commands_update":
|
|
6502
6646
|
live.commands = commandsOf(u.availableCommands);
|
|
6503
6647
|
break;
|
|
@@ -6567,7 +6711,7 @@ var AcpFollower = class {
|
|
|
6567
6711
|
};
|
|
6568
6712
|
//#endregion
|
|
6569
6713
|
//#region packages/server/src/version.ts
|
|
6570
|
-
const VERSION = "0.
|
|
6714
|
+
const VERSION = "0.18.1";
|
|
6571
6715
|
//#endregion
|
|
6572
6716
|
//#region packages/server/src/connect.ts
|
|
6573
6717
|
const HARNESSES = [{
|
|
@@ -6593,9 +6737,13 @@ const HARNESSES = [{
|
|
|
6593
6737
|
]
|
|
6594
6738
|
}]
|
|
6595
6739
|
}];
|
|
6596
|
-
/** Where the base's harness config goes: its repository,
|
|
6740
|
+
/** Where the base's harness config goes: its repository; in none, the folder holding a base named `notes` (what
|
|
6741
|
+
* `isorite` makes in a project, M37), so agents are started in the project and not in its notes; else the base's own
|
|
6742
|
+
* folder. Never a home folder or the root, which only look like a project's. */
|
|
6597
6743
|
function projectOf(notes) {
|
|
6598
|
-
const
|
|
6744
|
+
const up = dirname(resolve(notes));
|
|
6745
|
+
const beside = basename(notes) === "notes" && up !== homedir() && dirname(up) !== up;
|
|
6746
|
+
const repo = repoOf(notes) ?? (beside ? up : notes);
|
|
6599
6747
|
return {
|
|
6600
6748
|
repo,
|
|
6601
6749
|
rel: relative(repo, notes).split(sep).join("/") || "."
|
|
@@ -6870,8 +7018,8 @@ function missingKey(command, env = process.env) {
|
|
|
6870
7018
|
//#region packages/server/src/agents.ts
|
|
6871
7019
|
init_self();
|
|
6872
7020
|
const FRESH = 36e5;
|
|
6873
|
-
const ID = /^[a-z0-9][a-z0-9._-]{0,63}$/;
|
|
6874
|
-
const dataDir = (env) => join(env.XDG_DATA_HOME || join(homedir(), ".local", "share"), "isorite", "agents");
|
|
7021
|
+
const ID$2 = /^[a-z0-9][a-z0-9._-]{0,63}$/;
|
|
7022
|
+
const dataDir$1 = (env) => join(env.XDG_DATA_HOME || join(homedir(), ".local", "share"), "isorite", "agents");
|
|
6875
7023
|
/** The registry's name for this platform. */
|
|
6876
7024
|
const platform$1 = () => `${process.platform === "win32" ? "windows" : process.platform}-${process.arch === "arm64" ? "aarch64" : process.arch === "x64" ? "x86_64" : process.arch}`;
|
|
6877
7025
|
/** A word a shell takes as it is. */
|
|
@@ -6883,7 +7031,7 @@ async function readRegistry(env = process.env) {
|
|
|
6883
7031
|
const from = env.ISORITE_ACP_REGISTRY || "https://cdn.agentclientprotocol.com/registry/v1/latest/registry.json";
|
|
6884
7032
|
const parse = (text) => {
|
|
6885
7033
|
const agents = JSON.parse(text).agents;
|
|
6886
|
-
return Array.isArray(agents) ? agents.filter((a) => typeof a?.id === "string" && ID.test(a.id) && typeof a.name === "string") : [];
|
|
7034
|
+
return Array.isArray(agents) ? agents.filter((a) => typeof a?.id === "string" && ID$2.test(a.id) && typeof a.name === "string") : [];
|
|
6887
7035
|
};
|
|
6888
7036
|
if (!/^https?:/.test(from)) return parse(readFileSync(from, "utf8"));
|
|
6889
7037
|
const kept = join(cacheDir(env), "acp-registry.json");
|
|
@@ -6926,7 +7074,7 @@ async function commandOf(e, env = process.env) {
|
|
|
6926
7074
|
const d = e.distribution ?? {};
|
|
6927
7075
|
const bin = d.binary?.[platform$1()];
|
|
6928
7076
|
if (bin) {
|
|
6929
|
-
const dir = join(dataDir(env), e.id, (e.version || "latest").replace(/[^\w.-]/g, "_"));
|
|
7077
|
+
const dir = join(dataDir$1(env), e.id, (e.version || "latest").replace(/[^\w.-]/g, "_"));
|
|
6930
7078
|
const cmd = resolve(dir, bin.cmd);
|
|
6931
7079
|
if (!cmd.startsWith(dir)) throw new Error(`${e.name}: its program is outside its folder`);
|
|
6932
7080
|
if (!existsSync(cmd)) await fetchProgram(bin, dir);
|
|
@@ -7001,7 +7149,7 @@ async function fetchProgram(bin, dir) {
|
|
|
7001
7149
|
/** What bases.toml says this base has chosen. */
|
|
7002
7150
|
function chosenAgent(base, config) {
|
|
7003
7151
|
const row = table(readConfig(basesPath(config))[base]);
|
|
7004
|
-
if (typeof row.agent === "string" && ID.test(row.agent)) return { id: row.agent };
|
|
7152
|
+
if (typeof row.agent === "string" && ID$2.test(row.agent)) return { id: row.agent };
|
|
7005
7153
|
if (typeof row.agent_command === "string" && row.agent_command.trim()) return { command: row.agent_command.trim() };
|
|
7006
7154
|
return null;
|
|
7007
7155
|
}
|
|
@@ -7291,7 +7439,8 @@ const AgentStatus = z.object({
|
|
|
7291
7439
|
session: z.string().min(1).max(200),
|
|
7292
7440
|
busy: z.boolean()
|
|
7293
7441
|
}).strict();
|
|
7294
|
-
function createApp({ api, token, loopback, web, allowHosts = [], notes, acp, bases, base, agents, config }) {
|
|
7442
|
+
function createApp({ api, token, loopback, web, allowHosts = [], notes, acp, terminal, bases, base, agents, config, pi }) {
|
|
7443
|
+
const pane = terminal ?? acp;
|
|
7295
7444
|
const app = new Hono();
|
|
7296
7445
|
const allowed = new Set([...allowHosts].map((h) => h.toLowerCase()));
|
|
7297
7446
|
const reply = (c, status, value) => c.body(JSON.stringify(value), status, {
|
|
@@ -7473,6 +7622,14 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes, acp, bas
|
|
|
7473
7622
|
if (!await bases.lend(b.name, b.lend)) throw new EngineError(`no base ${b.name} is known here`, 404);
|
|
7474
7623
|
return { bases: bases.list() };
|
|
7475
7624
|
});
|
|
7625
|
+
post("/api/bases/title", z.object({
|
|
7626
|
+
name: z.string().max(40),
|
|
7627
|
+
title: z.string().max(60)
|
|
7628
|
+
}).strict(), async (c, b) => {
|
|
7629
|
+
mine(c);
|
|
7630
|
+
if (!bases.title(b.name, b.title)) throw new EngineError(`no base ${b.name} is known here`, 404);
|
|
7631
|
+
return { bases: bases.list() };
|
|
7632
|
+
});
|
|
7476
7633
|
post("/api/bases/open", z.object({ path: z.string().min(1).max(4e3) }).strict(), async (c, b) => {
|
|
7477
7634
|
mine(c);
|
|
7478
7635
|
if (!isAbsolute(b.path) || !existsSync(b.path) || !statSync(b.path).isDirectory()) throw new EngineError(`no folder at ${b.path}`, 404);
|
|
@@ -7550,8 +7707,54 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes, acp, bas
|
|
|
7550
7707
|
return agents.choose(b.choice);
|
|
7551
7708
|
});
|
|
7552
7709
|
}
|
|
7553
|
-
if (
|
|
7554
|
-
|
|
7710
|
+
if (pi) {
|
|
7711
|
+
const mine = (c) => {
|
|
7712
|
+
if (!local(c)) throw new EngineError("the model and keys are set only from this machine", 403);
|
|
7713
|
+
};
|
|
7714
|
+
app.get("/api/pi", (c) => answer(c, async () => {
|
|
7715
|
+
mine(c);
|
|
7716
|
+
return pi.state();
|
|
7717
|
+
}));
|
|
7718
|
+
const Provider = z.string().regex(/^[a-z0-9-]{1,40}$/);
|
|
7719
|
+
const invalid = async (fn) => {
|
|
7720
|
+
try {
|
|
7721
|
+
await fn();
|
|
7722
|
+
} catch (e) {
|
|
7723
|
+
throw new EngineError(e.message, 400);
|
|
7724
|
+
}
|
|
7725
|
+
};
|
|
7726
|
+
post("/api/pi/key", z.object({
|
|
7727
|
+
provider: Provider,
|
|
7728
|
+
key: z.string().max(2e3)
|
|
7729
|
+
}).strict(), async (c, b) => {
|
|
7730
|
+
mine(c);
|
|
7731
|
+
await invalid(() => pi.creds.set(b.provider, b.key));
|
|
7732
|
+
});
|
|
7733
|
+
post("/api/pi/key/remove", z.object({ provider: Provider }).strict(), async (c, b) => {
|
|
7734
|
+
mine(c);
|
|
7735
|
+
await invalid(() => pi.creds.remove(b.provider));
|
|
7736
|
+
});
|
|
7737
|
+
post("/api/pi/reach", z.object({
|
|
7738
|
+
files: z.enum([
|
|
7739
|
+
"off",
|
|
7740
|
+
"read",
|
|
7741
|
+
"write"
|
|
7742
|
+
]),
|
|
7743
|
+
shell: z.boolean()
|
|
7744
|
+
}).strict(), async (c, b) => {
|
|
7745
|
+
mine(c);
|
|
7746
|
+
await invalid(() => pi.setReach(b));
|
|
7747
|
+
});
|
|
7748
|
+
post("/api/pi/choose", z.object({
|
|
7749
|
+
provider: Provider,
|
|
7750
|
+
model: z.string().min(1).max(200)
|
|
7751
|
+
}).strict(), async (c, b) => {
|
|
7752
|
+
mine(c);
|
|
7753
|
+
await invalid(() => pi.choose(b.provider, b.model));
|
|
7754
|
+
});
|
|
7755
|
+
}
|
|
7756
|
+
if (pane) {
|
|
7757
|
+
app.get("/api/terminal", (c) => answer(c, async () => pane.view()));
|
|
7555
7758
|
const failed = (e) => {
|
|
7556
7759
|
throw new EngineError(e.message, 409);
|
|
7557
7760
|
};
|
|
@@ -7563,11 +7766,22 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes, acp, bas
|
|
|
7563
7766
|
const broken = await api.brokenNews();
|
|
7564
7767
|
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("; ")}.)` : "";
|
|
7565
7768
|
const title = (id) => titles.get(id) ?? id;
|
|
7566
|
-
await
|
|
7567
|
-
await
|
|
7769
|
+
await pane.give(b.given, (parts) => given("", parts, title).trim(), (parts) => givenEmbedded(parts, title, base ?? "notes")).catch(failed);
|
|
7770
|
+
await pane.prompt(b.asked.trim() + news).catch(failed);
|
|
7568
7771
|
});
|
|
7772
|
+
post("/api/terminal/steer", z.object({
|
|
7773
|
+
asked: z.string().min(1).max(8e3),
|
|
7774
|
+
given: z.array(Given).max(40)
|
|
7775
|
+
}).strict(), async (_c, b) => {
|
|
7776
|
+
if (!pane.steer) throw new EngineError("this agent cannot be steered while it works: stop it and ask again", 409);
|
|
7777
|
+
const titles = new Map((await api.notes()).notes.map((n) => [n.id, n.title]));
|
|
7778
|
+
const title = (id) => titles.get(id) ?? id;
|
|
7779
|
+
await pane.give(b.given, (parts) => given("", parts, title).trim(), (parts) => givenEmbedded(parts, title, base ?? "notes")).catch(failed);
|
|
7780
|
+
await pane.steer(b.asked.trim()).catch(failed);
|
|
7781
|
+
});
|
|
7782
|
+
post("/api/terminal/withdraw", z.object({ given: Given }).strict(), async (_c, b) => ({ withdrawn: pane.withdraw ? await pane.withdraw(b.given) : false }));
|
|
7569
7783
|
post("/api/terminal/stop", z.object({}).strict(), async () => {
|
|
7570
|
-
await
|
|
7784
|
+
await pane.interrupt().catch(failed);
|
|
7571
7785
|
});
|
|
7572
7786
|
post("/api/terminal/permission", z.object({
|
|
7573
7787
|
id: z.string().max(200),
|
|
@@ -7577,7 +7791,7 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes, acp, bas
|
|
|
7577
7791
|
"reject"
|
|
7578
7792
|
])
|
|
7579
7793
|
}).strict(), async (_c, b) => {
|
|
7580
|
-
await
|
|
7794
|
+
await pane.reply(b.id, b.decision).catch(failed);
|
|
7581
7795
|
});
|
|
7582
7796
|
}
|
|
7583
7797
|
const endpoint = new McpEndpoint(api, {
|
|
@@ -7610,7 +7824,7 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes, acp, bas
|
|
|
7610
7824
|
const quiet = Number(c.req.query("quiet"));
|
|
7611
7825
|
const who = (rpc) => {
|
|
7612
7826
|
const agent = named && /^[a-z0-9][a-z0-9-]{0,39}$/.test(named) ? named : agentName(clientName(rpc) ?? "mcp");
|
|
7613
|
-
const asked =
|
|
7827
|
+
const asked = pane?.asked(agent) ?? "";
|
|
7614
7828
|
return {
|
|
7615
7829
|
agent,
|
|
7616
7830
|
session: (c.req.query("session") ?? "http").slice(0, 200),
|
|
@@ -7626,25 +7840,38 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes, acp, bas
|
|
|
7626
7840
|
if (acp) {
|
|
7627
7841
|
const render = async () => {
|
|
7628
7842
|
const titles = new Map((await api.notes()).notes.map((n) => [n.id, n.title]));
|
|
7629
|
-
|
|
7843
|
+
const title = (id) => titles.get(id) ?? id;
|
|
7844
|
+
return {
|
|
7845
|
+
text: (parts) => given("", parts, title).trim(),
|
|
7846
|
+
embed: (parts) => givenEmbedded(parts, title, base ?? "notes")
|
|
7847
|
+
};
|
|
7630
7848
|
};
|
|
7849
|
+
const host = pane ?? acp;
|
|
7631
7850
|
const refused = (e) => {
|
|
7632
7851
|
throw new EngineError(e.message, 409);
|
|
7633
7852
|
};
|
|
7634
7853
|
post("/api/acp/give", z.object({ given: z.array(Given).max(40) }).strict(), async (_c, b) => {
|
|
7635
|
-
|
|
7854
|
+
const r = await render();
|
|
7855
|
+
await host.give(b.given, r.text, r.embed).catch(refused);
|
|
7636
7856
|
});
|
|
7637
7857
|
post("/api/acp/follow", z.object({
|
|
7638
7858
|
agent: z.string().max(40),
|
|
7639
7859
|
session: z.string().min(1).max(200)
|
|
7640
7860
|
}).strict(), async (_c, b) => {
|
|
7641
|
-
await
|
|
7861
|
+
await host.follow(b.agent, b.session).catch(refused);
|
|
7862
|
+
});
|
|
7863
|
+
post("/api/acp/fork", z.object({
|
|
7864
|
+
agent: z.string().max(40),
|
|
7865
|
+
session: z.string().min(1).max(200)
|
|
7866
|
+
}).strict(), async (_c, b) => {
|
|
7867
|
+
if (!host.fork) throw new EngineError("this agent's sessions cannot be forked here", 409);
|
|
7868
|
+
await host.fork(b.agent, b.session).catch(refused);
|
|
7642
7869
|
});
|
|
7643
7870
|
post("/api/acp/forget", z.object({
|
|
7644
7871
|
agent: z.string().max(40),
|
|
7645
7872
|
session: z.string().min(1).max(200)
|
|
7646
7873
|
}).strict(), async (_c, b) => {
|
|
7647
|
-
await
|
|
7874
|
+
await host.forget(b.agent, b.session).catch(refused);
|
|
7648
7875
|
});
|
|
7649
7876
|
post("/api/acp/set", z.object({
|
|
7650
7877
|
kind: z.enum(["mode", "config"]),
|
|
@@ -7652,16 +7879,16 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes, acp, bas
|
|
|
7652
7879
|
value: z.string().max(200)
|
|
7653
7880
|
}).strict(), async (_c, b) => {
|
|
7654
7881
|
try {
|
|
7655
|
-
|
|
7882
|
+
host.set(b.kind, b.id, b.value);
|
|
7656
7883
|
} catch (e) {
|
|
7657
7884
|
throw new EngineError(e.message, 409);
|
|
7658
7885
|
}
|
|
7659
7886
|
});
|
|
7660
7887
|
post("/api/acp/new", z.object({ agent: z.string().max(40) }).strict(), async (_c, b) => {
|
|
7661
|
-
await
|
|
7888
|
+
await host.fresh(b.agent).catch(refused);
|
|
7662
7889
|
});
|
|
7663
7890
|
post("/api/acp/list", z.object({}).strict(), async () => {
|
|
7664
|
-
|
|
7891
|
+
host.refresh();
|
|
7665
7892
|
});
|
|
7666
7893
|
post("/api/acp/tap", z.object({
|
|
7667
7894
|
link: z.string().min(1).max(100),
|
|
@@ -7733,7 +7960,7 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes, acp, bas
|
|
|
7733
7960
|
})
|
|
7734
7961
|
}).catch(() => void 0);
|
|
7735
7962
|
};
|
|
7736
|
-
const unfollow =
|
|
7963
|
+
const unfollow = pane?.listen(send);
|
|
7737
7964
|
const beat = setInterval(() => {
|
|
7738
7965
|
stream.write(": still here\n\n");
|
|
7739
7966
|
}, 25e3);
|
|
@@ -7741,7 +7968,7 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes, acp, bas
|
|
|
7741
7968
|
event: "open",
|
|
7742
7969
|
data: "{}"
|
|
7743
7970
|
});
|
|
7744
|
-
if (
|
|
7971
|
+
if (pane?.view().connected) send(pane.view());
|
|
7745
7972
|
streams.set(stream, c.req.query("agent") === void 0);
|
|
7746
7973
|
watchers();
|
|
7747
7974
|
await new Promise((done) => stream.onAbort(done));
|
|
@@ -7775,6 +8002,1268 @@ function staticFile(c, web) {
|
|
|
7775
8002
|
});
|
|
7776
8003
|
}
|
|
7777
8004
|
//#endregion
|
|
8005
|
+
//#region packages/server/src/pi/store.ts
|
|
8006
|
+
/** Said when a saved session cannot be read: names the file, so the person can look at it or move it away. */
|
|
8007
|
+
var StoreError = class extends Error {
|
|
8008
|
+
file;
|
|
8009
|
+
constructor(file, why) {
|
|
8010
|
+
super(`The saved pi session ${file} could not be read (${why}). It was left where it is.`);
|
|
8011
|
+
this.file = file;
|
|
8012
|
+
}
|
|
8013
|
+
};
|
|
8014
|
+
const ID$1 = /^[0-9a-f-]{8,64}$/i;
|
|
8015
|
+
var FileStore = class {
|
|
8016
|
+
#dir;
|
|
8017
|
+
constructor(dir) {
|
|
8018
|
+
this.#dir = dir;
|
|
8019
|
+
}
|
|
8020
|
+
async save(r) {
|
|
8021
|
+
if (!ID$1.test(r.session)) throw new Error("not a session id");
|
|
8022
|
+
await mkdir(this.#dir, {
|
|
8023
|
+
recursive: true,
|
|
8024
|
+
mode: 448
|
|
8025
|
+
});
|
|
8026
|
+
const file = join(this.#dir, `${r.session}.json`), tmp = `${file}.${process.pid}.tmp`;
|
|
8027
|
+
await writeFile(tmp, JSON.stringify(r), { mode: 384 });
|
|
8028
|
+
await rename(tmp, file);
|
|
8029
|
+
}
|
|
8030
|
+
async latest() {
|
|
8031
|
+
let names;
|
|
8032
|
+
try {
|
|
8033
|
+
names = (await readdir(this.#dir)).filter((n) => n.endsWith(".json"));
|
|
8034
|
+
} catch {
|
|
8035
|
+
return null;
|
|
8036
|
+
}
|
|
8037
|
+
const newest = (await Promise.all(names.map(async (n) => ({
|
|
8038
|
+
n,
|
|
8039
|
+
t: (await stat(join(this.#dir, n))).mtimeMs
|
|
8040
|
+
})))).sort((a, b) => b.t - a.t)[0];
|
|
8041
|
+
if (!newest) return null;
|
|
8042
|
+
return this.#read(join(this.#dir, newest.n));
|
|
8043
|
+
}
|
|
8044
|
+
async #read(file) {
|
|
8045
|
+
let record;
|
|
8046
|
+
try {
|
|
8047
|
+
record = JSON.parse(await readFile(file, "utf8"));
|
|
8048
|
+
} catch (e) {
|
|
8049
|
+
throw new StoreError(file, e.message);
|
|
8050
|
+
}
|
|
8051
|
+
const r = record;
|
|
8052
|
+
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");
|
|
8053
|
+
return r;
|
|
8054
|
+
}
|
|
8055
|
+
async list() {
|
|
8056
|
+
let names;
|
|
8057
|
+
try {
|
|
8058
|
+
names = (await readdir(this.#dir)).filter((n) => n.endsWith(".json"));
|
|
8059
|
+
} catch {
|
|
8060
|
+
return {
|
|
8061
|
+
records: [],
|
|
8062
|
+
unreadable: []
|
|
8063
|
+
};
|
|
8064
|
+
}
|
|
8065
|
+
const records = [], unreadable = [];
|
|
8066
|
+
for (const n of names) try {
|
|
8067
|
+
records.push(await this.#read(join(this.#dir, n)));
|
|
8068
|
+
} catch (e) {
|
|
8069
|
+
unreadable.push(e instanceof StoreError ? e.file : join(this.#dir, n));
|
|
8070
|
+
}
|
|
8071
|
+
return {
|
|
8072
|
+
records: records.sort((a, b) => a.last < b.last ? 1 : -1),
|
|
8073
|
+
unreadable
|
|
8074
|
+
};
|
|
8075
|
+
}
|
|
8076
|
+
async load(session) {
|
|
8077
|
+
if (!ID$1.test(session)) return null;
|
|
8078
|
+
const file = join(this.#dir, `${session}.json`);
|
|
8079
|
+
try {
|
|
8080
|
+
await stat(file);
|
|
8081
|
+
} catch {
|
|
8082
|
+
return null;
|
|
8083
|
+
}
|
|
8084
|
+
return this.#read(file);
|
|
8085
|
+
}
|
|
8086
|
+
async remove(session) {
|
|
8087
|
+
if (!ID$1.test(session)) throw new Error("not a session id");
|
|
8088
|
+
await rm(join(this.#dir, `${session}.json`), { force: true });
|
|
8089
|
+
}
|
|
8090
|
+
};
|
|
8091
|
+
/** Where a base's pi sessions are kept: isorite's data folder, apart from the notes and the repository. */
|
|
8092
|
+
function storeDir(notes, env = process.env) {
|
|
8093
|
+
const key = createHash("sha1").update(notes).digest("hex").slice(0, 12);
|
|
8094
|
+
return join(env.XDG_DATA_HOME || join(homedir(), ".local", "share"), "isorite", "pi", key);
|
|
8095
|
+
}
|
|
8096
|
+
//#endregion
|
|
8097
|
+
//#region packages/server/src/pi/tools.ts
|
|
8098
|
+
/** `who` names the agent and session as the base knows them; `asked` is what is being asked now, for a change the agent makes. */
|
|
8099
|
+
async function isoriteTools(api, who, asked) {
|
|
8100
|
+
return (await api.agentTools()).map(({ function: f }) => ({
|
|
8101
|
+
name: f.name,
|
|
8102
|
+
label: f.name,
|
|
8103
|
+
description: f.description,
|
|
8104
|
+
parameters: f.parameters,
|
|
8105
|
+
execute: async (_id, params) => {
|
|
8106
|
+
const now = asked();
|
|
8107
|
+
const { text } = await api.agentLook({
|
|
8108
|
+
...who,
|
|
8109
|
+
tool: f.name,
|
|
8110
|
+
args: params,
|
|
8111
|
+
...now ? { asked: now } : {}
|
|
8112
|
+
});
|
|
8113
|
+
return {
|
|
8114
|
+
content: [{
|
|
8115
|
+
type: "text",
|
|
8116
|
+
text
|
|
8117
|
+
}],
|
|
8118
|
+
details: void 0
|
|
8119
|
+
};
|
|
8120
|
+
}
|
|
8121
|
+
}));
|
|
8122
|
+
}
|
|
8123
|
+
/** What a call acted on, in a line: a query, a note, a path, a command. */
|
|
8124
|
+
function titleOf(args) {
|
|
8125
|
+
if (!args || typeof args !== "object") return "";
|
|
8126
|
+
const a = args;
|
|
8127
|
+
for (const k of [
|
|
8128
|
+
"query",
|
|
8129
|
+
"note",
|
|
8130
|
+
"id",
|
|
8131
|
+
"path",
|
|
8132
|
+
"command",
|
|
8133
|
+
"question",
|
|
8134
|
+
"text"
|
|
8135
|
+
]) if (typeof a[k] === "string" && a[k]) return String(a[k]).slice(0, 200);
|
|
8136
|
+
const first = Object.values(a).find((v) => typeof v === "string" && v);
|
|
8137
|
+
return typeof first === "string" ? first.slice(0, 200) : "";
|
|
8138
|
+
}
|
|
8139
|
+
//#endregion
|
|
8140
|
+
//#region packages/server/src/pi/host.ts
|
|
8141
|
+
const OUTPUT = 4e3;
|
|
8142
|
+
/** What the summary of an earlier conversation begins with, in the message that stands for it. */
|
|
8143
|
+
const SUMMARY = "[The earlier part of this conversation, summarised to make room]\n";
|
|
8144
|
+
const EMPTY = {
|
|
8145
|
+
connected: true,
|
|
8146
|
+
agent: "",
|
|
8147
|
+
session: null,
|
|
8148
|
+
title: "",
|
|
8149
|
+
busy: false,
|
|
8150
|
+
turns: [],
|
|
8151
|
+
asking: [],
|
|
8152
|
+
cost: 0,
|
|
8153
|
+
tokens: {
|
|
8154
|
+
input: 0,
|
|
8155
|
+
output: 0
|
|
8156
|
+
},
|
|
8157
|
+
model: "",
|
|
8158
|
+
sessions: []
|
|
8159
|
+
};
|
|
8160
|
+
/** 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
|
|
8161
|
+
* taken back out of what the model is sent when the chip is removed (M36, step 10). */
|
|
8162
|
+
const TAG = /^\[context (\S+) @(\S+)\]\n/;
|
|
8163
|
+
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.]";
|
|
8164
|
+
const tagged = (key, turn, block) => `[context ${encodeURIComponent(key)} @${turn}]\n${block}\n[/context]\n\n`;
|
|
8165
|
+
const textOf = (content) => (Array.isArray(content) ? content : []).map((b) => b?.type === "text" ? b.text ?? "" : "").join("");
|
|
8166
|
+
const userText = (content) => typeof content === "string" ? content : textOf(content);
|
|
8167
|
+
var PiHost = class {
|
|
8168
|
+
#api;
|
|
8169
|
+
#model;
|
|
8170
|
+
#streamFn;
|
|
8171
|
+
#getApiKey;
|
|
8172
|
+
#id;
|
|
8173
|
+
#name;
|
|
8174
|
+
#ask;
|
|
8175
|
+
#more;
|
|
8176
|
+
#guard;
|
|
8177
|
+
#now;
|
|
8178
|
+
#store;
|
|
8179
|
+
#compaction;
|
|
8180
|
+
/** The thinking item the assistant message now streaming writes into. */
|
|
8181
|
+
#thinkingAt = -1;
|
|
8182
|
+
/** Cancels a summary being written. */
|
|
8183
|
+
#summarising = null;
|
|
8184
|
+
/** Why the last thing asked of the agent could not be done, shown in the pane. */
|
|
8185
|
+
#problem = "";
|
|
8186
|
+
/** The other sessions kept, by id (the one shown is in the fields below); and the last write, waited for before one is left. */
|
|
8187
|
+
#kept = /* @__PURE__ */ new Map();
|
|
8188
|
+
#saving = Promise.resolve();
|
|
8189
|
+
#agent = null;
|
|
8190
|
+
#session = "";
|
|
8191
|
+
#at = "";
|
|
8192
|
+
#title = "";
|
|
8193
|
+
#turns = [];
|
|
8194
|
+
#busy = false;
|
|
8195
|
+
/** The person stopped it: an aborted run is then a stop, not a failure. */
|
|
8196
|
+
#stopping = false;
|
|
8197
|
+
#cost = 0;
|
|
8198
|
+
#used = 0;
|
|
8199
|
+
#asking = /* @__PURE__ */ new Map();
|
|
8200
|
+
#allowed = /* @__PURE__ */ new Set();
|
|
8201
|
+
#pending = null;
|
|
8202
|
+
/** The blocks taken back out: `<key>@<turn>` of each, kept with the session. */
|
|
8203
|
+
#withdrawn = /* @__PURE__ */ new Set();
|
|
8204
|
+
#given = /* @__PURE__ */ new Set();
|
|
8205
|
+
/** 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. */
|
|
8206
|
+
#queued = [];
|
|
8207
|
+
/** The first user message of a run is the prompt itself: it is not a queued message landing. */
|
|
8208
|
+
#primed = false;
|
|
8209
|
+
/** The text item the assistant message now streaming writes into. */
|
|
8210
|
+
#writing = -1;
|
|
8211
|
+
#view = EMPTY;
|
|
8212
|
+
#listeners = /* @__PURE__ */ new Set();
|
|
8213
|
+
constructor({ api, model, streamFn, id = "pi", name = "pi", ask = () => false, more = () => [], guard = () => void 0, clock = Date.now, store, compaction, getApiKey }) {
|
|
8214
|
+
this.#getApiKey = getApiKey;
|
|
8215
|
+
this.#api = api;
|
|
8216
|
+
this.#model = model;
|
|
8217
|
+
this.#streamFn = streamFn;
|
|
8218
|
+
this.#id = id;
|
|
8219
|
+
this.#name = name;
|
|
8220
|
+
this.#ask = ask;
|
|
8221
|
+
this.#more = more;
|
|
8222
|
+
this.#guard = guard;
|
|
8223
|
+
this.#now = clock;
|
|
8224
|
+
this.#store = store ?? null;
|
|
8225
|
+
this.#compaction = {
|
|
8226
|
+
enabled: compaction?.enabled ?? DEFAULT_COMPACTION_SETTINGS.enabled ?? true,
|
|
8227
|
+
reserveTokens: compaction?.reserveTokens ?? DEFAULT_COMPACTION_SETTINGS.reserveTokens ?? 16384,
|
|
8228
|
+
keepRecentTokens: compaction?.keepRecentTokens ?? DEFAULT_COMPACTION_SETTINGS.keepRecentTokens ?? 2e4
|
|
8229
|
+
};
|
|
8230
|
+
api.events?.((e) => {
|
|
8231
|
+
if (e.type !== "agent-item" || e.agent !== this.#id || !this.#turns.length) return;
|
|
8232
|
+
this.#edit((items) => [...items, e.item]);
|
|
8233
|
+
this.#tell();
|
|
8234
|
+
});
|
|
8235
|
+
this.#tell();
|
|
8236
|
+
}
|
|
8237
|
+
/** Carry on with another model (and the way to reach it): the session stays, and the next request goes to it. */
|
|
8238
|
+
use(model, streamFn, name) {
|
|
8239
|
+
this.#model = model;
|
|
8240
|
+
this.#streamFn = streamFn;
|
|
8241
|
+
if (name) this.#name = name;
|
|
8242
|
+
if (this.#agent) this.#agent.state.model = model;
|
|
8243
|
+
this.#tell();
|
|
8244
|
+
}
|
|
8245
|
+
view() {
|
|
8246
|
+
return this.#view;
|
|
8247
|
+
}
|
|
8248
|
+
listen(fn) {
|
|
8249
|
+
this.#listeners.add(fn);
|
|
8250
|
+
return () => {
|
|
8251
|
+
this.#listeners.delete(fn);
|
|
8252
|
+
};
|
|
8253
|
+
}
|
|
8254
|
+
asked(agent) {
|
|
8255
|
+
const t = this.#turns.at(-1);
|
|
8256
|
+
return agent === this.#id && t?.state === "running" ? t.asked : "";
|
|
8257
|
+
}
|
|
8258
|
+
async give(parts, render) {
|
|
8259
|
+
const fresh = parts.filter((g) => !this.#given.has(givenKey(g)));
|
|
8260
|
+
this.#pending = fresh.length ? {
|
|
8261
|
+
parts: fresh,
|
|
8262
|
+
blocks: fresh.map((g) => render([g]))
|
|
8263
|
+
} : null;
|
|
8264
|
+
}
|
|
8265
|
+
async prompt(text) {
|
|
8266
|
+
if (this.#busy) {
|
|
8267
|
+
await this.#say(text, "followUp");
|
|
8268
|
+
return;
|
|
8269
|
+
}
|
|
8270
|
+
this.#problem = "";
|
|
8271
|
+
if (!this.#agent) this.#open();
|
|
8272
|
+
const agent = this.#agent;
|
|
8273
|
+
const pending = this.#pending;
|
|
8274
|
+
this.#pending = null;
|
|
8275
|
+
if (pending) for (const g of pending.parts) this.#given.add(givenKey(g));
|
|
8276
|
+
agent.state.tools = [...await isoriteTools(this.#api, {
|
|
8277
|
+
agent: this.#id,
|
|
8278
|
+
session: this.#session
|
|
8279
|
+
}, () => this.asked(this.#id)), ...this.#more()];
|
|
8280
|
+
const turn = {
|
|
8281
|
+
id: randomUUID(),
|
|
8282
|
+
asked: text,
|
|
8283
|
+
from: "here",
|
|
8284
|
+
items: [],
|
|
8285
|
+
state: "running",
|
|
8286
|
+
...pending ? { given: pending.parts } : {},
|
|
8287
|
+
at: new Date(this.#now()).toISOString()
|
|
8288
|
+
};
|
|
8289
|
+
this.#turns = [...this.#turns, turn];
|
|
8290
|
+
if (!this.#title) this.#title = text.slice(0, 80);
|
|
8291
|
+
this.#busy = true;
|
|
8292
|
+
this.#primed = false;
|
|
8293
|
+
this.#queued = [];
|
|
8294
|
+
this.#writing = -1;
|
|
8295
|
+
this.#thinkingAt = -1;
|
|
8296
|
+
this.#api.agentStatus(this.#id, this.#session, true).catch(() => void 0);
|
|
8297
|
+
this.#tell();
|
|
8298
|
+
const message = this.#message(pending, turn.id.slice(0, 8), text);
|
|
8299
|
+
(async () => {
|
|
8300
|
+
await this.#compactIfNeeded(agent);
|
|
8301
|
+
if (this.#stopping) throw new Error("stopped");
|
|
8302
|
+
await agent.prompt(message);
|
|
8303
|
+
})().then(() => void 0, (e) => {
|
|
8304
|
+
if (this.#stopping) this.#set("stopped");
|
|
8305
|
+
else this.#fail(e.message);
|
|
8306
|
+
}).then(() => this.#end());
|
|
8307
|
+
}
|
|
8308
|
+
/** The person's message for the model: each block given with it, tagged, then what they asked. */
|
|
8309
|
+
#message(pending, turn, text) {
|
|
8310
|
+
return {
|
|
8311
|
+
role: "user",
|
|
8312
|
+
content: [...(pending?.parts ?? []).map((g, i) => tagged(givenKey(g), turn, pending.blocks[i] ?? "")), text].map((t) => ({
|
|
8313
|
+
type: "text",
|
|
8314
|
+
text: t
|
|
8315
|
+
})),
|
|
8316
|
+
timestamp: this.#now()
|
|
8317
|
+
};
|
|
8318
|
+
}
|
|
8319
|
+
/** What the model is sent: the messages, with each withdrawn block replaced by a line saying so. */
|
|
8320
|
+
#redact(messages) {
|
|
8321
|
+
if (!this.#withdrawn.size) return messages;
|
|
8322
|
+
return messages.map((m) => {
|
|
8323
|
+
const msg = m;
|
|
8324
|
+
if (msg.role !== "user" || !Array.isArray(msg.content)) return m;
|
|
8325
|
+
let changed = false;
|
|
8326
|
+
const content = msg.content.map((c) => {
|
|
8327
|
+
const tag = c?.type === "text" && c.text ? TAG.exec(c.text) : null;
|
|
8328
|
+
if (!tag || !this.#withdrawn.has(`${decodeURIComponent(tag[1])}@${tag[2]}`)) return c;
|
|
8329
|
+
changed = true;
|
|
8330
|
+
return {
|
|
8331
|
+
type: "text",
|
|
8332
|
+
text: WITHDRAWN
|
|
8333
|
+
};
|
|
8334
|
+
});
|
|
8335
|
+
return changed ? {
|
|
8336
|
+
...m,
|
|
8337
|
+
content
|
|
8338
|
+
} : m;
|
|
8339
|
+
});
|
|
8340
|
+
}
|
|
8341
|
+
/** The person took a chip out of the tray: the block given with it comes out of everything the model is sent from now
|
|
8342
|
+
* on. The turns it came in stay as they were, marked. Says whether there was anything to take out. */
|
|
8343
|
+
async withdraw(g) {
|
|
8344
|
+
const key = givenKey(g);
|
|
8345
|
+
if (this.#pending) {
|
|
8346
|
+
const keep = this.#pending.parts.map((p, i) => ({
|
|
8347
|
+
p,
|
|
8348
|
+
b: this.#pending.blocks[i]
|
|
8349
|
+
})).filter((x) => givenKey(x.p) !== key);
|
|
8350
|
+
this.#pending = keep.length ? {
|
|
8351
|
+
parts: keep.map((x) => x.p),
|
|
8352
|
+
blocks: keep.map((x) => x.b)
|
|
8353
|
+
} : null;
|
|
8354
|
+
}
|
|
8355
|
+
if (!this.#given.has(key)) return false;
|
|
8356
|
+
this.#given.delete(key);
|
|
8357
|
+
for (const m of this.#agent?.state.messages ?? []) {
|
|
8358
|
+
const msg = m;
|
|
8359
|
+
if (msg.role !== "user" || !Array.isArray(msg.content)) continue;
|
|
8360
|
+
for (const c of msg.content) {
|
|
8361
|
+
const tag = c?.type === "text" && c.text ? TAG.exec(c.text) : null;
|
|
8362
|
+
if (tag && decodeURIComponent(tag[1]) === key) this.#withdrawn.add(`${key}@${tag[2]}`);
|
|
8363
|
+
}
|
|
8364
|
+
}
|
|
8365
|
+
this.#turns = this.#turns.map((t) => {
|
|
8366
|
+
const at = (t.given ?? []).flatMap((x, i) => givenKey(x) === key && !t.withdrawn?.includes(i) ? [i] : []);
|
|
8367
|
+
return at.length ? {
|
|
8368
|
+
...t,
|
|
8369
|
+
withdrawn: [...t.withdrawn ?? [], ...at]
|
|
8370
|
+
} : t;
|
|
8371
|
+
});
|
|
8372
|
+
this.#save();
|
|
8373
|
+
this.#keep();
|
|
8374
|
+
this.#tell();
|
|
8375
|
+
return true;
|
|
8376
|
+
}
|
|
8377
|
+
async steer(text) {
|
|
8378
|
+
if (!this.#busy) throw new Error("the agent is not working: ask it instead");
|
|
8379
|
+
await this.#say(text, "steer");
|
|
8380
|
+
}
|
|
8381
|
+
/** Said while it works: a steer comes in after the step now running, a follow-up when it would otherwise stop. */
|
|
8382
|
+
async #say(text, mode) {
|
|
8383
|
+
const agent = this.#agent;
|
|
8384
|
+
if (!agent) return;
|
|
8385
|
+
const pending = this.#pending;
|
|
8386
|
+
this.#pending = null;
|
|
8387
|
+
if (pending) for (const g of pending.parts) this.#given.add(givenKey(g));
|
|
8388
|
+
const id = randomUUID();
|
|
8389
|
+
const message = this.#message(pending, (this.#turns.at(-1)?.id ?? id).slice(0, 8), text);
|
|
8390
|
+
this.#queued.push({
|
|
8391
|
+
id,
|
|
8392
|
+
body: userText(message.content)
|
|
8393
|
+
});
|
|
8394
|
+
this.#edit((items) => [...items, {
|
|
8395
|
+
kind: "message",
|
|
8396
|
+
id,
|
|
8397
|
+
text,
|
|
8398
|
+
mode,
|
|
8399
|
+
state: "queued"
|
|
8400
|
+
}]);
|
|
8401
|
+
if (mode === "steer") agent.steer(message);
|
|
8402
|
+
else agent.followUp(message);
|
|
8403
|
+
this.#tell();
|
|
8404
|
+
}
|
|
8405
|
+
async interrupt() {
|
|
8406
|
+
this.#stopping = true;
|
|
8407
|
+
this.#summarising?.abort();
|
|
8408
|
+
this.#agent?.clearAllQueues();
|
|
8409
|
+
for (const a of this.#asking.values()) a.wait("reject");
|
|
8410
|
+
this.#agent?.abort();
|
|
8411
|
+
}
|
|
8412
|
+
async reply(id, decision) {
|
|
8413
|
+
const asked = this.#asking.get(id);
|
|
8414
|
+
if (!asked) throw new Error("the agent is not asking that");
|
|
8415
|
+
asked.wait(decision);
|
|
8416
|
+
}
|
|
8417
|
+
/** Show another kept session, and carry on in it. The one left is already kept (it is written after each turn). */
|
|
8418
|
+
async follow(_agent, session) {
|
|
8419
|
+
if (session === this.#session) return;
|
|
8420
|
+
if (this.#busy) throw new Error("the agent is working: stop it first");
|
|
8421
|
+
const kept = await this.#fetch(session);
|
|
8422
|
+
this.#restore(kept);
|
|
8423
|
+
}
|
|
8424
|
+
/** A new session from the pane; the one shown stays kept and can be shown again. */
|
|
8425
|
+
async fresh(_agent) {
|
|
8426
|
+
if (this.#busy) throw new Error("the agent is working: stop it first");
|
|
8427
|
+
await this.#saving;
|
|
8428
|
+
this.#blank();
|
|
8429
|
+
this.#tell();
|
|
8430
|
+
}
|
|
8431
|
+
/** A new session that begins where `session` is (its turns and the model's messages), going its own way from there;
|
|
8432
|
+
* its cost starts again at nothing. The original is not changed. */
|
|
8433
|
+
async fork(_agent, session) {
|
|
8434
|
+
if (this.#busy) throw new Error("the agent is working: stop it first");
|
|
8435
|
+
const from = session === this.#session && this.#agent ? this.#record() : await this.#fetch(session);
|
|
8436
|
+
if (!from || !this.#store) throw new Error("that session is not kept, so there is nothing to fork");
|
|
8437
|
+
const now = new Date(this.#now()).toISOString();
|
|
8438
|
+
const copy = {
|
|
8439
|
+
...JSON.parse(JSON.stringify(from)),
|
|
8440
|
+
session: randomUUID(),
|
|
8441
|
+
title: `${from.title || "A session"} (fork)`.slice(0, 90),
|
|
8442
|
+
at: now,
|
|
8443
|
+
last: now,
|
|
8444
|
+
cost: 0
|
|
8445
|
+
};
|
|
8446
|
+
await this.#store.save(copy);
|
|
8447
|
+
this.#kept.set(copy.session, this.#summary(copy));
|
|
8448
|
+
this.#restore(copy);
|
|
8449
|
+
}
|
|
8450
|
+
set() {
|
|
8451
|
+
throw new Error(`${this.#name} offers no modes or options yet`);
|
|
8452
|
+
}
|
|
8453
|
+
/** Remove what is kept of a session. The one shown is left first, for a new one. */
|
|
8454
|
+
async forget(_agent, session) {
|
|
8455
|
+
if (!this.#store) throw new Error("nothing is kept apart from the session shown");
|
|
8456
|
+
if (session === this.#session) {
|
|
8457
|
+
if (this.#busy) throw new Error("the agent is working: stop it first");
|
|
8458
|
+
await this.#saving;
|
|
8459
|
+
this.#blank();
|
|
8460
|
+
}
|
|
8461
|
+
await this.#store.remove(session);
|
|
8462
|
+
this.#kept.delete(session);
|
|
8463
|
+
this.#tell();
|
|
8464
|
+
}
|
|
8465
|
+
/** Read the list of kept sessions again. */
|
|
8466
|
+
refresh() {
|
|
8467
|
+
this.#index().then(() => this.#tell());
|
|
8468
|
+
}
|
|
8469
|
+
async #index() {
|
|
8470
|
+
if (!this.#store) return;
|
|
8471
|
+
const { records, unreadable } = await this.#store.list();
|
|
8472
|
+
this.#kept = new Map(records.map((r) => [r.session, this.#summary(r)]));
|
|
8473
|
+
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"}.`;
|
|
8474
|
+
}
|
|
8475
|
+
#summary(r) {
|
|
8476
|
+
return {
|
|
8477
|
+
session: r.session,
|
|
8478
|
+
title: r.title,
|
|
8479
|
+
asked: r.turns[0]?.asked ?? "",
|
|
8480
|
+
last: r.last,
|
|
8481
|
+
turns: r.turns.length,
|
|
8482
|
+
notes: 0,
|
|
8483
|
+
live: false,
|
|
8484
|
+
busy: false,
|
|
8485
|
+
kept: true,
|
|
8486
|
+
open: true
|
|
8487
|
+
};
|
|
8488
|
+
}
|
|
8489
|
+
async #fetch(session) {
|
|
8490
|
+
if (!this.#store) throw new Error("that session is not here to carry on");
|
|
8491
|
+
await this.#saving;
|
|
8492
|
+
const kept = await this.#store.load(session);
|
|
8493
|
+
if (!kept) throw new Error("that session is not kept");
|
|
8494
|
+
return kept;
|
|
8495
|
+
}
|
|
8496
|
+
/** No session shown: the next question begins a new one. Everything of the last is let go (it stays kept). */
|
|
8497
|
+
#blank() {
|
|
8498
|
+
this.#agent = null;
|
|
8499
|
+
this.#session = "";
|
|
8500
|
+
this.#at = "";
|
|
8501
|
+
this.#turns = [];
|
|
8502
|
+
this.#title = "";
|
|
8503
|
+
this.#cost = 0;
|
|
8504
|
+
this.#used = 0;
|
|
8505
|
+
this.#given = /* @__PURE__ */ new Set();
|
|
8506
|
+
this.#withdrawn = /* @__PURE__ */ new Set();
|
|
8507
|
+
this.#pending = null;
|
|
8508
|
+
this.#queued = [];
|
|
8509
|
+
this.#problem = "";
|
|
8510
|
+
this.#writing = -1;
|
|
8511
|
+
this.#thinkingAt = -1;
|
|
8512
|
+
this.#primed = false;
|
|
8513
|
+
this.#stopping = false;
|
|
8514
|
+
}
|
|
8515
|
+
/** Show `kept` and carry on from it: its turns for the pane, its messages for the model. A turn that was running when
|
|
8516
|
+
* the server went down is shown as stopped: it never finished. */
|
|
8517
|
+
#restore(kept) {
|
|
8518
|
+
this.#blank();
|
|
8519
|
+
this.#open(kept);
|
|
8520
|
+
this.#turns = kept.turns.map((t) => t.state === "running" ? {
|
|
8521
|
+
...t,
|
|
8522
|
+
state: "stopped"
|
|
8523
|
+
} : t);
|
|
8524
|
+
this.#title = kept.title;
|
|
8525
|
+
this.#cost = kept.cost;
|
|
8526
|
+
this.#used = kept.used;
|
|
8527
|
+
this.#given = new Set(kept.given);
|
|
8528
|
+
this.#withdrawn = new Set(kept.withdrawn ?? []);
|
|
8529
|
+
this.#tell();
|
|
8530
|
+
}
|
|
8531
|
+
/** Take up the session last kept, if there is one. An unreadable one is reported in the pane and left alone, and the
|
|
8532
|
+
* agent starts fresh. */
|
|
8533
|
+
async resume() {
|
|
8534
|
+
if (!this.#store || this.#agent) return;
|
|
8535
|
+
let kept;
|
|
8536
|
+
try {
|
|
8537
|
+
kept = await this.#store.latest();
|
|
8538
|
+
} catch (e) {
|
|
8539
|
+
this.#problem = e.message;
|
|
8540
|
+
this.#tell();
|
|
8541
|
+
return;
|
|
8542
|
+
}
|
|
8543
|
+
try {
|
|
8544
|
+
await this.#index();
|
|
8545
|
+
} catch {}
|
|
8546
|
+
if (!kept) {
|
|
8547
|
+
this.#tell();
|
|
8548
|
+
return;
|
|
8549
|
+
}
|
|
8550
|
+
this.#restore(kept);
|
|
8551
|
+
}
|
|
8552
|
+
#record() {
|
|
8553
|
+
return {
|
|
8554
|
+
version: 1,
|
|
8555
|
+
session: this.#session,
|
|
8556
|
+
title: this.#title,
|
|
8557
|
+
at: this.#at,
|
|
8558
|
+
last: this.#turns.at(-1)?.at ?? this.#at,
|
|
8559
|
+
cost: this.#cost,
|
|
8560
|
+
used: this.#used,
|
|
8561
|
+
given: [...this.#given],
|
|
8562
|
+
withdrawn: [...this.#withdrawn],
|
|
8563
|
+
turns: this.#turns,
|
|
8564
|
+
messages: JSON.parse(JSON.stringify(this.#agent.state.messages)).filter((m) => m.role !== "system")
|
|
8565
|
+
};
|
|
8566
|
+
}
|
|
8567
|
+
#save() {
|
|
8568
|
+
if (!this.#store || !this.#agent || !this.#turns.length) return;
|
|
8569
|
+
const record = this.#record();
|
|
8570
|
+
this.#kept.set(record.session, this.#summary(record));
|
|
8571
|
+
const store = this.#store;
|
|
8572
|
+
this.#saving = this.#saving.then(() => store.save(record)).catch((e) => {
|
|
8573
|
+
this.#problem = `Could not keep this session: ${e.message}`;
|
|
8574
|
+
this.#tell();
|
|
8575
|
+
});
|
|
8576
|
+
}
|
|
8577
|
+
#open(kept) {
|
|
8578
|
+
this.#session = kept?.session ?? randomUUID();
|
|
8579
|
+
this.#at = kept?.at ?? new Date(this.#now()).toISOString();
|
|
8580
|
+
this.#agent = new Agent({
|
|
8581
|
+
initialState: {
|
|
8582
|
+
systemPrompt: INSTRUCTIONS,
|
|
8583
|
+
model: this.#model,
|
|
8584
|
+
...kept ? { messages: kept.messages } : {}
|
|
8585
|
+
},
|
|
8586
|
+
transformContext: async (messages) => this.#redact(messages),
|
|
8587
|
+
streamFn: ((...args) => this.#streamFn(...args)),
|
|
8588
|
+
...this.#getApiKey ? { getApiKey: this.#getApiKey } : {},
|
|
8589
|
+
sessionId: this.#session,
|
|
8590
|
+
beforeToolCall: async ({ toolCall, args }) => {
|
|
8591
|
+
let refused;
|
|
8592
|
+
try {
|
|
8593
|
+
refused = this.#guard({
|
|
8594
|
+
tool: toolCall.name,
|
|
8595
|
+
args
|
|
8596
|
+
});
|
|
8597
|
+
} catch {
|
|
8598
|
+
refused = "that could not be checked";
|
|
8599
|
+
}
|
|
8600
|
+
if (refused) return {
|
|
8601
|
+
block: true,
|
|
8602
|
+
reason: refused
|
|
8603
|
+
};
|
|
8604
|
+
if (!this.#ask(toolCall.name) || this.#allowed.has(toolCall.name)) return void 0;
|
|
8605
|
+
const id = randomUUID();
|
|
8606
|
+
const decision = await new Promise((wait) => {
|
|
8607
|
+
this.#asking.set(id, {
|
|
8608
|
+
id,
|
|
8609
|
+
action: toolCall.name,
|
|
8610
|
+
resources: [titleOf(args)],
|
|
8611
|
+
tool: toolCall.name,
|
|
8612
|
+
wait
|
|
8613
|
+
});
|
|
8614
|
+
this.#tell();
|
|
8615
|
+
});
|
|
8616
|
+
this.#asking.delete(id);
|
|
8617
|
+
this.#tell();
|
|
8618
|
+
if (decision === "reject") return {
|
|
8619
|
+
block: true,
|
|
8620
|
+
reason: "The person declined this."
|
|
8621
|
+
};
|
|
8622
|
+
if (decision === "always") this.#allowed.add(toolCall.name);
|
|
8623
|
+
}
|
|
8624
|
+
});
|
|
8625
|
+
this.#agent.subscribe((e) => this.#on(e));
|
|
8626
|
+
}
|
|
8627
|
+
#on(e) {
|
|
8628
|
+
if (e.type === "message_start" && e.message.role === "user") {
|
|
8629
|
+
if (!this.#primed) {
|
|
8630
|
+
this.#primed = true;
|
|
8631
|
+
return;
|
|
8632
|
+
}
|
|
8633
|
+
const said = userText(e.message.content);
|
|
8634
|
+
const at = this.#queued.findIndex((q) => q.body === said);
|
|
8635
|
+
if (at < 0) return;
|
|
8636
|
+
const [q] = this.#queued.splice(at, 1);
|
|
8637
|
+
this.#edit((items) => {
|
|
8638
|
+
const was = items.find((i) => i.kind === "message" && i.id === q.id);
|
|
8639
|
+
return was ? [...items.filter((i) => i !== was), {
|
|
8640
|
+
...was,
|
|
8641
|
+
state: "landed"
|
|
8642
|
+
}] : items;
|
|
8643
|
+
});
|
|
8644
|
+
} else if (e.type === "message_start" && e.message.role === "assistant") {
|
|
8645
|
+
this.#edit((items) => [...items, {
|
|
8646
|
+
kind: "text",
|
|
8647
|
+
text: ""
|
|
8648
|
+
}]);
|
|
8649
|
+
this.#writing = (this.#turns.at(-1)?.items.length ?? 1) - 1;
|
|
8650
|
+
} else if (e.type === "message_update" && e.assistantMessageEvent.type === "thinking_delta") {
|
|
8651
|
+
const delta = e.assistantMessageEvent.delta;
|
|
8652
|
+
if (this.#thinkingAt < 0) {
|
|
8653
|
+
const at = Math.max(this.#writing, 0);
|
|
8654
|
+
this.#edit((items) => [
|
|
8655
|
+
...items.slice(0, at),
|
|
8656
|
+
{
|
|
8657
|
+
kind: "thinking",
|
|
8658
|
+
text: ""
|
|
8659
|
+
},
|
|
8660
|
+
...items.slice(at)
|
|
8661
|
+
]);
|
|
8662
|
+
this.#thinkingAt = at;
|
|
8663
|
+
this.#writing = at + 1;
|
|
8664
|
+
}
|
|
8665
|
+
const t = this.#thinkingAt;
|
|
8666
|
+
this.#edit((items) => items.map((i, k) => k === t && i.kind === "thinking" ? {
|
|
8667
|
+
...i,
|
|
8668
|
+
text: i.text + delta
|
|
8669
|
+
} : i));
|
|
8670
|
+
} else if (e.type === "message_update" && e.assistantMessageEvent.type === "text_delta") {
|
|
8671
|
+
const at = this.#writing, delta = e.assistantMessageEvent.delta;
|
|
8672
|
+
this.#edit((items) => items.map((i, k) => k === at && i.kind === "text" ? {
|
|
8673
|
+
...i,
|
|
8674
|
+
text: i.text + delta
|
|
8675
|
+
} : i));
|
|
8676
|
+
} else if (e.type === "message_end" && e.message.role === "assistant") {
|
|
8677
|
+
const m = e.message;
|
|
8678
|
+
const at = this.#writing, think = this.#thinkingAt, whole = textOf(m.content);
|
|
8679
|
+
const thought = (Array.isArray(m.content) ? m.content : []).map((b) => b?.type === "thinking" ? b.thinking ?? "" : "").join("");
|
|
8680
|
+
this.#edit((items) => items.flatMap((i, k) => k === think ? thought.trim() ? [{
|
|
8681
|
+
kind: "thinking",
|
|
8682
|
+
text: thought
|
|
8683
|
+
}] : [] : k !== at ? [i] : whole ? [{
|
|
8684
|
+
kind: "text",
|
|
8685
|
+
text: whole
|
|
8686
|
+
}] : []));
|
|
8687
|
+
this.#writing = -1;
|
|
8688
|
+
this.#thinkingAt = -1;
|
|
8689
|
+
this.#cost += m.usage?.cost?.total ?? 0;
|
|
8690
|
+
this.#used = m.usage?.totalTokens ?? this.#used;
|
|
8691
|
+
} else if (e.type === "tool_execution_start") this.#edit((items) => [...items, {
|
|
8692
|
+
kind: "tool",
|
|
8693
|
+
id: e.toolCallId,
|
|
8694
|
+
name: e.toolName,
|
|
8695
|
+
title: titleOf(e.args),
|
|
8696
|
+
status: "running",
|
|
8697
|
+
output: ""
|
|
8698
|
+
}]);
|
|
8699
|
+
else if (e.type === "tool_execution_end") {
|
|
8700
|
+
const out = textOf(e.result?.content).slice(0, OUTPUT);
|
|
8701
|
+
this.#edit((items) => items.map((i) => i.kind === "tool" && i.id === e.toolCallId ? {
|
|
8702
|
+
...i,
|
|
8703
|
+
status: e.isError ? "failed" : "done",
|
|
8704
|
+
output: out
|
|
8705
|
+
} : i));
|
|
8706
|
+
} else return;
|
|
8707
|
+
this.#tell();
|
|
8708
|
+
}
|
|
8709
|
+
#fail(message) {
|
|
8710
|
+
this.#edit((items) => [...items, {
|
|
8711
|
+
kind: "text",
|
|
8712
|
+
text: `(${message})`
|
|
8713
|
+
}]);
|
|
8714
|
+
this.#set("failed");
|
|
8715
|
+
}
|
|
8716
|
+
#end() {
|
|
8717
|
+
this.#dropQueued();
|
|
8718
|
+
const last = [...this.#agent?.state.messages ?? []].reverse().find((m) => m.role === "assistant");
|
|
8719
|
+
if (this.#turns.at(-1)?.state === "running") {
|
|
8720
|
+
if (this.#stopping || last?.stopReason === "aborted") this.#set("stopped");
|
|
8721
|
+
else if (last?.stopReason === "error") this.#fail(last.errorMessage ?? "the model failed");
|
|
8722
|
+
else this.#set("done");
|
|
8723
|
+
}
|
|
8724
|
+
this.#busy = false;
|
|
8725
|
+
this.#stopping = false;
|
|
8726
|
+
this.#api.agentStatus(this.#id, this.#session, false).catch(() => void 0);
|
|
8727
|
+
this.#keep();
|
|
8728
|
+
this.#save();
|
|
8729
|
+
this.#tell();
|
|
8730
|
+
}
|
|
8731
|
+
/** What the model would be sent now: the last answer's own count, and an estimate for what came after it. */
|
|
8732
|
+
#contextTokens(messages) {
|
|
8733
|
+
let last = -1;
|
|
8734
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
8735
|
+
const m = messages[i];
|
|
8736
|
+
if (m.role === "assistant" && m.usage?.totalTokens && m.stopReason !== "aborted" && m.stopReason !== "error") {
|
|
8737
|
+
last = i;
|
|
8738
|
+
break;
|
|
8739
|
+
}
|
|
8740
|
+
}
|
|
8741
|
+
return (last >= 0 ? messages[last].usage.totalTokens : 0) + messages.slice(last + 1).reduce((n, m) => n + estimateTokens(m), 0);
|
|
8742
|
+
}
|
|
8743
|
+
/** Before a prompt: if what the model would be sent nears its window, summarise the earlier part of the conversation
|
|
8744
|
+
* 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).
|
|
8745
|
+
* The turn shows it. If the summary cannot be made the conversation goes on as it was and the turn says so. */
|
|
8746
|
+
async #compactIfNeeded(agent) {
|
|
8747
|
+
const window = this.#model.contextWindow, settings = this.#compaction;
|
|
8748
|
+
if (!settings.enabled || !window) return;
|
|
8749
|
+
const messages = [...agent.state.messages];
|
|
8750
|
+
const before = this.#contextTokens(messages);
|
|
8751
|
+
if (!shouldCompact(before, window, settings)) return;
|
|
8752
|
+
const roleOf = (m) => m?.role;
|
|
8753
|
+
let head = 0;
|
|
8754
|
+
while (roleOf(messages[head]) === "system") head++;
|
|
8755
|
+
const earlier = messages[head]?.content;
|
|
8756
|
+
const previous = userText(earlier).startsWith(SUMMARY) ? userText(earlier).slice(65) : void 0;
|
|
8757
|
+
const from = head + (previous === void 0 ? 0 : 1);
|
|
8758
|
+
let acc = 0, cut = from;
|
|
8759
|
+
for (let i = messages.length - 1; i >= from; i--) {
|
|
8760
|
+
acc += estimateTokens(messages[i]);
|
|
8761
|
+
if (acc >= settings.keepRecentTokens) {
|
|
8762
|
+
cut = i;
|
|
8763
|
+
break;
|
|
8764
|
+
}
|
|
8765
|
+
}
|
|
8766
|
+
while (cut > from && roleOf(messages[cut]) !== "user") cut--;
|
|
8767
|
+
if (cut <= from) return;
|
|
8768
|
+
const id = this.#turns.at(-1)?.items.length ?? 0;
|
|
8769
|
+
this.#edit((items) => [...items, {
|
|
8770
|
+
kind: "compaction",
|
|
8771
|
+
state: "running",
|
|
8772
|
+
before
|
|
8773
|
+
}]);
|
|
8774
|
+
this.#tell();
|
|
8775
|
+
this.#summarising = new AbortController();
|
|
8776
|
+
try {
|
|
8777
|
+
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);
|
|
8778
|
+
const stand = {
|
|
8779
|
+
role: "user",
|
|
8780
|
+
content: [{
|
|
8781
|
+
type: "text",
|
|
8782
|
+
text: SUMMARY + summary
|
|
8783
|
+
}],
|
|
8784
|
+
timestamp: this.#now()
|
|
8785
|
+
};
|
|
8786
|
+
const recent = messages.slice(cut).map((m) => m.role === "assistant" ? {
|
|
8787
|
+
...m,
|
|
8788
|
+
usage: {
|
|
8789
|
+
...m.usage,
|
|
8790
|
+
totalTokens: 0
|
|
8791
|
+
}
|
|
8792
|
+
} : m);
|
|
8793
|
+
const kept = [
|
|
8794
|
+
...messages.slice(0, head),
|
|
8795
|
+
stand,
|
|
8796
|
+
...recent
|
|
8797
|
+
];
|
|
8798
|
+
agent.state.messages = kept;
|
|
8799
|
+
const after = this.#contextTokens(kept);
|
|
8800
|
+
this.#used = after;
|
|
8801
|
+
this.#edit((items) => items.map((i, k) => k === id && i.kind === "compaction" ? {
|
|
8802
|
+
...i,
|
|
8803
|
+
state: "done",
|
|
8804
|
+
after,
|
|
8805
|
+
summary
|
|
8806
|
+
} : i));
|
|
8807
|
+
} catch (e) {
|
|
8808
|
+
const error = this.#stopping ? "stopped" : e.message;
|
|
8809
|
+
this.#edit((items) => items.map((i, k) => k === id && i.kind === "compaction" ? {
|
|
8810
|
+
...i,
|
|
8811
|
+
state: "failed",
|
|
8812
|
+
error
|
|
8813
|
+
} : i));
|
|
8814
|
+
} finally {
|
|
8815
|
+
this.#summarising = null;
|
|
8816
|
+
this.#tell();
|
|
8817
|
+
}
|
|
8818
|
+
}
|
|
8819
|
+
/** Said but never taken up (the run ended or was stopped first). */
|
|
8820
|
+
#dropQueued() {
|
|
8821
|
+
if (!this.#queued.length) return;
|
|
8822
|
+
const ids = new Set(this.#queued.map((q) => q.id));
|
|
8823
|
+
this.#queued = [];
|
|
8824
|
+
this.#agent?.clearAllQueues();
|
|
8825
|
+
this.#edit((items) => items.map((i) => i.kind === "message" && ids.has(i.id) ? {
|
|
8826
|
+
...i,
|
|
8827
|
+
state: "dropped"
|
|
8828
|
+
} : i));
|
|
8829
|
+
}
|
|
8830
|
+
#set(state) {
|
|
8831
|
+
const turns = [...this.#turns], t = turns.at(-1);
|
|
8832
|
+
if (t) turns[turns.length - 1] = {
|
|
8833
|
+
...t,
|
|
8834
|
+
state
|
|
8835
|
+
};
|
|
8836
|
+
this.#turns = turns;
|
|
8837
|
+
}
|
|
8838
|
+
#edit(fn) {
|
|
8839
|
+
const turns = [...this.#turns], t = turns.at(-1);
|
|
8840
|
+
if (!t) return;
|
|
8841
|
+
turns[turns.length - 1] = {
|
|
8842
|
+
...t,
|
|
8843
|
+
items: fn([...t.items])
|
|
8844
|
+
};
|
|
8845
|
+
this.#turns = turns;
|
|
8846
|
+
}
|
|
8847
|
+
#keep() {
|
|
8848
|
+
if (!this.#api.keepAgentRecord || !this.#turns.length) return;
|
|
8849
|
+
const last = this.#turns.at(-1)?.at ?? this.#at;
|
|
8850
|
+
this.#api.keepAgentRecord({
|
|
8851
|
+
agent: this.#id,
|
|
8852
|
+
name: this.#name,
|
|
8853
|
+
session: this.#session,
|
|
8854
|
+
title: this.#title,
|
|
8855
|
+
at: this.#at,
|
|
8856
|
+
last,
|
|
8857
|
+
turns: this.#turns
|
|
8858
|
+
}).catch(() => void 0);
|
|
8859
|
+
}
|
|
8860
|
+
/** The session shown, with the others kept, newest first. */
|
|
8861
|
+
#sessions(entry) {
|
|
8862
|
+
const others = [...this.#kept.values()].filter((k) => k.session !== this.#session).map((k) => ({
|
|
8863
|
+
agent: this.#id,
|
|
8864
|
+
name: this.#name,
|
|
8865
|
+
...k
|
|
8866
|
+
}));
|
|
8867
|
+
return [...entry ? [entry] : [], ...others].sort((a, b) => a.last < b.last ? 1 : a.last > b.last ? -1 : 0);
|
|
8868
|
+
}
|
|
8869
|
+
#tell() {
|
|
8870
|
+
const last = this.#turns.at(-1)?.at ?? this.#at;
|
|
8871
|
+
const entry = this.#session ? {
|
|
8872
|
+
agent: this.#id,
|
|
8873
|
+
name: this.#name,
|
|
8874
|
+
session: this.#session,
|
|
8875
|
+
title: this.#title,
|
|
8876
|
+
asked: this.#turns[0]?.asked ?? "",
|
|
8877
|
+
last,
|
|
8878
|
+
turns: this.#turns.length,
|
|
8879
|
+
notes: 0,
|
|
8880
|
+
live: true,
|
|
8881
|
+
busy: this.#busy,
|
|
8882
|
+
kept: Boolean(this.#store),
|
|
8883
|
+
open: true
|
|
8884
|
+
} : null;
|
|
8885
|
+
this.#view = {
|
|
8886
|
+
connected: true,
|
|
8887
|
+
agent: this.#name,
|
|
8888
|
+
agentId: this.#id,
|
|
8889
|
+
session: this.#session || null,
|
|
8890
|
+
title: this.#title,
|
|
8891
|
+
busy: this.#busy,
|
|
8892
|
+
turns: this.#turns,
|
|
8893
|
+
asking: [...this.#asking.values()].map(({ wait: _, ...p }) => p),
|
|
8894
|
+
cost: this.#cost,
|
|
8895
|
+
tokens: {
|
|
8896
|
+
input: this.#used,
|
|
8897
|
+
output: 0
|
|
8898
|
+
},
|
|
8899
|
+
model: this.#model.id ?? "",
|
|
8900
|
+
...this.#problem ? { problem: this.#problem } : {},
|
|
8901
|
+
steer: true,
|
|
8902
|
+
...this.#model.contextWindow ? { size: this.#model.contextWindow } : {},
|
|
8903
|
+
fork: Boolean(this.#store),
|
|
8904
|
+
sessions: this.#sessions(entry)
|
|
8905
|
+
};
|
|
8906
|
+
for (const fn of this.#listeners) fn(this.#view);
|
|
8907
|
+
}
|
|
8908
|
+
};
|
|
8909
|
+
//#endregion
|
|
8910
|
+
//#region packages/server/src/pi/files.ts
|
|
8911
|
+
const READS = [
|
|
8912
|
+
"read",
|
|
8913
|
+
"grep",
|
|
8914
|
+
"find",
|
|
8915
|
+
"ls"
|
|
8916
|
+
];
|
|
8917
|
+
const WRITES = ["edit", "write"];
|
|
8918
|
+
function fileTools(cwd, { files, shell }) {
|
|
8919
|
+
const tools = [];
|
|
8920
|
+
if (files !== "off") tools.push(createReadTool(cwd), createGrepTool(cwd), createFindTool(cwd), createLsTool(cwd));
|
|
8921
|
+
if (files === "write") tools.push(createEditTool(cwd), createWriteTool(cwd));
|
|
8922
|
+
if (shell) tools.push(createBashTool(cwd));
|
|
8923
|
+
return tools;
|
|
8924
|
+
}
|
|
8925
|
+
/** A reason to refuse the call, or undefined to let it through. Anything that cannot be checked is refused. */
|
|
8926
|
+
function pathGuard(cwd) {
|
|
8927
|
+
const root = resolve(cwd);
|
|
8928
|
+
return ({ tool, args }) => {
|
|
8929
|
+
if (![...READS, ...WRITES].includes(tool)) return void 0;
|
|
8930
|
+
const given = args?.path;
|
|
8931
|
+
if (given === void 0 && (tool === "ls" || tool === "grep" || tool === "find")) return void 0;
|
|
8932
|
+
if (typeof given !== "string" || !given) return `${tool} needs a path inside the project`;
|
|
8933
|
+
const target = isAbsolute(given) ? resolve(given) : resolve(root, given);
|
|
8934
|
+
const inside = relative(root, target);
|
|
8935
|
+
if (inside.startsWith("..") || isAbsolute(inside)) return `${given} is outside the project folder`;
|
|
8936
|
+
if (WRITES.includes(tool) && inside.split(sep).includes(".git")) return `${tool} does not reach .git`;
|
|
8937
|
+
};
|
|
8938
|
+
}
|
|
8939
|
+
//#endregion
|
|
8940
|
+
//#region packages/server/src/pi/credentials.ts
|
|
8941
|
+
/** The providers the settings offer: those a person signs up to with a key of their own. Pi's subscription sign-ins
|
|
8942
|
+
* (for a ChatGPT or Copilot plan) are not offered: their terms are the provider's, not isorite's to rely on. */
|
|
8943
|
+
const PROVIDERS = [
|
|
8944
|
+
{
|
|
8945
|
+
id: "openrouter",
|
|
8946
|
+
name: "OpenRouter",
|
|
8947
|
+
env: "OPENROUTER_API_KEY"
|
|
8948
|
+
},
|
|
8949
|
+
{
|
|
8950
|
+
id: "anthropic",
|
|
8951
|
+
name: "Anthropic (API key)",
|
|
8952
|
+
env: "ANTHROPIC_API_KEY"
|
|
8953
|
+
},
|
|
8954
|
+
{
|
|
8955
|
+
id: "openai",
|
|
8956
|
+
name: "OpenAI (API key)",
|
|
8957
|
+
env: "OPENAI_API_KEY"
|
|
8958
|
+
},
|
|
8959
|
+
{
|
|
8960
|
+
id: "google",
|
|
8961
|
+
name: "Google Gemini",
|
|
8962
|
+
env: "GEMINI_API_KEY"
|
|
8963
|
+
},
|
|
8964
|
+
{
|
|
8965
|
+
id: "mistral",
|
|
8966
|
+
name: "Mistral",
|
|
8967
|
+
env: "MISTRAL_API_KEY"
|
|
8968
|
+
},
|
|
8969
|
+
{
|
|
8970
|
+
id: "groq",
|
|
8971
|
+
name: "Groq",
|
|
8972
|
+
env: "GROQ_API_KEY"
|
|
8973
|
+
},
|
|
8974
|
+
{
|
|
8975
|
+
id: "xai",
|
|
8976
|
+
name: "xAI",
|
|
8977
|
+
env: "XAI_API_KEY"
|
|
8978
|
+
},
|
|
8979
|
+
{
|
|
8980
|
+
id: "deepseek",
|
|
8981
|
+
name: "DeepSeek",
|
|
8982
|
+
env: "DEEPSEEK_API_KEY"
|
|
8983
|
+
},
|
|
8984
|
+
{
|
|
8985
|
+
id: "together",
|
|
8986
|
+
name: "Together",
|
|
8987
|
+
env: "TOGETHER_API_KEY"
|
|
8988
|
+
},
|
|
8989
|
+
{
|
|
8990
|
+
id: "fireworks",
|
|
8991
|
+
name: "Fireworks",
|
|
8992
|
+
env: "FIREWORKS_API_KEY"
|
|
8993
|
+
},
|
|
8994
|
+
{
|
|
8995
|
+
id: "cerebras",
|
|
8996
|
+
name: "Cerebras",
|
|
8997
|
+
env: "CEREBRAS_API_KEY"
|
|
8998
|
+
}
|
|
8999
|
+
];
|
|
9000
|
+
/** The default: the project's files to read, nothing to change them with, no shell. */
|
|
9001
|
+
const DEFAULT_REACH = {
|
|
9002
|
+
files: "read",
|
|
9003
|
+
shell: false
|
|
9004
|
+
};
|
|
9005
|
+
const ID = /^[a-z0-9-]{1,40}$/;
|
|
9006
|
+
/** Where isorite keeps what is its own, not the base's. */
|
|
9007
|
+
function dataDir(env = process.env) {
|
|
9008
|
+
return join(env.XDG_DATA_HOME || join(homedir(), ".local", "share"), "isorite");
|
|
9009
|
+
}
|
|
9010
|
+
var Credentials = class {
|
|
9011
|
+
#file;
|
|
9012
|
+
#dir;
|
|
9013
|
+
#env;
|
|
9014
|
+
#kept = null;
|
|
9015
|
+
#writing = Promise.resolve();
|
|
9016
|
+
constructor(env = process.env) {
|
|
9017
|
+
this.#env = env;
|
|
9018
|
+
this.#dir = dataDir(env);
|
|
9019
|
+
this.#file = join(this.#dir, "credentials.json");
|
|
9020
|
+
}
|
|
9021
|
+
async #load() {
|
|
9022
|
+
if (this.#kept) return this.#kept;
|
|
9023
|
+
let text;
|
|
9024
|
+
try {
|
|
9025
|
+
text = await readFile(this.#file, "utf8");
|
|
9026
|
+
} catch (e) {
|
|
9027
|
+
if (e.code === "ENOENT") return this.#kept = {
|
|
9028
|
+
version: 1,
|
|
9029
|
+
keys: {}
|
|
9030
|
+
};
|
|
9031
|
+
throw new Error(`The keys file ${this.#file} could not be read (${e.code ?? "unreadable"}). It was left where it is.`);
|
|
9032
|
+
}
|
|
9033
|
+
let parsed;
|
|
9034
|
+
try {
|
|
9035
|
+
parsed = JSON.parse(text);
|
|
9036
|
+
} catch {
|
|
9037
|
+
throw new Error(`The keys file ${this.#file} is not valid JSON. It was left where it is.`);
|
|
9038
|
+
}
|
|
9039
|
+
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.`);
|
|
9040
|
+
return this.#kept = {
|
|
9041
|
+
version: 1,
|
|
9042
|
+
keys: parsed.keys,
|
|
9043
|
+
...parsed.choice ? { choice: parsed.choice } : {},
|
|
9044
|
+
...parsed.reach ? { reach: parsed.reach } : {}
|
|
9045
|
+
};
|
|
9046
|
+
}
|
|
9047
|
+
async #save(next) {
|
|
9048
|
+
const run = async () => {
|
|
9049
|
+
await mkdir(this.#dir, {
|
|
9050
|
+
recursive: true,
|
|
9051
|
+
mode: 448
|
|
9052
|
+
});
|
|
9053
|
+
const tmp = `${this.#file}.${process.pid}.tmp`;
|
|
9054
|
+
await writeFile(tmp, JSON.stringify(next), { mode: 384 });
|
|
9055
|
+
await rename(tmp, this.#file);
|
|
9056
|
+
this.#kept = next;
|
|
9057
|
+
};
|
|
9058
|
+
const turn = this.#writing.then(run, run);
|
|
9059
|
+
this.#writing = turn.catch(() => void 0);
|
|
9060
|
+
await turn;
|
|
9061
|
+
}
|
|
9062
|
+
/** The key to use for `provider`: the one saved here, else the provider's own variable in the environment. */
|
|
9063
|
+
async key(provider) {
|
|
9064
|
+
const kept = (await this.#load()).keys[provider];
|
|
9065
|
+
if (kept) return kept;
|
|
9066
|
+
const env = PROVIDERS.find((p) => p.id === provider)?.env;
|
|
9067
|
+
return env && this.#env[env] || void 0;
|
|
9068
|
+
}
|
|
9069
|
+
/** Where each provider's key comes from, never the key. */
|
|
9070
|
+
async sources() {
|
|
9071
|
+
const kept = (await this.#load()).keys;
|
|
9072
|
+
return Object.fromEntries(PROVIDERS.map((p) => [p.id, kept[p.id] ? "saved" : this.#env[p.env] ? "environment" : null]));
|
|
9073
|
+
}
|
|
9074
|
+
async set(provider, key) {
|
|
9075
|
+
if (!PROVIDERS.some((p) => p.id === provider)) throw new Error(`"${provider}" is not a provider isorite offers`);
|
|
9076
|
+
const k = key.trim();
|
|
9077
|
+
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)");
|
|
9078
|
+
const now = await this.#load();
|
|
9079
|
+
await this.#save({
|
|
9080
|
+
...now,
|
|
9081
|
+
keys: {
|
|
9082
|
+
...now.keys,
|
|
9083
|
+
[provider]: k
|
|
9084
|
+
}
|
|
9085
|
+
});
|
|
9086
|
+
}
|
|
9087
|
+
async remove(provider) {
|
|
9088
|
+
const now = await this.#load();
|
|
9089
|
+
if (!(provider in now.keys)) return;
|
|
9090
|
+
const { [provider]: _gone, ...keys } = now.keys;
|
|
9091
|
+
await this.#save({
|
|
9092
|
+
...now,
|
|
9093
|
+
keys
|
|
9094
|
+
});
|
|
9095
|
+
}
|
|
9096
|
+
/** What was chosen in the settings, else the default. Anything else in the file is not believed. */
|
|
9097
|
+
async reach() {
|
|
9098
|
+
const r = (await this.#load()).reach;
|
|
9099
|
+
return r && [
|
|
9100
|
+
"off",
|
|
9101
|
+
"read",
|
|
9102
|
+
"write"
|
|
9103
|
+
].includes(r.files) && typeof r.shell === "boolean" ? {
|
|
9104
|
+
files: r.files,
|
|
9105
|
+
shell: r.shell
|
|
9106
|
+
} : { ...DEFAULT_REACH };
|
|
9107
|
+
}
|
|
9108
|
+
async setReach(reach) {
|
|
9109
|
+
if (![
|
|
9110
|
+
"off",
|
|
9111
|
+
"read",
|
|
9112
|
+
"write"
|
|
9113
|
+
].includes(reach.files) || typeof reach.shell !== "boolean") throw new Error("files is off, read or write; shell is on or off");
|
|
9114
|
+
const now = await this.#load();
|
|
9115
|
+
await this.#save({
|
|
9116
|
+
...now,
|
|
9117
|
+
reach: {
|
|
9118
|
+
files: reach.files,
|
|
9119
|
+
shell: reach.shell
|
|
9120
|
+
}
|
|
9121
|
+
});
|
|
9122
|
+
}
|
|
9123
|
+
async choice() {
|
|
9124
|
+
return (await this.#load()).choice ?? null;
|
|
9125
|
+
}
|
|
9126
|
+
async choose(provider, model) {
|
|
9127
|
+
if (!PROVIDERS.some((p) => p.id === provider) || !ID.test(provider)) throw new Error(`"${provider}" is not a provider isorite offers`);
|
|
9128
|
+
if (!model.trim() || model.length > 200) throw new Error("a model is needed");
|
|
9129
|
+
const now = await this.#load();
|
|
9130
|
+
await this.#save({
|
|
9131
|
+
...now,
|
|
9132
|
+
choice: {
|
|
9133
|
+
provider,
|
|
9134
|
+
model: model.trim()
|
|
9135
|
+
}
|
|
9136
|
+
});
|
|
9137
|
+
}
|
|
9138
|
+
};
|
|
9139
|
+
//#endregion
|
|
9140
|
+
//#region packages/server/src/pi/index.ts
|
|
9141
|
+
const camel = (s) => s.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
|
|
9142
|
+
/** The model `provider/model` and the way to reach it. Throws (naming what is wrong) where pi has no such provider or model. */
|
|
9143
|
+
async function modelFor(provider, model) {
|
|
9144
|
+
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}"`);
|
|
9145
|
+
let make;
|
|
9146
|
+
try {
|
|
9147
|
+
make = (await import(`@earendil-works/pi-ai/providers/${provider}`))[`${camel(provider)}Provider`];
|
|
9148
|
+
} catch {}
|
|
9149
|
+
if (typeof make !== "function") throw new Error(`pi has no provider "${provider}"`);
|
|
9150
|
+
const models = createModels();
|
|
9151
|
+
models.setProvider(make());
|
|
9152
|
+
const chosen = models.getModel(provider, model);
|
|
9153
|
+
if (!chosen) throw new Error(`pi's ${provider} has no model "${model}"`);
|
|
9154
|
+
return {
|
|
9155
|
+
chosen,
|
|
9156
|
+
streamFn: models.streamSimple.bind(models)
|
|
9157
|
+
};
|
|
9158
|
+
}
|
|
9159
|
+
/** The model named by ISORITE_PI, else the one chosen in the settings; null where there is none. */
|
|
9160
|
+
async function wanted(env, creds) {
|
|
9161
|
+
const named = env.ISORITE_PI?.trim();
|
|
9162
|
+
if (named) {
|
|
9163
|
+
const [provider, ...rest] = named.split("/");
|
|
9164
|
+
if (!provider || !rest.length) throw new Error(`ISORITE_PI is <provider>/<model>, as anthropic/claude-sonnet-4-6; it was "${named}"`);
|
|
9165
|
+
return {
|
|
9166
|
+
choice: {
|
|
9167
|
+
provider,
|
|
9168
|
+
model: rest.join("/")
|
|
9169
|
+
},
|
|
9170
|
+
source: "environment"
|
|
9171
|
+
};
|
|
9172
|
+
}
|
|
9173
|
+
const saved = await creds.choice();
|
|
9174
|
+
return saved ? {
|
|
9175
|
+
choice: saved,
|
|
9176
|
+
source: "settings"
|
|
9177
|
+
} : null;
|
|
9178
|
+
}
|
|
9179
|
+
/** What the settings read and change: which model the native agent uses and where its key comes from, for the
|
|
9180
|
+
* hosts running (one for each base). It never hands out a key. */
|
|
9181
|
+
var PiControl = class {
|
|
9182
|
+
creds;
|
|
9183
|
+
#env;
|
|
9184
|
+
#hosts = /* @__PURE__ */ new Set();
|
|
9185
|
+
constructor(env = process.env, creds = new Credentials(env)) {
|
|
9186
|
+
this.#env = env;
|
|
9187
|
+
this.creds = creds;
|
|
9188
|
+
}
|
|
9189
|
+
/** What the agent may reach now, read synchronously where a prompt starts. Loaded by `load`. */
|
|
9190
|
+
reach = { ...DEFAULT_REACH };
|
|
9191
|
+
/** True where the environment (ISORITE_PI_FILES, ISORITE_PI_SHELL) sets it, and so wins. */
|
|
9192
|
+
reachFromEnvironment = false;
|
|
9193
|
+
async load() {
|
|
9194
|
+
const files = this.#env.ISORITE_PI_FILES?.trim(), shell = this.#env.ISORITE_PI_SHELL?.trim();
|
|
9195
|
+
if (files !== void 0 && files !== "" && ![
|
|
9196
|
+
"off",
|
|
9197
|
+
"read",
|
|
9198
|
+
"write"
|
|
9199
|
+
].includes(files)) throw new Error(`ISORITE_PI_FILES is off, read or write; it was "${files}"`);
|
|
9200
|
+
this.reachFromEnvironment = Boolean(files) || shell !== void 0 && shell !== "";
|
|
9201
|
+
const saved = await this.creds.reach();
|
|
9202
|
+
this.reach = {
|
|
9203
|
+
files: files || saved.files,
|
|
9204
|
+
shell: shell ? [
|
|
9205
|
+
"1",
|
|
9206
|
+
"true",
|
|
9207
|
+
"on"
|
|
9208
|
+
].includes(shell) : saved.shell
|
|
9209
|
+
};
|
|
9210
|
+
}
|
|
9211
|
+
async setReach(reach) {
|
|
9212
|
+
await this.creds.setReach(reach);
|
|
9213
|
+
if (!this.reachFromEnvironment) this.reach = { ...reach };
|
|
9214
|
+
}
|
|
9215
|
+
attach(host) {
|
|
9216
|
+
this.#hosts.add(host);
|
|
9217
|
+
}
|
|
9218
|
+
async state() {
|
|
9219
|
+
await this.load().catch(() => void 0);
|
|
9220
|
+
const want = await wanted(this.#env, this.creds).catch(() => null);
|
|
9221
|
+
return {
|
|
9222
|
+
/** True where the pane of this base is the native agent's now. */
|
|
9223
|
+
active: this.#hosts.size > 0,
|
|
9224
|
+
choice: want?.choice ?? null,
|
|
9225
|
+
/** Where the choice is made: the environment wins over the settings while it is set. */
|
|
9226
|
+
source: want?.source ?? null,
|
|
9227
|
+
providers: PROVIDERS.map((p) => ({
|
|
9228
|
+
id: p.id,
|
|
9229
|
+
name: p.name,
|
|
9230
|
+
env: p.env
|
|
9231
|
+
})),
|
|
9232
|
+
keys: await this.creds.sources(),
|
|
9233
|
+
reach: this.reach,
|
|
9234
|
+
reachFromEnvironment: this.reachFromEnvironment
|
|
9235
|
+
};
|
|
9236
|
+
}
|
|
9237
|
+
async choose(provider, model) {
|
|
9238
|
+
const { chosen, streamFn } = await modelFor(provider, model.trim());
|
|
9239
|
+
await this.creds.choose(provider, chosen.id);
|
|
9240
|
+
if (!this.#env.ISORITE_PI?.trim()) for (const h of this.#hosts) h.use(chosen, streamFn, `pi (${chosen.id})`);
|
|
9241
|
+
}
|
|
9242
|
+
};
|
|
9243
|
+
/** 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. */
|
|
9244
|
+
async function piHostFor(api, env = process.env, project = process.cwd(), notes = project, control = new PiControl(env)) {
|
|
9245
|
+
const want = await wanted(env, control.creds);
|
|
9246
|
+
if (!want) return null;
|
|
9247
|
+
const { provider, model } = want.choice;
|
|
9248
|
+
const { chosen, streamFn } = await modelFor(provider, model);
|
|
9249
|
+
await control.load();
|
|
9250
|
+
const asked = new Set((env.ISORITE_PI_ASK ?? "").split(",").map((t) => t.trim()).filter(Boolean));
|
|
9251
|
+
const host = new PiHost({
|
|
9252
|
+
api,
|
|
9253
|
+
store: new FileStore(storeDir(notes, env)),
|
|
9254
|
+
model: chosen,
|
|
9255
|
+
streamFn,
|
|
9256
|
+
name: `pi (${model})`,
|
|
9257
|
+
getApiKey: (p) => control.creds.key(p),
|
|
9258
|
+
...asked.size ? { ask: (tool) => asked.has(tool) } : {},
|
|
9259
|
+
more: () => fileTools(project, control.reach),
|
|
9260
|
+
guard: pathGuard(project)
|
|
9261
|
+
});
|
|
9262
|
+
await host.resume();
|
|
9263
|
+
control.attach(host);
|
|
9264
|
+
return host;
|
|
9265
|
+
}
|
|
9266
|
+
//#endregion
|
|
7778
9267
|
//#region packages/server/src/nodefiles.ts
|
|
7779
9268
|
const gone = (e) => ["ENOENT", "ENOTDIR"].includes(e.code ?? "");
|
|
7780
9269
|
var NodeFiles = class {
|
|
@@ -8124,6 +9613,7 @@ async function openServer(path, { host = "127.0.0.1", port = 4767, allowHosts =
|
|
|
8124
9613
|
const embedder = model ? nodeEmbedder(model) : null;
|
|
8125
9614
|
const token = randomBytes(24).toString("base64url"), loopback = LOOPBACK.has(host);
|
|
8126
9615
|
const web = existsSync(join(WEB_DIR, "index.html")) ? WEB_DIR : null;
|
|
9616
|
+
const pi = new PiControl();
|
|
8127
9617
|
const bases = new Bases({
|
|
8128
9618
|
config,
|
|
8129
9619
|
first,
|
|
@@ -8138,6 +9628,7 @@ async function openServer(path, { host = "127.0.0.1", port = 4767, allowHosts =
|
|
|
8138
9628
|
api,
|
|
8139
9629
|
notes: known.path
|
|
8140
9630
|
});
|
|
9631
|
+
const terminal = await piHostFor(api, process.env, projectOf(known.path).repo, known.path, pi);
|
|
8141
9632
|
const agents = new AgentRunner({
|
|
8142
9633
|
base: known.name,
|
|
8143
9634
|
url: `http://127.0.0.1:${port}${known.name === first ? "" : `/b/${known.name}`}`,
|
|
@@ -8155,6 +9646,8 @@ async function openServer(path, { host = "127.0.0.1", port = 4767, allowHosts =
|
|
|
8155
9646
|
allowHosts,
|
|
8156
9647
|
notes: known.path,
|
|
8157
9648
|
acp,
|
|
9649
|
+
...terminal ? { terminal } : {},
|
|
9650
|
+
...loopback ? { pi } : {},
|
|
8158
9651
|
bases,
|
|
8159
9652
|
base: known.name,
|
|
8160
9653
|
...loopback ? { agents } : {},
|
|
@@ -8646,13 +10139,17 @@ async function acp(path, { agent, port = 4767, input, output, env = process.env,
|
|
|
8646
10139
|
const given = /* @__PURE__ */ new Map();
|
|
8647
10140
|
const withGiven = (session, prompt) => {
|
|
8648
10141
|
const blocks = Array.isArray(prompt) ? prompt : [];
|
|
8649
|
-
const
|
|
8650
|
-
if (!
|
|
10142
|
+
const g = given.get(session);
|
|
10143
|
+
if (!g) return blocks;
|
|
8651
10144
|
given.delete(session);
|
|
8652
|
-
return [
|
|
8653
|
-
|
|
8654
|
-
|
|
8655
|
-
|
|
10145
|
+
return [
|
|
10146
|
+
{
|
|
10147
|
+
type: "text",
|
|
10148
|
+
text: g.text
|
|
10149
|
+
},
|
|
10150
|
+
...g.blocks ?? [],
|
|
10151
|
+
...blocks
|
|
10152
|
+
];
|
|
8656
10153
|
};
|
|
8657
10154
|
let batch = [];
|
|
8658
10155
|
let flushing = null;
|
|
@@ -8942,8 +10439,18 @@ async function acp(path, { agent, port = 4767, input, output, env = process.env,
|
|
|
8942
10439
|
});
|
|
8943
10440
|
sendWaiting();
|
|
8944
10441
|
}
|
|
10442
|
+
if (c.type === "steer") ask("_session/steering", {
|
|
10443
|
+
sessionId: c.session,
|
|
10444
|
+
prompt: [{
|
|
10445
|
+
type: "text",
|
|
10446
|
+
text: c.text
|
|
10447
|
+
}]
|
|
10448
|
+
});
|
|
8945
10449
|
if (c.type === "give") {
|
|
8946
|
-
if (c.text) given.set(c.session,
|
|
10450
|
+
if (c.text) given.set(c.session, {
|
|
10451
|
+
text: c.text,
|
|
10452
|
+
...c.blocks?.length ? { blocks: c.blocks } : {}
|
|
10453
|
+
});
|
|
8947
10454
|
else given.delete(c.session);
|
|
8948
10455
|
}
|
|
8949
10456
|
if (c.type === "cancel") {
|
|
@@ -8968,11 +10475,15 @@ async function acp(path, { agent, port = 4767, input, output, env = process.env,
|
|
|
8968
10475
|
cwd,
|
|
8969
10476
|
mcpServers: [isoriteMcp()]
|
|
8970
10477
|
};
|
|
8971
|
-
|
|
10478
|
+
const give = c.give ? {
|
|
10479
|
+
text: c.give,
|
|
10480
|
+
...c.giveBlocks?.length ? { blocks: c.giveBlocks } : {}
|
|
10481
|
+
} : void 0;
|
|
10482
|
+
if (!c.session) ask("session/new", params, c.then, give);
|
|
8972
10483
|
else ask(can.load ? "session/load" : "session/resume", {
|
|
8973
10484
|
...params,
|
|
8974
10485
|
sessionId: c.session
|
|
8975
|
-
}, c.then,
|
|
10486
|
+
}, c.then, give);
|
|
8976
10487
|
}
|
|
8977
10488
|
if (c.type === "permission" && agentAsked.has(c.id)) {
|
|
8978
10489
|
const id = agentAsked.get(c.id);
|
|
@@ -9001,7 +10512,8 @@ async function acp(path, { agent, port = 4767, input, output, env = process.env,
|
|
|
9001
10512
|
readTextFile: false,
|
|
9002
10513
|
writeTextFile: false
|
|
9003
10514
|
},
|
|
9004
|
-
terminal: false
|
|
10515
|
+
terminal: false,
|
|
10516
|
+
subagents: {}
|
|
9005
10517
|
},
|
|
9006
10518
|
clientInfo: {
|
|
9007
10519
|
name: "isorite",
|
|
@@ -10251,7 +11763,7 @@ function main(argv) {
|
|
|
10251
11763
|
if (command === "check" && rest.find((r) => !r.startsWith("--"))) return check(rest.find((r) => !r.startsWith("--")), console.log, rest.includes("--unused"), rest.includes("--fix"));
|
|
10252
11764
|
if (command === "new" && rest[0]) {
|
|
10253
11765
|
const made = newBase(rest[0]);
|
|
10254
|
-
const next = `${made}: a knowledge base, empty. Open it in the browser, and attach your agent from
|
|
11766
|
+
const next = `${made}: a knowledge base, empty. Open it in the browser, and attach your agent from the Agent pane:\n isorite serve ${made}`;
|
|
10255
11767
|
if (process.stdin.isTTY && process.stdout.isTTY && repoOf(made)) return (async () => {
|
|
10256
11768
|
const { createInterface } = await import("node:readline/promises");
|
|
10257
11769
|
const rl = createInterface({
|
|
@@ -10301,7 +11813,7 @@ function main(argv) {
|
|
|
10301
11813
|
});
|
|
10302
11814
|
}
|
|
10303
11815
|
if (command === "term" || command === "opencode") {
|
|
10304
|
-
console.error("isorite term and isorite opencode are retired: attach OpenCode as any agent, from
|
|
11816
|
+
console.error("isorite term and isorite opencode are retired: attach OpenCode as any agent, from the Agent pane or with isorite acp --agent \"opencode acp\", or give it isorite's MCP (isorite doctor offers the project's config)");
|
|
10305
11817
|
return 2;
|
|
10306
11818
|
}
|
|
10307
11819
|
if (command === "mcp") {
|