isorite 0.17.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/dist/main.js +1541 -59
  2. package/package.json +4 -1
  3. package/web/_app/immutable/assets/{0.DVQOzs4l.css → 0.BGpjfMlq.css} +1 -1
  4. package/web/_app/immutable/chunks/{2BcU70Iq.js → -_CDfwKC.js} +1 -1
  5. package/web/_app/immutable/chunks/{DrPF-vh8.js → -oRvYXNk.js} +1 -1
  6. package/web/_app/immutable/chunks/0qd6IUbO.js +1 -0
  7. package/web/_app/immutable/chunks/{yy6sD_us.js → 7faei0hL.js} +1 -1
  8. package/web/_app/immutable/chunks/{BNlziJ_1.js → BArcFmbE.js} +3 -3
  9. package/web/_app/immutable/chunks/{NgkOM4d0.js → BF1TezEe.js} +1 -1
  10. package/web/_app/immutable/chunks/{DK9wjWIk.js → BF3mdiSX.js} +1 -1
  11. package/web/_app/immutable/chunks/BFOLKM4z.js +1 -0
  12. package/web/_app/immutable/chunks/{BbvUCVY_.js → BezGs1Zj.js} +1 -1
  13. package/web/_app/immutable/chunks/Bg-39frG.js +1 -0
  14. package/web/_app/immutable/chunks/{CM4EA3Wo.js → BleKqNM1.js} +1 -1
  15. package/web/_app/immutable/chunks/{BjLgp18-.js → BpHE30PI.js} +1 -1
  16. package/web/_app/immutable/chunks/{Ds9L6FPB.js → BtF1frCE.js} +1 -1
  17. package/web/_app/immutable/chunks/{D_xpsaTJ.js → BuNfZSU6.js} +1 -1
  18. package/web/_app/immutable/chunks/{0KrpTYPM.js → BubCsN15.js} +1 -1
  19. package/web/_app/immutable/chunks/{_zY4mrA0.js → BvhVny3O.js} +1 -1
  20. package/web/_app/immutable/chunks/{CcRcjPTM.js → Bvs-JCN-.js} +1 -1
  21. package/web/_app/immutable/chunks/{1W6iRjg5.js → C-F98t31.js} +1 -1
  22. package/web/_app/immutable/chunks/{rv_cF9pp.js → C1OaBy7n.js} +1 -1
  23. package/web/_app/immutable/chunks/{CMN7rSZK.js → C4SqDe8e.js} +1 -1
  24. package/web/_app/immutable/chunks/{BBU5FdEo.js → C74H8jNb.js} +1 -1
  25. package/web/_app/immutable/chunks/{Cn7j5NKi.js → C7bouMY8.js} +1 -1
  26. package/web/_app/immutable/chunks/{vGOSIW7X.js → C8IrXs1w.js} +1 -1
  27. package/web/_app/immutable/chunks/{CwcA6hab.js → CGGjDDVZ.js} +1 -1
  28. package/web/_app/immutable/chunks/{CccIbhmH.js → CL9QeiGC.js} +2 -2
  29. package/web/_app/immutable/chunks/{CabmfniL.js → CMAnq3x6.js} +1 -1
  30. package/web/_app/immutable/chunks/{Bx_Vz2qr.js → CMe14KUo.js} +1 -1
  31. package/web/_app/immutable/chunks/{DQJRaTNz.js → CXc9Iibq.js} +1 -1
  32. package/web/_app/immutable/chunks/Cb-KPahQ.js +1 -0
  33. package/web/_app/immutable/chunks/{CpJ4vfn-.js → Cf13Ew0A.js} +1 -1
  34. package/web/_app/immutable/chunks/{CQhfj5-f.js → CkSjC-iE.js} +3 -3
  35. package/web/_app/immutable/chunks/{sBy91tGs.js → Cn8Mg7_E.js} +1 -1
  36. package/web/_app/immutable/chunks/{1MewxdCy.js → CoeYsgbS.js} +1 -1
  37. package/web/_app/immutable/chunks/{DJHLXLij.js → CrqOdm1D.js} +1 -1
  38. package/web/_app/immutable/chunks/{B-04K_Pl.js → CyJXJYoE.js} +1 -1
  39. package/web/_app/immutable/chunks/{CxEu-Di8.js → Cz6kYWmu.js} +1 -1
  40. package/web/_app/immutable/chunks/{ZDjPMhAb.js → CzbFy3_5.js} +1 -1
  41. package/web/_app/immutable/chunks/{yYKJ4mbq.js → D-9suc4b.js} +1 -1
  42. package/web/_app/immutable/chunks/{qfBACIi9.js → D37hxBaz.js} +1 -1
  43. package/web/_app/immutable/chunks/D7yT4F_8.js +517 -0
  44. package/web/_app/immutable/chunks/{CmFzeyfe.js → DNs5kWvM.js} +1 -1
  45. package/web/_app/immutable/chunks/DQp-M3XN.js +1 -0
  46. package/web/_app/immutable/chunks/{C2EzU0qX.js → DR5d84Z7.js} +1 -1
  47. package/web/_app/immutable/chunks/{C2QvvHIN.js → DaEL1AOW.js} +1 -1
  48. package/web/_app/immutable/chunks/{BCsFs14H.js → Dazeexma.js} +1 -1
  49. package/web/_app/immutable/chunks/{DqZ4UzUH.js → Dc2_04md.js} +1 -1
  50. package/web/_app/immutable/chunks/{DfLI-CvW.js → Dcjgv4h2.js} +1 -1
  51. package/web/_app/immutable/chunks/{CRbUrapz.js → DgI3Vj5v.js} +1 -1
  52. package/web/_app/immutable/chunks/{fpNFwizw.js → Dgko1xMH.js} +1 -1
  53. package/web/_app/immutable/chunks/{DS7XcdHs.js → DlgOmQ_G.js} +1 -1
  54. package/web/_app/immutable/chunks/{DMTkA0aK.js → DmS5Qyvl.js} +1 -1
  55. package/web/_app/immutable/chunks/{DeazQJkV.js → DqnDX3G8.js} +1 -1
  56. package/web/_app/immutable/chunks/{DMpFmiFg.js → DyZdoKBK.js} +1 -1
  57. package/web/_app/immutable/chunks/{DEwfz-Il.js → DzAOHjhV.js} +1 -1
  58. package/web/_app/immutable/chunks/{DRKiKCov.js → Iqegk1SI.js} +1 -1
  59. package/web/_app/immutable/chunks/{Cl-Xmm9N.js → ONqdwA3r.js} +1 -1
  60. package/web/_app/immutable/chunks/{Dns-yz2Q.js → Oikibjm2.js} +1 -1
  61. package/web/_app/immutable/chunks/{CdD6waB-.js → T3u7Skc8.js} +1 -1
  62. package/web/_app/immutable/chunks/{Df3spPXn.js → cWWhqL1F.js} +1 -1
  63. package/web/_app/immutable/chunks/{CfTuc06U.js → fRXyV4Ec.js} +1 -1
  64. package/web/_app/immutable/chunks/{Dzu8v7nB.js → jAC4YCqW.js} +1 -1
  65. package/web/_app/immutable/chunks/{CB6ztdCQ.js → kFEqmLql.js} +1 -1
  66. package/web/_app/immutable/chunks/{BBhr3bLI.js → y0C1JQ1v.js} +1 -1
  67. package/web/_app/immutable/chunks/{CAuYgj5q.js → yvD4_bP8.js} +1 -1
  68. package/web/_app/immutable/entry/app.CyEmP0i2.js +2 -0
  69. package/web/_app/immutable/entry/start.BxCHvKWL.js +1 -0
  70. package/web/_app/immutable/nodes/0.Bkbiw_sA.js +11 -0
  71. package/web/_app/immutable/nodes/1.kdx6h8ts.js +1 -0
  72. package/web/_app/immutable/nodes/2.CtK7BGB8.js +1 -0
  73. package/web/_app/immutable/nodes/3.D5PZa2kH.js +1 -0
  74. package/web/_app/immutable/nodes/4.DzS8oPUH.js +3 -0
  75. package/web/_app/immutable/nodes/5.B-1BDKsL.js +19 -0
  76. package/web/_app/immutable/nodes/6.DDT1DtCg.js +1 -0
  77. package/web/_app/immutable/nodes/7.CCNCaItj.js +1 -0
  78. package/web/_app/version.json +1 -1
  79. package/web/index.html +17 -17
  80. package/web/_app/immutable/chunks/BJOSVfg-.js +0 -1
  81. package/web/_app/immutable/chunks/CHjmZsHD.js +0 -1
  82. package/web/_app/immutable/chunks/DH2bqH7t.js +0 -517
  83. package/web/_app/immutable/chunks/Ddw9FbDu.js +0 -1
  84. package/web/_app/immutable/chunks/IECP4ppk.js +0 -1
  85. package/web/_app/immutable/chunks/ZDkogIhD.js +0 -1
  86. package/web/_app/immutable/entry/app.CX19wIuG.js +0 -2
  87. package/web/_app/immutable/entry/start.DaiosA4i.js +0 -1
  88. package/web/_app/immutable/nodes/0.B6_Ycc14.js +0 -3
  89. package/web/_app/immutable/nodes/1.JA1sPybf.js +0 -1
  90. package/web/_app/immutable/nodes/2.DN2phYmE.js +0 -1
  91. package/web/_app/immutable/nodes/3.CbnLNp2F.js +0 -1
  92. package/web/_app/immutable/nodes/4.CIsG7Kdu.js +0 -3
  93. package/web/_app/immutable/nodes/5.D__u9ji8.js +0 -19
  94. package/web/_app/immutable/nodes/6.DBP0fpLt.js +0 -1
  95. 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"] : []];
@@ -5705,7 +5708,23 @@ function given(asked, parts, title) {
5705
5708
  if (passages.length) out.push(passages.join("\n\n"));
5706
5709
  return out.join("\n\n");
5707
5710
  }
5708
- const OUTPUT = 4e3;
5711
+ /** The same for an agent that takes embedded context: notes and folders as text for the agent to read with isorite's
5712
+ * tools, and each passage as a resource block (the note's address, the passage as its text) that the text points to. */
5713
+ function givenEmbedded(parts, title, base) {
5714
+ const passages = parts.flatMap((p) => p.kind === "passage" ? [p] : []);
5715
+ return {
5716
+ text: [given("", parts.filter((p) => p.kind !== "passage"), title).trim(), passages.length ? `${passages.length === 1 ? "A passage I selected is" : `${passages.length} passages I selected are`} attached as ${passages.length === 1 ? "a resource" : "resources"}, named for the note ${passages.length === 1 ? "it is" : "each is"} from (the note may have changed since: read it for the current text):\n${passages.map((p) => `- from ${title(p.id)} (${p.id})`).join("\n")}` : ""].filter(Boolean).join("\n\n"),
5717
+ blocks: passages.map((p) => ({
5718
+ type: "resource",
5719
+ resource: {
5720
+ uri: `isorite://${base}/${p.id}`,
5721
+ mimeType: "text/markdown",
5722
+ text: p.text
5723
+ }
5724
+ }))
5725
+ };
5726
+ }
5727
+ const OUTPUT$1 = 4e3;
5709
5728
  const NOTE_TOOLS = /(^|[_:./-])(read_note|outline_note|edit_note|write_note)$/;
5710
5729
  const CHANGES = /* @__PURE__ */ new Set([
5711
5730
  "edit",
@@ -5721,7 +5740,7 @@ const ACTIONS = {
5721
5740
  read: "read",
5722
5741
  fetch: "webfetch"
5723
5742
  };
5724
- const EMPTY = {
5743
+ const EMPTY$1 = {
5725
5744
  connected: false,
5726
5745
  agent: "",
5727
5746
  session: null,
@@ -5737,7 +5756,7 @@ const EMPTY = {
5737
5756
  model: "",
5738
5757
  sessions: []
5739
5758
  };
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("");
5759
+ const textOf$1 = (blocks) => (Array.isArray(blocks) ? blocks : [blocks]).map((b) => b?.type === "text" ? b.text ?? "" : b?.type === "resource_link" ? `@${b.name ?? b.uri ?? ""}` : "").join("");
5741
5760
  const lines = (v) => v ? v.replace(/\n$/, "").split("\n") : [];
5742
5761
  const keyOf = (agent, session) => `${agent}\u0000${session}`;
5743
5762
  const str = (v) => typeof v === "string" ? v : "";
@@ -5790,7 +5809,7 @@ var AcpFollower = class {
5790
5809
  #shown = null;
5791
5810
  #chosen = false;
5792
5811
  #reading = null;
5793
- #view = EMPTY;
5812
+ #view = EMPTY$1;
5794
5813
  #ours = 0;
5795
5814
  /** Why what the pane last asked of an agent was refused (signing in, a session it does not have). */
5796
5815
  #problem = "";
@@ -5863,12 +5882,13 @@ var AcpFollower = class {
5863
5882
  size: live.size,
5864
5883
  ours: this.#ours,
5865
5884
  sessions: this.#sessions(),
5885
+ ...live.link.steer ? { steer: true } : {},
5866
5886
  commands: live.commands,
5867
5887
  settings: [...live.modes ? [live.modes] : [], ...live.configs],
5868
5888
  ...problem
5869
5889
  };
5870
5890
  else if (read) this.#view = {
5871
- ...EMPTY,
5891
+ ...EMPTY$1,
5872
5892
  connected,
5873
5893
  reading: true,
5874
5894
  agent: read.name,
@@ -5882,7 +5902,7 @@ var AcpFollower = class {
5882
5902
  else {
5883
5903
  const link = [...this.#links.values()].at(-1);
5884
5904
  this.#view = {
5885
- ...EMPTY,
5905
+ ...EMPTY$1,
5886
5906
  connected,
5887
5907
  agent: link?.name ?? "",
5888
5908
  agentId: link?.agent ?? "",
@@ -5963,6 +5983,8 @@ var AcpFollower = class {
5963
5983
  resume: false,
5964
5984
  list: false
5965
5985
  },
5986
+ embedded: false,
5987
+ steer: false,
5966
5988
  toAgent: /* @__PURE__ */ new Map(),
5967
5989
  fromAgent: /* @__PURE__ */ new Map(),
5968
5990
  listed: [],
@@ -6017,11 +6039,36 @@ var AcpFollower = class {
6017
6039
  type: "open",
6018
6040
  ...this.#reading ? { session: this.#reading.session } : {},
6019
6041
  then: text,
6020
- ...this.#unopened ? { give: this.#unopened.text } : {}
6042
+ ...this.#unopened ? {
6043
+ give: this.#unopened.text,
6044
+ ...this.#unopened.blocks?.length ? { giveBlocks: this.#unopened.blocks } : {}
6045
+ } : {}
6021
6046
  });
6022
6047
  }
6048
+ /** Subagents the agent announced (`subagent_update`), by their session: each is a session of its own that the pane
6049
+ * does not list, and what it reads and changes in the base is told under its own name, `<agent>-<title>-<n>`. */
6050
+ #children = /* @__PURE__ */ new Map();
6051
+ #childCount = /* @__PURE__ */ new Map();
6023
6052
  /** Given before a session was open: for the one the next prompt opens. */
6024
6053
  #unopened = null;
6054
+ async steer(text) {
6055
+ const live = this.#need();
6056
+ if (!live.link.steer) throw new Error(`${live.link.name || "this agent"} cannot be steered while it works: stop it and ask again`);
6057
+ if (!live.busy) throw new Error("the agent is not working: ask it instead");
6058
+ live.link.send({
6059
+ type: "steer",
6060
+ session: live.session,
6061
+ text
6062
+ });
6063
+ this.#edit(live, (items) => [...items, {
6064
+ kind: "message",
6065
+ id: `steer-${live.turns.length}-${items.length}`,
6066
+ text,
6067
+ mode: "steer",
6068
+ state: "sent"
6069
+ }]);
6070
+ this.#tell();
6071
+ }
6025
6072
  async interrupt() {
6026
6073
  const live = this.#need();
6027
6074
  live.link.send({
@@ -6030,24 +6077,31 @@ var AcpFollower = class {
6030
6077
  });
6031
6078
  }
6032
6079
  /** Notes and passages given from the pane: those not given in the session yet go before its next prompt. */
6033
- async give(parts, render) {
6080
+ async give(parts, render, embed) {
6034
6081
  const live = this.#shownLive();
6082
+ const made = (p, embedded) => {
6083
+ const e = embedded && embed ? embed(p) : null;
6084
+ return e ? {
6085
+ parts: p,
6086
+ text: e.text,
6087
+ ...e.blocks.length ? { blocks: e.blocks } : {}
6088
+ } : {
6089
+ parts: p,
6090
+ text: render(p)
6091
+ };
6092
+ };
6035
6093
  if (!live) {
6036
- this.#unopened = parts.length ? {
6037
- parts,
6038
- text: render(parts)
6039
- } : null;
6094
+ const link = [...this.#links.values()].at(-1);
6095
+ this.#unopened = parts.length ? made(parts, link?.embedded === true) : null;
6040
6096
  return;
6041
6097
  }
6042
6098
  const fresh = parts.filter((g) => !live.given.has(givenKey(g)));
6043
- live.pending = fresh.length ? {
6044
- parts: fresh,
6045
- text: render(fresh)
6046
- } : null;
6099
+ live.pending = fresh.length ? made(fresh, live.link.embedded) : null;
6047
6100
  live.link.send({
6048
6101
  type: "give",
6049
6102
  session: live.session,
6050
- text: live.pending?.text ?? ""
6103
+ text: live.pending?.text ?? "",
6104
+ ...live.pending?.blocks ? { blocks: live.pending.blocks } : {}
6051
6105
  });
6052
6106
  }
6053
6107
  async reply(id, decision) {
@@ -6200,6 +6254,8 @@ var AcpFollower = class {
6200
6254
  case "initialize": {
6201
6255
  const info = m.result?.agentInfo;
6202
6256
  const caps = m.result?.agentCapabilities ?? {};
6257
+ link.embedded = caps.promptCapabilities?.embeddedContext === true;
6258
+ link.steer = (m.result?._meta)?.steering?.supported === true;
6203
6259
  link.agent = agentName(info?.name ?? "agent");
6204
6260
  link.name = info?.title ?? info?.name ?? "The agent";
6205
6261
  link.can = {
@@ -6278,7 +6334,7 @@ var AcpFollower = class {
6278
6334
  return;
6279
6335
  }
6280
6336
  if (m.method === "session/update") {
6281
- const live = this.#lives.get(keyOf(link.agent, sessionId));
6337
+ const live = this.#lives.get(keyOf(link.agent, sessionId)) ?? this.#children.get(keyOf(link.agent, sessionId));
6282
6338
  if (live) this.#update(live, m.params?.update ?? {});
6283
6339
  }
6284
6340
  }
@@ -6354,7 +6410,7 @@ var AcpFollower = class {
6354
6410
  live.busy = true;
6355
6411
  live.turns = [...live.turns, {
6356
6412
  id: key,
6357
- asked: textOf(given ? blocks.slice(1) : blocks),
6413
+ asked: textOf$1(given ? blocks.slice(1) : blocks),
6358
6414
  from: way === "ours" ? "here" : "terminal",
6359
6415
  items: [],
6360
6416
  state: "running",
@@ -6397,7 +6453,7 @@ var AcpFollower = class {
6397
6453
  switch (u.sessionUpdate) {
6398
6454
  case "user_message_chunk": {
6399
6455
  if (!live.loading) break;
6400
- const text = textOf(u.content), t = live.turns.at(-1);
6456
+ const text = textOf$1(u.content), t = live.turns.at(-1);
6401
6457
  if (live.replaying && t && !t.items.length) live.turns = [...live.turns.slice(0, -1), {
6402
6458
  ...t,
6403
6459
  asked: t.asked + text
@@ -6414,7 +6470,7 @@ var AcpFollower = class {
6414
6470
  }
6415
6471
  case "agent_message_chunk": {
6416
6472
  live.replaying = false;
6417
- const text = textOf(u.content);
6473
+ const text = textOf$1(u.content);
6418
6474
  this.#edit(live, (items) => {
6419
6475
  const end = items.at(-1);
6420
6476
  if (end?.kind === "text") items[items.length - 1] = {
@@ -6429,6 +6485,24 @@ var AcpFollower = class {
6429
6485
  });
6430
6486
  break;
6431
6487
  }
6488
+ case "agent_thought_chunk": {
6489
+ live.replaying = false;
6490
+ const text = textOf$1(u.content);
6491
+ if (!text) break;
6492
+ this.#edit(live, (items) => {
6493
+ const end = items.at(-1);
6494
+ if (end?.kind === "thinking") items[items.length - 1] = {
6495
+ kind: "thinking",
6496
+ text: end.text + text
6497
+ };
6498
+ else items.push({
6499
+ kind: "thinking",
6500
+ text
6501
+ });
6502
+ return items;
6503
+ });
6504
+ break;
6505
+ }
6432
6506
  case "tool_call":
6433
6507
  case "tool_call_update": {
6434
6508
  live.replaying = false;
@@ -6446,7 +6520,7 @@ var AcpFollower = class {
6446
6520
  } : items[at];
6447
6521
  const content = Array.isArray(u.content) ? u.content : null;
6448
6522
  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;
6523
+ const output = content ? content.filter((c) => c.type === "content").map((c) => textOf$1(c.content)).join("\n").slice(0, OUTPUT$1) : was.output;
6450
6524
  const input = u.rawInput ?? {};
6451
6525
  kind = typeof u.kind === "string" ? u.kind : diff ? "edit" : "";
6452
6526
  const name = typeof u.name === "string" ? u.name : kind && at < 0 ? kind : was.name;
@@ -6498,6 +6572,58 @@ var AcpFollower = class {
6498
6572
  case "session_info_update":
6499
6573
  if (typeof u.title === "string") live.title = u.title;
6500
6574
  break;
6575
+ case "subagent_update": {
6576
+ const id = str(u.sessionId);
6577
+ if (!id || live.link.agent === "") break;
6578
+ const key = keyOf(live.link.agent, id), title = str(u.title) || str(u.description) || "subagent";
6579
+ let child = this.#children.get(key);
6580
+ if (!child) {
6581
+ const n = (this.#childCount.get(live.link.agent) ?? 0) + 1;
6582
+ this.#childCount.set(live.link.agent, n);
6583
+ const agent = agentName(`${live.link.agent}-${title.split(/\s+/).slice(0, 3).join("-")}-${n}`);
6584
+ child = {
6585
+ ...live,
6586
+ link: {
6587
+ ...live.link,
6588
+ agent
6589
+ },
6590
+ session: id,
6591
+ title,
6592
+ turns: [{
6593
+ id,
6594
+ asked: title,
6595
+ from: live.from,
6596
+ items: [],
6597
+ state: "running"
6598
+ }],
6599
+ seen: /* @__PURE__ */ new Set(),
6600
+ asking: /* @__PURE__ */ new Map(),
6601
+ pending: null,
6602
+ given: /* @__PURE__ */ new Set(),
6603
+ loading: false,
6604
+ replaying: false
6605
+ };
6606
+ this.#children.set(key, child);
6607
+ }
6608
+ const state = u.state ?? {};
6609
+ const status = state.state === "idle" ? state.error || state.stopReason === "error" ? "failed" : "done" : "running";
6610
+ this.#edit(live, (items) => {
6611
+ const at = items.findIndex((i) => i.kind === "tool" && i.id === `subagent-${id}`);
6612
+ const was = at < 0 ? null : items[at];
6613
+ const now = {
6614
+ kind: "tool",
6615
+ id: `subagent-${id}`,
6616
+ name: "subagent",
6617
+ title: was?.title || title,
6618
+ status,
6619
+ output: str(u.description) || was?.output || ""
6620
+ };
6621
+ if (at < 0) items.push(now);
6622
+ else items[at] = now;
6623
+ return items;
6624
+ });
6625
+ break;
6626
+ }
6501
6627
  case "available_commands_update":
6502
6628
  live.commands = commandsOf(u.availableCommands);
6503
6629
  break;
@@ -6567,7 +6693,7 @@ var AcpFollower = class {
6567
6693
  };
6568
6694
  //#endregion
6569
6695
  //#region packages/server/src/version.ts
6570
- const VERSION = "0.17.0";
6696
+ const VERSION = "0.18.0";
6571
6697
  //#endregion
6572
6698
  //#region packages/server/src/connect.ts
6573
6699
  const HARNESSES = [{
@@ -6870,8 +6996,8 @@ function missingKey(command, env = process.env) {
6870
6996
  //#region packages/server/src/agents.ts
6871
6997
  init_self();
6872
6998
  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");
6999
+ const ID$2 = /^[a-z0-9][a-z0-9._-]{0,63}$/;
7000
+ const dataDir$1 = (env) => join(env.XDG_DATA_HOME || join(homedir(), ".local", "share"), "isorite", "agents");
6875
7001
  /** The registry's name for this platform. */
6876
7002
  const platform$1 = () => `${process.platform === "win32" ? "windows" : process.platform}-${process.arch === "arm64" ? "aarch64" : process.arch === "x64" ? "x86_64" : process.arch}`;
6877
7003
  /** A word a shell takes as it is. */
@@ -6883,7 +7009,7 @@ async function readRegistry(env = process.env) {
6883
7009
  const from = env.ISORITE_ACP_REGISTRY || "https://cdn.agentclientprotocol.com/registry/v1/latest/registry.json";
6884
7010
  const parse = (text) => {
6885
7011
  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") : [];
7012
+ return Array.isArray(agents) ? agents.filter((a) => typeof a?.id === "string" && ID$2.test(a.id) && typeof a.name === "string") : [];
6887
7013
  };
6888
7014
  if (!/^https?:/.test(from)) return parse(readFileSync(from, "utf8"));
6889
7015
  const kept = join(cacheDir(env), "acp-registry.json");
@@ -6926,7 +7052,7 @@ async function commandOf(e, env = process.env) {
6926
7052
  const d = e.distribution ?? {};
6927
7053
  const bin = d.binary?.[platform$1()];
6928
7054
  if (bin) {
6929
- const dir = join(dataDir(env), e.id, (e.version || "latest").replace(/[^\w.-]/g, "_"));
7055
+ const dir = join(dataDir$1(env), e.id, (e.version || "latest").replace(/[^\w.-]/g, "_"));
6930
7056
  const cmd = resolve(dir, bin.cmd);
6931
7057
  if (!cmd.startsWith(dir)) throw new Error(`${e.name}: its program is outside its folder`);
6932
7058
  if (!existsSync(cmd)) await fetchProgram(bin, dir);
@@ -7001,7 +7127,7 @@ async function fetchProgram(bin, dir) {
7001
7127
  /** What bases.toml says this base has chosen. */
7002
7128
  function chosenAgent(base, config) {
7003
7129
  const row = table(readConfig(basesPath(config))[base]);
7004
- if (typeof row.agent === "string" && ID.test(row.agent)) return { id: row.agent };
7130
+ if (typeof row.agent === "string" && ID$2.test(row.agent)) return { id: row.agent };
7005
7131
  if (typeof row.agent_command === "string" && row.agent_command.trim()) return { command: row.agent_command.trim() };
7006
7132
  return null;
7007
7133
  }
@@ -7291,7 +7417,8 @@ const AgentStatus = z.object({
7291
7417
  session: z.string().min(1).max(200),
7292
7418
  busy: z.boolean()
7293
7419
  }).strict();
7294
- function createApp({ api, token, loopback, web, allowHosts = [], notes, acp, bases, base, agents, config }) {
7420
+ function createApp({ api, token, loopback, web, allowHosts = [], notes, acp, terminal, bases, base, agents, config, pi }) {
7421
+ const pane = terminal ?? acp;
7295
7422
  const app = new Hono();
7296
7423
  const allowed = new Set([...allowHosts].map((h) => h.toLowerCase()));
7297
7424
  const reply = (c, status, value) => c.body(JSON.stringify(value), status, {
@@ -7550,8 +7677,54 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes, acp, bas
7550
7677
  return agents.choose(b.choice);
7551
7678
  });
7552
7679
  }
7553
- if (acp) {
7554
- app.get("/api/terminal", (c) => answer(c, async () => acp.view()));
7680
+ if (pi) {
7681
+ const mine = (c) => {
7682
+ if (!local(c)) throw new EngineError("the model and keys are set only from this machine", 403);
7683
+ };
7684
+ app.get("/api/pi", (c) => answer(c, async () => {
7685
+ mine(c);
7686
+ return pi.state();
7687
+ }));
7688
+ const Provider = z.string().regex(/^[a-z0-9-]{1,40}$/);
7689
+ const invalid = async (fn) => {
7690
+ try {
7691
+ await fn();
7692
+ } catch (e) {
7693
+ throw new EngineError(e.message, 400);
7694
+ }
7695
+ };
7696
+ post("/api/pi/key", z.object({
7697
+ provider: Provider,
7698
+ key: z.string().max(2e3)
7699
+ }).strict(), async (c, b) => {
7700
+ mine(c);
7701
+ await invalid(() => pi.creds.set(b.provider, b.key));
7702
+ });
7703
+ post("/api/pi/key/remove", z.object({ provider: Provider }).strict(), async (c, b) => {
7704
+ mine(c);
7705
+ await invalid(() => pi.creds.remove(b.provider));
7706
+ });
7707
+ post("/api/pi/reach", z.object({
7708
+ files: z.enum([
7709
+ "off",
7710
+ "read",
7711
+ "write"
7712
+ ]),
7713
+ shell: z.boolean()
7714
+ }).strict(), async (c, b) => {
7715
+ mine(c);
7716
+ await invalid(() => pi.setReach(b));
7717
+ });
7718
+ post("/api/pi/choose", z.object({
7719
+ provider: Provider,
7720
+ model: z.string().min(1).max(200)
7721
+ }).strict(), async (c, b) => {
7722
+ mine(c);
7723
+ await invalid(() => pi.choose(b.provider, b.model));
7724
+ });
7725
+ }
7726
+ if (pane) {
7727
+ app.get("/api/terminal", (c) => answer(c, async () => pane.view()));
7555
7728
  const failed = (e) => {
7556
7729
  throw new EngineError(e.message, 409);
7557
7730
  };
@@ -7563,11 +7736,22 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes, acp, bas
7563
7736
  const broken = await api.brokenNews();
7564
7737
  const news = broken.length ? `\n\n(isorite: code links in the notes that no longer resolve: ${broken.map((x) => `${x.href} in /${x.id}.md`).join("; ")}.)` : "";
7565
7738
  const title = (id) => titles.get(id) ?? id;
7566
- await acp.give(b.given, (parts) => given("", parts, title).trim()).catch(failed);
7567
- await acp.prompt(b.asked.trim() + news).catch(failed);
7739
+ await pane.give(b.given, (parts) => given("", parts, title).trim(), (parts) => givenEmbedded(parts, title, base ?? "notes")).catch(failed);
7740
+ await pane.prompt(b.asked.trim() + news).catch(failed);
7741
+ });
7742
+ post("/api/terminal/steer", z.object({
7743
+ asked: z.string().min(1).max(8e3),
7744
+ given: z.array(Given).max(40)
7745
+ }).strict(), async (_c, b) => {
7746
+ if (!pane.steer) throw new EngineError("this agent cannot be steered while it works: stop it and ask again", 409);
7747
+ const titles = new Map((await api.notes()).notes.map((n) => [n.id, n.title]));
7748
+ const title = (id) => titles.get(id) ?? id;
7749
+ await pane.give(b.given, (parts) => given("", parts, title).trim(), (parts) => givenEmbedded(parts, title, base ?? "notes")).catch(failed);
7750
+ await pane.steer(b.asked.trim()).catch(failed);
7568
7751
  });
7752
+ post("/api/terminal/withdraw", z.object({ given: Given }).strict(), async (_c, b) => ({ withdrawn: pane.withdraw ? await pane.withdraw(b.given) : false }));
7569
7753
  post("/api/terminal/stop", z.object({}).strict(), async () => {
7570
- await acp.interrupt().catch(failed);
7754
+ await pane.interrupt().catch(failed);
7571
7755
  });
7572
7756
  post("/api/terminal/permission", z.object({
7573
7757
  id: z.string().max(200),
@@ -7577,7 +7761,7 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes, acp, bas
7577
7761
  "reject"
7578
7762
  ])
7579
7763
  }).strict(), async (_c, b) => {
7580
- await acp.reply(b.id, b.decision).catch(failed);
7764
+ await pane.reply(b.id, b.decision).catch(failed);
7581
7765
  });
7582
7766
  }
7583
7767
  const endpoint = new McpEndpoint(api, {
@@ -7610,7 +7794,7 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes, acp, bas
7610
7794
  const quiet = Number(c.req.query("quiet"));
7611
7795
  const who = (rpc) => {
7612
7796
  const agent = named && /^[a-z0-9][a-z0-9-]{0,39}$/.test(named) ? named : agentName(clientName(rpc) ?? "mcp");
7613
- const asked = acp?.asked(agent) ?? "";
7797
+ const asked = pane?.asked(agent) ?? "";
7614
7798
  return {
7615
7799
  agent,
7616
7800
  session: (c.req.query("session") ?? "http").slice(0, 200),
@@ -7626,25 +7810,38 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes, acp, bas
7626
7810
  if (acp) {
7627
7811
  const render = async () => {
7628
7812
  const titles = new Map((await api.notes()).notes.map((n) => [n.id, n.title]));
7629
- return (parts) => given("", parts, (id) => titles.get(id) ?? id).trim();
7813
+ const title = (id) => titles.get(id) ?? id;
7814
+ return {
7815
+ text: (parts) => given("", parts, title).trim(),
7816
+ embed: (parts) => givenEmbedded(parts, title, base ?? "notes")
7817
+ };
7630
7818
  };
7819
+ const host = pane ?? acp;
7631
7820
  const refused = (e) => {
7632
7821
  throw new EngineError(e.message, 409);
7633
7822
  };
7634
7823
  post("/api/acp/give", z.object({ given: z.array(Given).max(40) }).strict(), async (_c, b) => {
7635
- await acp.give(b.given, await render()).catch(refused);
7824
+ const r = await render();
7825
+ await host.give(b.given, r.text, r.embed).catch(refused);
7636
7826
  });
7637
7827
  post("/api/acp/follow", z.object({
7638
7828
  agent: z.string().max(40),
7639
7829
  session: z.string().min(1).max(200)
7640
7830
  }).strict(), async (_c, b) => {
7641
- await acp.follow(b.agent, b.session).catch(refused);
7831
+ await host.follow(b.agent, b.session).catch(refused);
7832
+ });
7833
+ post("/api/acp/fork", z.object({
7834
+ agent: z.string().max(40),
7835
+ session: z.string().min(1).max(200)
7836
+ }).strict(), async (_c, b) => {
7837
+ if (!host.fork) throw new EngineError("this agent's sessions cannot be forked here", 409);
7838
+ await host.fork(b.agent, b.session).catch(refused);
7642
7839
  });
7643
7840
  post("/api/acp/forget", z.object({
7644
7841
  agent: z.string().max(40),
7645
7842
  session: z.string().min(1).max(200)
7646
7843
  }).strict(), async (_c, b) => {
7647
- await acp.forget(b.agent, b.session).catch(refused);
7844
+ await host.forget(b.agent, b.session).catch(refused);
7648
7845
  });
7649
7846
  post("/api/acp/set", z.object({
7650
7847
  kind: z.enum(["mode", "config"]),
@@ -7652,16 +7849,16 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes, acp, bas
7652
7849
  value: z.string().max(200)
7653
7850
  }).strict(), async (_c, b) => {
7654
7851
  try {
7655
- acp.set(b.kind, b.id, b.value);
7852
+ host.set(b.kind, b.id, b.value);
7656
7853
  } catch (e) {
7657
7854
  throw new EngineError(e.message, 409);
7658
7855
  }
7659
7856
  });
7660
7857
  post("/api/acp/new", z.object({ agent: z.string().max(40) }).strict(), async (_c, b) => {
7661
- await acp.fresh(b.agent).catch(refused);
7858
+ await host.fresh(b.agent).catch(refused);
7662
7859
  });
7663
7860
  post("/api/acp/list", z.object({}).strict(), async () => {
7664
- acp.refresh();
7861
+ host.refresh();
7665
7862
  });
7666
7863
  post("/api/acp/tap", z.object({
7667
7864
  link: z.string().min(1).max(100),
@@ -7733,7 +7930,7 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes, acp, bas
7733
7930
  })
7734
7931
  }).catch(() => void 0);
7735
7932
  };
7736
- const unfollow = acp?.listen(send);
7933
+ const unfollow = pane?.listen(send);
7737
7934
  const beat = setInterval(() => {
7738
7935
  stream.write(": still here\n\n");
7739
7936
  }, 25e3);
@@ -7741,7 +7938,7 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes, acp, bas
7741
7938
  event: "open",
7742
7939
  data: "{}"
7743
7940
  });
7744
- if (acp?.view().connected) send(acp.view());
7941
+ if (pane?.view().connected) send(pane.view());
7745
7942
  streams.set(stream, c.req.query("agent") === void 0);
7746
7943
  watchers();
7747
7944
  await new Promise((done) => stream.onAbort(done));
@@ -7775,6 +7972,1268 @@ function staticFile(c, web) {
7775
7972
  });
7776
7973
  }
7777
7974
  //#endregion
7975
+ //#region packages/server/src/pi/store.ts
7976
+ /** Said when a saved session cannot be read: names the file, so the person can look at it or move it away. */
7977
+ var StoreError = class extends Error {
7978
+ file;
7979
+ constructor(file, why) {
7980
+ super(`The saved pi session ${file} could not be read (${why}). It was left where it is.`);
7981
+ this.file = file;
7982
+ }
7983
+ };
7984
+ const ID$1 = /^[0-9a-f-]{8,64}$/i;
7985
+ var FileStore = class {
7986
+ #dir;
7987
+ constructor(dir) {
7988
+ this.#dir = dir;
7989
+ }
7990
+ async save(r) {
7991
+ if (!ID$1.test(r.session)) throw new Error("not a session id");
7992
+ await mkdir(this.#dir, {
7993
+ recursive: true,
7994
+ mode: 448
7995
+ });
7996
+ const file = join(this.#dir, `${r.session}.json`), tmp = `${file}.${process.pid}.tmp`;
7997
+ await writeFile(tmp, JSON.stringify(r), { mode: 384 });
7998
+ await rename(tmp, file);
7999
+ }
8000
+ async latest() {
8001
+ let names;
8002
+ try {
8003
+ names = (await readdir(this.#dir)).filter((n) => n.endsWith(".json"));
8004
+ } catch {
8005
+ return null;
8006
+ }
8007
+ const newest = (await Promise.all(names.map(async (n) => ({
8008
+ n,
8009
+ t: (await stat(join(this.#dir, n))).mtimeMs
8010
+ })))).sort((a, b) => b.t - a.t)[0];
8011
+ if (!newest) return null;
8012
+ return this.#read(join(this.#dir, newest.n));
8013
+ }
8014
+ async #read(file) {
8015
+ let record;
8016
+ try {
8017
+ record = JSON.parse(await readFile(file, "utf8"));
8018
+ } catch (e) {
8019
+ throw new StoreError(file, e.message);
8020
+ }
8021
+ const r = record;
8022
+ if (!r || r.version !== 1 || typeof r.session !== "string" || !Array.isArray(r.turns) || !Array.isArray(r.messages)) throw new StoreError(file, "it is not a session this version of isorite wrote");
8023
+ return r;
8024
+ }
8025
+ async list() {
8026
+ let names;
8027
+ try {
8028
+ names = (await readdir(this.#dir)).filter((n) => n.endsWith(".json"));
8029
+ } catch {
8030
+ return {
8031
+ records: [],
8032
+ unreadable: []
8033
+ };
8034
+ }
8035
+ const records = [], unreadable = [];
8036
+ for (const n of names) try {
8037
+ records.push(await this.#read(join(this.#dir, n)));
8038
+ } catch (e) {
8039
+ unreadable.push(e instanceof StoreError ? e.file : join(this.#dir, n));
8040
+ }
8041
+ return {
8042
+ records: records.sort((a, b) => a.last < b.last ? 1 : -1),
8043
+ unreadable
8044
+ };
8045
+ }
8046
+ async load(session) {
8047
+ if (!ID$1.test(session)) return null;
8048
+ const file = join(this.#dir, `${session}.json`);
8049
+ try {
8050
+ await stat(file);
8051
+ } catch {
8052
+ return null;
8053
+ }
8054
+ return this.#read(file);
8055
+ }
8056
+ async remove(session) {
8057
+ if (!ID$1.test(session)) throw new Error("not a session id");
8058
+ await rm(join(this.#dir, `${session}.json`), { force: true });
8059
+ }
8060
+ };
8061
+ /** Where a base's pi sessions are kept: isorite's data folder, apart from the notes and the repository. */
8062
+ function storeDir(notes, env = process.env) {
8063
+ const key = createHash("sha1").update(notes).digest("hex").slice(0, 12);
8064
+ return join(env.XDG_DATA_HOME || join(homedir(), ".local", "share"), "isorite", "pi", key);
8065
+ }
8066
+ //#endregion
8067
+ //#region packages/server/src/pi/tools.ts
8068
+ /** `who` names the agent and session as the base knows them; `asked` is what is being asked now, for a change the agent makes. */
8069
+ async function isoriteTools(api, who, asked) {
8070
+ return (await api.agentTools()).map(({ function: f }) => ({
8071
+ name: f.name,
8072
+ label: f.name,
8073
+ description: f.description,
8074
+ parameters: f.parameters,
8075
+ execute: async (_id, params) => {
8076
+ const now = asked();
8077
+ const { text } = await api.agentLook({
8078
+ ...who,
8079
+ tool: f.name,
8080
+ args: params,
8081
+ ...now ? { asked: now } : {}
8082
+ });
8083
+ return {
8084
+ content: [{
8085
+ type: "text",
8086
+ text
8087
+ }],
8088
+ details: void 0
8089
+ };
8090
+ }
8091
+ }));
8092
+ }
8093
+ /** What a call acted on, in a line: a query, a note, a path, a command. */
8094
+ function titleOf(args) {
8095
+ if (!args || typeof args !== "object") return "";
8096
+ const a = args;
8097
+ for (const k of [
8098
+ "query",
8099
+ "note",
8100
+ "id",
8101
+ "path",
8102
+ "command",
8103
+ "question",
8104
+ "text"
8105
+ ]) if (typeof a[k] === "string" && a[k]) return String(a[k]).slice(0, 200);
8106
+ const first = Object.values(a).find((v) => typeof v === "string" && v);
8107
+ return typeof first === "string" ? first.slice(0, 200) : "";
8108
+ }
8109
+ //#endregion
8110
+ //#region packages/server/src/pi/host.ts
8111
+ const OUTPUT = 4e3;
8112
+ /** What the summary of an earlier conversation begins with, in the message that stands for it. */
8113
+ const SUMMARY = "[The earlier part of this conversation, summarised to make room]\n";
8114
+ const EMPTY = {
8115
+ connected: true,
8116
+ agent: "",
8117
+ session: null,
8118
+ title: "",
8119
+ busy: false,
8120
+ turns: [],
8121
+ asking: [],
8122
+ cost: 0,
8123
+ tokens: {
8124
+ input: 0,
8125
+ output: 0
8126
+ },
8127
+ model: "",
8128
+ sessions: []
8129
+ };
8130
+ /** A block of context given from the tray is tagged with the chip it came from and the turn it came in with, so it can be
8131
+ * taken back out of what the model is sent when the chip is removed (M36, step 10). */
8132
+ const TAG = /^\[context (\S+) @(\S+)\]\n/;
8133
+ const WITHDRAWN = "[Context the person gave earlier here was withdrawn: they took it out of the conversation. Do not rely on it; read the notes again if you need them.]";
8134
+ const tagged = (key, turn, block) => `[context ${encodeURIComponent(key)} @${turn}]\n${block}\n[/context]\n\n`;
8135
+ const textOf = (content) => (Array.isArray(content) ? content : []).map((b) => b?.type === "text" ? b.text ?? "" : "").join("");
8136
+ const userText = (content) => typeof content === "string" ? content : textOf(content);
8137
+ var PiHost = class {
8138
+ #api;
8139
+ #model;
8140
+ #streamFn;
8141
+ #getApiKey;
8142
+ #id;
8143
+ #name;
8144
+ #ask;
8145
+ #more;
8146
+ #guard;
8147
+ #now;
8148
+ #store;
8149
+ #compaction;
8150
+ /** The thinking item the assistant message now streaming writes into. */
8151
+ #thinkingAt = -1;
8152
+ /** Cancels a summary being written. */
8153
+ #summarising = null;
8154
+ /** Why the last thing asked of the agent could not be done, shown in the pane. */
8155
+ #problem = "";
8156
+ /** The other sessions kept, by id (the one shown is in the fields below); and the last write, waited for before one is left. */
8157
+ #kept = /* @__PURE__ */ new Map();
8158
+ #saving = Promise.resolve();
8159
+ #agent = null;
8160
+ #session = "";
8161
+ #at = "";
8162
+ #title = "";
8163
+ #turns = [];
8164
+ #busy = false;
8165
+ /** The person stopped it: an aborted run is then a stop, not a failure. */
8166
+ #stopping = false;
8167
+ #cost = 0;
8168
+ #used = 0;
8169
+ #asking = /* @__PURE__ */ new Map();
8170
+ #allowed = /* @__PURE__ */ new Set();
8171
+ #pending = null;
8172
+ /** The blocks taken back out: `<key>@<turn>` of each, kept with the session. */
8173
+ #withdrawn = /* @__PURE__ */ new Set();
8174
+ #given = /* @__PURE__ */ new Set();
8175
+ /** What was said while it worked and has not reached the model: the item's id, and the text as the model will have it. */
8176
+ #queued = [];
8177
+ /** The first user message of a run is the prompt itself: it is not a queued message landing. */
8178
+ #primed = false;
8179
+ /** The text item the assistant message now streaming writes into. */
8180
+ #writing = -1;
8181
+ #view = EMPTY;
8182
+ #listeners = /* @__PURE__ */ new Set();
8183
+ constructor({ api, model, streamFn, id = "pi", name = "pi", ask = () => false, more = () => [], guard = () => void 0, clock = Date.now, store, compaction, getApiKey }) {
8184
+ this.#getApiKey = getApiKey;
8185
+ this.#api = api;
8186
+ this.#model = model;
8187
+ this.#streamFn = streamFn;
8188
+ this.#id = id;
8189
+ this.#name = name;
8190
+ this.#ask = ask;
8191
+ this.#more = more;
8192
+ this.#guard = guard;
8193
+ this.#now = clock;
8194
+ this.#store = store ?? null;
8195
+ this.#compaction = {
8196
+ enabled: compaction?.enabled ?? DEFAULT_COMPACTION_SETTINGS.enabled ?? true,
8197
+ reserveTokens: compaction?.reserveTokens ?? DEFAULT_COMPACTION_SETTINGS.reserveTokens ?? 16384,
8198
+ keepRecentTokens: compaction?.keepRecentTokens ?? DEFAULT_COMPACTION_SETTINGS.keepRecentTokens ?? 2e4
8199
+ };
8200
+ api.events?.((e) => {
8201
+ if (e.type !== "agent-item" || e.agent !== this.#id || !this.#turns.length) return;
8202
+ this.#edit((items) => [...items, e.item]);
8203
+ this.#tell();
8204
+ });
8205
+ this.#tell();
8206
+ }
8207
+ /** Carry on with another model (and the way to reach it): the session stays, and the next request goes to it. */
8208
+ use(model, streamFn, name) {
8209
+ this.#model = model;
8210
+ this.#streamFn = streamFn;
8211
+ if (name) this.#name = name;
8212
+ if (this.#agent) this.#agent.state.model = model;
8213
+ this.#tell();
8214
+ }
8215
+ view() {
8216
+ return this.#view;
8217
+ }
8218
+ listen(fn) {
8219
+ this.#listeners.add(fn);
8220
+ return () => {
8221
+ this.#listeners.delete(fn);
8222
+ };
8223
+ }
8224
+ asked(agent) {
8225
+ const t = this.#turns.at(-1);
8226
+ return agent === this.#id && t?.state === "running" ? t.asked : "";
8227
+ }
8228
+ async give(parts, render) {
8229
+ const fresh = parts.filter((g) => !this.#given.has(givenKey(g)));
8230
+ this.#pending = fresh.length ? {
8231
+ parts: fresh,
8232
+ blocks: fresh.map((g) => render([g]))
8233
+ } : null;
8234
+ }
8235
+ async prompt(text) {
8236
+ if (this.#busy) {
8237
+ await this.#say(text, "followUp");
8238
+ return;
8239
+ }
8240
+ this.#problem = "";
8241
+ if (!this.#agent) this.#open();
8242
+ const agent = this.#agent;
8243
+ const pending = this.#pending;
8244
+ this.#pending = null;
8245
+ if (pending) for (const g of pending.parts) this.#given.add(givenKey(g));
8246
+ agent.state.tools = [...await isoriteTools(this.#api, {
8247
+ agent: this.#id,
8248
+ session: this.#session
8249
+ }, () => this.asked(this.#id)), ...this.#more()];
8250
+ const turn = {
8251
+ id: randomUUID(),
8252
+ asked: text,
8253
+ from: "here",
8254
+ items: [],
8255
+ state: "running",
8256
+ ...pending ? { given: pending.parts } : {},
8257
+ at: new Date(this.#now()).toISOString()
8258
+ };
8259
+ this.#turns = [...this.#turns, turn];
8260
+ if (!this.#title) this.#title = text.slice(0, 80);
8261
+ this.#busy = true;
8262
+ this.#primed = false;
8263
+ this.#queued = [];
8264
+ this.#writing = -1;
8265
+ this.#thinkingAt = -1;
8266
+ this.#api.agentStatus(this.#id, this.#session, true).catch(() => void 0);
8267
+ this.#tell();
8268
+ const message = this.#message(pending, turn.id.slice(0, 8), text);
8269
+ (async () => {
8270
+ await this.#compactIfNeeded(agent);
8271
+ if (this.#stopping) throw new Error("stopped");
8272
+ await agent.prompt(message);
8273
+ })().then(() => void 0, (e) => {
8274
+ if (this.#stopping) this.#set("stopped");
8275
+ else this.#fail(e.message);
8276
+ }).then(() => this.#end());
8277
+ }
8278
+ /** The person's message for the model: each block given with it, tagged, then what they asked. */
8279
+ #message(pending, turn, text) {
8280
+ return {
8281
+ role: "user",
8282
+ content: [...(pending?.parts ?? []).map((g, i) => tagged(givenKey(g), turn, pending.blocks[i] ?? "")), text].map((t) => ({
8283
+ type: "text",
8284
+ text: t
8285
+ })),
8286
+ timestamp: this.#now()
8287
+ };
8288
+ }
8289
+ /** What the model is sent: the messages, with each withdrawn block replaced by a line saying so. */
8290
+ #redact(messages) {
8291
+ if (!this.#withdrawn.size) return messages;
8292
+ return messages.map((m) => {
8293
+ const msg = m;
8294
+ if (msg.role !== "user" || !Array.isArray(msg.content)) return m;
8295
+ let changed = false;
8296
+ const content = msg.content.map((c) => {
8297
+ const tag = c?.type === "text" && c.text ? TAG.exec(c.text) : null;
8298
+ if (!tag || !this.#withdrawn.has(`${decodeURIComponent(tag[1])}@${tag[2]}`)) return c;
8299
+ changed = true;
8300
+ return {
8301
+ type: "text",
8302
+ text: WITHDRAWN
8303
+ };
8304
+ });
8305
+ return changed ? {
8306
+ ...m,
8307
+ content
8308
+ } : m;
8309
+ });
8310
+ }
8311
+ /** The person took a chip out of the tray: the block given with it comes out of everything the model is sent from now
8312
+ * on. The turns it came in stay as they were, marked. Says whether there was anything to take out. */
8313
+ async withdraw(g) {
8314
+ const key = givenKey(g);
8315
+ if (this.#pending) {
8316
+ const keep = this.#pending.parts.map((p, i) => ({
8317
+ p,
8318
+ b: this.#pending.blocks[i]
8319
+ })).filter((x) => givenKey(x.p) !== key);
8320
+ this.#pending = keep.length ? {
8321
+ parts: keep.map((x) => x.p),
8322
+ blocks: keep.map((x) => x.b)
8323
+ } : null;
8324
+ }
8325
+ if (!this.#given.has(key)) return false;
8326
+ this.#given.delete(key);
8327
+ for (const m of this.#agent?.state.messages ?? []) {
8328
+ const msg = m;
8329
+ if (msg.role !== "user" || !Array.isArray(msg.content)) continue;
8330
+ for (const c of msg.content) {
8331
+ const tag = c?.type === "text" && c.text ? TAG.exec(c.text) : null;
8332
+ if (tag && decodeURIComponent(tag[1]) === key) this.#withdrawn.add(`${key}@${tag[2]}`);
8333
+ }
8334
+ }
8335
+ this.#turns = this.#turns.map((t) => {
8336
+ const at = (t.given ?? []).flatMap((x, i) => givenKey(x) === key && !t.withdrawn?.includes(i) ? [i] : []);
8337
+ return at.length ? {
8338
+ ...t,
8339
+ withdrawn: [...t.withdrawn ?? [], ...at]
8340
+ } : t;
8341
+ });
8342
+ this.#save();
8343
+ this.#keep();
8344
+ this.#tell();
8345
+ return true;
8346
+ }
8347
+ async steer(text) {
8348
+ if (!this.#busy) throw new Error("the agent is not working: ask it instead");
8349
+ await this.#say(text, "steer");
8350
+ }
8351
+ /** Said while it works: a steer comes in after the step now running, a follow-up when it would otherwise stop. */
8352
+ async #say(text, mode) {
8353
+ const agent = this.#agent;
8354
+ if (!agent) return;
8355
+ const pending = this.#pending;
8356
+ this.#pending = null;
8357
+ if (pending) for (const g of pending.parts) this.#given.add(givenKey(g));
8358
+ const id = randomUUID();
8359
+ const message = this.#message(pending, (this.#turns.at(-1)?.id ?? id).slice(0, 8), text);
8360
+ this.#queued.push({
8361
+ id,
8362
+ body: userText(message.content)
8363
+ });
8364
+ this.#edit((items) => [...items, {
8365
+ kind: "message",
8366
+ id,
8367
+ text,
8368
+ mode,
8369
+ state: "queued"
8370
+ }]);
8371
+ if (mode === "steer") agent.steer(message);
8372
+ else agent.followUp(message);
8373
+ this.#tell();
8374
+ }
8375
+ async interrupt() {
8376
+ this.#stopping = true;
8377
+ this.#summarising?.abort();
8378
+ this.#agent?.clearAllQueues();
8379
+ for (const a of this.#asking.values()) a.wait("reject");
8380
+ this.#agent?.abort();
8381
+ }
8382
+ async reply(id, decision) {
8383
+ const asked = this.#asking.get(id);
8384
+ if (!asked) throw new Error("the agent is not asking that");
8385
+ asked.wait(decision);
8386
+ }
8387
+ /** Show another kept session, and carry on in it. The one left is already kept (it is written after each turn). */
8388
+ async follow(_agent, session) {
8389
+ if (session === this.#session) return;
8390
+ if (this.#busy) throw new Error("the agent is working: stop it first");
8391
+ const kept = await this.#fetch(session);
8392
+ this.#restore(kept);
8393
+ }
8394
+ /** A new session from the pane; the one shown stays kept and can be shown again. */
8395
+ async fresh(_agent) {
8396
+ if (this.#busy) throw new Error("the agent is working: stop it first");
8397
+ await this.#saving;
8398
+ this.#blank();
8399
+ this.#tell();
8400
+ }
8401
+ /** A new session that begins where `session` is (its turns and the model's messages), going its own way from there;
8402
+ * its cost starts again at nothing. The original is not changed. */
8403
+ async fork(_agent, session) {
8404
+ if (this.#busy) throw new Error("the agent is working: stop it first");
8405
+ const from = session === this.#session && this.#agent ? this.#record() : await this.#fetch(session);
8406
+ if (!from || !this.#store) throw new Error("that session is not kept, so there is nothing to fork");
8407
+ const now = new Date(this.#now()).toISOString();
8408
+ const copy = {
8409
+ ...JSON.parse(JSON.stringify(from)),
8410
+ session: randomUUID(),
8411
+ title: `${from.title || "A session"} (fork)`.slice(0, 90),
8412
+ at: now,
8413
+ last: now,
8414
+ cost: 0
8415
+ };
8416
+ await this.#store.save(copy);
8417
+ this.#kept.set(copy.session, this.#summary(copy));
8418
+ this.#restore(copy);
8419
+ }
8420
+ set() {
8421
+ throw new Error(`${this.#name} offers no modes or options yet`);
8422
+ }
8423
+ /** Remove what is kept of a session. The one shown is left first, for a new one. */
8424
+ async forget(_agent, session) {
8425
+ if (!this.#store) throw new Error("nothing is kept apart from the session shown");
8426
+ if (session === this.#session) {
8427
+ if (this.#busy) throw new Error("the agent is working: stop it first");
8428
+ await this.#saving;
8429
+ this.#blank();
8430
+ }
8431
+ await this.#store.remove(session);
8432
+ this.#kept.delete(session);
8433
+ this.#tell();
8434
+ }
8435
+ /** Read the list of kept sessions again. */
8436
+ refresh() {
8437
+ this.#index().then(() => this.#tell());
8438
+ }
8439
+ async #index() {
8440
+ if (!this.#store) return;
8441
+ const { records, unreadable } = await this.#store.list();
8442
+ this.#kept = new Map(records.map((r) => [r.session, this.#summary(r)]));
8443
+ if (unreadable.length) this.#problem = `${unreadable.length === 1 ? "A saved session" : `${unreadable.length} saved sessions`} could not be read (${unreadable.join(", ")}). ${unreadable.length === 1 ? "It was" : "They were"} left where ${unreadable.length === 1 ? "it is" : "they are"}.`;
8444
+ }
8445
+ #summary(r) {
8446
+ return {
8447
+ session: r.session,
8448
+ title: r.title,
8449
+ asked: r.turns[0]?.asked ?? "",
8450
+ last: r.last,
8451
+ turns: r.turns.length,
8452
+ notes: 0,
8453
+ live: false,
8454
+ busy: false,
8455
+ kept: true,
8456
+ open: true
8457
+ };
8458
+ }
8459
+ async #fetch(session) {
8460
+ if (!this.#store) throw new Error("that session is not here to carry on");
8461
+ await this.#saving;
8462
+ const kept = await this.#store.load(session);
8463
+ if (!kept) throw new Error("that session is not kept");
8464
+ return kept;
8465
+ }
8466
+ /** No session shown: the next question begins a new one. Everything of the last is let go (it stays kept). */
8467
+ #blank() {
8468
+ this.#agent = null;
8469
+ this.#session = "";
8470
+ this.#at = "";
8471
+ this.#turns = [];
8472
+ this.#title = "";
8473
+ this.#cost = 0;
8474
+ this.#used = 0;
8475
+ this.#given = /* @__PURE__ */ new Set();
8476
+ this.#withdrawn = /* @__PURE__ */ new Set();
8477
+ this.#pending = null;
8478
+ this.#queued = [];
8479
+ this.#problem = "";
8480
+ this.#writing = -1;
8481
+ this.#thinkingAt = -1;
8482
+ this.#primed = false;
8483
+ this.#stopping = false;
8484
+ }
8485
+ /** Show `kept` and carry on from it: its turns for the pane, its messages for the model. A turn that was running when
8486
+ * the server went down is shown as stopped: it never finished. */
8487
+ #restore(kept) {
8488
+ this.#blank();
8489
+ this.#open(kept);
8490
+ this.#turns = kept.turns.map((t) => t.state === "running" ? {
8491
+ ...t,
8492
+ state: "stopped"
8493
+ } : t);
8494
+ this.#title = kept.title;
8495
+ this.#cost = kept.cost;
8496
+ this.#used = kept.used;
8497
+ this.#given = new Set(kept.given);
8498
+ this.#withdrawn = new Set(kept.withdrawn ?? []);
8499
+ this.#tell();
8500
+ }
8501
+ /** Take up the session last kept, if there is one. An unreadable one is reported in the pane and left alone, and the
8502
+ * agent starts fresh. */
8503
+ async resume() {
8504
+ if (!this.#store || this.#agent) return;
8505
+ let kept;
8506
+ try {
8507
+ kept = await this.#store.latest();
8508
+ } catch (e) {
8509
+ this.#problem = e.message;
8510
+ this.#tell();
8511
+ return;
8512
+ }
8513
+ try {
8514
+ await this.#index();
8515
+ } catch {}
8516
+ if (!kept) {
8517
+ this.#tell();
8518
+ return;
8519
+ }
8520
+ this.#restore(kept);
8521
+ }
8522
+ #record() {
8523
+ return {
8524
+ version: 1,
8525
+ session: this.#session,
8526
+ title: this.#title,
8527
+ at: this.#at,
8528
+ last: this.#turns.at(-1)?.at ?? this.#at,
8529
+ cost: this.#cost,
8530
+ used: this.#used,
8531
+ given: [...this.#given],
8532
+ withdrawn: [...this.#withdrawn],
8533
+ turns: this.#turns,
8534
+ messages: JSON.parse(JSON.stringify(this.#agent.state.messages)).filter((m) => m.role !== "system")
8535
+ };
8536
+ }
8537
+ #save() {
8538
+ if (!this.#store || !this.#agent || !this.#turns.length) return;
8539
+ const record = this.#record();
8540
+ this.#kept.set(record.session, this.#summary(record));
8541
+ const store = this.#store;
8542
+ this.#saving = this.#saving.then(() => store.save(record)).catch((e) => {
8543
+ this.#problem = `Could not keep this session: ${e.message}`;
8544
+ this.#tell();
8545
+ });
8546
+ }
8547
+ #open(kept) {
8548
+ this.#session = kept?.session ?? randomUUID();
8549
+ this.#at = kept?.at ?? new Date(this.#now()).toISOString();
8550
+ this.#agent = new Agent({
8551
+ initialState: {
8552
+ systemPrompt: INSTRUCTIONS,
8553
+ model: this.#model,
8554
+ ...kept ? { messages: kept.messages } : {}
8555
+ },
8556
+ transformContext: async (messages) => this.#redact(messages),
8557
+ streamFn: ((...args) => this.#streamFn(...args)),
8558
+ ...this.#getApiKey ? { getApiKey: this.#getApiKey } : {},
8559
+ sessionId: this.#session,
8560
+ beforeToolCall: async ({ toolCall, args }) => {
8561
+ let refused;
8562
+ try {
8563
+ refused = this.#guard({
8564
+ tool: toolCall.name,
8565
+ args
8566
+ });
8567
+ } catch {
8568
+ refused = "that could not be checked";
8569
+ }
8570
+ if (refused) return {
8571
+ block: true,
8572
+ reason: refused
8573
+ };
8574
+ if (!this.#ask(toolCall.name) || this.#allowed.has(toolCall.name)) return void 0;
8575
+ const id = randomUUID();
8576
+ const decision = await new Promise((wait) => {
8577
+ this.#asking.set(id, {
8578
+ id,
8579
+ action: toolCall.name,
8580
+ resources: [titleOf(args)],
8581
+ tool: toolCall.name,
8582
+ wait
8583
+ });
8584
+ this.#tell();
8585
+ });
8586
+ this.#asking.delete(id);
8587
+ this.#tell();
8588
+ if (decision === "reject") return {
8589
+ block: true,
8590
+ reason: "The person declined this."
8591
+ };
8592
+ if (decision === "always") this.#allowed.add(toolCall.name);
8593
+ }
8594
+ });
8595
+ this.#agent.subscribe((e) => this.#on(e));
8596
+ }
8597
+ #on(e) {
8598
+ if (e.type === "message_start" && e.message.role === "user") {
8599
+ if (!this.#primed) {
8600
+ this.#primed = true;
8601
+ return;
8602
+ }
8603
+ const said = userText(e.message.content);
8604
+ const at = this.#queued.findIndex((q) => q.body === said);
8605
+ if (at < 0) return;
8606
+ const [q] = this.#queued.splice(at, 1);
8607
+ this.#edit((items) => {
8608
+ const was = items.find((i) => i.kind === "message" && i.id === q.id);
8609
+ return was ? [...items.filter((i) => i !== was), {
8610
+ ...was,
8611
+ state: "landed"
8612
+ }] : items;
8613
+ });
8614
+ } else if (e.type === "message_start" && e.message.role === "assistant") {
8615
+ this.#edit((items) => [...items, {
8616
+ kind: "text",
8617
+ text: ""
8618
+ }]);
8619
+ this.#writing = (this.#turns.at(-1)?.items.length ?? 1) - 1;
8620
+ } else if (e.type === "message_update" && e.assistantMessageEvent.type === "thinking_delta") {
8621
+ const delta = e.assistantMessageEvent.delta;
8622
+ if (this.#thinkingAt < 0) {
8623
+ const at = Math.max(this.#writing, 0);
8624
+ this.#edit((items) => [
8625
+ ...items.slice(0, at),
8626
+ {
8627
+ kind: "thinking",
8628
+ text: ""
8629
+ },
8630
+ ...items.slice(at)
8631
+ ]);
8632
+ this.#thinkingAt = at;
8633
+ this.#writing = at + 1;
8634
+ }
8635
+ const t = this.#thinkingAt;
8636
+ this.#edit((items) => items.map((i, k) => k === t && i.kind === "thinking" ? {
8637
+ ...i,
8638
+ text: i.text + delta
8639
+ } : i));
8640
+ } else if (e.type === "message_update" && e.assistantMessageEvent.type === "text_delta") {
8641
+ const at = this.#writing, delta = e.assistantMessageEvent.delta;
8642
+ this.#edit((items) => items.map((i, k) => k === at && i.kind === "text" ? {
8643
+ ...i,
8644
+ text: i.text + delta
8645
+ } : i));
8646
+ } else if (e.type === "message_end" && e.message.role === "assistant") {
8647
+ const m = e.message;
8648
+ const at = this.#writing, think = this.#thinkingAt, whole = textOf(m.content);
8649
+ const thought = (Array.isArray(m.content) ? m.content : []).map((b) => b?.type === "thinking" ? b.thinking ?? "" : "").join("");
8650
+ this.#edit((items) => items.flatMap((i, k) => k === think ? thought.trim() ? [{
8651
+ kind: "thinking",
8652
+ text: thought
8653
+ }] : [] : k !== at ? [i] : whole ? [{
8654
+ kind: "text",
8655
+ text: whole
8656
+ }] : []));
8657
+ this.#writing = -1;
8658
+ this.#thinkingAt = -1;
8659
+ this.#cost += m.usage?.cost?.total ?? 0;
8660
+ this.#used = m.usage?.totalTokens ?? this.#used;
8661
+ } else if (e.type === "tool_execution_start") this.#edit((items) => [...items, {
8662
+ kind: "tool",
8663
+ id: e.toolCallId,
8664
+ name: e.toolName,
8665
+ title: titleOf(e.args),
8666
+ status: "running",
8667
+ output: ""
8668
+ }]);
8669
+ else if (e.type === "tool_execution_end") {
8670
+ const out = textOf(e.result?.content).slice(0, OUTPUT);
8671
+ this.#edit((items) => items.map((i) => i.kind === "tool" && i.id === e.toolCallId ? {
8672
+ ...i,
8673
+ status: e.isError ? "failed" : "done",
8674
+ output: out
8675
+ } : i));
8676
+ } else return;
8677
+ this.#tell();
8678
+ }
8679
+ #fail(message) {
8680
+ this.#edit((items) => [...items, {
8681
+ kind: "text",
8682
+ text: `(${message})`
8683
+ }]);
8684
+ this.#set("failed");
8685
+ }
8686
+ #end() {
8687
+ this.#dropQueued();
8688
+ const last = [...this.#agent?.state.messages ?? []].reverse().find((m) => m.role === "assistant");
8689
+ if (this.#turns.at(-1)?.state === "running") {
8690
+ if (this.#stopping || last?.stopReason === "aborted") this.#set("stopped");
8691
+ else if (last?.stopReason === "error") this.#fail(last.errorMessage ?? "the model failed");
8692
+ else this.#set("done");
8693
+ }
8694
+ this.#busy = false;
8695
+ this.#stopping = false;
8696
+ this.#api.agentStatus(this.#id, this.#session, false).catch(() => void 0);
8697
+ this.#keep();
8698
+ this.#save();
8699
+ this.#tell();
8700
+ }
8701
+ /** What the model would be sent now: the last answer's own count, and an estimate for what came after it. */
8702
+ #contextTokens(messages) {
8703
+ let last = -1;
8704
+ for (let i = messages.length - 1; i >= 0; i--) {
8705
+ const m = messages[i];
8706
+ if (m.role === "assistant" && m.usage?.totalTokens && m.stopReason !== "aborted" && m.stopReason !== "error") {
8707
+ last = i;
8708
+ break;
8709
+ }
8710
+ }
8711
+ return (last >= 0 ? messages[last].usage.totalTokens : 0) + messages.slice(last + 1).reduce((n, m) => n + estimateTokens(m), 0);
8712
+ }
8713
+ /** Before a prompt: if what the model would be sent nears its window, summarise the earlier part of the conversation
8714
+ * and keep the recent part as it was (cut at a message of the person's, so a tool call and its result stay together).
8715
+ * The turn shows it. If the summary cannot be made the conversation goes on as it was and the turn says so. */
8716
+ async #compactIfNeeded(agent) {
8717
+ const window = this.#model.contextWindow, settings = this.#compaction;
8718
+ if (!settings.enabled || !window) return;
8719
+ const messages = [...agent.state.messages];
8720
+ const before = this.#contextTokens(messages);
8721
+ if (!shouldCompact(before, window, settings)) return;
8722
+ const roleOf = (m) => m?.role;
8723
+ let head = 0;
8724
+ while (roleOf(messages[head]) === "system") head++;
8725
+ const earlier = messages[head]?.content;
8726
+ const previous = userText(earlier).startsWith(SUMMARY) ? userText(earlier).slice(65) : void 0;
8727
+ const from = head + (previous === void 0 ? 0 : 1);
8728
+ let acc = 0, cut = from;
8729
+ for (let i = messages.length - 1; i >= from; i--) {
8730
+ acc += estimateTokens(messages[i]);
8731
+ if (acc >= settings.keepRecentTokens) {
8732
+ cut = i;
8733
+ break;
8734
+ }
8735
+ }
8736
+ while (cut > from && roleOf(messages[cut]) !== "user") cut--;
8737
+ if (cut <= from) return;
8738
+ const id = this.#turns.at(-1)?.items.length ?? 0;
8739
+ this.#edit((items) => [...items, {
8740
+ kind: "compaction",
8741
+ state: "running",
8742
+ before
8743
+ }]);
8744
+ this.#tell();
8745
+ this.#summarising = new AbortController();
8746
+ try {
8747
+ const summary = await generateSummary(this.#redact(messages.slice(from, cut)), this.#model, settings.reserveTokens, void 0, void 0, this.#summarising.signal, void 0, previous, void 0, this.#streamFn);
8748
+ const stand = {
8749
+ role: "user",
8750
+ content: [{
8751
+ type: "text",
8752
+ text: SUMMARY + summary
8753
+ }],
8754
+ timestamp: this.#now()
8755
+ };
8756
+ const recent = messages.slice(cut).map((m) => m.role === "assistant" ? {
8757
+ ...m,
8758
+ usage: {
8759
+ ...m.usage,
8760
+ totalTokens: 0
8761
+ }
8762
+ } : m);
8763
+ const kept = [
8764
+ ...messages.slice(0, head),
8765
+ stand,
8766
+ ...recent
8767
+ ];
8768
+ agent.state.messages = kept;
8769
+ const after = this.#contextTokens(kept);
8770
+ this.#used = after;
8771
+ this.#edit((items) => items.map((i, k) => k === id && i.kind === "compaction" ? {
8772
+ ...i,
8773
+ state: "done",
8774
+ after,
8775
+ summary
8776
+ } : i));
8777
+ } catch (e) {
8778
+ const error = this.#stopping ? "stopped" : e.message;
8779
+ this.#edit((items) => items.map((i, k) => k === id && i.kind === "compaction" ? {
8780
+ ...i,
8781
+ state: "failed",
8782
+ error
8783
+ } : i));
8784
+ } finally {
8785
+ this.#summarising = null;
8786
+ this.#tell();
8787
+ }
8788
+ }
8789
+ /** Said but never taken up (the run ended or was stopped first). */
8790
+ #dropQueued() {
8791
+ if (!this.#queued.length) return;
8792
+ const ids = new Set(this.#queued.map((q) => q.id));
8793
+ this.#queued = [];
8794
+ this.#agent?.clearAllQueues();
8795
+ this.#edit((items) => items.map((i) => i.kind === "message" && ids.has(i.id) ? {
8796
+ ...i,
8797
+ state: "dropped"
8798
+ } : i));
8799
+ }
8800
+ #set(state) {
8801
+ const turns = [...this.#turns], t = turns.at(-1);
8802
+ if (t) turns[turns.length - 1] = {
8803
+ ...t,
8804
+ state
8805
+ };
8806
+ this.#turns = turns;
8807
+ }
8808
+ #edit(fn) {
8809
+ const turns = [...this.#turns], t = turns.at(-1);
8810
+ if (!t) return;
8811
+ turns[turns.length - 1] = {
8812
+ ...t,
8813
+ items: fn([...t.items])
8814
+ };
8815
+ this.#turns = turns;
8816
+ }
8817
+ #keep() {
8818
+ if (!this.#api.keepAgentRecord || !this.#turns.length) return;
8819
+ const last = this.#turns.at(-1)?.at ?? this.#at;
8820
+ this.#api.keepAgentRecord({
8821
+ agent: this.#id,
8822
+ name: this.#name,
8823
+ session: this.#session,
8824
+ title: this.#title,
8825
+ at: this.#at,
8826
+ last,
8827
+ turns: this.#turns
8828
+ }).catch(() => void 0);
8829
+ }
8830
+ /** The session shown, with the others kept, newest first. */
8831
+ #sessions(entry) {
8832
+ const others = [...this.#kept.values()].filter((k) => k.session !== this.#session).map((k) => ({
8833
+ agent: this.#id,
8834
+ name: this.#name,
8835
+ ...k
8836
+ }));
8837
+ return [...entry ? [entry] : [], ...others].sort((a, b) => a.last < b.last ? 1 : a.last > b.last ? -1 : 0);
8838
+ }
8839
+ #tell() {
8840
+ const last = this.#turns.at(-1)?.at ?? this.#at;
8841
+ const entry = this.#session ? {
8842
+ agent: this.#id,
8843
+ name: this.#name,
8844
+ session: this.#session,
8845
+ title: this.#title,
8846
+ asked: this.#turns[0]?.asked ?? "",
8847
+ last,
8848
+ turns: this.#turns.length,
8849
+ notes: 0,
8850
+ live: true,
8851
+ busy: this.#busy,
8852
+ kept: Boolean(this.#store),
8853
+ open: true
8854
+ } : null;
8855
+ this.#view = {
8856
+ connected: true,
8857
+ agent: this.#name,
8858
+ agentId: this.#id,
8859
+ session: this.#session || null,
8860
+ title: this.#title,
8861
+ busy: this.#busy,
8862
+ turns: this.#turns,
8863
+ asking: [...this.#asking.values()].map(({ wait: _, ...p }) => p),
8864
+ cost: this.#cost,
8865
+ tokens: {
8866
+ input: this.#used,
8867
+ output: 0
8868
+ },
8869
+ model: this.#model.id ?? "",
8870
+ ...this.#problem ? { problem: this.#problem } : {},
8871
+ steer: true,
8872
+ ...this.#model.contextWindow ? { size: this.#model.contextWindow } : {},
8873
+ fork: Boolean(this.#store),
8874
+ sessions: this.#sessions(entry)
8875
+ };
8876
+ for (const fn of this.#listeners) fn(this.#view);
8877
+ }
8878
+ };
8879
+ //#endregion
8880
+ //#region packages/server/src/pi/files.ts
8881
+ const READS = [
8882
+ "read",
8883
+ "grep",
8884
+ "find",
8885
+ "ls"
8886
+ ];
8887
+ const WRITES = ["edit", "write"];
8888
+ function fileTools(cwd, { files, shell }) {
8889
+ const tools = [];
8890
+ if (files !== "off") tools.push(createReadTool(cwd), createGrepTool(cwd), createFindTool(cwd), createLsTool(cwd));
8891
+ if (files === "write") tools.push(createEditTool(cwd), createWriteTool(cwd));
8892
+ if (shell) tools.push(createBashTool(cwd));
8893
+ return tools;
8894
+ }
8895
+ /** A reason to refuse the call, or undefined to let it through. Anything that cannot be checked is refused. */
8896
+ function pathGuard(cwd) {
8897
+ const root = resolve(cwd);
8898
+ return ({ tool, args }) => {
8899
+ if (![...READS, ...WRITES].includes(tool)) return void 0;
8900
+ const given = args?.path;
8901
+ if (given === void 0 && (tool === "ls" || tool === "grep" || tool === "find")) return void 0;
8902
+ if (typeof given !== "string" || !given) return `${tool} needs a path inside the project`;
8903
+ const target = isAbsolute(given) ? resolve(given) : resolve(root, given);
8904
+ const inside = relative(root, target);
8905
+ if (inside.startsWith("..") || isAbsolute(inside)) return `${given} is outside the project folder`;
8906
+ if (WRITES.includes(tool) && inside.split(sep).includes(".git")) return `${tool} does not reach .git`;
8907
+ };
8908
+ }
8909
+ //#endregion
8910
+ //#region packages/server/src/pi/credentials.ts
8911
+ /** The providers the settings offer: those a person signs up to with a key of their own. Pi's subscription sign-ins
8912
+ * (for a ChatGPT or Copilot plan) are not offered: their terms are the provider's, not isorite's to rely on. */
8913
+ const PROVIDERS = [
8914
+ {
8915
+ id: "openrouter",
8916
+ name: "OpenRouter",
8917
+ env: "OPENROUTER_API_KEY"
8918
+ },
8919
+ {
8920
+ id: "anthropic",
8921
+ name: "Anthropic (API key)",
8922
+ env: "ANTHROPIC_API_KEY"
8923
+ },
8924
+ {
8925
+ id: "openai",
8926
+ name: "OpenAI (API key)",
8927
+ env: "OPENAI_API_KEY"
8928
+ },
8929
+ {
8930
+ id: "google",
8931
+ name: "Google Gemini",
8932
+ env: "GEMINI_API_KEY"
8933
+ },
8934
+ {
8935
+ id: "mistral",
8936
+ name: "Mistral",
8937
+ env: "MISTRAL_API_KEY"
8938
+ },
8939
+ {
8940
+ id: "groq",
8941
+ name: "Groq",
8942
+ env: "GROQ_API_KEY"
8943
+ },
8944
+ {
8945
+ id: "xai",
8946
+ name: "xAI",
8947
+ env: "XAI_API_KEY"
8948
+ },
8949
+ {
8950
+ id: "deepseek",
8951
+ name: "DeepSeek",
8952
+ env: "DEEPSEEK_API_KEY"
8953
+ },
8954
+ {
8955
+ id: "together",
8956
+ name: "Together",
8957
+ env: "TOGETHER_API_KEY"
8958
+ },
8959
+ {
8960
+ id: "fireworks",
8961
+ name: "Fireworks",
8962
+ env: "FIREWORKS_API_KEY"
8963
+ },
8964
+ {
8965
+ id: "cerebras",
8966
+ name: "Cerebras",
8967
+ env: "CEREBRAS_API_KEY"
8968
+ }
8969
+ ];
8970
+ /** The default: the project's files to read, nothing to change them with, no shell. */
8971
+ const DEFAULT_REACH = {
8972
+ files: "read",
8973
+ shell: false
8974
+ };
8975
+ const ID = /^[a-z0-9-]{1,40}$/;
8976
+ /** Where isorite keeps what is its own, not the base's. */
8977
+ function dataDir(env = process.env) {
8978
+ return join(env.XDG_DATA_HOME || join(homedir(), ".local", "share"), "isorite");
8979
+ }
8980
+ var Credentials = class {
8981
+ #file;
8982
+ #dir;
8983
+ #env;
8984
+ #kept = null;
8985
+ #writing = Promise.resolve();
8986
+ constructor(env = process.env) {
8987
+ this.#env = env;
8988
+ this.#dir = dataDir(env);
8989
+ this.#file = join(this.#dir, "credentials.json");
8990
+ }
8991
+ async #load() {
8992
+ if (this.#kept) return this.#kept;
8993
+ let text;
8994
+ try {
8995
+ text = await readFile(this.#file, "utf8");
8996
+ } catch (e) {
8997
+ if (e.code === "ENOENT") return this.#kept = {
8998
+ version: 1,
8999
+ keys: {}
9000
+ };
9001
+ throw new Error(`The keys file ${this.#file} could not be read (${e.code ?? "unreadable"}). It was left where it is.`);
9002
+ }
9003
+ let parsed;
9004
+ try {
9005
+ parsed = JSON.parse(text);
9006
+ } catch {
9007
+ throw new Error(`The keys file ${this.#file} is not valid JSON. It was left where it is.`);
9008
+ }
9009
+ if (!parsed || parsed.version !== 1 || typeof parsed.keys !== "object" || parsed.keys === null) throw new Error(`The keys file ${this.#file} is not one this version of isorite wrote. It was left where it is.`);
9010
+ return this.#kept = {
9011
+ version: 1,
9012
+ keys: parsed.keys,
9013
+ ...parsed.choice ? { choice: parsed.choice } : {},
9014
+ ...parsed.reach ? { reach: parsed.reach } : {}
9015
+ };
9016
+ }
9017
+ async #save(next) {
9018
+ const run = async () => {
9019
+ await mkdir(this.#dir, {
9020
+ recursive: true,
9021
+ mode: 448
9022
+ });
9023
+ const tmp = `${this.#file}.${process.pid}.tmp`;
9024
+ await writeFile(tmp, JSON.stringify(next), { mode: 384 });
9025
+ await rename(tmp, this.#file);
9026
+ this.#kept = next;
9027
+ };
9028
+ const turn = this.#writing.then(run, run);
9029
+ this.#writing = turn.catch(() => void 0);
9030
+ await turn;
9031
+ }
9032
+ /** The key to use for `provider`: the one saved here, else the provider's own variable in the environment. */
9033
+ async key(provider) {
9034
+ const kept = (await this.#load()).keys[provider];
9035
+ if (kept) return kept;
9036
+ const env = PROVIDERS.find((p) => p.id === provider)?.env;
9037
+ return env && this.#env[env] || void 0;
9038
+ }
9039
+ /** Where each provider's key comes from, never the key. */
9040
+ async sources() {
9041
+ const kept = (await this.#load()).keys;
9042
+ return Object.fromEntries(PROVIDERS.map((p) => [p.id, kept[p.id] ? "saved" : this.#env[p.env] ? "environment" : null]));
9043
+ }
9044
+ async set(provider, key) {
9045
+ if (!PROVIDERS.some((p) => p.id === provider)) throw new Error(`"${provider}" is not a provider isorite offers`);
9046
+ const k = key.trim();
9047
+ if (!k || k.length > 1e3 || /\s/.test(k)) throw new Error("that does not look like a key (it is empty, has spaces, or is very long)");
9048
+ const now = await this.#load();
9049
+ await this.#save({
9050
+ ...now,
9051
+ keys: {
9052
+ ...now.keys,
9053
+ [provider]: k
9054
+ }
9055
+ });
9056
+ }
9057
+ async remove(provider) {
9058
+ const now = await this.#load();
9059
+ if (!(provider in now.keys)) return;
9060
+ const { [provider]: _gone, ...keys } = now.keys;
9061
+ await this.#save({
9062
+ ...now,
9063
+ keys
9064
+ });
9065
+ }
9066
+ /** What was chosen in the settings, else the default. Anything else in the file is not believed. */
9067
+ async reach() {
9068
+ const r = (await this.#load()).reach;
9069
+ return r && [
9070
+ "off",
9071
+ "read",
9072
+ "write"
9073
+ ].includes(r.files) && typeof r.shell === "boolean" ? {
9074
+ files: r.files,
9075
+ shell: r.shell
9076
+ } : { ...DEFAULT_REACH };
9077
+ }
9078
+ async setReach(reach) {
9079
+ if (![
9080
+ "off",
9081
+ "read",
9082
+ "write"
9083
+ ].includes(reach.files) || typeof reach.shell !== "boolean") throw new Error("files is off, read or write; shell is on or off");
9084
+ const now = await this.#load();
9085
+ await this.#save({
9086
+ ...now,
9087
+ reach: {
9088
+ files: reach.files,
9089
+ shell: reach.shell
9090
+ }
9091
+ });
9092
+ }
9093
+ async choice() {
9094
+ return (await this.#load()).choice ?? null;
9095
+ }
9096
+ async choose(provider, model) {
9097
+ if (!PROVIDERS.some((p) => p.id === provider) || !ID.test(provider)) throw new Error(`"${provider}" is not a provider isorite offers`);
9098
+ if (!model.trim() || model.length > 200) throw new Error("a model is needed");
9099
+ const now = await this.#load();
9100
+ await this.#save({
9101
+ ...now,
9102
+ choice: {
9103
+ provider,
9104
+ model: model.trim()
9105
+ }
9106
+ });
9107
+ }
9108
+ };
9109
+ //#endregion
9110
+ //#region packages/server/src/pi/index.ts
9111
+ const camel = (s) => s.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
9112
+ /** The model `provider/model` and the way to reach it. Throws (naming what is wrong) where pi has no such provider or model. */
9113
+ async function modelFor(provider, model) {
9114
+ if (!provider || !model || !/^[a-z0-9-]+$/.test(provider)) throw new Error(`a model is <provider>/<model>, as anthropic/claude-sonnet-4-6; it was "${provider}/${model}"`);
9115
+ let make;
9116
+ try {
9117
+ make = (await import(`@earendil-works/pi-ai/providers/${provider}`))[`${camel(provider)}Provider`];
9118
+ } catch {}
9119
+ if (typeof make !== "function") throw new Error(`pi has no provider "${provider}"`);
9120
+ const models = createModels();
9121
+ models.setProvider(make());
9122
+ const chosen = models.getModel(provider, model);
9123
+ if (!chosen) throw new Error(`pi's ${provider} has no model "${model}"`);
9124
+ return {
9125
+ chosen,
9126
+ streamFn: models.streamSimple.bind(models)
9127
+ };
9128
+ }
9129
+ /** The model named by ISORITE_PI, else the one chosen in the settings; null where there is none. */
9130
+ async function wanted(env, creds) {
9131
+ const named = env.ISORITE_PI?.trim();
9132
+ if (named) {
9133
+ const [provider, ...rest] = named.split("/");
9134
+ if (!provider || !rest.length) throw new Error(`ISORITE_PI is <provider>/<model>, as anthropic/claude-sonnet-4-6; it was "${named}"`);
9135
+ return {
9136
+ choice: {
9137
+ provider,
9138
+ model: rest.join("/")
9139
+ },
9140
+ source: "environment"
9141
+ };
9142
+ }
9143
+ const saved = await creds.choice();
9144
+ return saved ? {
9145
+ choice: saved,
9146
+ source: "settings"
9147
+ } : null;
9148
+ }
9149
+ /** What the settings read and change: which model the native agent uses and where its key comes from, for the
9150
+ * hosts running (one for each base). It never hands out a key. */
9151
+ var PiControl = class {
9152
+ creds;
9153
+ #env;
9154
+ #hosts = /* @__PURE__ */ new Set();
9155
+ constructor(env = process.env, creds = new Credentials(env)) {
9156
+ this.#env = env;
9157
+ this.creds = creds;
9158
+ }
9159
+ /** What the agent may reach now, read synchronously where a prompt starts. Loaded by `load`. */
9160
+ reach = { ...DEFAULT_REACH };
9161
+ /** True where the environment (ISORITE_PI_FILES, ISORITE_PI_SHELL) sets it, and so wins. */
9162
+ reachFromEnvironment = false;
9163
+ async load() {
9164
+ const files = this.#env.ISORITE_PI_FILES?.trim(), shell = this.#env.ISORITE_PI_SHELL?.trim();
9165
+ if (files !== void 0 && files !== "" && ![
9166
+ "off",
9167
+ "read",
9168
+ "write"
9169
+ ].includes(files)) throw new Error(`ISORITE_PI_FILES is off, read or write; it was "${files}"`);
9170
+ this.reachFromEnvironment = Boolean(files) || shell !== void 0 && shell !== "";
9171
+ const saved = await this.creds.reach();
9172
+ this.reach = {
9173
+ files: files || saved.files,
9174
+ shell: shell ? [
9175
+ "1",
9176
+ "true",
9177
+ "on"
9178
+ ].includes(shell) : saved.shell
9179
+ };
9180
+ }
9181
+ async setReach(reach) {
9182
+ await this.creds.setReach(reach);
9183
+ if (!this.reachFromEnvironment) this.reach = { ...reach };
9184
+ }
9185
+ attach(host) {
9186
+ this.#hosts.add(host);
9187
+ }
9188
+ async state() {
9189
+ await this.load().catch(() => void 0);
9190
+ const want = await wanted(this.#env, this.creds).catch(() => null);
9191
+ return {
9192
+ /** True where the pane of this base is the native agent's now. */
9193
+ active: this.#hosts.size > 0,
9194
+ choice: want?.choice ?? null,
9195
+ /** Where the choice is made: the environment wins over the settings while it is set. */
9196
+ source: want?.source ?? null,
9197
+ providers: PROVIDERS.map((p) => ({
9198
+ id: p.id,
9199
+ name: p.name,
9200
+ env: p.env
9201
+ })),
9202
+ keys: await this.creds.sources(),
9203
+ reach: this.reach,
9204
+ reachFromEnvironment: this.reachFromEnvironment
9205
+ };
9206
+ }
9207
+ async choose(provider, model) {
9208
+ const { chosen, streamFn } = await modelFor(provider, model.trim());
9209
+ await this.creds.choose(provider, chosen.id);
9210
+ if (!this.#env.ISORITE_PI?.trim()) for (const h of this.#hosts) h.use(chosen, streamFn, `pi (${chosen.id})`);
9211
+ }
9212
+ };
9213
+ /** The pi host for `api`, or null where pi is not chosen. Throws (naming what is wrong) where it is chosen and cannot be had. */
9214
+ async function piHostFor(api, env = process.env, project = process.cwd(), notes = project, control = new PiControl(env)) {
9215
+ const want = await wanted(env, control.creds);
9216
+ if (!want) return null;
9217
+ const { provider, model } = want.choice;
9218
+ const { chosen, streamFn } = await modelFor(provider, model);
9219
+ await control.load();
9220
+ const asked = new Set((env.ISORITE_PI_ASK ?? "").split(",").map((t) => t.trim()).filter(Boolean));
9221
+ const host = new PiHost({
9222
+ api,
9223
+ store: new FileStore(storeDir(notes, env)),
9224
+ model: chosen,
9225
+ streamFn,
9226
+ name: `pi (${model})`,
9227
+ getApiKey: (p) => control.creds.key(p),
9228
+ ...asked.size ? { ask: (tool) => asked.has(tool) } : {},
9229
+ more: () => fileTools(project, control.reach),
9230
+ guard: pathGuard(project)
9231
+ });
9232
+ await host.resume();
9233
+ control.attach(host);
9234
+ return host;
9235
+ }
9236
+ //#endregion
7778
9237
  //#region packages/server/src/nodefiles.ts
7779
9238
  const gone = (e) => ["ENOENT", "ENOTDIR"].includes(e.code ?? "");
7780
9239
  var NodeFiles = class {
@@ -8124,6 +9583,7 @@ async function openServer(path, { host = "127.0.0.1", port = 4767, allowHosts =
8124
9583
  const embedder = model ? nodeEmbedder(model) : null;
8125
9584
  const token = randomBytes(24).toString("base64url"), loopback = LOOPBACK.has(host);
8126
9585
  const web = existsSync(join(WEB_DIR, "index.html")) ? WEB_DIR : null;
9586
+ const pi = new PiControl();
8127
9587
  const bases = new Bases({
8128
9588
  config,
8129
9589
  first,
@@ -8138,6 +9598,7 @@ async function openServer(path, { host = "127.0.0.1", port = 4767, allowHosts =
8138
9598
  api,
8139
9599
  notes: known.path
8140
9600
  });
9601
+ const terminal = await piHostFor(api, process.env, projectOf(known.path).repo, known.path, pi);
8141
9602
  const agents = new AgentRunner({
8142
9603
  base: known.name,
8143
9604
  url: `http://127.0.0.1:${port}${known.name === first ? "" : `/b/${known.name}`}`,
@@ -8155,6 +9616,8 @@ async function openServer(path, { host = "127.0.0.1", port = 4767, allowHosts =
8155
9616
  allowHosts,
8156
9617
  notes: known.path,
8157
9618
  acp,
9619
+ ...terminal ? { terminal } : {},
9620
+ ...loopback ? { pi } : {},
8158
9621
  bases,
8159
9622
  base: known.name,
8160
9623
  ...loopback ? { agents } : {},
@@ -8646,13 +10109,17 @@ async function acp(path, { agent, port = 4767, input, output, env = process.env,
8646
10109
  const given = /* @__PURE__ */ new Map();
8647
10110
  const withGiven = (session, prompt) => {
8648
10111
  const blocks = Array.isArray(prompt) ? prompt : [];
8649
- const text = given.get(session);
8650
- if (!text) return blocks;
10112
+ const g = given.get(session);
10113
+ if (!g) return blocks;
8651
10114
  given.delete(session);
8652
- return [{
8653
- type: "text",
8654
- text
8655
- }, ...blocks];
10115
+ return [
10116
+ {
10117
+ type: "text",
10118
+ text: g.text
10119
+ },
10120
+ ...g.blocks ?? [],
10121
+ ...blocks
10122
+ ];
8656
10123
  };
8657
10124
  let batch = [];
8658
10125
  let flushing = null;
@@ -8942,8 +10409,18 @@ async function acp(path, { agent, port = 4767, input, output, env = process.env,
8942
10409
  });
8943
10410
  sendWaiting();
8944
10411
  }
10412
+ if (c.type === "steer") ask("_session/steering", {
10413
+ sessionId: c.session,
10414
+ prompt: [{
10415
+ type: "text",
10416
+ text: c.text
10417
+ }]
10418
+ });
8945
10419
  if (c.type === "give") {
8946
- if (c.text) given.set(c.session, c.text);
10420
+ if (c.text) given.set(c.session, {
10421
+ text: c.text,
10422
+ ...c.blocks?.length ? { blocks: c.blocks } : {}
10423
+ });
8947
10424
  else given.delete(c.session);
8948
10425
  }
8949
10426
  if (c.type === "cancel") {
@@ -8968,11 +10445,15 @@ async function acp(path, { agent, port = 4767, input, output, env = process.env,
8968
10445
  cwd,
8969
10446
  mcpServers: [isoriteMcp()]
8970
10447
  };
8971
- if (!c.session) ask("session/new", params, c.then, c.give);
10448
+ const give = c.give ? {
10449
+ text: c.give,
10450
+ ...c.giveBlocks?.length ? { blocks: c.giveBlocks } : {}
10451
+ } : void 0;
10452
+ if (!c.session) ask("session/new", params, c.then, give);
8972
10453
  else ask(can.load ? "session/load" : "session/resume", {
8973
10454
  ...params,
8974
10455
  sessionId: c.session
8975
- }, c.then, c.give);
10456
+ }, c.then, give);
8976
10457
  }
8977
10458
  if (c.type === "permission" && agentAsked.has(c.id)) {
8978
10459
  const id = agentAsked.get(c.id);
@@ -9001,7 +10482,8 @@ async function acp(path, { agent, port = 4767, input, output, env = process.env,
9001
10482
  readTextFile: false,
9002
10483
  writeTextFile: false
9003
10484
  },
9004
- terminal: false
10485
+ terminal: false,
10486
+ subagents: {}
9005
10487
  },
9006
10488
  clientInfo: {
9007
10489
  name: "isorite",