isorite 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core.js +125 -2
- package/dist/embed-worker.js +9 -2
- package/dist/main.js +2377 -394
- package/package.json +2 -1
- package/plugin/server.ts +9 -8
- package/plugin/tour.ts +5 -8
- package/web/_app/immutable/assets/0.s5xqevrq.css +1 -0
- package/web/_app/immutable/assets/4.PVZR2_N5.css +1 -0
- package/web/_app/immutable/assets/5.DCxNIgyW.css +1 -0
- package/web/_app/immutable/assets/9.CFA9_nYZ.css +1 -0
- package/web/_app/immutable/assets/FolderField._A4_sc1Z.css +1 -0
- package/web/_app/immutable/assets/NoteEditor.ChlpmyHO.css +1 -0
- package/web/_app/immutable/assets/NoteList.F579Zvj_.css +1 -0
- package/web/_app/immutable/assets/{Timeline.UMF1LjSO.css → Timeline.KTc3LKuK.css} +1 -1
- package/web/_app/immutable/assets/Tutorial.DOfsM49T.css +1 -0
- package/web/_app/immutable/chunks/B0NIb88Q.js +1 -0
- package/web/_app/immutable/chunks/B7ZjnqOt.js +20 -0
- package/web/_app/immutable/chunks/{B-si2ell.js → BEVz4Dgg.js} +1 -1
- package/web/_app/immutable/chunks/BEk5idXo.js +1 -0
- package/web/_app/immutable/chunks/B_nIecMW.js +667 -0
- package/web/_app/immutable/chunks/C8UdwlAg.js +3 -0
- package/web/_app/immutable/chunks/CRzLMJPS.js +1 -0
- package/web/_app/immutable/chunks/CcQ3P3p1.js +13 -0
- package/web/_app/immutable/chunks/CcRs965g.js +1 -0
- package/web/_app/immutable/chunks/Cw2GjAJR.js +1 -0
- package/web/_app/immutable/chunks/DjBHskLm.js +1 -0
- package/web/_app/immutable/chunks/Dm229u8R.js +1 -0
- package/web/_app/immutable/chunks/UM_L7-f7.js +1 -0
- package/web/_app/immutable/chunks/b-AOhhy1.js +1 -0
- package/web/_app/immutable/chunks/k55859zq.js +1 -0
- package/web/_app/immutable/entry/app.B3QXxjjH.js +2 -0
- package/web/_app/immutable/entry/start.BauJm-6k.js +1 -0
- package/web/_app/immutable/nodes/0.DP4c84Wn.js +14 -0
- package/web/_app/immutable/nodes/1.BcwcVDJZ.js +1 -0
- package/web/_app/immutable/nodes/2.DgGnQZ7P.js +1 -0
- package/web/_app/immutable/nodes/3.BudzO7We.js +1 -0
- package/web/_app/immutable/nodes/4.DmswyrSt.js +2 -0
- package/web/_app/immutable/nodes/5.DjXsfjL5.js +5 -0
- package/web/_app/immutable/nodes/6.uBh-sdQx.js +1 -0
- package/web/_app/immutable/nodes/7.BYBNXM1A.js +1 -0
- package/web/_app/immutable/nodes/8.Tbz9Rh77.js +1 -0
- package/web/_app/immutable/nodes/9.NF5w8YC2.js +486 -0
- package/web/_app/immutable/workers/worker-rKDNrEKQ.js +1 -0
- package/web/_app/version.json +1 -1
- package/web/icon-180.png +0 -0
- package/web/icon-192.png +0 -0
- package/web/icon-512.png +0 -0
- package/web/icon-maskable-512.png +0 -0
- package/web/index.html +35 -12
- package/web/manifest.webmanifest +14 -0
- package/web/_app/immutable/assets/0.CbWSsYpm.css +0 -1
- package/web/_app/immutable/assets/3.Bgs8n1LK.css +0 -1
- package/web/_app/immutable/assets/4.CCFEPTFN.css +0 -1
- package/web/_app/immutable/assets/5.BCvLF9Xz.css +0 -1
- package/web/_app/immutable/assets/6.kPo5mcJx.css +0 -1
- package/web/_app/immutable/assets/NoteEditor.Bp-YDqYj.css +0 -1
- package/web/_app/immutable/chunks/BD1Dpm2e.js +0 -3
- package/web/_app/immutable/chunks/C0fpmehI.js +0 -1
- package/web/_app/immutable/chunks/CWYACT2V.js +0 -1
- package/web/_app/immutable/chunks/CdQZ-ArP.js +0 -1
- package/web/_app/immutable/chunks/DN3dk6rD.js +0 -147
- package/web/_app/immutable/chunks/DtAOoM-T.js +0 -1
- package/web/_app/immutable/chunks/DykrN7z5.js +0 -535
- package/web/_app/immutable/chunks/cmGruEKS.js +0 -1
- package/web/_app/immutable/chunks/gRYnlonl.js +0 -1
- package/web/_app/immutable/entry/app.DgJiXIYs.js +0 -2
- package/web/_app/immutable/entry/start.l9vV7NaJ.js +0 -1
- package/web/_app/immutable/nodes/0.DWPeBju2.js +0 -1
- package/web/_app/immutable/nodes/1.CJVDS8FT.js +0 -1
- package/web/_app/immutable/nodes/2.o8ItfT-a.js +0 -1
- package/web/_app/immutable/nodes/3.uhHyOPGK.js +0 -13
- package/web/_app/immutable/nodes/4.BYKkAZTp.js +0 -2
- package/web/_app/immutable/nodes/5.BzEQO7Xv.js +0 -4
- package/web/_app/immutable/nodes/6.GxiAUa6J.js +0 -1
- package/web/_app/immutable/nodes/7.d2Ere8Jb.js +0 -1
- package/web/_app/immutable/nodes/8.CvPHZL8j.js +0 -1
- package/web/_app/immutable/workers/worker-d9GxGKde.js +0 -1
package/dist/main.js
CHANGED
|
@@ -9,13 +9,15 @@ import { createHash, randomBytes } from "node:crypto";
|
|
|
9
9
|
import { serve } from "@hono/node-server";
|
|
10
10
|
import { homedir, tmpdir } from "node:os";
|
|
11
11
|
import { Worker } from "node:worker_threads";
|
|
12
|
+
import { execFileSync, spawn, spawnSync } from "node:child_process";
|
|
12
13
|
import { Hono } from "hono";
|
|
13
14
|
import { streamSSE } from "hono/streaming";
|
|
14
15
|
import { z } from "zod";
|
|
16
|
+
import { OpenCode } from "@opencode/client";
|
|
15
17
|
import { appendFile, mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
16
|
-
import { execFileSync, spawn } from "node:child_process";
|
|
17
18
|
import { createInterface } from "node:readline";
|
|
18
|
-
import { createServer } from "node:
|
|
19
|
+
import { createServer } from "node:net";
|
|
20
|
+
import { createServer as createServer$1 } from "node:http";
|
|
19
21
|
//#region packages/core/src/text.ts
|
|
20
22
|
const WS = "\\t\\n\\x0b\\x0c\\r\\x1c-\\x1f \\x85\\xa0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000";
|
|
21
23
|
const LEAD = new RegExp(`^[${WS}]+`, "u");
|
|
@@ -1407,7 +1409,8 @@ function moments(entries) {
|
|
|
1407
1409
|
const what = e.why === "start" ? e.who === "external" ? "found" : "wrote" : e.why === "edit" ? "edited" : e.why === "restore" ? "restored" : e.why === "keep" ? "kept" : e.why === "move" ? "moved" : "changed elsewhere";
|
|
1408
1410
|
out.push({
|
|
1409
1411
|
...base,
|
|
1410
|
-
what
|
|
1412
|
+
what,
|
|
1413
|
+
...e.asked ? { asked: e.asked } : {}
|
|
1411
1414
|
});
|
|
1412
1415
|
}
|
|
1413
1416
|
}
|
|
@@ -1574,6 +1577,11 @@ function understanding(events, now = Date.now()) {
|
|
|
1574
1577
|
s.last = when;
|
|
1575
1578
|
s.evidence.push(e.id);
|
|
1576
1579
|
}
|
|
1580
|
+
} else if (e.event === "understood" && typeof e.note === "string") {
|
|
1581
|
+
const s = at(e.note), by = e.by;
|
|
1582
|
+
s.level = clamp(s.level + (typeof by === "number" && Number.isFinite(by) ? Math.max(-1, Math.min(1, by)) : 0));
|
|
1583
|
+
s.last = when;
|
|
1584
|
+
s.evidence.push(e.id);
|
|
1577
1585
|
} else if (e.event === "moved" && typeof e.from === "string" && typeof e.to === "string") {
|
|
1578
1586
|
const s = state.get(e.from);
|
|
1579
1587
|
if (s) {
|
|
@@ -2055,7 +2063,7 @@ async function rounds(o) {
|
|
|
2055
2063
|
callId: c.id,
|
|
2056
2064
|
content: text
|
|
2057
2065
|
});
|
|
2058
|
-
o.onStep(step);
|
|
2066
|
+
if (step) o.onStep(step);
|
|
2059
2067
|
}
|
|
2060
2068
|
}
|
|
2061
2069
|
}
|
|
@@ -2063,7 +2071,7 @@ async function rounds(o) {
|
|
|
2063
2071
|
//#region packages/engine/src/iso.ts
|
|
2064
2072
|
/** The point in the note where new text is asked for. Not part of the note. */
|
|
2065
2073
|
const HERE = "⟦HERE⟧";
|
|
2066
|
-
const WHO = `You are Iso, the companion in isorite, an app for understanding a subject by
|
|
2074
|
+
const WHO$1 = `You are Iso, the companion in isorite, an app for understanding a subject by
|
|
2067
2075
|
writing it down. The person is writing a note in their own knowledge base: a
|
|
2068
2076
|
folder of Markdown notes with links between them. They have asked you for
|
|
2069
2077
|
something at one place in the note they have open.`;
|
|
@@ -2145,6 +2153,8 @@ const FORMAT = `# How notes work here
|
|
|
2145
2153
|
equation as a paragraph of its own, with $$ alone on the line before it
|
|
2146
2154
|
and $$ alone on the line after.
|
|
2147
2155
|
- Ordinary Markdown otherwise. No HTML.`;
|
|
2156
|
+
/** How notes are written, for any request that writes or quotes them. */
|
|
2157
|
+
const FORMAT_RULES = FORMAT;
|
|
2148
2158
|
const LOOKUP = (rounds, meaning) => `# Looking things up
|
|
2149
2159
|
|
|
2150
2160
|
Before you reply you may look things up in their knowledge base with the
|
|
@@ -2245,14 +2255,23 @@ function windowed(lines, keep, budget = NOTE_BUDGET) {
|
|
|
2245
2255
|
flush();
|
|
2246
2256
|
return out.join("\n");
|
|
2247
2257
|
}
|
|
2258
|
+
/** The notes a note links to, as they are sent with a request in it. */
|
|
2259
|
+
function linkedSection(linked, intro = "The notes this note links to") {
|
|
2260
|
+
return linked.length ? `# ${intro}\n\n${linked.map((l) => `## ${l.title} (/${l.id}.md)\n\n${l.text}`).join("\n\n")}` : "";
|
|
2261
|
+
}
|
|
2262
|
+
const NO_LOOKUP = `# Looking things up
|
|
2263
|
+
|
|
2264
|
+
You have no tools this time: they chose what you are sent. Answer from it, and
|
|
2265
|
+
say plainly what you would need to look up to say more.`;
|
|
2248
2266
|
/** The messages for new text at a point, and what they hold, part by part. */
|
|
2249
2267
|
function insertMessages(a) {
|
|
2250
2268
|
const system = [
|
|
2251
|
-
WHO,
|
|
2269
|
+
WHO$1,
|
|
2252
2270
|
INSERT$1,
|
|
2253
2271
|
FORMAT,
|
|
2254
|
-
|
|
2272
|
+
a.lookup === false ? NO_LOOKUP : looked$1(a)
|
|
2255
2273
|
].join("\n\n");
|
|
2274
|
+
const linked = linkedSection(a.linked ?? []);
|
|
2256
2275
|
const note = `# Their note: ${a.title} (/${a.id}.md)\n\n${a.marked}` + (a.unkept.length ? `\n\n# Text you offered in this note that they have not kept yet\n\nIt is not part of the note. Do not write it again.\n\n${a.unkept.join("\n\n---\n\n")}` : "");
|
|
2257
2276
|
const earlier = a.conversation ? `# Earlier in this session\n\n${a.conversation}` : "";
|
|
2258
2277
|
const request = `# What they ask now\n\n${a.asked}`;
|
|
@@ -2267,47 +2286,54 @@ function insertMessages(a) {
|
|
|
2267
2286
|
role: "user",
|
|
2268
2287
|
content: [
|
|
2269
2288
|
earlier,
|
|
2289
|
+
linked,
|
|
2270
2290
|
note,
|
|
2271
2291
|
request
|
|
2272
2292
|
].filter(Boolean).join("\n\n")
|
|
2273
2293
|
}],
|
|
2274
|
-
parts:
|
|
2275
|
-
{
|
|
2276
|
-
name: "instructions and tools",
|
|
2277
|
-
detail: "",
|
|
2278
|
-
tokens: estimate(system) + 500
|
|
2279
|
-
},
|
|
2280
|
-
{
|
|
2281
|
-
name: "study record",
|
|
2282
|
-
detail: "nothing yet",
|
|
2283
|
-
tokens: 0
|
|
2284
|
-
},
|
|
2285
|
-
{
|
|
2286
|
-
name: "your prompts and Iso's answers",
|
|
2287
|
-
detail: a.moments ? `${a.moments} ${a.moments === 1 ? "moment" : "moments"}` : "",
|
|
2288
|
-
tokens: estimate(earlier)
|
|
2289
|
-
},
|
|
2290
|
-
{
|
|
2291
|
-
name: "this note",
|
|
2292
|
-
detail: a.title,
|
|
2293
|
-
tokens: estimate(note) + estimate(request)
|
|
2294
|
-
},
|
|
2295
|
-
{
|
|
2296
|
-
name: "linked notes",
|
|
2297
|
-
detail: a.linked?.notes ? `${a.linked.notes} read` : "",
|
|
2298
|
-
tokens: a.linked?.tokens ?? 0
|
|
2299
|
-
}
|
|
2300
|
-
]
|
|
2294
|
+
parts: inPlaceParts(a, system, earlier, linked, note, request)
|
|
2301
2295
|
};
|
|
2302
2296
|
}
|
|
2297
|
+
/** The look-up instructions for a request in place, saying what is sent below it. */
|
|
2298
|
+
const looked$1 = (a) => LOOKUP(ROUNDS[a.tier], a.meaning).replace("below there is only the note.", a.linked?.length ? "below there is the note, and\nthe notes it links to that they chose to send." : "below there is only the note.");
|
|
2299
|
+
function inPlaceParts(a, system, earlier, linked, note, request) {
|
|
2300
|
+
return [
|
|
2301
|
+
{
|
|
2302
|
+
name: "instructions and tools",
|
|
2303
|
+
detail: a.lookup === false ? "no tools" : "",
|
|
2304
|
+
tokens: estimate(system) + (a.lookup === false ? 0 : 500)
|
|
2305
|
+
},
|
|
2306
|
+
{
|
|
2307
|
+
name: "study record",
|
|
2308
|
+
detail: "nothing yet",
|
|
2309
|
+
tokens: 0
|
|
2310
|
+
},
|
|
2311
|
+
{
|
|
2312
|
+
name: "your prompts and Iso's answers",
|
|
2313
|
+
detail: a.moments ? `${a.moments} ${a.moments === 1 ? "moment" : "moments"}` : "",
|
|
2314
|
+
tokens: estimate(earlier)
|
|
2315
|
+
},
|
|
2316
|
+
{
|
|
2317
|
+
name: "this note",
|
|
2318
|
+
detail: a.title,
|
|
2319
|
+
tokens: estimate(note) + estimate(request)
|
|
2320
|
+
},
|
|
2321
|
+
{
|
|
2322
|
+
name: "linked notes",
|
|
2323
|
+
detail: a.linked?.length ? `${a.linked.length} sent` : "",
|
|
2324
|
+
tokens: estimate(linked)
|
|
2325
|
+
}
|
|
2326
|
+
];
|
|
2327
|
+
}
|
|
2303
2328
|
/** The messages for a rewrite of chosen paragraphs. */
|
|
2304
2329
|
function rewriteMessages(a) {
|
|
2305
2330
|
const system = [
|
|
2306
|
-
WHO,
|
|
2331
|
+
WHO$1,
|
|
2307
2332
|
REWRITE(a.count),
|
|
2308
2333
|
FORMAT,
|
|
2309
|
-
|
|
2334
|
+
a.lookup === false ? NO_LOOKUP : looked$1(a)
|
|
2310
2335
|
].join("\n\n");
|
|
2336
|
+
const linked = linkedSection(a.linked ?? []);
|
|
2311
2337
|
const note = `# Their note: ${a.title} (/${a.id}.md)\n\n${a.marked}`;
|
|
2312
2338
|
const earlier = a.conversation ? `# Earlier in this session\n\n${a.conversation}` : "";
|
|
2313
2339
|
const request = `# What they ask now, of the marked ${a.count === 1 ? "passage" : "passages"}\n\n${a.asked}`;
|
|
@@ -2322,37 +2348,12 @@ function rewriteMessages(a) {
|
|
|
2322
2348
|
role: "user",
|
|
2323
2349
|
content: [
|
|
2324
2350
|
earlier,
|
|
2351
|
+
linked,
|
|
2325
2352
|
note,
|
|
2326
2353
|
request
|
|
2327
2354
|
].filter(Boolean).join("\n\n")
|
|
2328
2355
|
}],
|
|
2329
|
-
parts:
|
|
2330
|
-
{
|
|
2331
|
-
name: "instructions and tools",
|
|
2332
|
-
detail: "",
|
|
2333
|
-
tokens: estimate(system) + 500
|
|
2334
|
-
},
|
|
2335
|
-
{
|
|
2336
|
-
name: "study record",
|
|
2337
|
-
detail: "nothing yet",
|
|
2338
|
-
tokens: 0
|
|
2339
|
-
},
|
|
2340
|
-
{
|
|
2341
|
-
name: "your prompts and Iso's answers",
|
|
2342
|
-
detail: a.moments ? `${a.moments} ${a.moments === 1 ? "moment" : "moments"}` : "",
|
|
2343
|
-
tokens: estimate(earlier)
|
|
2344
|
-
},
|
|
2345
|
-
{
|
|
2346
|
-
name: "this note",
|
|
2347
|
-
detail: a.title,
|
|
2348
|
-
tokens: estimate(note) + estimate(request)
|
|
2349
|
-
},
|
|
2350
|
-
{
|
|
2351
|
-
name: "linked notes",
|
|
2352
|
-
detail: a.linked?.notes ? `${a.linked.notes} read` : "",
|
|
2353
|
-
tokens: a.linked?.tokens ?? 0
|
|
2354
|
-
}
|
|
2355
|
-
]
|
|
2356
|
+
parts: inPlaceParts(a, system, earlier, linked, note, request)
|
|
2356
2357
|
};
|
|
2357
2358
|
}
|
|
2358
2359
|
/** A rewrite's reply, taken apart: the new text for each of the `count` passages (null where it left one as it
|
|
@@ -2396,7 +2397,7 @@ const LINKED_BUDGET = 8e3;
|
|
|
2396
2397
|
/** The messages for a note written for Iso. */
|
|
2397
2398
|
function noteMessages(a) {
|
|
2398
2399
|
const system = [
|
|
2399
|
-
WHO.replace("They have asked you for\nsomething at one place in the note they have open.", "This time they have written to you at length."),
|
|
2400
|
+
WHO$1.replace("They have asked you for\nsomething at one place in the note they have open.", "This time they have written to you at length."),
|
|
2400
2401
|
NOTE,
|
|
2401
2402
|
FORMAT,
|
|
2402
2403
|
LOOKUP(ROUNDS[a.tier], a.meaning).replace("below there is only the note.", "below there is what they wrote, and the notes they linked.")
|
|
@@ -2654,8 +2655,8 @@ function exerciseReply(reply) {
|
|
|
2654
2655
|
}
|
|
2655
2656
|
//#endregion
|
|
2656
2657
|
//#region packages/engine/src/lookup.ts
|
|
2657
|
-
const str = { type: "string" };
|
|
2658
|
-
const fn = (name, description, properties, required) => ({
|
|
2658
|
+
const str$1 = { type: "string" };
|
|
2659
|
+
const fn$1 = (name, description, properties, required) => ({
|
|
2659
2660
|
type: "function",
|
|
2660
2661
|
function: {
|
|
2661
2662
|
name,
|
|
@@ -2669,10 +2670,10 @@ const fn = (name, description, properties, required) => ({
|
|
|
2669
2670
|
}
|
|
2670
2671
|
});
|
|
2671
2672
|
const TOOLS = [
|
|
2672
|
-
fn("search_notes", "Keyword search over the knowledge base. Returns note ids, titles, types, one-sentence descriptions and a one-line snippet. Search by the words a note would use.", {
|
|
2673
|
-
query: str,
|
|
2673
|
+
fn$1("search_notes", "Keyword search over the knowledge base. Returns note ids, titles, types, one-sentence descriptions and a one-line snippet. Search by the words a note would use.", {
|
|
2674
|
+
query: str$1,
|
|
2674
2675
|
under: {
|
|
2675
|
-
...str,
|
|
2676
|
+
...str$1,
|
|
2676
2677
|
description: "Only notes in this folder."
|
|
2677
2678
|
},
|
|
2678
2679
|
limit: {
|
|
@@ -2680,25 +2681,25 @@ const TOOLS = [
|
|
|
2680
2681
|
description: "How many, 8 unless said, 15 at most."
|
|
2681
2682
|
}
|
|
2682
2683
|
}, ["query"]),
|
|
2683
|
-
fn("outline_note", "A note's description, its headings, the notes it links to and the notes that link to it, without its body. Use it to choose a section to read.", { id: {
|
|
2684
|
-
...str,
|
|
2684
|
+
fn$1("outline_note", "A note's description, its headings, the notes it links to and the notes that link to it, without its body. Use it to choose a section to read.", { id: {
|
|
2685
|
+
...str$1,
|
|
2685
2686
|
description: "The note's id or path: basics/entropy or /basics/entropy.md."
|
|
2686
2687
|
} }, ["id"]),
|
|
2687
|
-
fn("read_note", "Read one section of a note, by its heading, or the whole note when no section is named. Prefer a section.", {
|
|
2688
|
-
id: str,
|
|
2688
|
+
fn$1("read_note", "Read one section of a note, by its heading, or the whole note when no section is named. Prefer a section.", {
|
|
2689
|
+
id: str$1,
|
|
2689
2690
|
section: {
|
|
2690
|
-
...str,
|
|
2691
|
+
...str$1,
|
|
2691
2692
|
description: "A heading's text, as outline_note gives it."
|
|
2692
2693
|
}
|
|
2693
2694
|
}, ["id"])
|
|
2694
2695
|
];
|
|
2695
|
-
const FIND_SIMILAR = fn("find_similar", "Find notes by meaning: notes that say something like the text you give, even in quite other words. Slower and vaguer than search_notes: use it after search_notes and the links of the notes in hand have not found what you need, or when you cannot guess the words a note would use. Give a question or a sentence, not keywords. Returns each note's nearest section, with how alike it is (0 to 1).", {
|
|
2696
|
+
const FIND_SIMILAR = fn$1("find_similar", "Find notes by meaning: notes that say something like the text you give, even in quite other words. Slower and vaguer than search_notes: use it after search_notes and the links of the notes in hand have not found what you need, or when you cannot guess the words a note would use. Give a question or a sentence, not keywords. Returns each note's nearest section, with how alike it is (0 to 1).", {
|
|
2696
2697
|
text: {
|
|
2697
|
-
...str,
|
|
2698
|
+
...str$1,
|
|
2698
2699
|
description: "A question, or a sentence saying what you are looking for."
|
|
2699
2700
|
},
|
|
2700
2701
|
under: {
|
|
2701
|
-
...str,
|
|
2702
|
+
...str$1,
|
|
2702
2703
|
description: "Only notes in this folder."
|
|
2703
2704
|
},
|
|
2704
2705
|
limit: {
|
|
@@ -2867,89 +2868,341 @@ var Lookup = class {
|
|
|
2867
2868
|
}
|
|
2868
2869
|
};
|
|
2869
2870
|
//#endregion
|
|
2870
|
-
//#region packages/engine/src/
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
/** The
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
if (
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
}
|
|
2871
|
+
//#region packages/engine/src/understand.ts
|
|
2872
|
+
const UNDERSTANDING_PREFIX = "@understanding/";
|
|
2873
|
+
const UNDERSTANDING_DIR = "understanding";
|
|
2874
|
+
const isUnderstandingId = (id) => id.startsWith(UNDERSTANDING_PREFIX);
|
|
2875
|
+
const understandingId = (note) => UNDERSTANDING_PREFIX + note;
|
|
2876
|
+
const READING = "## Iso's reading";
|
|
2877
|
+
const PAD = "## Scratchpad";
|
|
2878
|
+
/** The two parts of an Understanding note's file. A file in another shape is read as all scratchpad. */
|
|
2879
|
+
function parseUnderstanding(text) {
|
|
2880
|
+
if (!text) return {
|
|
2881
|
+
reading: "",
|
|
2882
|
+
scratchpad: ""
|
|
2883
|
+
};
|
|
2884
|
+
const body = text.replace(/^---\n[\s\S]*?\n---\n/, "");
|
|
2885
|
+
const r = body.indexOf(READING), p = body.indexOf(PAD);
|
|
2886
|
+
if (r < 0 && p < 0) return {
|
|
2887
|
+
reading: "",
|
|
2888
|
+
scratchpad: body.trim()
|
|
2889
|
+
};
|
|
2890
|
+
return {
|
|
2891
|
+
reading: r < 0 ? "" : body.slice(r + 16, p > r ? p : void 0).trim(),
|
|
2892
|
+
scratchpad: p < 0 ? "" : body.slice(p + 13, r > p ? r : void 0).trim()
|
|
2893
|
+
};
|
|
2893
2894
|
}
|
|
2894
|
-
/**
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2895
|
+
/** An Understanding note's file, from its parts. */
|
|
2896
|
+
function understandingText(note, title, parts) {
|
|
2897
|
+
return `---\ntype: Understanding\ntitle: ${JSON.stringify(`Understanding · ${title}`)}\nof: ${note}\n---\n\n${READING}\n\n${parts.reading.trim()}\n\n${PAD}\n\n${parts.scratchpad.trim()}\n`;
|
|
2898
|
+
}
|
|
2899
|
+
//#endregion
|
|
2900
|
+
//#region packages/engine/src/turn.ts
|
|
2901
|
+
/** What Iso may do. Build needs a project and a shell, and is the terminal's (M11). */
|
|
2902
|
+
const MODES = [
|
|
2903
|
+
"ask",
|
|
2904
|
+
"write",
|
|
2905
|
+
"build",
|
|
2906
|
+
"understand"
|
|
2907
|
+
];
|
|
2908
|
+
const CONVERSATIONS_DIR = "conversations";
|
|
2909
|
+
const conversationPath = (id) => `${CONVERSATIONS_DIR}/${id}.jsonl`;
|
|
2910
|
+
/** A conversation's turns from its file, oldest first. A line that cannot be read is passed over. */
|
|
2911
|
+
function readTurns(text) {
|
|
2912
|
+
const turns = /* @__PURE__ */ new Map();
|
|
2913
|
+
for (const raw of text.split("\n")) {
|
|
2914
|
+
if (!raw.trim()) continue;
|
|
2915
|
+
let line;
|
|
2916
|
+
try {
|
|
2917
|
+
line = JSON.parse(raw);
|
|
2918
|
+
} catch {
|
|
2919
|
+
continue;
|
|
2911
2920
|
}
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
if (last && s.from <= last.to) last.to = Math.max(last.to, s.to);
|
|
2921
|
-
else merged.push({ ...s });
|
|
2922
|
-
}
|
|
2923
|
-
const moved = (i, counts) => i + hunks.filter((h) => counts(h.at)).reduce((d, h) => d + h.ins.length - h.del.length, 0);
|
|
2924
|
-
const start = (i) => moved(i, (at) => at < i), end = (i) => moved(i, (at) => at <= i);
|
|
2925
|
-
const offset = (i) => a.slice(0, i).reduce((n, line) => n + line.length + 1, 0);
|
|
2926
|
-
return merged.map(({ from, to }) => {
|
|
2927
|
-
const lines = b.slice(start(from), end(to));
|
|
2928
|
-
let text = a.slice(from, to).join("\n"), at = offset(from);
|
|
2929
|
-
if (!lines.length) {
|
|
2930
|
-
if (to < a.length) text += "\n";
|
|
2931
|
-
else if (from > 0) {
|
|
2932
|
-
text = "\n" + text;
|
|
2933
|
-
at -= 1;
|
|
2934
|
-
}
|
|
2921
|
+
if (line.kind === "turn") {
|
|
2922
|
+
const { kind: _, ...t } = line;
|
|
2923
|
+
turns.set(t.id, {
|
|
2924
|
+
...t,
|
|
2925
|
+
items: [],
|
|
2926
|
+
state: "running"
|
|
2927
|
+
});
|
|
2928
|
+
continue;
|
|
2935
2929
|
}
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2930
|
+
const t = turns.get(line.turn);
|
|
2931
|
+
if (!t) continue;
|
|
2932
|
+
if (line.kind === "item") t.items.push(line.item);
|
|
2933
|
+
else if (line.kind === "answer") {
|
|
2934
|
+
for (const i of t.items) if (i.kind === "permission" && i.id === line.permission) i.answer = line.decision;
|
|
2935
|
+
} else {
|
|
2936
|
+
t.state = line.state;
|
|
2937
|
+
if (line.message) t.message = line.message;
|
|
2938
|
+
if (line.cost) t.cost = line.cost;
|
|
2939
|
+
}
|
|
2940
|
+
}
|
|
2941
|
+
return [...turns.values()].map((t) => t.state === "running" ? {
|
|
2942
|
+
...t,
|
|
2943
|
+
state: "stopped"
|
|
2944
|
+
} : t).sort((a, b) => a.id < b.id ? -1 : 1);
|
|
2945
|
+
}
|
|
2946
|
+
const WHO = `You are Iso, the companion in isorite, an app for understanding a subject by
|
|
2947
|
+
writing it down. The person keeps a knowledge base: a folder of Markdown notes
|
|
2948
|
+
with links between them. They are talking with you in isorite's Iso pane,
|
|
2949
|
+
beside their notes and the Atlas, the map of their base. What you look up and
|
|
2950
|
+
what you change is shown to them there as you do it.`;
|
|
2951
|
+
const MODE_RULES = {
|
|
2952
|
+
ask: `# Ask mode: what you may do
|
|
2953
|
+
|
|
2954
|
+
You read their notes and answer. You cannot change anything in this mode: no
|
|
2955
|
+
note, no folder.
|
|
2956
|
+
|
|
2957
|
+
- Answer what they ask, in plain prose. Short: a few sentences to a few
|
|
2958
|
+
paragraphs, unless more is asked for. Markdown and maths are shown as they
|
|
2959
|
+
are in their notes.
|
|
2960
|
+
- Rest on their notes where the base says something: look first, and link to
|
|
2961
|
+
the notes you used, so they can open them. Where the base does not hold what
|
|
2962
|
+
is asked, answer from what you know of the subject, exactly, and say that
|
|
2963
|
+
their notes do not cover it.
|
|
2964
|
+
- When one of their notes would help them and they may not know it, point it
|
|
2965
|
+
out with point_to_note. Use it sparingly: one or two notes.
|
|
2966
|
+
- Never invent a note, a link or a quotation.`,
|
|
2967
|
+
write: `# Write mode: what you may do
|
|
2968
|
+
|
|
2969
|
+
You may change their knowledge base: edit notes, make new ones, move them and
|
|
2970
|
+
delete them. What you change goes into their notes at once. They see every
|
|
2971
|
+
change marked as yours, and can undo it.
|
|
2972
|
+
|
|
2973
|
+
- Do what they ask, and only that. Change the least that does it: edit the
|
|
2974
|
+
passage that needs it with edit_note, not the whole note. Keep their words,
|
|
2975
|
+
links, maths and voice wherever the request does not call for a change.
|
|
2976
|
+
- Look before you write: read the part of the note you change, and the notes
|
|
2977
|
+
it rests on, so what you write agrees with the base and links to it.
|
|
2978
|
+
- A new note (write_note) is written as one of theirs: frontmatter with type
|
|
2979
|
+
and title, their notation, their level, plain words, linked to the notes it
|
|
2980
|
+
rests on.
|
|
2981
|
+
- Moving or deleting a note is asked of them before it happens. Do it only
|
|
2982
|
+
when they asked for it.
|
|
2983
|
+
- When you are done, say in a sentence or two what you changed and why. Do not
|
|
2984
|
+
repeat the text you wrote: they see it in the note.
|
|
2985
|
+
- Never invent a note, a link or a quotation.`,
|
|
2986
|
+
understand: `# Understand mode: what you may do
|
|
2987
|
+
|
|
2988
|
+
You help them understand: what is in their notes, what changed in them, or
|
|
2989
|
+
something they want to understand more deeply. You do not change their notes.
|
|
2990
|
+
You keep a record of their understanding, note by note, in their study base.
|
|
2991
|
+
|
|
2992
|
+
- Start from where they are. Below, for each note in hand, is your reading of
|
|
2993
|
+
their understanding of it so far and what changed it. Use it: do not explain
|
|
2994
|
+
what they have shown they know; go to what they have not yet shown.
|
|
2995
|
+
- Ask before you explain, where a question would show you where they stand.
|
|
2996
|
+
One question at a time. Short turns.
|
|
2997
|
+
- When they show understanding, or a gap, record it with
|
|
2998
|
+
record_understanding: how far this turn is evidence that their
|
|
2999
|
+
understanding of the note differs from what you had (-1, -0.5, 0, 0.5 or 1)
|
|
3000
|
+
and why, in one plain sentence to them. At most once a note a turn.
|
|
3001
|
+
- At the end of a conversation about a note, or when your reading of them has
|
|
3002
|
+
changed, rewrite your reading with write_understanding: a few plain
|
|
3003
|
+
sentences on what they can do with the note and what they have not yet
|
|
3004
|
+
shown. It is theirs to read.
|
|
3005
|
+
- Never invent a note, a link or a quotation.`
|
|
3006
|
+
};
|
|
3007
|
+
const LOOKING = (rounds, meaning, lookup) => lookup ? `# Looking things up
|
|
3008
|
+
|
|
3009
|
+
- Look up only what the request needs. search_notes finds notes by their
|
|
3010
|
+
words; outline_note shows a note's headings and links; read_note reads one
|
|
3011
|
+
section. Prefer a section to a whole note. Follow links one step at a time.${meaning ? `
|
|
3012
|
+
- find_similar finds notes by meaning, when their words have not found them.` : ""}
|
|
3013
|
+
- You may call several tools at once, over ${rounds} rounds at most; then reply.
|
|
3014
|
+
- Say nothing between tool calls. Only the reply is shown.` : `# Looking things up
|
|
3015
|
+
|
|
3016
|
+
They chose what you are sent and asked you not to look further: you have no
|
|
3017
|
+
tools for searching or reading. Answer from what is below, and say plainly what
|
|
3018
|
+
you would need to look at to say more.`;
|
|
3019
|
+
const str = { type: "string" };
|
|
3020
|
+
const fn = (name, description, properties, required) => ({
|
|
3021
|
+
type: "function",
|
|
3022
|
+
function: {
|
|
3023
|
+
name,
|
|
3024
|
+
description,
|
|
3025
|
+
parameters: {
|
|
3026
|
+
type: "object",
|
|
3027
|
+
properties,
|
|
3028
|
+
required,
|
|
3029
|
+
additionalProperties: false
|
|
3030
|
+
}
|
|
3031
|
+
}
|
|
3032
|
+
});
|
|
3033
|
+
const POINT_TO_NOTE = fn("point_to_note", "Point the person to one of their notes: it is marked on their Atlas, with why. For a note that would help them that they may not know of.", {
|
|
3034
|
+
id: {
|
|
3035
|
+
...str,
|
|
3036
|
+
description: "The note's id or path."
|
|
3037
|
+
},
|
|
3038
|
+
why: {
|
|
3039
|
+
...str,
|
|
3040
|
+
description: "Why, in a short sentence to them."
|
|
3041
|
+
}
|
|
3042
|
+
}, ["id", "why"]);
|
|
3043
|
+
const EDIT_NOTE$1 = fn("edit_note", "Replace one passage of a note with new text. `old` must be exactly as it is in the note, and must occur in it once: read it first. Give enough of it to be unique. An empty `new` takes the passage out.", {
|
|
3044
|
+
id: {
|
|
3045
|
+
...str,
|
|
3046
|
+
description: "The note's id or path."
|
|
3047
|
+
},
|
|
3048
|
+
old: {
|
|
3049
|
+
...str,
|
|
3050
|
+
description: "The passage as it is now, exactly."
|
|
3051
|
+
},
|
|
3052
|
+
new: {
|
|
3053
|
+
...str,
|
|
3054
|
+
description: "What it becomes."
|
|
3055
|
+
}
|
|
3056
|
+
}, [
|
|
3057
|
+
"id",
|
|
3058
|
+
"old",
|
|
3059
|
+
"new"
|
|
3060
|
+
]);
|
|
3061
|
+
const WRITE_NOTE$1 = fn("write_note", "Make a new note, or write a note's whole text anew. For a new note give its whole file: frontmatter (type, title) then the body. Prefer edit_note for a change to a note that exists.", {
|
|
3062
|
+
id: {
|
|
3063
|
+
...str,
|
|
3064
|
+
description: "The note's id: folder/name, lower case with hyphens. A new folder is made for it."
|
|
3065
|
+
},
|
|
3066
|
+
text: {
|
|
3067
|
+
...str,
|
|
3068
|
+
description: "The note's whole file."
|
|
3069
|
+
}
|
|
3070
|
+
}, ["id", "text"]);
|
|
3071
|
+
const MOVE_NOTE = fn("move_note", "Move or rename a note. Links to it are changed to match. They are asked first.", {
|
|
3072
|
+
id: str,
|
|
3073
|
+
to: {
|
|
3074
|
+
...str,
|
|
3075
|
+
description: "Its new id: folder/name."
|
|
3076
|
+
}
|
|
3077
|
+
}, ["id", "to"]);
|
|
3078
|
+
const DELETE_NOTE = fn("delete_note", "Delete a note. They are asked first. Its history stays.", { id: str }, ["id"]);
|
|
3079
|
+
const RECORD_UNDERSTANDING = fn("record_understanding", "Record evidence of their understanding of one note from this turn: how far it differs from what you had, and why.", {
|
|
3080
|
+
id: {
|
|
3081
|
+
...str,
|
|
3082
|
+
description: "The note's id."
|
|
3083
|
+
},
|
|
3084
|
+
by: {
|
|
3085
|
+
type: "number",
|
|
3086
|
+
enum: [
|
|
3087
|
+
-1,
|
|
3088
|
+
-.5,
|
|
3089
|
+
0,
|
|
3090
|
+
.5,
|
|
3091
|
+
1
|
|
3092
|
+
]
|
|
3093
|
+
},
|
|
3094
|
+
why: {
|
|
3095
|
+
...str,
|
|
3096
|
+
description: "One plain sentence, to them."
|
|
3097
|
+
}
|
|
3098
|
+
}, [
|
|
3099
|
+
"id",
|
|
3100
|
+
"by",
|
|
3101
|
+
"why"
|
|
3102
|
+
]);
|
|
3103
|
+
const WRITE_UNDERSTANDING = fn("write_understanding", "Rewrite your reading of their understanding of one note: a few plain sentences on what they can do with it and what they have not yet shown. Replaces the reading you had.", {
|
|
3104
|
+
id: {
|
|
3105
|
+
...str,
|
|
3106
|
+
description: "The note's id."
|
|
3107
|
+
},
|
|
3108
|
+
reading: str
|
|
3109
|
+
}, ["id", "reading"]);
|
|
3110
|
+
/** The tools each mode adds to looking things up. */
|
|
3111
|
+
const MODE_TOOLS = {
|
|
3112
|
+
ask: [POINT_TO_NOTE],
|
|
3113
|
+
write: [
|
|
3114
|
+
EDIT_NOTE$1,
|
|
3115
|
+
WRITE_NOTE$1,
|
|
3116
|
+
MOVE_NOTE,
|
|
3117
|
+
DELETE_NOTE,
|
|
3118
|
+
POINT_TO_NOTE
|
|
3119
|
+
],
|
|
3120
|
+
understand: [
|
|
3121
|
+
RECORD_UNDERSTANDING,
|
|
3122
|
+
WRITE_UNDERSTANDING,
|
|
3123
|
+
POINT_TO_NOTE
|
|
3124
|
+
]
|
|
3125
|
+
};
|
|
3126
|
+
function sentText(s) {
|
|
3127
|
+
if (s.kind === "note") return `## ${s.title} (/${s.id}.md)\n\n${windowed(s.text.split("\n"), [0])}`;
|
|
3128
|
+
if (s.kind === "passage") return `## A passage they chose in ${s.title} (/${s.id}.md)\n\n${s.text.split("\n").map((l) => `> ${l}`).join("\n")}`;
|
|
3129
|
+
if (s.kind === "folder") return `## The folder ${s.id || "at the top of the base"}: its notes\n\n${s.notes.map((n) => `- ${n.title} (/${n.id}.md)${n.description ? `: ${n.description}` : ""}`).join("\n") || "(empty)"}`;
|
|
3130
|
+
return `## Their understanding of ${s.title} (/${s.id}.md): ${s.squares} of 6 squares\n\nYour reading so far: ${s.reading || "none yet."}\n\nWhat changed it, newest first:\n${s.events.map((e) => `- ${e}`).join("\n") || "- nothing yet"}`;
|
|
3131
|
+
}
|
|
3132
|
+
/** An earlier turn as it is sent: in the shape `conversation()` shortens. */
|
|
3133
|
+
function turnExchange(t) {
|
|
3134
|
+
const text = t.items.filter((i) => i.kind === "text").map((i) => i.text).join("\n\n").trim();
|
|
3135
|
+
const changed = t.items.flatMap((i) => i.kind === "change" ? [`${i.why === "create" ? "made" : i.why === "delete" ? "deleted" : i.why === "move" ? "moved" : "changed"} ${i.title} (/${i.note}.md)`] : []);
|
|
3136
|
+
const gave = t.given.filter((g) => g.kind !== "understanding").map((g) => g.kind === "passage" ? `a passage of /${g.id}.md` : `/${g.id}${g.kind === "note" ? ".md" : "/"}`);
|
|
3137
|
+
return {
|
|
3138
|
+
id: t.id,
|
|
3139
|
+
note: "",
|
|
3140
|
+
text: `In ${t.mode} mode they asked${gave.length ? `, giving you ${gave.join(", ")}` : ""}: ${t.asked}\n\nYou wrote:\n\n${text || "(nothing)"}\n\n${changed.length ? `You ${changed.join("; ")}.` : t.state === "done" ? "" : `The turn was ${t.state}.`}`.trim()
|
|
3141
|
+
};
|
|
2944
3142
|
}
|
|
2945
|
-
/** The
|
|
2946
|
-
function
|
|
2947
|
-
|
|
3143
|
+
/** The messages for a turn, and what they hold, part by part. */
|
|
3144
|
+
function turnMessages(a) {
|
|
3145
|
+
const system = [
|
|
3146
|
+
WHO,
|
|
3147
|
+
MODE_RULES[a.mode],
|
|
3148
|
+
FORMAT_RULES,
|
|
3149
|
+
LOOKING(ROUNDS[a.tier], a.meaning, a.lookup)
|
|
3150
|
+
].join("\n\n");
|
|
3151
|
+
const talk = conversation(a.earlier.map(turnExchange));
|
|
3152
|
+
const earlier = talk ? `# Earlier in this conversation\n\n${talk}` : "";
|
|
3153
|
+
const understood = a.sent.filter((s) => s.kind === "understanding");
|
|
3154
|
+
const given = a.sent.filter((s) => s.kind !== "understanding");
|
|
3155
|
+
const gave = given.length ? `# What they gave you\n\n${given.map(sentText).join("\n\n")}` : "";
|
|
3156
|
+
const known = understood.length ? `# Their understanding, as you have recorded it\n\n${understood.map(sentText).join("\n\n")}` : "";
|
|
3157
|
+
const request = `# What they ask now\n\n${a.asked}`;
|
|
3158
|
+
return {
|
|
3159
|
+
messages: [{
|
|
3160
|
+
role: "system",
|
|
3161
|
+
content: [{
|
|
3162
|
+
text: system,
|
|
3163
|
+
cache: true
|
|
3164
|
+
}]
|
|
3165
|
+
}, {
|
|
3166
|
+
role: "user",
|
|
3167
|
+
content: [
|
|
3168
|
+
earlier,
|
|
3169
|
+
known,
|
|
3170
|
+
gave,
|
|
3171
|
+
request
|
|
3172
|
+
].filter(Boolean).join("\n\n")
|
|
3173
|
+
}],
|
|
3174
|
+
parts: [
|
|
3175
|
+
{
|
|
3176
|
+
name: "instructions and tools",
|
|
3177
|
+
detail: `${a.mode} mode${a.lookup ? "" : ", no look-ups"}`,
|
|
3178
|
+
tokens: estimate(system) + (a.lookup ? 500 : 0) + MODE_TOOLS[a.mode].length * 120
|
|
3179
|
+
},
|
|
3180
|
+
{
|
|
3181
|
+
name: "earlier in this conversation",
|
|
3182
|
+
detail: a.earlier.length ? `${a.earlier.length} ${a.earlier.length === 1 ? "turn" : "turns"}` : "",
|
|
3183
|
+
tokens: estimate(earlier)
|
|
3184
|
+
},
|
|
3185
|
+
{
|
|
3186
|
+
name: "what you gave",
|
|
3187
|
+
detail: given.length ? `${given.length} ${given.length === 1 ? "part" : "parts"}` : "nothing",
|
|
3188
|
+
tokens: estimate(gave)
|
|
3189
|
+
},
|
|
3190
|
+
...a.mode === "understand" ? [{
|
|
3191
|
+
name: "your understanding",
|
|
3192
|
+
detail: `${understood.length} ${understood.length === 1 ? "note" : "notes"}`,
|
|
3193
|
+
tokens: estimate(known)
|
|
3194
|
+
}] : [],
|
|
3195
|
+
{
|
|
3196
|
+
name: "your message",
|
|
3197
|
+
detail: "",
|
|
3198
|
+
tokens: estimate(request)
|
|
3199
|
+
}
|
|
3200
|
+
]
|
|
3201
|
+
};
|
|
2948
3202
|
}
|
|
2949
3203
|
//#endregion
|
|
2950
3204
|
//#region packages/engine/src/meaning.ts
|
|
2951
3205
|
const MAX_PIECE = 1800;
|
|
2952
|
-
const QUERY = "Represent this sentence for searching relevant passages: ";
|
|
2953
3206
|
const fingerprint = (text) => sha256(text).slice(0, 20);
|
|
2954
3207
|
/** Every note cut into pieces: at its first- and second-level headings (not inside a code fence), and a long section at its paragraphs. */
|
|
2955
3208
|
function pieces(b) {
|
|
@@ -3105,7 +3358,7 @@ var Meaning = class {
|
|
|
3105
3358
|
pending: missing.length,
|
|
3106
3359
|
ready: false
|
|
3107
3360
|
};
|
|
3108
|
-
const q = pack(await this.embedder.embed(
|
|
3361
|
+
const q = pack(await this.embedder.embed((this.embedder.query ?? "") + text)), dim = this.embedder.dim;
|
|
3109
3362
|
const under = opts.under?.replace(/^\/+|\/+$/g, "");
|
|
3110
3363
|
const best = /* @__PURE__ */ new Map();
|
|
3111
3364
|
for (const p of ps) {
|
|
@@ -3140,9 +3393,9 @@ const DRAFT_PREFIX = "@draft/";
|
|
|
3140
3393
|
const DRAFT_DIR = "drafts";
|
|
3141
3394
|
const isDraftId = (id) => id.startsWith(DRAFT_PREFIX);
|
|
3142
3395
|
/** A note's id without the prefix that says which folder of the study base it is in. */
|
|
3143
|
-
const bare = (id) => isStudyId(id) ? id.slice(7) : isDraftId(id) ? id.slice(7) : id;
|
|
3396
|
+
const bare = (id) => isStudyId(id) ? id.slice(7) : isDraftId(id) ? id.slice(7) : isUnderstandingId(id) ? id.slice(15) : id;
|
|
3144
3397
|
/** In the knowledge base itself: neither an exercise nor a draft. */
|
|
3145
|
-
const inBase = (id) => !isStudyId(id) && !isDraftId(id);
|
|
3398
|
+
const inBase = (id) => !isStudyId(id) && !isDraftId(id) && !isUnderstandingId(id);
|
|
3146
3399
|
const BAD_PART = /^\.|[\0\\:*?"<>|]|^\s|\s$/;
|
|
3147
3400
|
/** A note's id, checked before it becomes a path: no "..", nothing hidden, no absolute path. */
|
|
3148
3401
|
function checkNoteId(id) {
|
|
@@ -3150,6 +3403,8 @@ function checkNoteId(id) {
|
|
|
3150
3403
|
if (!id || id.length > 400 || parts.some((p) => !p || BAD_PART.test(p))) throw new EngineError(`not a note's id: ${JSON.stringify(id)}`);
|
|
3151
3404
|
return id;
|
|
3152
3405
|
}
|
|
3406
|
+
/** A conversation's or turn's id: a ULID. */
|
|
3407
|
+
const TURN_ID = /^[0-9A-HJKMNP-TV-Z]{26}$/;
|
|
3153
3408
|
const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3154
3409
|
async function walk(files, root) {
|
|
3155
3410
|
const out = [];
|
|
@@ -3195,12 +3450,12 @@ const NOTE_ARG = {
|
|
|
3195
3450
|
type: "string",
|
|
3196
3451
|
description: "The note's id or path in the knowledge base: physics/softening or /physics/softening.md."
|
|
3197
3452
|
};
|
|
3198
|
-
const
|
|
3199
|
-
const
|
|
3453
|
+
const MARKED = "The change is written at once, under your name: the person sees your words marked in the note, and can undo it from the browser. Use this, never edit or write, for any note of the knowledge base.";
|
|
3454
|
+
const EDIT_NOTE = {
|
|
3200
3455
|
type: "function",
|
|
3201
3456
|
function: {
|
|
3202
|
-
name: "
|
|
3203
|
-
description: `
|
|
3457
|
+
name: "edit_note",
|
|
3458
|
+
description: `Change a note of the knowledge base: oldString replaced by newString, as the edit tool does. ${MARKED}`,
|
|
3204
3459
|
parameters: {
|
|
3205
3460
|
type: "object",
|
|
3206
3461
|
properties: {
|
|
@@ -3224,11 +3479,11 @@ const PROPOSE_EDIT = {
|
|
|
3224
3479
|
}
|
|
3225
3480
|
}
|
|
3226
3481
|
};
|
|
3227
|
-
const
|
|
3482
|
+
const WRITE_NOTE = {
|
|
3228
3483
|
type: "function",
|
|
3229
3484
|
function: {
|
|
3230
|
-
name: "
|
|
3231
|
-
description: `
|
|
3485
|
+
name: "write_note",
|
|
3486
|
+
description: `Write a whole note: a new note of the knowledge base, at folder/name (lower case, hyphens), with its frontmatter (type, title), or one written anew. ${MARKED}`,
|
|
3232
3487
|
parameters: {
|
|
3233
3488
|
type: "object",
|
|
3234
3489
|
properties: {
|
|
@@ -3341,6 +3596,14 @@ var Engine = class Engine {
|
|
|
3341
3596
|
step: read.get(c.id)?.step ?? 0,
|
|
3342
3597
|
tokens: estimate(c.body.trim())
|
|
3343
3598
|
};
|
|
3599
|
+
for (const [id, u] of read) {
|
|
3600
|
+
if (!notes[id] || u.last === null || !study) continue;
|
|
3601
|
+
const changed = (await this.journal(id)).findLast((e) => e.kind === "change" && e.who !== "you" && e.why !== "move");
|
|
3602
|
+
if (changed && Date.parse(changed.at) > u.last) notes[id] = {
|
|
3603
|
+
...notes[id],
|
|
3604
|
+
changedSince: changed.at
|
|
3605
|
+
};
|
|
3606
|
+
}
|
|
3344
3607
|
for (const c of exercises?.concepts.values() ?? []) notes[studyId(c.id)] = {
|
|
3345
3608
|
title: c.title,
|
|
3346
3609
|
step: 0,
|
|
@@ -3371,23 +3634,23 @@ var Engine = class Engine {
|
|
|
3371
3634
|
TOOLS[0],
|
|
3372
3635
|
...this.similar ? [FIND_SIMILAR] : [],
|
|
3373
3636
|
...TOOLS.slice(1),
|
|
3374
|
-
|
|
3375
|
-
|
|
3637
|
+
EDIT_NOTE,
|
|
3638
|
+
WRITE_NOTE
|
|
3376
3639
|
];
|
|
3377
3640
|
}
|
|
3378
3641
|
async agentLook({ agent, session, tool, args, asked = "" }) {
|
|
3379
3642
|
if (tool === BRIEF.function.name) return { text: this.brief() };
|
|
3380
|
-
if (tool ===
|
|
3643
|
+
if (tool === EDIT_NOTE.function.name || tool === WRITE_NOTE.function.name) {
|
|
3381
3644
|
const str = (v, name) => {
|
|
3382
3645
|
if (typeof v !== "string") throw new EngineError(`${tool} needs ${name}, as text`, 400);
|
|
3383
3646
|
return v;
|
|
3384
3647
|
};
|
|
3385
|
-
const edit = tool ===
|
|
3648
|
+
const edit = tool === WRITE_NOTE.function.name ? { content: str(args.content, "content") } : {
|
|
3386
3649
|
oldString: str(args.oldString, "oldString"),
|
|
3387
3650
|
newString: str(args.newString, "newString"),
|
|
3388
3651
|
replaceAll: args.replaceAll === true
|
|
3389
3652
|
};
|
|
3390
|
-
return this.
|
|
3653
|
+
return this.agentWrite({
|
|
3391
3654
|
agent,
|
|
3392
3655
|
session,
|
|
3393
3656
|
note: str(args.note, "note"),
|
|
@@ -3419,44 +3682,43 @@ var Engine = class Engine {
|
|
|
3419
3682
|
});
|
|
3420
3683
|
}
|
|
3421
3684
|
}
|
|
3422
|
-
async
|
|
3423
|
-
|
|
3424
|
-
const
|
|
3425
|
-
const
|
|
3685
|
+
async agentWrite({ agent, session, note, asked, edit }) {
|
|
3686
|
+
const who = `agent:${agent}`;
|
|
3687
|
+
const raw = note.replace(/^\/+/, "").replace(/\.md$/i, "");
|
|
3688
|
+
const existing = noteId(this.bundle, raw) ?? await this.onDisk(raw);
|
|
3689
|
+
if (!existing) {
|
|
3690
|
+
if (!("content" in edit)) throw new EngineError(`there is no note ${raw}: to make one, write the whole of it`, 404);
|
|
3691
|
+
const id = this.newNoteId(raw);
|
|
3692
|
+
if (!id) throw new EngineError(`${raw} is not a note's id in this base: give folder/name, lower case with hyphens`, 400);
|
|
3693
|
+
let text = edit.content;
|
|
3694
|
+
if (!/^---\n/.test(text)) text = `---\ntype: Concept\ntitle: ${JSON.stringify(/^#\s+(.+)$/m.exec(text)?.[1]?.trim() ?? id.split("/").at(-1).replace(/-/g, " "))}\n---\n\n${text.replace(/^#\s+.+\n+/, "")}`;
|
|
3695
|
+
await this.one(() => this.put(id, text, null, "edit", { asked }, who));
|
|
3696
|
+
return { text: `Made ${this.titleOf(id, text)} (/${id}.md). The person sees it as yours, and can undo it.` };
|
|
3697
|
+
}
|
|
3698
|
+
const id = existing, now = await this.readNote(id);
|
|
3699
|
+
let text;
|
|
3700
|
+
if ("content" in edit) text = edit.content;
|
|
3701
|
+
else {
|
|
3702
|
+
const at = now.text.indexOf(edit.oldString);
|
|
3703
|
+
if (!edit.oldString || at < 0) throw new EngineError(`that passage is not in ${this.titleOf(id, now.text)} as you wrote it: read the note again and give the passage exactly`, 409);
|
|
3704
|
+
if (edit.replaceAll) text = now.text.split(edit.oldString).join(edit.newString);
|
|
3705
|
+
else {
|
|
3706
|
+
if (now.text.indexOf(edit.oldString, at + 1) >= 0) throw new EngineError(`that passage occurs more than once in ${this.titleOf(id, now.text)}: give more of it, or replaceAll`, 409);
|
|
3707
|
+
text = now.text.slice(0, at) + edit.newString + now.text.slice(at + edit.oldString.length);
|
|
3708
|
+
}
|
|
3709
|
+
}
|
|
3710
|
+
return { text: (await this.one(() => this.put(id, text, now.version, "edit", { asked }, who))).changed ? `Changed ${this.titleOf(id, text)}. The person sees your words marked, and can undo it.` : `That leaves ${this.titleOf(id, now.text)} as it is.` };
|
|
3711
|
+
}
|
|
3712
|
+
/** A note's id when its file is there, though the index may not have it yet; else null. */
|
|
3713
|
+
async onDisk(raw) {
|
|
3714
|
+
let id;
|
|
3715
|
+
try {
|
|
3716
|
+
id = checkNoteId(raw);
|
|
3717
|
+
} catch {
|
|
3718
|
+
return null;
|
|
3719
|
+
}
|
|
3426
3720
|
const { files, path } = this.where(id);
|
|
3427
|
-
|
|
3428
|
-
if (!("content" in edit)) throw new EngineError(`there is no note ${id}: to propose a new note, write the whole of it`, 404);
|
|
3429
|
-
let meta = null, body = edit.content;
|
|
3430
|
-
try {
|
|
3431
|
-
[meta, body] = splitFrontmatter(edit.content);
|
|
3432
|
-
} catch {}
|
|
3433
|
-
const title = typeof meta?.title === "string" && meta.title ? meta.title : id.split("/").at(-1);
|
|
3434
|
-
const draft = DRAFT_PREFIX + newId(this.opts.clock.now()).toLowerCase();
|
|
3435
|
-
await this.createNote(draft, `---\ntype: ${typeof meta?.type === "string" ? meta.type : "Concept"}\ntitle: ${JSON.stringify(title)}\n---\n`);
|
|
3436
|
-
await this.one(async () => this.enter(draft, await this.journal(draft), {
|
|
3437
|
-
...await this.stamp(who),
|
|
3438
|
-
kind: "proposal",
|
|
3439
|
-
form: "insert",
|
|
3440
|
-
after: null,
|
|
3441
|
-
before: null,
|
|
3442
|
-
text: body.replace(/^\n+/, ""),
|
|
3443
|
-
asked: asked || `a new note at ${id}`,
|
|
3444
|
-
read
|
|
3445
|
-
}));
|
|
3446
|
-
return { text: `The new note "${title}" waits for the person to review in the browser, as a note for review; they choose its folder when they keep it (you meant ${id.split("/").slice(0, -1).join("/") || "the top"}). Nothing was written to the knowledge base. Do not write it again.` };
|
|
3447
|
-
}
|
|
3448
|
-
const current = (await this.readNote(id)).text;
|
|
3449
|
-
const regions = regionsOf(current, edit);
|
|
3450
|
-
if (!regions.length) return { text: `That leaves ${this.titleOf(id, current)} as it is: nothing was proposed.` };
|
|
3451
|
-
await this.one(async () => this.enter(id, await this.journal(id), {
|
|
3452
|
-
...await this.stamp(who),
|
|
3453
|
-
kind: "proposal",
|
|
3454
|
-
form: "rewrite",
|
|
3455
|
-
regions,
|
|
3456
|
-
asked: asked || "an edit",
|
|
3457
|
-
read
|
|
3458
|
-
}));
|
|
3459
|
-
return { text: `Your edit to ${this.titleOf(id, current)} (${regions.length} ${regions.length === 1 ? "passage" : "passages"}) waits for the person to keep it, in the browser or the terminal's note view. The file is unchanged until they do. Do not write it again; go on as if it may not be kept.` };
|
|
3721
|
+
return await files.read(path) === null ? null : id;
|
|
3460
3722
|
}
|
|
3461
3723
|
async agentSend({ note, passage, said }) {
|
|
3462
3724
|
const id = checkNoteId(note), concept = this.bundle.concepts.get(id);
|
|
@@ -3521,7 +3783,8 @@ var Engine = class Engine {
|
|
|
3521
3783
|
createNote(id, text) {
|
|
3522
3784
|
return this.one(() => this.put(id, text, null));
|
|
3523
3785
|
}
|
|
3524
|
-
|
|
3786
|
+
/** `who` is you unless Iso moves it, in Write mode. */
|
|
3787
|
+
async moveNote(id, to, base, who = "you") {
|
|
3525
3788
|
to = to.trim().replace(/^\/+|\/+$/g, "").replace(/\.md$/, "");
|
|
3526
3789
|
if (!inBase(id) || !inBase(to)) throw new EngineError("only a note of the base is moved, and only within it");
|
|
3527
3790
|
checkNoteId(to);
|
|
@@ -3549,7 +3812,7 @@ var Engine = class Engine {
|
|
|
3549
3812
|
}
|
|
3550
3813
|
const hash = hashText(now);
|
|
3551
3814
|
await this.enter(to, entries, {
|
|
3552
|
-
...await this.stamp(
|
|
3815
|
+
...await this.stamp(who),
|
|
3553
3816
|
kind: "change",
|
|
3554
3817
|
why: "move",
|
|
3555
3818
|
before: hash,
|
|
@@ -3574,13 +3837,13 @@ var Engine = class Engine {
|
|
|
3574
3837
|
type: "note-changed",
|
|
3575
3838
|
id,
|
|
3576
3839
|
version: null,
|
|
3577
|
-
who
|
|
3840
|
+
who
|
|
3578
3841
|
});
|
|
3579
3842
|
this.publish({
|
|
3580
3843
|
type: "note-changed",
|
|
3581
3844
|
id: to,
|
|
3582
3845
|
version: hashText(now),
|
|
3583
|
-
who
|
|
3846
|
+
who
|
|
3584
3847
|
});
|
|
3585
3848
|
return now;
|
|
3586
3849
|
});
|
|
@@ -3588,10 +3851,7 @@ var Engine = class Engine {
|
|
|
3588
3851
|
for (const c of linking) {
|
|
3589
3852
|
const source = await this.readNote(c);
|
|
3590
3853
|
const changed = source.text.replace(old, `](/${to}.md`);
|
|
3591
|
-
if (changed !== source.text) await this.
|
|
3592
|
-
base: source.version,
|
|
3593
|
-
text: changed
|
|
3594
|
-
});
|
|
3854
|
+
if (changed !== source.text) await this.one(() => this.put(c, changed, source.version, "edit", {}, who));
|
|
3595
3855
|
}
|
|
3596
3856
|
if (study) for (const ex of Bundle.fromFiles(await walk(study, STUDY_DIR)).concepts.values()) {
|
|
3597
3857
|
if (!testsOf(ex.meta).includes(id)) continue;
|
|
@@ -3609,7 +3869,7 @@ var Engine = class Engine {
|
|
|
3609
3869
|
changed: true
|
|
3610
3870
|
};
|
|
3611
3871
|
}
|
|
3612
|
-
async deleteNote(id, base) {
|
|
3872
|
+
async deleteNote(id, base, who = "you") {
|
|
3613
3873
|
if (!inBase(id)) throw new EngineError("only a note of the base is deleted here");
|
|
3614
3874
|
const notes = this.opts.notes, path = `${checkNoteId(id)}.md`;
|
|
3615
3875
|
await this.one(async () => {
|
|
@@ -3631,7 +3891,7 @@ var Engine = class Engine {
|
|
|
3631
3891
|
type: "note-changed",
|
|
3632
3892
|
id,
|
|
3633
3893
|
version: null,
|
|
3634
|
-
who
|
|
3894
|
+
who
|
|
3635
3895
|
});
|
|
3636
3896
|
}
|
|
3637
3897
|
/** Writes happen one at a time, in the order asked. */
|
|
@@ -3646,7 +3906,7 @@ var Engine = class Engine {
|
|
|
3646
3906
|
if (!this.opts.study) throw new EngineError("there is no study base open", 404);
|
|
3647
3907
|
return {
|
|
3648
3908
|
files: this.opts.study,
|
|
3649
|
-
path: `${isDraftId(id) ? DRAFT_DIR : STUDY_DIR}/${checkNoteId(bare(id))}.md`
|
|
3909
|
+
path: `${isDraftId(id) ? DRAFT_DIR : isUnderstandingId(id) ? UNDERSTANDING_DIR : STUDY_DIR}/${checkNoteId(bare(id))}.md`
|
|
3650
3910
|
};
|
|
3651
3911
|
}
|
|
3652
3912
|
return {
|
|
@@ -3666,7 +3926,7 @@ var Engine = class Engine {
|
|
|
3666
3926
|
restore(id, entry, base) {
|
|
3667
3927
|
return this.one(async () => this.put(id, await this.version(id, entry), base, "restore", { to: entry }));
|
|
3668
3928
|
}
|
|
3669
|
-
async put(id, text, base, why = "edit", extra = {}) {
|
|
3929
|
+
async put(id, text, base, why = "edit", extra = {}, who = "you") {
|
|
3670
3930
|
const { files, path } = this.where(id);
|
|
3671
3931
|
if (text.length > 2e6) throw new EngineError("note too large", 413);
|
|
3672
3932
|
const now = await files.read(path);
|
|
@@ -3686,7 +3946,8 @@ var Engine = class Engine {
|
|
|
3686
3946
|
changed: false
|
|
3687
3947
|
};
|
|
3688
3948
|
await files.write(path, text);
|
|
3689
|
-
|
|
3949
|
+
const entry = change(entries, text, await this.stamp(who), why, extra);
|
|
3950
|
+
await this.enter(id, entries, entry);
|
|
3690
3951
|
if (inBase(id)) {
|
|
3691
3952
|
this.files.set(path, {
|
|
3692
3953
|
path,
|
|
@@ -3700,13 +3961,14 @@ var Engine = class Engine {
|
|
|
3700
3961
|
type: "note-changed",
|
|
3701
3962
|
id,
|
|
3702
3963
|
version,
|
|
3703
|
-
who
|
|
3964
|
+
who
|
|
3704
3965
|
});
|
|
3705
3966
|
return {
|
|
3706
3967
|
id,
|
|
3707
3968
|
text,
|
|
3708
3969
|
version,
|
|
3709
|
-
changed: true
|
|
3970
|
+
changed: true,
|
|
3971
|
+
...entry ? { entry: entry.id } : {}
|
|
3710
3972
|
};
|
|
3711
3973
|
}
|
|
3712
3974
|
get provider() {
|
|
@@ -3808,7 +4070,7 @@ var Engine = class Engine {
|
|
|
3808
4070
|
if (!key && /\/\/(?:[^/]*\.)?openrouter\.ai\b/.test(this.provider.url)) throw new EngineError("There is no provider key yet. Set one in settings, behind the logo.", 409);
|
|
3809
4071
|
const { messages, olds, text: sent } = await this.compose(req, asked);
|
|
3810
4072
|
const maxTokens = req.form === "insert" ? REPLY_TOKENS : req.form === "note" ? NOTE_TOKENS : req.form === "mark" ? MARK_TOKENS : req.form === "exercise" ? EXERCISE_TOKENS : Math.max(REPLY_TOKENS, Math.ceil(olds.join("").length / 3) + 1e3);
|
|
3811
|
-
const most = req.form === "mark" || req.form === "exercise" ? 0 : void 0;
|
|
4073
|
+
const most = req.form === "mark" || req.form === "exercise" || (req.form === "insert" || req.form === "rewrite") && req.choice?.lookup === false ? 0 : void 0;
|
|
3812
4074
|
const request = newId(this.opts.clock.now()), abort = new AbortController();
|
|
3813
4075
|
this.running.set(request, abort);
|
|
3814
4076
|
this.asking.set(req.note, request);
|
|
@@ -4042,20 +4304,7 @@ var Engine = class Engine {
|
|
|
4042
4304
|
body = splitFrontmatter(text)[1];
|
|
4043
4305
|
} catch {}
|
|
4044
4306
|
if (!body.trim()) throw new EngineError("write what you want from Iso first");
|
|
4045
|
-
const linked =
|
|
4046
|
-
let room = LINKED_BUDGET;
|
|
4047
|
-
for (const m of body.matchAll(/\]\(\/([^)\s#]+)\.md(?:#[^)\s]*)?(?:\s+"[^"]*")?\)/g)) {
|
|
4048
|
-
const c = this.bundle.concepts.get(decodeURIComponent(m[1]));
|
|
4049
|
-
if (!c || linked.some((l) => l.id === c.id)) continue;
|
|
4050
|
-
const piece = c.body.trim().slice(0, Math.max(0, room) * 4);
|
|
4051
|
-
if (piece.length < Math.min(c.body.trim().length, 400)) break;
|
|
4052
|
-
linked.push({
|
|
4053
|
-
id: c.id,
|
|
4054
|
-
title: c.title,
|
|
4055
|
-
text: piece + (piece.length < c.body.trim().length ? "\n[…cut: read the rest with read_note]" : "")
|
|
4056
|
-
});
|
|
4057
|
-
room -= estimate(piece);
|
|
4058
|
-
}
|
|
4307
|
+
const linked = this.linkedSent(body);
|
|
4059
4308
|
const earlier = await this.exchangesNow();
|
|
4060
4309
|
return {
|
|
4061
4310
|
...noteMessages({
|
|
@@ -4105,8 +4354,12 @@ var Engine = class Engine {
|
|
|
4105
4354
|
}
|
|
4106
4355
|
const title = this.titleOf(req.note, text);
|
|
4107
4356
|
const entries = await this.journal(req.note);
|
|
4108
|
-
const
|
|
4109
|
-
const
|
|
4357
|
+
const off = new Set(req.choice?.off ?? []);
|
|
4358
|
+
const earlier = (await this.exchangesNow()).filter((x) => !off.has(`x:${x.id}`));
|
|
4359
|
+
let body = text;
|
|
4360
|
+
try {
|
|
4361
|
+
body = splitFrontmatter(text)[1];
|
|
4362
|
+
} catch {}
|
|
4110
4363
|
const given = {
|
|
4111
4364
|
tier: req.tier,
|
|
4112
4365
|
meaning: Boolean(this.similar),
|
|
@@ -4117,10 +4370,8 @@ var Engine = class Engine {
|
|
|
4117
4370
|
unkept: pending(entries).map((p) => p.form === "insert" ? p.text : p.regions.map((r) => r.new).join("\n\n")),
|
|
4118
4371
|
conversation: conversation(earlier),
|
|
4119
4372
|
moments: earlier.length,
|
|
4120
|
-
linked:
|
|
4121
|
-
|
|
4122
|
-
tokens: [...read].reduce((n, id) => n + estimate(this.bundle.concepts.get(id)?.body ?? ""), 0)
|
|
4123
|
-
}
|
|
4373
|
+
linked: this.linkedSent(body, off, req.note),
|
|
4374
|
+
lookup: req.choice?.lookup !== false
|
|
4124
4375
|
};
|
|
4125
4376
|
return {
|
|
4126
4377
|
...req.form === "insert" ? insertMessages(given) : rewriteMessages({
|
|
@@ -4131,6 +4382,37 @@ var Engine = class Engine {
|
|
|
4131
4382
|
olds
|
|
4132
4383
|
};
|
|
4133
4384
|
}
|
|
4385
|
+
/** The notes a note's text links to, each once, in order: the note itself left out. */
|
|
4386
|
+
linksOf(body, self = "") {
|
|
4387
|
+
const out = [];
|
|
4388
|
+
for (const m of body.matchAll(/\]\(\/([^)\s#]+)\.md(?:#[^)\s]*)?(?:\s+"[^"]*")?\)/g)) {
|
|
4389
|
+
const c = this.bundle.concepts.get(decodeURIComponent(m[1]));
|
|
4390
|
+
if (c && c.id !== self && !out.some((l) => l.id === c.id)) out.push({
|
|
4391
|
+
id: c.id,
|
|
4392
|
+
title: c.title,
|
|
4393
|
+
body: c.body.trim()
|
|
4394
|
+
});
|
|
4395
|
+
}
|
|
4396
|
+
return out;
|
|
4397
|
+
}
|
|
4398
|
+
/** The linked notes sent with a request, those switched off left out, as far as there is room; the rest Iso
|
|
4399
|
+
* reads with its tools. */
|
|
4400
|
+
linkedSent(body, off = /* @__PURE__ */ new Set(), self = "") {
|
|
4401
|
+
const linked = [];
|
|
4402
|
+
let room = LINKED_BUDGET;
|
|
4403
|
+
for (const c of this.linksOf(body, self)) {
|
|
4404
|
+
if (off.has(`n:${c.id}`)) continue;
|
|
4405
|
+
const piece = c.body.slice(0, Math.max(0, room) * 4);
|
|
4406
|
+
if (piece.length < Math.min(c.body.length, 400)) break;
|
|
4407
|
+
linked.push({
|
|
4408
|
+
id: c.id,
|
|
4409
|
+
title: c.title,
|
|
4410
|
+
text: piece + (piece.length < c.body.length ? "\n[…cut: read the rest with read_note]" : "")
|
|
4411
|
+
});
|
|
4412
|
+
room -= estimate(piece);
|
|
4413
|
+
}
|
|
4414
|
+
return linked;
|
|
4415
|
+
}
|
|
4134
4416
|
titleOf(id, text) {
|
|
4135
4417
|
try {
|
|
4136
4418
|
const t = splitFrontmatter(text)[0]?.title;
|
|
@@ -4208,22 +4490,43 @@ var Engine = class Engine {
|
|
|
4208
4490
|
if (!this.opts.study) return;
|
|
4209
4491
|
this.cleared = await this.one(() => this.record({ event: "context-cleared" }));
|
|
4210
4492
|
}
|
|
4211
|
-
async context(note) {
|
|
4212
|
-
let parts = null;
|
|
4493
|
+
async context(note, choice) {
|
|
4494
|
+
let parts = null, items = [];
|
|
4213
4495
|
if (note && isDraftId(note)) parts = await this.exerciseDraft(note).catch(() => null) ? await this.readNote(note).then(async (s) => (await this.exerciseCompose(note, s.text, await this.exerciseAsked(note))).parts, () => null) : await this.compose({
|
|
4214
4496
|
note,
|
|
4215
4497
|
form: "note",
|
|
4216
4498
|
tier: "mid"
|
|
4217
4499
|
}, "").then((c) => c.parts, () => null);
|
|
4218
4500
|
else if (note && isStudyId(note)) parts = await this.readNote(note).then(async (s) => (await this.markCompose(note, s.text, null)).parts, () => null);
|
|
4219
|
-
else if (note)
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4501
|
+
else if (note) {
|
|
4502
|
+
({parts} = await this.compose({
|
|
4503
|
+
note,
|
|
4504
|
+
form: "insert",
|
|
4505
|
+
after: null,
|
|
4506
|
+
before: null,
|
|
4507
|
+
asked: "",
|
|
4508
|
+
tier: "mid",
|
|
4509
|
+
...choice ? { choice } : {}
|
|
4510
|
+
}, ""));
|
|
4511
|
+
const off = new Set(choice?.off ?? []);
|
|
4512
|
+
let body = (await this.readNote(note)).text;
|
|
4513
|
+
try {
|
|
4514
|
+
body = splitFrontmatter(body)[1];
|
|
4515
|
+
} catch {}
|
|
4516
|
+
items = [...(await this.exchangesNow()).map((x) => ({
|
|
4517
|
+
id: `x:${x.id}`,
|
|
4518
|
+
kind: "exchange",
|
|
4519
|
+
title: x.text.split("\n")[0].replace(/^In (.+?) \(\/[^)]*\) they asked[^:]*: /, "$1: "),
|
|
4520
|
+
tokens: estimate(x.text),
|
|
4521
|
+
on: !off.has(`x:${x.id}`)
|
|
4522
|
+
})), ...this.linksOf(body, note).map((c) => ({
|
|
4523
|
+
id: `n:${c.id}`,
|
|
4524
|
+
kind: "linked",
|
|
4525
|
+
title: c.title,
|
|
4526
|
+
tokens: estimate(c.body),
|
|
4527
|
+
on: !off.has(`n:${c.id}`)
|
|
4528
|
+
}))];
|
|
4529
|
+
}
|
|
4227
4530
|
if (!parts) {
|
|
4228
4531
|
const earlier = await this.exchangesNow();
|
|
4229
4532
|
({parts} = insertMessages({
|
|
@@ -4243,6 +4546,18 @@ var Engine = class Engine {
|
|
|
4243
4546
|
tokens: 0
|
|
4244
4547
|
} : p);
|
|
4245
4548
|
}
|
|
4549
|
+
return {
|
|
4550
|
+
tokens: parts.reduce((n, p) => n + p.tokens, 0),
|
|
4551
|
+
limit: this.provider.context ?? 2e5,
|
|
4552
|
+
parts,
|
|
4553
|
+
session: await this.sessionCost(),
|
|
4554
|
+
items,
|
|
4555
|
+
lookup: choice?.lookup !== false
|
|
4556
|
+
};
|
|
4557
|
+
}
|
|
4558
|
+
/** What the session has cost: every reply offered, kept or not, every turn in Iso's pane, and every reply that came
|
|
4559
|
+
* to nothing. */
|
|
4560
|
+
async sessionCost() {
|
|
4246
4561
|
const session = await this.sessionNow();
|
|
4247
4562
|
let usd = 0, requests = 0;
|
|
4248
4563
|
for (const entries of (await this.sessionEntries()).values()) for (const e of entries) if ((e.kind === "proposal" && !e.copyOf || e.kind === "marks") && e.cost) {
|
|
@@ -4250,20 +4565,683 @@ var Engine = class Engine {
|
|
|
4250
4565
|
requests++;
|
|
4251
4566
|
}
|
|
4252
4567
|
const text = this.opts.study ? await this.opts.study.read(recordPath(this.opts.device)) : null;
|
|
4253
|
-
for (const e of readRecord(text ?? "")) if (e.event === "iso-unused" && e.session === session) {
|
|
4568
|
+
for (const e of readRecord(text ?? "")) if ((e.event === "iso-unused" || e.event === "iso-turn") && e.session === session) {
|
|
4254
4569
|
usd += typeof e.usd === "number" ? e.usd : 0;
|
|
4255
4570
|
requests++;
|
|
4256
4571
|
}
|
|
4257
4572
|
return {
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
parts,
|
|
4261
|
-
session: {
|
|
4262
|
-
usd,
|
|
4263
|
-
requests
|
|
4264
|
-
}
|
|
4573
|
+
usd,
|
|
4574
|
+
requests
|
|
4265
4575
|
};
|
|
4266
4576
|
}
|
|
4577
|
+
/** The turn running in each conversation: one at a time. */
|
|
4578
|
+
conversing = /* @__PURE__ */ new Map();
|
|
4579
|
+
/** What Iso asked before doing something, waiting for your answer, by the permission's id. */
|
|
4580
|
+
waitingAnswer = /* @__PURE__ */ new Map();
|
|
4581
|
+
async conversation(id) {
|
|
4582
|
+
const study = this.opts.study;
|
|
4583
|
+
if (!study) return null;
|
|
4584
|
+
if (!id) {
|
|
4585
|
+
id = (await study.list(CONVERSATIONS_DIR).catch(() => [])).filter((f) => !f.dir && f.name.endsWith(".jsonl")).map((f) => f.name.slice(0, -6)).sort().at(-1);
|
|
4586
|
+
if (!id) return null;
|
|
4587
|
+
}
|
|
4588
|
+
if (!TURN_ID.test(id)) throw new EngineError("not a conversation's id");
|
|
4589
|
+
const text = await study.read(conversationPath(id));
|
|
4590
|
+
return text === null ? null : {
|
|
4591
|
+
id,
|
|
4592
|
+
turns: readTurns(text).map((t) => this.running.has(t.id) ? {
|
|
4593
|
+
...t,
|
|
4594
|
+
state: "running"
|
|
4595
|
+
} : t)
|
|
4596
|
+
};
|
|
4597
|
+
}
|
|
4598
|
+
/** What was given, checked: notes and passages of the base, and its folders. */
|
|
4599
|
+
given(raw) {
|
|
4600
|
+
if (!Array.isArray(raw) || raw.length > 40) throw new EngineError("give Iso between none and forty notes, folders and passages");
|
|
4601
|
+
return raw.map((g) => {
|
|
4602
|
+
const id = typeof g?.id === "string" ? g.id.replace(/^\/+|\/+$/g, "") : "";
|
|
4603
|
+
if (g?.kind === "folder") {
|
|
4604
|
+
if (id) checkNoteId(id);
|
|
4605
|
+
return {
|
|
4606
|
+
kind: "folder",
|
|
4607
|
+
id
|
|
4608
|
+
};
|
|
4609
|
+
}
|
|
4610
|
+
if (!inBase(checkNoteId(id))) throw new EngineError("only notes of the base are given to Iso");
|
|
4611
|
+
if (g?.kind === "note") return {
|
|
4612
|
+
kind: "note",
|
|
4613
|
+
id
|
|
4614
|
+
};
|
|
4615
|
+
if (g?.kind === "understanding") return {
|
|
4616
|
+
kind: "understanding",
|
|
4617
|
+
id
|
|
4618
|
+
};
|
|
4619
|
+
if (g?.kind === "passage" && typeof g.text === "string" && g.text.trim() && g.text.length <= 2e4) return {
|
|
4620
|
+
kind: "passage",
|
|
4621
|
+
id,
|
|
4622
|
+
text: g.text
|
|
4623
|
+
};
|
|
4624
|
+
throw new EngineError("a part given to Iso is a note, a folder or a passage");
|
|
4625
|
+
});
|
|
4626
|
+
}
|
|
4627
|
+
/** What was given, read now, as it is sent. A note that is gone is left out. */
|
|
4628
|
+
async sentOf(given) {
|
|
4629
|
+
const out = [];
|
|
4630
|
+
for (const g of given) {
|
|
4631
|
+
if (g.kind === "folder") {
|
|
4632
|
+
const under = g.id ? `${g.id}/` : "";
|
|
4633
|
+
out.push({
|
|
4634
|
+
kind: "folder",
|
|
4635
|
+
id: g.id,
|
|
4636
|
+
notes: [...this.bundle.concepts.values()].filter((c) => c.id.startsWith(under) && inBase(c.id)).map((c) => ({
|
|
4637
|
+
id: c.id,
|
|
4638
|
+
title: c.title,
|
|
4639
|
+
description: c.description
|
|
4640
|
+
}))
|
|
4641
|
+
});
|
|
4642
|
+
continue;
|
|
4643
|
+
}
|
|
4644
|
+
const c = this.bundle.concepts.get(g.id);
|
|
4645
|
+
if (!c) continue;
|
|
4646
|
+
if (g.kind === "understanding") {
|
|
4647
|
+
const u = await this.understanding(g.id);
|
|
4648
|
+
const said = (e) => `${e.at.slice(0, 10)}: ${e.by > 0 ? "+" : ""}${e.by}${e.event === "wind-back" ? " (they wound you back)" : e.event === "attempt" ? " (an exercise)" : ""}${e.why ? `, ${e.why}` : ""}`;
|
|
4649
|
+
out.push({
|
|
4650
|
+
kind: "understanding",
|
|
4651
|
+
id: g.id,
|
|
4652
|
+
title: c.title,
|
|
4653
|
+
reading: u.reading,
|
|
4654
|
+
events: u.evidence.slice(-8).reverse().map(said),
|
|
4655
|
+
squares: u.squares
|
|
4656
|
+
});
|
|
4657
|
+
continue;
|
|
4658
|
+
}
|
|
4659
|
+
if (g.kind === "passage") out.push({
|
|
4660
|
+
kind: "passage",
|
|
4661
|
+
id: g.id,
|
|
4662
|
+
title: c.title,
|
|
4663
|
+
text: g.text
|
|
4664
|
+
});
|
|
4665
|
+
else out.push({
|
|
4666
|
+
kind: "note",
|
|
4667
|
+
id: g.id,
|
|
4668
|
+
title: c.title,
|
|
4669
|
+
text: (await this.readNote(g.id)).text
|
|
4670
|
+
});
|
|
4671
|
+
}
|
|
4672
|
+
return out;
|
|
4673
|
+
}
|
|
4674
|
+
async turnContext(req) {
|
|
4675
|
+
const mode = req.mode === "build" ? "ask" : req.mode;
|
|
4676
|
+
const all = req.conversation ? (await this.conversation(req.conversation))?.turns ?? [] : [];
|
|
4677
|
+
const off = new Set(req.off ?? []), earlier = all.filter((t) => !off.has(t.id));
|
|
4678
|
+
const sent = await this.sentOf(this.given(req.given ?? []));
|
|
4679
|
+
const { parts } = turnMessages({
|
|
4680
|
+
mode,
|
|
4681
|
+
tier: req.tier,
|
|
4682
|
+
meaning: Boolean(this.similar),
|
|
4683
|
+
lookup: req.lookup !== false,
|
|
4684
|
+
earlier,
|
|
4685
|
+
sent,
|
|
4686
|
+
asked: ""
|
|
4687
|
+
});
|
|
4688
|
+
const items = all.map((t) => ({
|
|
4689
|
+
id: t.id,
|
|
4690
|
+
kind: "exchange",
|
|
4691
|
+
title: t.asked,
|
|
4692
|
+
tokens: estimate(turnExchange(t).text),
|
|
4693
|
+
on: !off.has(t.id)
|
|
4694
|
+
}));
|
|
4695
|
+
return {
|
|
4696
|
+
tokens: parts.reduce((n, p) => n + p.tokens, 0),
|
|
4697
|
+
limit: this.provider.context ?? 2e5,
|
|
4698
|
+
parts,
|
|
4699
|
+
session: await this.sessionCost(),
|
|
4700
|
+
items,
|
|
4701
|
+
lookup: req.lookup !== false,
|
|
4702
|
+
given: sent.map((g) => ({
|
|
4703
|
+
kind: g.kind,
|
|
4704
|
+
id: g.id,
|
|
4705
|
+
...g.kind === "passage" ? { text: g.text } : {},
|
|
4706
|
+
tokens: estimate(sentText(g))
|
|
4707
|
+
}))
|
|
4708
|
+
};
|
|
4709
|
+
}
|
|
4710
|
+
async converse(req) {
|
|
4711
|
+
const http = this.opts.http, study = this.opts.study;
|
|
4712
|
+
if (!http) throw new EngineError("Iso cannot be asked here", 409);
|
|
4713
|
+
if (!study) throw new EngineError("Iso's conversations are kept in a study base, and none is open (isorite study init)", 409);
|
|
4714
|
+
if (!MODES.includes(req.mode)) throw new EngineError("the mode is ask, write, build or understand");
|
|
4715
|
+
if (req.mode === "build") throw new EngineError("Build mode is the terminal's: start isorite term", 409);
|
|
4716
|
+
if (!TIERS.includes(req.tier)) throw new EngineError("the tier is low, mid or max");
|
|
4717
|
+
const asked = String(req.asked ?? "").trim();
|
|
4718
|
+
if (!asked) throw new EngineError("say what you want from Iso");
|
|
4719
|
+
if (asked.length > 4e3) throw new EngineError("that is too long to ask at once", 413);
|
|
4720
|
+
const given = this.given(req.given ?? []);
|
|
4721
|
+
if (req.conversation !== void 0 && !TURN_ID.test(req.conversation)) throw new EngineError("not a conversation's id");
|
|
4722
|
+
const conversation = req.conversation ?? newId(this.opts.clock.now());
|
|
4723
|
+
if (this.conversing.has(conversation)) throw new EngineError("Iso is still answering in this conversation. Wait for it, or stop it.", 409);
|
|
4724
|
+
this.conversing.set(conversation, "");
|
|
4725
|
+
try {
|
|
4726
|
+
return {
|
|
4727
|
+
conversation,
|
|
4728
|
+
turn: await this.beginTurn(conversation, {
|
|
4729
|
+
...req,
|
|
4730
|
+
mode: req.mode,
|
|
4731
|
+
asked,
|
|
4732
|
+
given
|
|
4733
|
+
}, http)
|
|
4734
|
+
};
|
|
4735
|
+
} catch (err) {
|
|
4736
|
+
this.conversing.delete(conversation);
|
|
4737
|
+
throw err;
|
|
4738
|
+
}
|
|
4739
|
+
}
|
|
4740
|
+
async beginTurn(conversation, req, http) {
|
|
4741
|
+
const study = this.opts.study;
|
|
4742
|
+
const key = await this.opts.secrets?.get("provider.key") ?? null;
|
|
4743
|
+
if (!key && /\/\/(?:[^/]*\.)?openrouter\.ai\b/.test(this.provider.url)) throw new EngineError("There is no provider key yet. Set one in settings, behind the logo.", 409);
|
|
4744
|
+
const off = new Set(req.off ?? []), earlier = ((await this.conversation(conversation))?.turns ?? []).filter((t) => !off.has(t.id));
|
|
4745
|
+
const lookupOn = req.lookup !== false, mode = req.mode;
|
|
4746
|
+
const { messages } = turnMessages({
|
|
4747
|
+
mode,
|
|
4748
|
+
tier: req.tier,
|
|
4749
|
+
meaning: Boolean(this.similar),
|
|
4750
|
+
lookup: lookupOn,
|
|
4751
|
+
earlier,
|
|
4752
|
+
sent: await this.sentOf(req.given),
|
|
4753
|
+
asked: req.asked
|
|
4754
|
+
});
|
|
4755
|
+
const s = await this.stamp("you"), turn = s.id, abort = new AbortController(), path = conversationPath(conversation);
|
|
4756
|
+
this.last = {
|
|
4757
|
+
at: s.at,
|
|
4758
|
+
session: s.session
|
|
4759
|
+
};
|
|
4760
|
+
const line = (l) => this.one(() => study.append(path, JSON.stringify(l) + "\n"));
|
|
4761
|
+
await line({
|
|
4762
|
+
kind: "turn",
|
|
4763
|
+
id: turn,
|
|
4764
|
+
conversation,
|
|
4765
|
+
at: s.at,
|
|
4766
|
+
session: s.session,
|
|
4767
|
+
mode,
|
|
4768
|
+
asked: req.asked,
|
|
4769
|
+
given: req.given,
|
|
4770
|
+
tier: req.tier
|
|
4771
|
+
});
|
|
4772
|
+
this.running.set(turn, abort);
|
|
4773
|
+
this.conversing.set(conversation, turn);
|
|
4774
|
+
this.publish({
|
|
4775
|
+
type: "iso-start",
|
|
4776
|
+
request: turn,
|
|
4777
|
+
note: "",
|
|
4778
|
+
asked: req.asked,
|
|
4779
|
+
tier: req.tier,
|
|
4780
|
+
place: {
|
|
4781
|
+
form: "turn",
|
|
4782
|
+
conversation,
|
|
4783
|
+
mode,
|
|
4784
|
+
given: req.given
|
|
4785
|
+
}
|
|
4786
|
+
});
|
|
4787
|
+
const item = async (i) => {
|
|
4788
|
+
await line({
|
|
4789
|
+
kind: "item",
|
|
4790
|
+
turn,
|
|
4791
|
+
item: i
|
|
4792
|
+
});
|
|
4793
|
+
this.publish({
|
|
4794
|
+
type: "iso-item",
|
|
4795
|
+
request: turn,
|
|
4796
|
+
conversation,
|
|
4797
|
+
item: i
|
|
4798
|
+
});
|
|
4799
|
+
};
|
|
4800
|
+
const who = `iso:${this.provider.name}/${this.provider.tiers[req.tier]}`;
|
|
4801
|
+
const lookup = new Lookup(this.bundle, req.given.find((g) => g.kind !== "folder")?.id ?? "", this.similar);
|
|
4802
|
+
const looks = new Set(lookup.defs().map((d) => d.function.name));
|
|
4803
|
+
const tools = {
|
|
4804
|
+
defs: () => [...lookupOn ? lookup.defs() : [], ...MODE_TOOLS[mode]],
|
|
4805
|
+
call: async (name, raw) => {
|
|
4806
|
+
if (looks.has(name)) return lookupOn ? lookup.call(name, raw) : { text: "Looking things up is off: they chose what you are sent." };
|
|
4807
|
+
let args = {};
|
|
4808
|
+
try {
|
|
4809
|
+
const parsed = raw.trim() ? JSON.parse(raw) : {};
|
|
4810
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) args = parsed;
|
|
4811
|
+
} catch {}
|
|
4812
|
+
if (!MODE_TOOLS[mode].some((d) => d.function.name === name)) return { text: `There is no tool ${name} in ${mode} mode.` };
|
|
4813
|
+
return { text: await this.turnTool(name, args, {
|
|
4814
|
+
turn,
|
|
4815
|
+
conversation,
|
|
4816
|
+
mode,
|
|
4817
|
+
item,
|
|
4818
|
+
signal: abort.signal,
|
|
4819
|
+
who,
|
|
4820
|
+
asked: req.asked
|
|
4821
|
+
}) };
|
|
4822
|
+
}
|
|
4823
|
+
};
|
|
4824
|
+
(async () => {
|
|
4825
|
+
try {
|
|
4826
|
+
const got = await rounds({
|
|
4827
|
+
http,
|
|
4828
|
+
provider: this.provider,
|
|
4829
|
+
key,
|
|
4830
|
+
tier: req.tier,
|
|
4831
|
+
messages,
|
|
4832
|
+
lookup: tools,
|
|
4833
|
+
maxTokens: REPLY_TOKENS * 2,
|
|
4834
|
+
signal: abort.signal,
|
|
4835
|
+
most: ROUNDS[req.tier],
|
|
4836
|
+
onText: (delta, again) => this.publish({
|
|
4837
|
+
type: "iso-text",
|
|
4838
|
+
request: turn,
|
|
4839
|
+
note: "",
|
|
4840
|
+
delta,
|
|
4841
|
+
again
|
|
4842
|
+
}),
|
|
4843
|
+
onStep: (step) => {
|
|
4844
|
+
item({
|
|
4845
|
+
kind: "look",
|
|
4846
|
+
step
|
|
4847
|
+
});
|
|
4848
|
+
this.publish({
|
|
4849
|
+
type: "iso-step",
|
|
4850
|
+
request: turn,
|
|
4851
|
+
note: "",
|
|
4852
|
+
step
|
|
4853
|
+
});
|
|
4854
|
+
}
|
|
4855
|
+
});
|
|
4856
|
+
if (abort.signal.aborted) throw Object.assign(/* @__PURE__ */ new Error("stopped"), { name: "AbortError" });
|
|
4857
|
+
const cost = {
|
|
4858
|
+
model: got.usage.model,
|
|
4859
|
+
tier: req.tier,
|
|
4860
|
+
tokensIn: got.usage.tokensIn,
|
|
4861
|
+
tokensOut: got.usage.tokensOut,
|
|
4862
|
+
tokensCached: got.usage.tokensCached,
|
|
4863
|
+
usd: got.usage.usd
|
|
4864
|
+
};
|
|
4865
|
+
const said = got.text.trim() + (got.cut ? "\n\n[Cut off at its length limit.]" : "");
|
|
4866
|
+
if (said) await item({
|
|
4867
|
+
kind: "text",
|
|
4868
|
+
text: said
|
|
4869
|
+
});
|
|
4870
|
+
await line({
|
|
4871
|
+
kind: "end",
|
|
4872
|
+
turn,
|
|
4873
|
+
state: "done",
|
|
4874
|
+
cost
|
|
4875
|
+
});
|
|
4876
|
+
if (cost.usd > 0) await this.one(async () => this.record({
|
|
4877
|
+
event: "iso-turn",
|
|
4878
|
+
conversation,
|
|
4879
|
+
turn,
|
|
4880
|
+
session: s.session,
|
|
4881
|
+
model: cost.model,
|
|
4882
|
+
usd: cost.usd
|
|
4883
|
+
}));
|
|
4884
|
+
this.publish({
|
|
4885
|
+
type: "iso-done",
|
|
4886
|
+
request: turn,
|
|
4887
|
+
note: "",
|
|
4888
|
+
proposal: "",
|
|
4889
|
+
cost
|
|
4890
|
+
});
|
|
4891
|
+
} catch (err) {
|
|
4892
|
+
const stopped = abort.signal.aborted || err.name === "AbortError";
|
|
4893
|
+
const message = err.message || "Iso could not answer";
|
|
4894
|
+
await line(stopped ? {
|
|
4895
|
+
kind: "end",
|
|
4896
|
+
turn,
|
|
4897
|
+
state: "stopped"
|
|
4898
|
+
} : {
|
|
4899
|
+
kind: "end",
|
|
4900
|
+
turn,
|
|
4901
|
+
state: "failed",
|
|
4902
|
+
message
|
|
4903
|
+
}).catch(() => void 0);
|
|
4904
|
+
if (stopped) this.publish({
|
|
4905
|
+
type: "iso-stopped",
|
|
4906
|
+
request: turn,
|
|
4907
|
+
note: ""
|
|
4908
|
+
});
|
|
4909
|
+
else this.publish({
|
|
4910
|
+
type: "iso-failed",
|
|
4911
|
+
request: turn,
|
|
4912
|
+
note: "",
|
|
4913
|
+
message
|
|
4914
|
+
});
|
|
4915
|
+
} finally {
|
|
4916
|
+
this.running.delete(turn);
|
|
4917
|
+
this.conversing.delete(conversation);
|
|
4918
|
+
for (const [id, w] of this.waitingAnswer) if (w.turn === turn) {
|
|
4919
|
+
this.waitingAnswer.delete(id);
|
|
4920
|
+
w.settle("reject");
|
|
4921
|
+
}
|
|
4922
|
+
}
|
|
4923
|
+
})();
|
|
4924
|
+
return turn;
|
|
4925
|
+
}
|
|
4926
|
+
/** What you allowed for the rest of a conversation ("always"): by conversation, the tools. */
|
|
4927
|
+
allowed = /* @__PURE__ */ new Map();
|
|
4928
|
+
/** Ask you before doing something; wait for your answer. "always" holds for the rest of the conversation. */
|
|
4929
|
+
async permit(t, tool, note, what) {
|
|
4930
|
+
if (this.allowed.get(t.conversation)?.has(tool)) return "always";
|
|
4931
|
+
const id = newId(this.opts.clock.now());
|
|
4932
|
+
const answer = new Promise((settle) => this.waitingAnswer.set(id, {
|
|
4933
|
+
turn: t.turn,
|
|
4934
|
+
conversation: t.conversation,
|
|
4935
|
+
settle
|
|
4936
|
+
}));
|
|
4937
|
+
await t.item({
|
|
4938
|
+
kind: "permission",
|
|
4939
|
+
id,
|
|
4940
|
+
tool,
|
|
4941
|
+
note,
|
|
4942
|
+
what
|
|
4943
|
+
});
|
|
4944
|
+
const stop = new Promise((settle) => t.signal.addEventListener("abort", () => settle("reject"), { once: true }));
|
|
4945
|
+
const d = await Promise.race([answer, stop]);
|
|
4946
|
+
this.waitingAnswer.delete(id);
|
|
4947
|
+
if (d === "always") this.allowed.set(t.conversation, /* @__PURE__ */ new Set([...this.allowed.get(t.conversation) ?? [], tool]));
|
|
4948
|
+
return d;
|
|
4949
|
+
}
|
|
4950
|
+
/** A note's id as Iso writes one for a new note: folder/name, without a leading slash or `.md`. */
|
|
4951
|
+
newNoteId(raw) {
|
|
4952
|
+
const id = String(raw ?? "").trim().replace(/^\/+|\/+$/g, "").replace(/\.md$/i, "");
|
|
4953
|
+
try {
|
|
4954
|
+
checkNoteId(id);
|
|
4955
|
+
} catch {
|
|
4956
|
+
return null;
|
|
4957
|
+
}
|
|
4958
|
+
return inBase(id) ? id : null;
|
|
4959
|
+
}
|
|
4960
|
+
/** One of a mode's own tools, in a turn: what is said back to Iso. */
|
|
4961
|
+
async turnTool(name, args, t) {
|
|
4962
|
+
const title = (id) => this.bundle.concepts.get(id)?.title ?? id;
|
|
4963
|
+
if (name === "point_to_note") {
|
|
4964
|
+
const id = noteId(this.bundle, args.id);
|
|
4965
|
+
if (!id) return `There is no note ${String(args.id ?? "")}.`;
|
|
4966
|
+
await t.item({
|
|
4967
|
+
kind: "point",
|
|
4968
|
+
note: id,
|
|
4969
|
+
why: String(args.why ?? "").trim().slice(0, 300)
|
|
4970
|
+
});
|
|
4971
|
+
return `${title(id)} is marked on their Atlas.`;
|
|
4972
|
+
}
|
|
4973
|
+
const counted = (before, after) => {
|
|
4974
|
+
const hunks = diffLines(before.split("\n"), after.split("\n"));
|
|
4975
|
+
return {
|
|
4976
|
+
added: hunks.reduce((n, h) => n + h.ins.length, 0),
|
|
4977
|
+
removed: hunks.reduce((n, h) => n + h.del.length, 0)
|
|
4978
|
+
};
|
|
4979
|
+
};
|
|
4980
|
+
if (name === "edit_note") {
|
|
4981
|
+
const id = noteId(this.bundle, args.id);
|
|
4982
|
+
if (!id) return `There is no note ${String(args.id ?? "")}. Search for it by its words.`;
|
|
4983
|
+
const old = String(args.old ?? ""), next = String(args.new ?? "");
|
|
4984
|
+
if (!old) return "Give the passage as it is now, in old.";
|
|
4985
|
+
const now = await this.readNote(id);
|
|
4986
|
+
const at = now.text.indexOf(old), again = at < 0 ? -1 : now.text.indexOf(old, at + 1);
|
|
4987
|
+
if (at < 0) return `That passage is not in ${title(id)} as you wrote it. Read the note again and give the passage exactly.`;
|
|
4988
|
+
if (again >= 0) return `That passage occurs more than once in ${title(id)}. Give more of it, so it is found once.`;
|
|
4989
|
+
const text = now.text.slice(0, at) + next + now.text.slice(at + old.length);
|
|
4990
|
+
const saved = await this.one(() => this.put(id, text, now.version, "edit", { asked: t.asked }, t.who));
|
|
4991
|
+
if (saved.entry) await t.item({
|
|
4992
|
+
kind: "change",
|
|
4993
|
+
note: id,
|
|
4994
|
+
entry: saved.entry,
|
|
4995
|
+
why: "edit",
|
|
4996
|
+
title: title(id),
|
|
4997
|
+
...counted(now.text, text)
|
|
4998
|
+
});
|
|
4999
|
+
return `Changed ${title(id)}.`;
|
|
5000
|
+
}
|
|
5001
|
+
if (name === "write_note") {
|
|
5002
|
+
const id = this.newNoteId(args.id);
|
|
5003
|
+
if (!id) return `${String(args.id ?? "")} is not a note's id in their base: give folder/name, lower case with hyphens.`;
|
|
5004
|
+
let text = String(args.text ?? "");
|
|
5005
|
+
if (!text.trim()) return "Give the note's whole text.";
|
|
5006
|
+
if (!/^---\n/.test(text)) text = `---\ntype: Concept\ntitle: ${JSON.stringify(/^#\s+(.+)$/m.exec(text)?.[1]?.trim() ?? id.split("/").at(-1).replace(/-/g, " "))}\n---\n\n${text.replace(/^#\s+.+\n+/, "")}`;
|
|
5007
|
+
const was = this.bundle.concepts.has(id) ? await this.readNote(id) : null;
|
|
5008
|
+
const saved = await this.one(() => this.put(id, text, was?.version ?? null, "edit", { asked: t.asked }, t.who));
|
|
5009
|
+
if (saved.entry) await t.item({
|
|
5010
|
+
kind: "change",
|
|
5011
|
+
note: id,
|
|
5012
|
+
entry: saved.entry,
|
|
5013
|
+
why: was ? "edit" : "create",
|
|
5014
|
+
title: title(id),
|
|
5015
|
+
...counted(was?.text ?? "", text)
|
|
5016
|
+
});
|
|
5017
|
+
return was ? `Wrote ${title(id)} anew.` : `Made ${title(id)} (/${id}.md).`;
|
|
5018
|
+
}
|
|
5019
|
+
if (name === "move_note") {
|
|
5020
|
+
const id = noteId(this.bundle, args.id), to = this.newNoteId(args.to);
|
|
5021
|
+
if (!id) return `There is no note ${String(args.id ?? "")}.`;
|
|
5022
|
+
if (!to) return `${String(args.to ?? "")} is not a note's id in their base.`;
|
|
5023
|
+
if (await this.permit(t, "move_note", id, `move ${title(id)} to ${to}`) === "reject") return "They did not allow it. It was not moved.";
|
|
5024
|
+
const now = await this.readNote(id), before = title(id);
|
|
5025
|
+
try {
|
|
5026
|
+
await this.moveNote(id, to, now.version, t.who);
|
|
5027
|
+
} catch (e) {
|
|
5028
|
+
return `It could not be moved: ${e.message}`;
|
|
5029
|
+
}
|
|
5030
|
+
const entry = (await this.journal(to)).at(-1)?.id ?? "";
|
|
5031
|
+
await t.item({
|
|
5032
|
+
kind: "change",
|
|
5033
|
+
note: to,
|
|
5034
|
+
entry,
|
|
5035
|
+
why: "move",
|
|
5036
|
+
to,
|
|
5037
|
+
title: before,
|
|
5038
|
+
added: 0,
|
|
5039
|
+
removed: 0
|
|
5040
|
+
});
|
|
5041
|
+
return `Moved ${before} to /${to}.md.`;
|
|
5042
|
+
}
|
|
5043
|
+
if (name === "delete_note") {
|
|
5044
|
+
const id = noteId(this.bundle, args.id);
|
|
5045
|
+
if (!id) return `There is no note ${String(args.id ?? "")}.`;
|
|
5046
|
+
if (await this.permit(t, "delete_note", id, `delete ${title(id)}`) === "reject") return "They did not allow it. It was not deleted.";
|
|
5047
|
+
const now = await this.readNote(id), before = title(id);
|
|
5048
|
+
try {
|
|
5049
|
+
await this.deleteNote(id, now.version, t.who);
|
|
5050
|
+
} catch (e) {
|
|
5051
|
+
return `It could not be deleted: ${e.message}`;
|
|
5052
|
+
}
|
|
5053
|
+
await t.item({
|
|
5054
|
+
kind: "change",
|
|
5055
|
+
note: id,
|
|
5056
|
+
entry: (await this.journal(id)).at(-1)?.id ?? "",
|
|
5057
|
+
why: "delete",
|
|
5058
|
+
title: before,
|
|
5059
|
+
added: 0,
|
|
5060
|
+
removed: now.text.split("\n").length
|
|
5061
|
+
});
|
|
5062
|
+
return `Deleted ${before}. Its history stays.`;
|
|
5063
|
+
}
|
|
5064
|
+
if (name === "record_understanding") {
|
|
5065
|
+
const id = noteId(this.bundle, args.id);
|
|
5066
|
+
if (!id) return `There is no note ${String(args.id ?? "")}.`;
|
|
5067
|
+
const by = Number(args.by);
|
|
5068
|
+
if (![
|
|
5069
|
+
-1,
|
|
5070
|
+
-.5,
|
|
5071
|
+
0,
|
|
5072
|
+
.5,
|
|
5073
|
+
1
|
|
5074
|
+
].includes(by)) return "by is one of -1, -0.5, 0, 0.5, 1.";
|
|
5075
|
+
const why = String(args.why ?? "").trim().slice(0, 300);
|
|
5076
|
+
if (this.recorded.get(t.turn)?.has(id)) return `You recorded ${title(id)} once this turn already.`;
|
|
5077
|
+
this.recorded.set(t.turn, /* @__PURE__ */ new Set([...this.recorded.get(t.turn) ?? [], id]));
|
|
5078
|
+
const event = await this.one(() => this.record({
|
|
5079
|
+
event: "understood",
|
|
5080
|
+
note: id,
|
|
5081
|
+
by,
|
|
5082
|
+
why,
|
|
5083
|
+
conversation: t.conversation,
|
|
5084
|
+
turn: t.turn
|
|
5085
|
+
}));
|
|
5086
|
+
await t.item({
|
|
5087
|
+
kind: "understood",
|
|
5088
|
+
note: id,
|
|
5089
|
+
event,
|
|
5090
|
+
by,
|
|
5091
|
+
why
|
|
5092
|
+
});
|
|
5093
|
+
return `Recorded for ${title(id)}.`;
|
|
5094
|
+
}
|
|
5095
|
+
if (name === "write_understanding") {
|
|
5096
|
+
const id = noteId(this.bundle, args.id);
|
|
5097
|
+
if (!id) return `There is no note ${String(args.id ?? "")}.`;
|
|
5098
|
+
const reading = String(args.reading ?? "").trim();
|
|
5099
|
+
if (!reading) return "Give your reading.";
|
|
5100
|
+
const uid = understandingId(id), was = await this.readNote(uid).catch(() => null);
|
|
5101
|
+
const text = understandingText(id, title(id), {
|
|
5102
|
+
...parseUnderstanding(was?.text ?? null),
|
|
5103
|
+
reading
|
|
5104
|
+
});
|
|
5105
|
+
const saved = await this.one(() => this.put(uid, text, was?.version ?? null, "edit", {}, t.who));
|
|
5106
|
+
if (saved.entry) await t.item({
|
|
5107
|
+
kind: "change",
|
|
5108
|
+
note: uid,
|
|
5109
|
+
entry: saved.entry,
|
|
5110
|
+
why: was ? "edit" : "create",
|
|
5111
|
+
title: `Understanding · ${title(id)}`,
|
|
5112
|
+
added: 0,
|
|
5113
|
+
removed: 0
|
|
5114
|
+
});
|
|
5115
|
+
return `Your reading of ${title(id)} is written.`;
|
|
5116
|
+
}
|
|
5117
|
+
return `${name} cannot be used yet.`;
|
|
5118
|
+
}
|
|
5119
|
+
/** The notes each turn recorded understanding of: once a note a turn. */
|
|
5120
|
+
recorded = /* @__PURE__ */ new Map();
|
|
5121
|
+
/** A note's Understanding note (M10): Iso's reading, your scratchpad, and what the record says of it. */
|
|
5122
|
+
async understanding(note) {
|
|
5123
|
+
if (!inBase(checkNoteId(note))) throw new EngineError("only a note of the base has an Understanding note");
|
|
5124
|
+
const uid = understandingId(note);
|
|
5125
|
+
const source = this.opts.study ? await this.readNote(uid).catch(() => null) : null;
|
|
5126
|
+
const read = (await this.study()).notes[note];
|
|
5127
|
+
return {
|
|
5128
|
+
note,
|
|
5129
|
+
...parseUnderstanding(source?.text ?? null),
|
|
5130
|
+
version: source?.version ?? null,
|
|
5131
|
+
level: read?.level ?? 0,
|
|
5132
|
+
squares: read?.squares ?? 0,
|
|
5133
|
+
step: read?.step ?? 0,
|
|
5134
|
+
due: read?.due ?? false,
|
|
5135
|
+
evidence: read?.evidence ?? []
|
|
5136
|
+
};
|
|
5137
|
+
}
|
|
5138
|
+
/** Your scratchpad in a note's Understanding note: yours alone; Iso does not write it. `base` as for saveNote, or
|
|
5139
|
+
* null when there is no Understanding note yet. */
|
|
5140
|
+
async saveScratchpad(note, scratchpad, base) {
|
|
5141
|
+
if (!this.opts.study) throw new EngineError("there is no study base open", 409);
|
|
5142
|
+
if (!inBase(checkNoteId(note))) throw new EngineError("only a note of the base has an Understanding note");
|
|
5143
|
+
const uid = understandingId(note), was = await this.readNote(uid).catch(() => null);
|
|
5144
|
+
const title = this.bundle.concepts.get(note)?.title ?? note;
|
|
5145
|
+
await this.one(() => this.put(uid, understandingText(note, title, {
|
|
5146
|
+
...parseUnderstanding(was?.text ?? null),
|
|
5147
|
+
scratchpad
|
|
5148
|
+
}), base));
|
|
5149
|
+
return this.understanding(note);
|
|
5150
|
+
}
|
|
5151
|
+
async undoTurn(conversation, turn) {
|
|
5152
|
+
const t = (await this.conversation(conversation))?.turns.find((x) => x.id === turn);
|
|
5153
|
+
if (!t) throw new EngineError("there is no such turn", 404);
|
|
5154
|
+
if (t.state === "running") throw new EngineError("wait for Iso to finish, or stop it, first", 409);
|
|
5155
|
+
if (t.items.some((i) => i.kind === "undone")) throw new EngineError("that turn was undone already", 409);
|
|
5156
|
+
const changes = t.items.filter((i) => i.kind === "change");
|
|
5157
|
+
const notes = [], left = [];
|
|
5158
|
+
const done = /* @__PURE__ */ new Set();
|
|
5159
|
+
for (const ch of [...changes].reverse()) {
|
|
5160
|
+
const key = ch.why === "move" ? ch.to : ch.note;
|
|
5161
|
+
if (done.has(key)) continue;
|
|
5162
|
+
done.add(key);
|
|
5163
|
+
try {
|
|
5164
|
+
if (ch.why === "delete") {
|
|
5165
|
+
const was = replay(await this.journal(ch.note))?.text;
|
|
5166
|
+
if (was === void 0 || this.bundle.concepts.has(ch.note)) {
|
|
5167
|
+
left.push(ch.note);
|
|
5168
|
+
continue;
|
|
5169
|
+
}
|
|
5170
|
+
await this.createNote(ch.note, was);
|
|
5171
|
+
} else if (ch.why === "move") {
|
|
5172
|
+
const from = (await this.journal(ch.to)).find((e) => e.id === ch.entry);
|
|
5173
|
+
const back = from && "from" in from ? from.from : void 0;
|
|
5174
|
+
if (!back || !this.bundle.concepts.has(ch.to)) {
|
|
5175
|
+
left.push(ch.to);
|
|
5176
|
+
continue;
|
|
5177
|
+
}
|
|
5178
|
+
await this.moveNote(ch.to, back, (await this.readNote(ch.to)).version);
|
|
5179
|
+
} else {
|
|
5180
|
+
const entries = await this.journal(ch.note);
|
|
5181
|
+
const mine = changes.filter((x) => x.note === ch.note && x.why !== "move").map((x) => x.entry);
|
|
5182
|
+
const first = entries.findIndex((e) => mine.includes(e.id)), last = entries.findLastIndex((e) => e.kind === "change");
|
|
5183
|
+
if (first < 0 || !mine.includes(entries[last]?.id ?? "")) {
|
|
5184
|
+
left.push(ch.note);
|
|
5185
|
+
continue;
|
|
5186
|
+
}
|
|
5187
|
+
const now = await this.readNote(ch.note);
|
|
5188
|
+
if (hashText(now.text) !== hashText(replay(entries).text)) {
|
|
5189
|
+
left.push(ch.note);
|
|
5190
|
+
continue;
|
|
5191
|
+
}
|
|
5192
|
+
if (first === 0) await this.deleteNote(ch.note, now.version);
|
|
5193
|
+
else await this.restore(ch.note, entries.slice(0, first).findLast((e) => e.kind === "change").id, now.version);
|
|
5194
|
+
}
|
|
5195
|
+
notes.push(key);
|
|
5196
|
+
} catch {
|
|
5197
|
+
left.push(key);
|
|
5198
|
+
}
|
|
5199
|
+
}
|
|
5200
|
+
const item = {
|
|
5201
|
+
kind: "undone",
|
|
5202
|
+
notes,
|
|
5203
|
+
left
|
|
5204
|
+
};
|
|
5205
|
+
await this.one(() => this.opts.study.append(conversationPath(conversation), JSON.stringify({
|
|
5206
|
+
kind: "item",
|
|
5207
|
+
turn,
|
|
5208
|
+
item
|
|
5209
|
+
}) + "\n"));
|
|
5210
|
+
this.publish({
|
|
5211
|
+
type: "iso-item",
|
|
5212
|
+
request: turn,
|
|
5213
|
+
conversation,
|
|
5214
|
+
item
|
|
5215
|
+
});
|
|
5216
|
+
return {
|
|
5217
|
+
notes,
|
|
5218
|
+
left
|
|
5219
|
+
};
|
|
5220
|
+
}
|
|
5221
|
+
async answer(turn, permission, decision) {
|
|
5222
|
+
if (![
|
|
5223
|
+
"once",
|
|
5224
|
+
"always",
|
|
5225
|
+
"reject"
|
|
5226
|
+
].includes(decision)) throw new EngineError("the answer is once, always or reject");
|
|
5227
|
+
const w = this.waitingAnswer.get(permission);
|
|
5228
|
+
if (!w || w.turn !== turn) throw new EngineError("Iso is not waiting for that answer", 409);
|
|
5229
|
+
this.waitingAnswer.delete(permission);
|
|
5230
|
+
await this.one(() => this.opts.study.append(conversationPath(w.conversation), JSON.stringify({
|
|
5231
|
+
kind: "answer",
|
|
5232
|
+
turn,
|
|
5233
|
+
permission,
|
|
5234
|
+
decision
|
|
5235
|
+
}) + "\n"));
|
|
5236
|
+
this.publish({
|
|
5237
|
+
type: "iso-answer",
|
|
5238
|
+
request: turn,
|
|
5239
|
+
conversation: w.conversation,
|
|
5240
|
+
permission,
|
|
5241
|
+
decision
|
|
5242
|
+
});
|
|
5243
|
+
w.settle(decision);
|
|
5244
|
+
}
|
|
4267
5245
|
async drafts() {
|
|
4268
5246
|
const study = this.opts.study;
|
|
4269
5247
|
if (!study) return [];
|
|
@@ -4538,6 +5516,14 @@ var Engine = class Engine {
|
|
|
4538
5516
|
event: "wind-back",
|
|
4539
5517
|
by: -1
|
|
4540
5518
|
}];
|
|
5519
|
+
if (e.event === "understood") return [{
|
|
5520
|
+
id,
|
|
5521
|
+
at: String(e.at),
|
|
5522
|
+
event: "understood",
|
|
5523
|
+
by: typeof e.by === "number" ? e.by : 0,
|
|
5524
|
+
...typeof e.why === "string" && e.why ? { why: e.why } : {},
|
|
5525
|
+
...typeof e.conversation === "string" ? { conversation: e.conversation } : {}
|
|
5526
|
+
}];
|
|
4541
5527
|
const move = e.moves?.[note];
|
|
4542
5528
|
return [{
|
|
4543
5529
|
id,
|
|
@@ -4854,54 +5840,116 @@ function providerConfig(path = configPath()) {
|
|
|
4854
5840
|
}
|
|
4855
5841
|
//#endregion
|
|
4856
5842
|
//#region packages/server/src/model.ts
|
|
4857
|
-
const
|
|
5843
|
+
const NAME$1 = "granite-embedding-small-english-r2";
|
|
5844
|
+
/** The npm package that carries the files, and the version this isorite expects. */
|
|
5845
|
+
const PACKAGE = "isorite-model-granite@1.0.0";
|
|
5846
|
+
const HUB = `https://huggingface.co/onnx-community/granite-embedding-small-english-r2-ONNX/resolve/1dc7835ba0cb9c76a3618d0bf0c427c97671b3c8`;
|
|
5847
|
+
/** Each file: where it is on Hugging Face, its name here, and its SHA-256. The weights are kept apart from the
|
|
5848
|
+
* graph, under the name the graph gives them. */
|
|
4858
5849
|
const FILES = [
|
|
4859
5850
|
[
|
|
4860
5851
|
"onnx/model_quantized.onnx",
|
|
4861
5852
|
"model.onnx",
|
|
4862
|
-
"
|
|
5853
|
+
"a3fad524afc3f060216a8ddbb1ac89c9b6498fba8995b5718bde879076a2e9ba"
|
|
5854
|
+
],
|
|
5855
|
+
[
|
|
5856
|
+
"onnx/model_quantized.onnx_data",
|
|
5857
|
+
"model_quantized.onnx_data",
|
|
5858
|
+
"1f4cf47e4adec7f7ae09db03d071ba8667e07f9a4203142c7efa8d37fe453597"
|
|
4863
5859
|
],
|
|
4864
5860
|
[
|
|
4865
5861
|
"tokenizer.json",
|
|
4866
5862
|
"tokenizer.json",
|
|
4867
|
-
"
|
|
5863
|
+
"feeb83348dcb033bc6b9d2e1f7906ca9eb2d122845000c9416d894d7c2927149"
|
|
4868
5864
|
],
|
|
4869
5865
|
[
|
|
4870
5866
|
"tokenizer_config.json",
|
|
4871
5867
|
"tokenizer_config.json",
|
|
4872
|
-
"
|
|
5868
|
+
"ce06781b38bb393db68c9e0709bddd31ef5d88f2c6fbb3fd9f369778fb85e451"
|
|
4873
5869
|
]
|
|
4874
5870
|
];
|
|
4875
5871
|
const sha = (buf) => createHash("sha256").update(buf).digest("hex");
|
|
4876
5872
|
/** Where `isorite model` keeps the model: the user's data folder, outside any repository. */
|
|
4877
|
-
const userModelDir = (env = process.env) => join(env.XDG_DATA_HOME || join(homedir(), ".local", "share"), "isorite", "models",
|
|
4878
|
-
/**
|
|
4879
|
-
|
|
5873
|
+
const userModelDir = (env = process.env) => join(env.XDG_DATA_HOME || join(homedir(), ".local", "share"), "isorite", "models", NAME$1);
|
|
5874
|
+
/** Keep `buf` as `out/to` if it is the file expected. */
|
|
5875
|
+
function keep(out, to, want, buf, say) {
|
|
5876
|
+
if (sha(buf) !== want) throw new Error(`${to}: not the file expected (its SHA-256 differs), so it was not kept`);
|
|
5877
|
+
writeFileSync(join(out, to) + ".tmp", buf);
|
|
5878
|
+
renameSync(join(out, to) + ".tmp", join(out, to));
|
|
5879
|
+
say(`${to}: ${(buf.length / 1e6).toFixed(1)} MB`);
|
|
5880
|
+
}
|
|
5881
|
+
/** The package's files, unpacked into a temporary folder by npm itself (so its registry, proxy and mirror settings
|
|
5882
|
+
* apply). The folder is the caller's to remove. */
|
|
5883
|
+
function unpack(pkg) {
|
|
5884
|
+
const tmp = mkdtempSync(join(tmpdir(), "isorite-model-"));
|
|
5885
|
+
const win = process.platform === "win32";
|
|
5886
|
+
const run = (cmd, args) => spawnSync(cmd, args, {
|
|
5887
|
+
cwd: tmp,
|
|
5888
|
+
encoding: "utf8",
|
|
5889
|
+
shell: win
|
|
5890
|
+
});
|
|
5891
|
+
const packed = run("npm", [
|
|
5892
|
+
"pack",
|
|
5893
|
+
pkg,
|
|
5894
|
+
"--loglevel=error"
|
|
5895
|
+
]);
|
|
5896
|
+
const tgz = packed.stdout?.trim().split("\n").at(-1);
|
|
5897
|
+
if (packed.status !== 0 || !tgz) {
|
|
5898
|
+
rmSync(tmp, {
|
|
5899
|
+
recursive: true,
|
|
5900
|
+
force: true
|
|
5901
|
+
});
|
|
5902
|
+
throw new Error((packed.stderr?.split("\n").find((l) => /^npm error \d{3} /.test(l)) ?? packed.stderr?.split("\n").find((l) => /error/i.test(l)))?.replace(/^npm error\s*/, "").trim() || packed.error?.message || "npm pack failed");
|
|
5903
|
+
}
|
|
5904
|
+
if (run("tar", ["-xzf", tgz]).status !== 0) {
|
|
5905
|
+
rmSync(tmp, {
|
|
5906
|
+
recursive: true,
|
|
5907
|
+
force: true
|
|
5908
|
+
});
|
|
5909
|
+
throw new Error("could not unpack " + tgz);
|
|
5910
|
+
}
|
|
5911
|
+
return tmp;
|
|
5912
|
+
}
|
|
5913
|
+
/** Fetch the model's files into `out`, keeping only files whose SHA-256 is the one expected: from a folder if one is
|
|
5914
|
+
* named, else from npm, else from Hugging Face. */
|
|
5915
|
+
async function fetchModel(out, say = console.log, { from, pkg = PACKAGE, files = FILES } = {}) {
|
|
4880
5916
|
mkdirSync(out, { recursive: true });
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
5917
|
+
const missing = files.filter(([, to, want]) => !(existsSync(join(out, to)) && sha(readFileSync(join(out, to))) === want));
|
|
5918
|
+
for (const [, to] of files) if (!missing.some((f) => f[1] === to)) say(`${to}: already here`);
|
|
5919
|
+
if (missing.length && from) for (const [, to, want] of missing) {
|
|
5920
|
+
if (!existsSync(join(from, to))) throw new Error(`${to} is not in ${from}`);
|
|
5921
|
+
keep(out, to, want, readFileSync(join(from, to)), say);
|
|
5922
|
+
}
|
|
5923
|
+
else if (missing.length && pkg) {
|
|
5924
|
+
let dir = null;
|
|
5925
|
+
try {
|
|
5926
|
+
dir = unpack(pkg);
|
|
5927
|
+
for (const [, to, want] of missing) keep(out, to, want, readFileSync(join(dir, "package", to)), say);
|
|
5928
|
+
missing.length = 0;
|
|
5929
|
+
} catch (e) {
|
|
5930
|
+
say(`From npm (${pkg}): ${e.message}. Trying Hugging Face.`);
|
|
5931
|
+
} finally {
|
|
5932
|
+
if (dir) rmSync(dir, {
|
|
5933
|
+
recursive: true,
|
|
5934
|
+
force: true
|
|
5935
|
+
});
|
|
4886
5936
|
}
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
const
|
|
4890
|
-
if (
|
|
4891
|
-
|
|
4892
|
-
renameSync(path + ".tmp", path);
|
|
4893
|
-
say(`${to}: ${(buf.length / 1e6).toFixed(1)} MB`);
|
|
5937
|
+
}
|
|
5938
|
+
if (missing.length && !from) for (const [hub, to, want] of missing.filter(([, to, want]) => !(existsSync(join(out, to)) && sha(readFileSync(join(out, to))) === want))) {
|
|
5939
|
+
const res = await fetch(`${HUB}/${hub}`);
|
|
5940
|
+
if (!res.ok) throw new Error(`${hub}: ${res.status} ${res.statusText}`);
|
|
5941
|
+
keep(out, to, want, new Uint8Array(await res.arrayBuffer()), say);
|
|
4894
5942
|
}
|
|
4895
5943
|
say(`The model is in ${out}`);
|
|
4896
5944
|
}
|
|
4897
5945
|
//#endregion
|
|
4898
5946
|
//#region packages/server/src/embedder.ts
|
|
4899
|
-
const MODEL =
|
|
4900
|
-
/** Where the model's files are (npm run model in a checkout, isorite model when installed), or
|
|
4901
|
-
* not there. */
|
|
5947
|
+
const MODEL = `${NAME$1}-q8`;
|
|
5948
|
+
/** Where the model's files are (npm run model in a checkout, into packages/model; isorite model when installed), or
|
|
5949
|
+
* null when they are not there. */
|
|
4902
5950
|
function modelDir(env = process.env) {
|
|
4903
5951
|
if (env.ISORITE_EMBED === "off") return null;
|
|
4904
|
-
return (env.ISORITE_EMBED_DIR ? [env.ISORITE_EMBED_DIR] : [join(dirname(fileURLToPath(import.meta.url)), "..", "
|
|
5952
|
+
return (env.ISORITE_EMBED_DIR ? [env.ISORITE_EMBED_DIR] : [join(dirname(fileURLToPath(import.meta.url)), "..", "..", "model"), userModelDir(env)]).find((dir) => existsSync(join(dir, "model.onnx")) && existsSync(join(dir, "tokenizer.json"))) ?? null;
|
|
4905
5953
|
}
|
|
4906
5954
|
/** The folder caches are kept in: outside both repositories, rebuilt from the notes whenever it is lost. */
|
|
4907
5955
|
const cacheDir = (env = process.env) => join(env.XDG_CACHE_HOME || join(homedir(), ".cache"), "isorite");
|
|
@@ -4952,6 +6000,341 @@ function nodeEmbedder(dir) {
|
|
|
4952
6000
|
};
|
|
4953
6001
|
}
|
|
4954
6002
|
//#endregion
|
|
6003
|
+
//#region packages/server/src/terminal.ts
|
|
6004
|
+
const OUTPUT = 4e3;
|
|
6005
|
+
const LOOKUPS = /* @__PURE__ */ new Set([
|
|
6006
|
+
"search_notes",
|
|
6007
|
+
"find_similar",
|
|
6008
|
+
"outline_note",
|
|
6009
|
+
"read_note",
|
|
6010
|
+
"brief_base"
|
|
6011
|
+
]);
|
|
6012
|
+
/** isorite's tools that change a note: their line opens it, where the agent's words are marked. */
|
|
6013
|
+
const CHANGES = /* @__PURE__ */ new Set(["edit_note", "write_note"]);
|
|
6014
|
+
/** What a tool call acted on, in a line. */
|
|
6015
|
+
function titleOf(name, input = {}) {
|
|
6016
|
+
const s = (k) => typeof input[k] === "string" ? input[k] : "";
|
|
6017
|
+
if (name === "shell" || name === "bash") return s("command");
|
|
6018
|
+
if (LOOKUPS.has(name)) return s("query") || s("text") || s("id") + (s("section") ? ` § ${s("section")}` : "");
|
|
6019
|
+
if (CHANGES.has(name)) return s("note");
|
|
6020
|
+
return s("path") || s("filePath") || s("pattern") || s("url") || s("description") || "";
|
|
6021
|
+
}
|
|
6022
|
+
/** An edit's lines as a diff: what it took out, then what it put in. A write is all put in. */
|
|
6023
|
+
function diffOf(name, input) {
|
|
6024
|
+
const lines = (v) => typeof v === "string" ? v.replace(/\n$/, "").split("\n") : [];
|
|
6025
|
+
if ((name === "edit" || name === "edit_note") && typeof input.oldString === "string") return [...lines(input.oldString).map((l) => "-" + l), ...lines(input.newString).map((l) => "+" + l)].slice(0, 200);
|
|
6026
|
+
if ((name === "write" || name === "write_note") && typeof input.content === "string") return lines(input.content).map((l) => "+" + l).slice(0, 200);
|
|
6027
|
+
}
|
|
6028
|
+
/** The session's messages (oldest first) as the pane's turns. Text still arriving (`live`, by assistant message) is
|
|
6029
|
+
* added where the listed message does not have it yet; `ours` are the prompts asked from a page. */
|
|
6030
|
+
function toTurns(messages, { live = /* @__PURE__ */ new Map(), ours = /* @__PURE__ */ new Set(), busy = false } = {}) {
|
|
6031
|
+
const turns = [];
|
|
6032
|
+
for (const m of messages) {
|
|
6033
|
+
if (m.type === "user" && "text" in m) {
|
|
6034
|
+
turns.push({
|
|
6035
|
+
id: m.id,
|
|
6036
|
+
asked: m.text,
|
|
6037
|
+
from: ours.has(m.id) || ours.has(m.text) ? "here" : "terminal",
|
|
6038
|
+
items: [],
|
|
6039
|
+
state: "running"
|
|
6040
|
+
});
|
|
6041
|
+
continue;
|
|
6042
|
+
}
|
|
6043
|
+
const turn = turns.at(-1);
|
|
6044
|
+
if (!turn) continue;
|
|
6045
|
+
if (m.type === "idle" && "outcome" in m) {
|
|
6046
|
+
turn.state = m.outcome === "succeeded" ? "done" : /interrupt|cancel|abort/.test(m.outcome) ? "stopped" : "failed";
|
|
6047
|
+
continue;
|
|
6048
|
+
}
|
|
6049
|
+
if (m.type !== "assistant" || !("content" in m)) continue;
|
|
6050
|
+
let texts = 0;
|
|
6051
|
+
for (const c of m.content) if (c.type === "text" && "text" in c) {
|
|
6052
|
+
texts++;
|
|
6053
|
+
if (c.text) turn.items.push({
|
|
6054
|
+
kind: "text",
|
|
6055
|
+
text: c.text
|
|
6056
|
+
});
|
|
6057
|
+
} else if (c.type === "tool" && "state" in c) {
|
|
6058
|
+
const input = c.state.input ?? {};
|
|
6059
|
+
const output = (c.state.content ?? []).map((x) => x.text ?? "").join("").slice(0, OUTPUT);
|
|
6060
|
+
const status = c.state.status === "completed" ? "done" : /error|fail/.test(c.state.status) ? "failed" : "running";
|
|
6061
|
+
const item = {
|
|
6062
|
+
kind: "tool",
|
|
6063
|
+
id: c.id,
|
|
6064
|
+
name: c.name,
|
|
6065
|
+
title: titleOf(c.name, input),
|
|
6066
|
+
status,
|
|
6067
|
+
output
|
|
6068
|
+
};
|
|
6069
|
+
const named = LOOKUPS.has(c.name) ? input.id : CHANGES.has(c.name) ? input.note : void 0;
|
|
6070
|
+
if (typeof named === "string" && named) item.note = named.replace(/^\//, "").replace(/\.md$/, "");
|
|
6071
|
+
const diff = diffOf(c.name, input);
|
|
6072
|
+
if (diff) item.diff = diff;
|
|
6073
|
+
turn.items.push(item);
|
|
6074
|
+
}
|
|
6075
|
+
const streaming = live.get(m.id);
|
|
6076
|
+
if (streaming && !texts) turn.items.push({
|
|
6077
|
+
kind: "text",
|
|
6078
|
+
text: streaming
|
|
6079
|
+
});
|
|
6080
|
+
}
|
|
6081
|
+
const listed = new Set(messages.map((m) => m.id));
|
|
6082
|
+
const last = turns.at(-1);
|
|
6083
|
+
if (last && last.state === "running") {
|
|
6084
|
+
for (const [id, text] of live) if (!listed.has(id) && text) last.items.push({
|
|
6085
|
+
kind: "text",
|
|
6086
|
+
text
|
|
6087
|
+
});
|
|
6088
|
+
}
|
|
6089
|
+
if (!busy) {
|
|
6090
|
+
for (const t of turns) if (t.state === "running") t.state = "done";
|
|
6091
|
+
}
|
|
6092
|
+
return turns;
|
|
6093
|
+
}
|
|
6094
|
+
/** What is asked from a page, with what was given in the Iso pane's context: notes and folders by name, for the
|
|
6095
|
+
* agent to read with isorite's tools, and passages as they are. */
|
|
6096
|
+
function given(asked, parts, title) {
|
|
6097
|
+
const notes = parts.filter((p) => p.kind === "note" || p.kind === "understanding").map((p) => `- ${title(p.id)} (${p.id})`);
|
|
6098
|
+
const folders = parts.filter((p) => p.kind === "folder").map((p) => `- the folder ${p.id}`);
|
|
6099
|
+
const passages = parts.flatMap((p) => p.kind === "passage" ? [`From ${title(p.id)} (${p.id}):\n${p.text.split("\n").map((l) => "> " + l).join("\n")}`] : []);
|
|
6100
|
+
const out = [asked.trim()];
|
|
6101
|
+
if (notes.length || folders.length) out.push(`From my knowledge base (read them with read_note, or outline_note first):\n${[...notes, ...folders].join("\n")}`);
|
|
6102
|
+
if (passages.length) out.push(passages.join("\n\n"));
|
|
6103
|
+
return out.join("\n\n");
|
|
6104
|
+
}
|
|
6105
|
+
const EMPTY = {
|
|
6106
|
+
connected: false,
|
|
6107
|
+
session: null,
|
|
6108
|
+
title: "",
|
|
6109
|
+
busy: false,
|
|
6110
|
+
turns: [],
|
|
6111
|
+
asking: [],
|
|
6112
|
+
cost: 0,
|
|
6113
|
+
tokens: {
|
|
6114
|
+
input: 0,
|
|
6115
|
+
output: 0
|
|
6116
|
+
},
|
|
6117
|
+
model: ""
|
|
6118
|
+
};
|
|
6119
|
+
/** Events after which the session is read again; text arrives as deltas in between. */
|
|
6120
|
+
const REREAD = /^session\.(step\.ended|tool\.|inbox\.delivered|execution\.|created|renamed)|^permission\./;
|
|
6121
|
+
/** The terminal's session, followed. One terminal at a time: a second `isorite term` on the same server takes over. */
|
|
6122
|
+
var Terminal = class {
|
|
6123
|
+
#view = EMPTY;
|
|
6124
|
+
#client = null;
|
|
6125
|
+
#stop = null;
|
|
6126
|
+
#listeners = /* @__PURE__ */ new Set();
|
|
6127
|
+
#live = /* @__PURE__ */ new Map();
|
|
6128
|
+
#ours = /* @__PURE__ */ new Set();
|
|
6129
|
+
#asking = /* @__PURE__ */ new Map();
|
|
6130
|
+
#timer = null;
|
|
6131
|
+
#directory = "";
|
|
6132
|
+
view() {
|
|
6133
|
+
return this.#view;
|
|
6134
|
+
}
|
|
6135
|
+
listen(fn) {
|
|
6136
|
+
this.#listeners.add(fn);
|
|
6137
|
+
return () => this.#listeners.delete(fn);
|
|
6138
|
+
}
|
|
6139
|
+
#tell() {
|
|
6140
|
+
for (const fn of this.#listeners) fn(this.#view);
|
|
6141
|
+
}
|
|
6142
|
+
async attach({ url, password, directory }) {
|
|
6143
|
+
this.detach();
|
|
6144
|
+
const client = OpenCode.make({
|
|
6145
|
+
baseUrl: url,
|
|
6146
|
+
headers: { authorization: "Basic " + Buffer.from(`opencode:${password}`).toString("base64") }
|
|
6147
|
+
});
|
|
6148
|
+
await client.server.info();
|
|
6149
|
+
this.#client = client;
|
|
6150
|
+
this.#directory = directory;
|
|
6151
|
+
this.#view = {
|
|
6152
|
+
...EMPTY,
|
|
6153
|
+
connected: true
|
|
6154
|
+
};
|
|
6155
|
+
const stop = this.#stop = new AbortController();
|
|
6156
|
+
this.#follow(client, stop.signal);
|
|
6157
|
+
await this.#choose();
|
|
6158
|
+
await this.#read();
|
|
6159
|
+
}
|
|
6160
|
+
detach() {
|
|
6161
|
+
this.#stop?.abort();
|
|
6162
|
+
this.#stop = null;
|
|
6163
|
+
this.#client = null;
|
|
6164
|
+
this.#live.clear();
|
|
6165
|
+
this.#asking.clear();
|
|
6166
|
+
if (this.#timer) clearTimeout(this.#timer);
|
|
6167
|
+
if (this.#view.connected) {
|
|
6168
|
+
this.#view = EMPTY;
|
|
6169
|
+
this.#tell();
|
|
6170
|
+
}
|
|
6171
|
+
}
|
|
6172
|
+
/** Ask in the session the terminal shows, from a page. While it works, the prompt steers the turn under way. */
|
|
6173
|
+
async prompt(text) {
|
|
6174
|
+
const client = this.#need();
|
|
6175
|
+
if (!this.#view.session) await this.#choose(true);
|
|
6176
|
+
this.#ours.add(text);
|
|
6177
|
+
const sent = await client.session.prompt({
|
|
6178
|
+
sessionID: this.#view.session,
|
|
6179
|
+
text
|
|
6180
|
+
});
|
|
6181
|
+
if (sent?.id) this.#ours.add(sent.id);
|
|
6182
|
+
this.#soon();
|
|
6183
|
+
}
|
|
6184
|
+
async interrupt() {
|
|
6185
|
+
const client = this.#need();
|
|
6186
|
+
if (this.#view.session) await client.session.interrupt({ sessionID: this.#view.session });
|
|
6187
|
+
this.#soon();
|
|
6188
|
+
}
|
|
6189
|
+
/** Answer what the agent asked; only what this session is asking can be answered. */
|
|
6190
|
+
async reply(id, decision) {
|
|
6191
|
+
const client = this.#need();
|
|
6192
|
+
if (!this.#asking.has(id) || !this.#view.session) throw new Error("the terminal is not asking that");
|
|
6193
|
+
await client.permission.reply({
|
|
6194
|
+
sessionID: this.#view.session,
|
|
6195
|
+
requestID: id,
|
|
6196
|
+
decision
|
|
6197
|
+
});
|
|
6198
|
+
this.#asking.delete(id);
|
|
6199
|
+
this.#soon();
|
|
6200
|
+
}
|
|
6201
|
+
#need() {
|
|
6202
|
+
if (!this.#client) throw new Error("no terminal is attached: start one with isorite term");
|
|
6203
|
+
return this.#client;
|
|
6204
|
+
}
|
|
6205
|
+
/** The session to follow: the one the terminal last showed, else the latest. With `make`, one is made when none is. */
|
|
6206
|
+
async #choose(make = false) {
|
|
6207
|
+
const client = this.#client;
|
|
6208
|
+
if (!client) return;
|
|
6209
|
+
const sessions = [...(await client.session.list().catch(() => null))?.data ?? []].sort((a, b) => (b.time?.updated ?? 0) - (a.time?.updated ?? 0));
|
|
6210
|
+
if (sessions[0]) {
|
|
6211
|
+
this.#show(sessions[0].id);
|
|
6212
|
+
return;
|
|
6213
|
+
}
|
|
6214
|
+
if (make) {
|
|
6215
|
+
const made = await client.session.create({ location: { directory: this.#directory } });
|
|
6216
|
+
this.#show(made.id);
|
|
6217
|
+
}
|
|
6218
|
+
}
|
|
6219
|
+
#show(session) {
|
|
6220
|
+
if (this.#view.session === session) return;
|
|
6221
|
+
this.#live.clear();
|
|
6222
|
+
this.#asking.clear();
|
|
6223
|
+
this.#view = {
|
|
6224
|
+
...this.#view,
|
|
6225
|
+
session,
|
|
6226
|
+
turns: [],
|
|
6227
|
+
asking: []
|
|
6228
|
+
};
|
|
6229
|
+
this.#pending();
|
|
6230
|
+
}
|
|
6231
|
+
async #pending() {
|
|
6232
|
+
const client = this.#client, session = this.#view.session;
|
|
6233
|
+
if (!client || !session) return;
|
|
6234
|
+
const asked = await client.permission.list({ sessionID: session }).catch(() => []);
|
|
6235
|
+
for (const p of asked ?? []) this.#asking.set(p.id, {
|
|
6236
|
+
id: p.id,
|
|
6237
|
+
action: p.action,
|
|
6238
|
+
resources: p.resources ?? [],
|
|
6239
|
+
...p.source?.id ? { tool: p.source.id } : {}
|
|
6240
|
+
});
|
|
6241
|
+
this.#soon();
|
|
6242
|
+
}
|
|
6243
|
+
async #follow(client, signal) {
|
|
6244
|
+
try {
|
|
6245
|
+
for await (const raw of client.event.subscribe()) {
|
|
6246
|
+
if (signal.aborted) return;
|
|
6247
|
+
const e = raw;
|
|
6248
|
+
const d = e.data ?? {};
|
|
6249
|
+
const session = typeof d.sessionID === "string" ? d.sessionID : null;
|
|
6250
|
+
if ((e.type === "session.viewed" || e.type === "session.created") && session) this.#show(session);
|
|
6251
|
+
if (session && session !== this.#view.session) continue;
|
|
6252
|
+
if (e.type === "session.execution.started") this.#view = {
|
|
6253
|
+
...this.#view,
|
|
6254
|
+
busy: true
|
|
6255
|
+
};
|
|
6256
|
+
if (/^session\.execution\.(succeeded|failed|interrupted|cancelled|stopped)/.test(e.type)) {
|
|
6257
|
+
this.#view = {
|
|
6258
|
+
...this.#view,
|
|
6259
|
+
busy: false
|
|
6260
|
+
};
|
|
6261
|
+
this.#live.clear();
|
|
6262
|
+
}
|
|
6263
|
+
if (e.type === "session.text.delta" && typeof d.assistantMessageID === "string") {
|
|
6264
|
+
this.#live.set(d.assistantMessageID, (this.#live.get(d.assistantMessageID) ?? "") + String(d.delta ?? ""));
|
|
6265
|
+
this.#view = {
|
|
6266
|
+
...this.#view,
|
|
6267
|
+
turns: this.#turns(this.#messages)
|
|
6268
|
+
};
|
|
6269
|
+
this.#tell();
|
|
6270
|
+
continue;
|
|
6271
|
+
}
|
|
6272
|
+
if (e.type === "permission.asked" && typeof d.id === "string") {
|
|
6273
|
+
const source = d.source;
|
|
6274
|
+
this.#asking.set(d.id, {
|
|
6275
|
+
id: d.id,
|
|
6276
|
+
action: String(d.action ?? ""),
|
|
6277
|
+
resources: d.resources ?? [],
|
|
6278
|
+
...source?.id ? { tool: source.id } : {}
|
|
6279
|
+
});
|
|
6280
|
+
}
|
|
6281
|
+
if (e.type === "permission.replied" && typeof d.requestID === "string") this.#asking.delete(d.requestID);
|
|
6282
|
+
if (e.type === "session.usage.updated") this.#view = {
|
|
6283
|
+
...this.#view,
|
|
6284
|
+
cost: Number(d.cost ?? 0),
|
|
6285
|
+
tokens: {
|
|
6286
|
+
input: Number(d.tokens?.input ?? 0),
|
|
6287
|
+
output: Number(d.tokens?.output ?? 0)
|
|
6288
|
+
}
|
|
6289
|
+
};
|
|
6290
|
+
if (REREAD.test(e.type) || e.type === "session.usage.updated") this.#soon();
|
|
6291
|
+
}
|
|
6292
|
+
} catch {
|
|
6293
|
+
if (!signal.aborted && this.#client === client) this.detach();
|
|
6294
|
+
}
|
|
6295
|
+
}
|
|
6296
|
+
#messages = [];
|
|
6297
|
+
#turns(messages) {
|
|
6298
|
+
return toTurns(messages, {
|
|
6299
|
+
live: this.#live,
|
|
6300
|
+
ours: this.#ours,
|
|
6301
|
+
busy: this.#view.busy
|
|
6302
|
+
});
|
|
6303
|
+
}
|
|
6304
|
+
/** Read the session again shortly, once for a burst of events. */
|
|
6305
|
+
#soon() {
|
|
6306
|
+
if (this.#timer) return;
|
|
6307
|
+
this.#timer = setTimeout(() => {
|
|
6308
|
+
this.#timer = null;
|
|
6309
|
+
this.#read();
|
|
6310
|
+
}, 80);
|
|
6311
|
+
}
|
|
6312
|
+
async #read() {
|
|
6313
|
+
const client = this.#client, session = this.#view.session;
|
|
6314
|
+
if (!client) return;
|
|
6315
|
+
if (!session) {
|
|
6316
|
+
this.#tell();
|
|
6317
|
+
return;
|
|
6318
|
+
}
|
|
6319
|
+
const [listed, info] = await Promise.all([client.message.list({
|
|
6320
|
+
sessionID: session,
|
|
6321
|
+
limit: 200,
|
|
6322
|
+
order: "desc"
|
|
6323
|
+
}).catch(() => null), client.session.get({ sessionID: session }).catch(() => null)]);
|
|
6324
|
+
if (this.#client !== client || this.#view.session !== session) return;
|
|
6325
|
+
this.#messages = [...listed?.data ?? []].reverse();
|
|
6326
|
+
const model = [...this.#messages].reverse().find((m) => m.type === "assistant" && "content" in m)?.model?.id ?? info?.model?.id ?? this.#view.model;
|
|
6327
|
+
this.#view = {
|
|
6328
|
+
...this.#view,
|
|
6329
|
+
title: info?.title ?? this.#view.title,
|
|
6330
|
+
model,
|
|
6331
|
+
turns: this.#turns(this.#messages),
|
|
6332
|
+
asking: [...this.#asking.values()]
|
|
6333
|
+
};
|
|
6334
|
+
this.#tell();
|
|
6335
|
+
}
|
|
6336
|
+
};
|
|
6337
|
+
//#endregion
|
|
4955
6338
|
//#region packages/server/src/http.ts
|
|
4956
6339
|
const LOOPBACK = /* @__PURE__ */ new Set([
|
|
4957
6340
|
"localhost",
|
|
@@ -4964,6 +6347,7 @@ const TYPES = {
|
|
|
4964
6347
|
".js": "text/javascript; charset=utf-8",
|
|
4965
6348
|
".css": "text/css; charset=utf-8",
|
|
4966
6349
|
".json": "application/json",
|
|
6350
|
+
".webmanifest": "application/manifest+json",
|
|
4967
6351
|
".svg": "image/svg+xml",
|
|
4968
6352
|
".png": "image/png",
|
|
4969
6353
|
".woff2": "font/woff2",
|
|
@@ -4986,6 +6370,11 @@ const Tier = z.enum([
|
|
|
4986
6370
|
"mid",
|
|
4987
6371
|
"max"
|
|
4988
6372
|
]);
|
|
6373
|
+
/** What you chose of Iso's context (I3): parts switched off by id, and whether it may look up more. */
|
|
6374
|
+
const ChoiceSchema = z.object({
|
|
6375
|
+
off: z.array(z.string().max(400)).max(500).optional(),
|
|
6376
|
+
lookup: z.boolean().optional()
|
|
6377
|
+
}).strict();
|
|
4989
6378
|
const Ask = z.discriminatedUnion("form", [
|
|
4990
6379
|
z.object({
|
|
4991
6380
|
note: z.string(),
|
|
@@ -4993,7 +6382,8 @@ const Ask = z.discriminatedUnion("form", [
|
|
|
4993
6382
|
after: Anchor.nullable(),
|
|
4994
6383
|
before: Anchor.nullable(),
|
|
4995
6384
|
asked: z.string(),
|
|
4996
|
-
tier: Tier
|
|
6385
|
+
tier: Tier,
|
|
6386
|
+
choice: ChoiceSchema.optional()
|
|
4997
6387
|
}).strict(),
|
|
4998
6388
|
z.object({
|
|
4999
6389
|
note: z.string(),
|
|
@@ -5002,7 +6392,8 @@ const Ask = z.discriminatedUnion("form", [
|
|
|
5002
6392
|
after: Anchor.nullable(),
|
|
5003
6393
|
before: Anchor.nullable(),
|
|
5004
6394
|
asked: z.string(),
|
|
5005
|
-
tier: Tier
|
|
6395
|
+
tier: Tier,
|
|
6396
|
+
choice: ChoiceSchema.optional()
|
|
5006
6397
|
}).strict(),
|
|
5007
6398
|
z.object({
|
|
5008
6399
|
note: z.string(),
|
|
@@ -5021,6 +6412,41 @@ const Ask = z.discriminatedUnion("form", [
|
|
|
5021
6412
|
tier: Tier
|
|
5022
6413
|
}).strict()
|
|
5023
6414
|
]);
|
|
6415
|
+
const Given = z.discriminatedUnion("kind", [
|
|
6416
|
+
z.object({
|
|
6417
|
+
kind: z.literal("note"),
|
|
6418
|
+
id: z.string().max(400)
|
|
6419
|
+
}).strict(),
|
|
6420
|
+
z.object({
|
|
6421
|
+
kind: z.literal("folder"),
|
|
6422
|
+
id: z.string().max(400)
|
|
6423
|
+
}).strict(),
|
|
6424
|
+
z.object({
|
|
6425
|
+
kind: z.literal("passage"),
|
|
6426
|
+
id: z.string().max(400),
|
|
6427
|
+
text: z.string().max(2e4)
|
|
6428
|
+
}).strict(),
|
|
6429
|
+
z.object({
|
|
6430
|
+
kind: z.literal("understanding"),
|
|
6431
|
+
id: z.string().max(400)
|
|
6432
|
+
}).strict()
|
|
6433
|
+
]);
|
|
6434
|
+
const Mode = z.enum([
|
|
6435
|
+
"ask",
|
|
6436
|
+
"write",
|
|
6437
|
+
"build",
|
|
6438
|
+
"understand"
|
|
6439
|
+
]);
|
|
6440
|
+
const TurnAsk = z.object({
|
|
6441
|
+
conversation: z.string().max(64).optional(),
|
|
6442
|
+
mode: Mode,
|
|
6443
|
+
asked: z.string().max(8e3),
|
|
6444
|
+
given: z.array(Given).max(40),
|
|
6445
|
+
tier: Tier,
|
|
6446
|
+
lookup: z.boolean().optional(),
|
|
6447
|
+
off: z.array(z.string().max(64)).max(500).optional()
|
|
6448
|
+
}).strict();
|
|
6449
|
+
const TurnShape = TurnAsk.omit({ asked: true });
|
|
5024
6450
|
const ExerciseAsk = z.object({
|
|
5025
6451
|
folder: z.string().max(400),
|
|
5026
6452
|
tests: z.array(z.string().max(400)).min(1).max(20),
|
|
@@ -5048,7 +6474,7 @@ const AgentEdit = z.union([z.object({
|
|
|
5048
6474
|
newString: z.string(),
|
|
5049
6475
|
replaceAll: z.boolean().optional()
|
|
5050
6476
|
}).strict(), z.object({ content: z.string() }).strict()]);
|
|
5051
|
-
const
|
|
6477
|
+
const AgentChange = z.object({
|
|
5052
6478
|
agent: AgentName,
|
|
5053
6479
|
session: z.string().min(1).max(200),
|
|
5054
6480
|
note: z.string().max(400),
|
|
@@ -5060,7 +6486,7 @@ const AgentStatus = z.object({
|
|
|
5060
6486
|
session: z.string().min(1).max(200),
|
|
5061
6487
|
busy: z.boolean()
|
|
5062
6488
|
}).strict();
|
|
5063
|
-
function createApp({ api, token, loopback, web, allowHosts = [], notes }) {
|
|
6489
|
+
function createApp({ api, token, loopback, web, allowHosts = [], notes, terminal }) {
|
|
5064
6490
|
const app = new Hono();
|
|
5065
6491
|
const allowed = new Set([...allowHosts].map((h) => h.toLowerCase()));
|
|
5066
6492
|
const reply = (c, status, value) => c.body(JSON.stringify(value), status, {
|
|
@@ -5144,6 +6570,36 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes }) {
|
|
|
5144
6570
|
return sent instanceof Response ? sent : answer(c, async () => await act(c, sent) ?? {});
|
|
5145
6571
|
});
|
|
5146
6572
|
post("/api/iso/ask", Ask, (_c, b) => api.ask(b));
|
|
6573
|
+
post("/api/iso/converse", TurnAsk, (_c, b) => api.converse(b));
|
|
6574
|
+
app.get("/api/understanding", (c) => answer(c, () => api.understanding(id(c))));
|
|
6575
|
+
post("/api/understanding/scratchpad", z.object({
|
|
6576
|
+
scratchpad: z.string().max(2e5),
|
|
6577
|
+
base: z.string().max(64).nullable()
|
|
6578
|
+
}).strict(), (c, b) => api.saveScratchpad(id(c), b.scratchpad, b.base));
|
|
6579
|
+
post("/api/iso/undo", z.object({
|
|
6580
|
+
conversation: z.string().max(64),
|
|
6581
|
+
turn: z.string().max(64)
|
|
6582
|
+
}).strict(), (_c, b) => api.undoTurn(b.conversation, b.turn));
|
|
6583
|
+
post("/api/iso/answer", z.object({
|
|
6584
|
+
turn: z.string().max(64),
|
|
6585
|
+
permission: z.string().max(64),
|
|
6586
|
+
decision: z.enum([
|
|
6587
|
+
"once",
|
|
6588
|
+
"always",
|
|
6589
|
+
"reject"
|
|
6590
|
+
])
|
|
6591
|
+
}).strict(), (_c, b) => api.answer(b.turn, b.permission, b.decision));
|
|
6592
|
+
app.get("/api/conversation", (c) => answer(c, async () => await api.conversation(c.req.query("id") || void 0) ?? {
|
|
6593
|
+
id: "",
|
|
6594
|
+
turns: []
|
|
6595
|
+
}));
|
|
6596
|
+
app.get("/api/turn/context", (c) => answer(c, () => {
|
|
6597
|
+
let raw = {};
|
|
6598
|
+
try {
|
|
6599
|
+
raw = JSON.parse(c.req.query("turn") ?? "{}");
|
|
6600
|
+
} catch {}
|
|
6601
|
+
return api.turnContext(TurnShape.parse(raw));
|
|
6602
|
+
}));
|
|
5147
6603
|
post("/api/iso/stop", z.object({ request: z.string().max(64) }).strict(), (_c, b) => api.stop(b.request));
|
|
5148
6604
|
post("/api/note/restore", z.object({
|
|
5149
6605
|
entry: z.string().max(64),
|
|
@@ -5168,7 +6624,7 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes }) {
|
|
|
5168
6624
|
post("/api/exercise/draft", ExerciseAsk, (_c, b) => api.createExerciseDraft(b.folder, b.tests, b.after ?? null));
|
|
5169
6625
|
post("/api/exercise/keep", z.object({ proposal: z.string().max(64) }).strict(), (c, b) => api.keepExercise(id(c), b.proposal));
|
|
5170
6626
|
post("/api/exercise/wind-back", z.object({}).strict(), (c) => api.windBack(id(c)));
|
|
5171
|
-
app.get("/api/context", (c) => answer(c, () => api.context(c.req.query("id") || void 0)));
|
|
6627
|
+
app.get("/api/context", (c) => answer(c, () => api.context(c.req.query("id") || void 0, choiceOf(c.req.query("choice")))));
|
|
5172
6628
|
post("/api/context/clear", z.object({}).strict(), () => api.clearConversation());
|
|
5173
6629
|
app.get("/api/settings", (c) => answer(c, () => api.settings()));
|
|
5174
6630
|
post("/api/settings/key", Key, (_c, b) => api.setKey(b.key), "put");
|
|
@@ -5180,7 +6636,7 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes }) {
|
|
|
5180
6636
|
app.get("/api/agent/tools", (c) => answer(c, () => api.agentTools()));
|
|
5181
6637
|
post("/api/agent/look", AgentLook, (_c, b) => api.agentLook(b));
|
|
5182
6638
|
app.get("/api/agent/base", (c) => answer(c, async () => ({ notes: notes ?? null })));
|
|
5183
|
-
post("/api/agent/
|
|
6639
|
+
post("/api/agent/write", AgentChange, (_c, b) => api.agentWrite(b));
|
|
5184
6640
|
post("/api/agent/send", z.object({
|
|
5185
6641
|
note: z.string().max(400),
|
|
5186
6642
|
passage: z.string().max(1e4).optional(),
|
|
@@ -5191,6 +6647,53 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes }) {
|
|
|
5191
6647
|
agent: AgentName
|
|
5192
6648
|
}).strict(), (_c, b) => api.agentReceived(b.id, b.agent));
|
|
5193
6649
|
post("/api/agent/status", AgentStatus, (_c, b) => api.agentStatus(b.agent, b.session, b.busy));
|
|
6650
|
+
if (terminal) {
|
|
6651
|
+
const Local = z.string().max(200).refine((u) => {
|
|
6652
|
+
try {
|
|
6653
|
+
const h = new URL(u).hostname;
|
|
6654
|
+
return h === "127.0.0.1" || h === "localhost" || h === "[::1]";
|
|
6655
|
+
} catch {
|
|
6656
|
+
return false;
|
|
6657
|
+
}
|
|
6658
|
+
}, "OpenCode's server must be on this machine");
|
|
6659
|
+
post("/api/terminal/attach", z.object({
|
|
6660
|
+
url: Local,
|
|
6661
|
+
password: z.string().min(1).max(200),
|
|
6662
|
+
directory: z.string().max(4e3)
|
|
6663
|
+
}).strict(), async (_c, b) => {
|
|
6664
|
+
await terminal.attach(b).catch((e) => {
|
|
6665
|
+
throw new EngineError(`OpenCode's server did not answer: ${e.message}`, 502);
|
|
6666
|
+
});
|
|
6667
|
+
});
|
|
6668
|
+
post("/api/terminal/detach", z.object({}).strict(), async () => {
|
|
6669
|
+
terminal.detach();
|
|
6670
|
+
});
|
|
6671
|
+
app.get("/api/terminal", (c) => answer(c, async () => terminal.view()));
|
|
6672
|
+
const failed = (e) => {
|
|
6673
|
+
throw new EngineError(e.message, 409);
|
|
6674
|
+
};
|
|
6675
|
+
post("/api/terminal/prompt", z.object({
|
|
6676
|
+
asked: z.string().min(1).max(8e3),
|
|
6677
|
+
given: z.array(Given).max(40)
|
|
6678
|
+
}).strict(), async (_c, b) => {
|
|
6679
|
+
const titles = new Map((await api.notes()).notes.map((n) => [n.id, n.title]));
|
|
6680
|
+
const text = given(b.asked, b.given, (id) => titles.get(id) ?? id);
|
|
6681
|
+
await terminal.prompt(text).catch(failed);
|
|
6682
|
+
});
|
|
6683
|
+
post("/api/terminal/stop", z.object({}).strict(), async () => {
|
|
6684
|
+
await terminal.interrupt().catch(failed);
|
|
6685
|
+
});
|
|
6686
|
+
post("/api/terminal/permission", z.object({
|
|
6687
|
+
id: z.string().max(200),
|
|
6688
|
+
decision: z.enum([
|
|
6689
|
+
"once",
|
|
6690
|
+
"always",
|
|
6691
|
+
"reject"
|
|
6692
|
+
])
|
|
6693
|
+
}).strict(), async (_c, b) => {
|
|
6694
|
+
await terminal.reply(b.id, b.decision).catch(failed);
|
|
6695
|
+
});
|
|
6696
|
+
}
|
|
5194
6697
|
const streams = /* @__PURE__ */ new Map();
|
|
5195
6698
|
const watchers = () => {
|
|
5196
6699
|
const browsers = [...streams.values()].filter(Boolean).length;
|
|
@@ -5209,6 +6712,16 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes }) {
|
|
|
5209
6712
|
data: JSON.stringify(event)
|
|
5210
6713
|
});
|
|
5211
6714
|
});
|
|
6715
|
+
const send = (view) => {
|
|
6716
|
+
stream.writeSSE({
|
|
6717
|
+
event: "terminal",
|
|
6718
|
+
data: JSON.stringify({
|
|
6719
|
+
type: "terminal",
|
|
6720
|
+
terminal: view
|
|
6721
|
+
})
|
|
6722
|
+
}).catch(() => void 0);
|
|
6723
|
+
};
|
|
6724
|
+
const unfollow = terminal?.listen(send);
|
|
5212
6725
|
const beat = setInterval(() => {
|
|
5213
6726
|
stream.write(": still here\n\n");
|
|
5214
6727
|
}, 25e3);
|
|
@@ -5216,6 +6729,7 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes }) {
|
|
|
5216
6729
|
event: "open",
|
|
5217
6730
|
data: "{}"
|
|
5218
6731
|
});
|
|
6732
|
+
if (terminal?.view().connected) send(terminal.view());
|
|
5219
6733
|
streams.set(stream, c.req.query("agent") === void 0);
|
|
5220
6734
|
watchers();
|
|
5221
6735
|
await new Promise((done) => stream.onAbort(done));
|
|
@@ -5223,6 +6737,7 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes }) {
|
|
|
5223
6737
|
watchers();
|
|
5224
6738
|
clearInterval(beat);
|
|
5225
6739
|
stop();
|
|
6740
|
+
unfollow?.();
|
|
5226
6741
|
}));
|
|
5227
6742
|
app.all("/api/*", (c) => refuse(c, 404, "not found"));
|
|
5228
6743
|
app.on(["GET", "HEAD"], "*", (c) => !hostOk(c) ? c.text(NOT_HERE, 403) : web ? staticFile(c, web) : c.text("Not found", 404));
|
|
@@ -5247,6 +6762,19 @@ function staticFile(c, web) {
|
|
|
5247
6762
|
"X-Content-Type-Options": "nosniff"
|
|
5248
6763
|
});
|
|
5249
6764
|
}
|
|
6765
|
+
/** The choice of what Iso is sent, as the context route takes it: JSON, and only its known fields kept. */
|
|
6766
|
+
function choiceOf(raw) {
|
|
6767
|
+
if (!raw) return void 0;
|
|
6768
|
+
try {
|
|
6769
|
+
const c = JSON.parse(raw);
|
|
6770
|
+
return {
|
|
6771
|
+
...Array.isArray(c.off) ? { off: c.off.filter((x) => typeof x === "string").slice(0, 500) } : {},
|
|
6772
|
+
...typeof c.lookup === "boolean" ? { lookup: c.lookup } : {}
|
|
6773
|
+
};
|
|
6774
|
+
} catch {
|
|
6775
|
+
return;
|
|
6776
|
+
}
|
|
6777
|
+
}
|
|
5250
6778
|
//#endregion
|
|
5251
6779
|
//#region packages/server/src/nodefiles.ts
|
|
5252
6780
|
const gone = (e) => ["ENOENT", "ENOTDIR"].includes(e.code ?? "");
|
|
@@ -5420,13 +6948,15 @@ async function serve$1(path, { host = "127.0.0.1", port = 4767, allowHosts = []
|
|
|
5420
6948
|
const opened = locate(path);
|
|
5421
6949
|
const api = await openEngine(opened);
|
|
5422
6950
|
const web = existsSync(join(WEB_DIR, "index.html")) ? WEB_DIR : null;
|
|
6951
|
+
const terminal = new Terminal();
|
|
5423
6952
|
const app = createApp({
|
|
5424
6953
|
api,
|
|
5425
6954
|
token: randomBytes(24).toString("base64url"),
|
|
5426
6955
|
loopback: LOOPBACK.has(host),
|
|
5427
6956
|
web,
|
|
5428
6957
|
allowHosts,
|
|
5429
|
-
notes: opened.notes
|
|
6958
|
+
notes: opened.notes,
|
|
6959
|
+
terminal
|
|
5430
6960
|
});
|
|
5431
6961
|
const server = serve({
|
|
5432
6962
|
fetch: app.fetch,
|
|
@@ -5462,100 +6992,333 @@ function scriptedHttp(answer, pace = 0) {
|
|
|
5462
6992
|
stop(Object.assign(/* @__PURE__ */ new Error("aborted"), { name: "AbortError" }));
|
|
5463
6993
|
return;
|
|
5464
6994
|
}
|
|
5465
|
-
const t = setTimeout(done, ms);
|
|
5466
|
-
signal?.addEventListener("abort", () => {
|
|
5467
|
-
clearTimeout(t);
|
|
5468
|
-
stop(Object.assign(/* @__PURE__ */ new Error("aborted"), { name: "AbortError" }));
|
|
5469
|
-
});
|
|
5470
|
-
});
|
|
5471
|
-
const body = new ReadableStream({ async start(c) {
|
|
5472
|
-
const enc = new TextEncoder(), send = (o) => c.enqueue(enc.encode(data(o)));
|
|
5473
|
-
try {
|
|
5474
|
-
const text = reply.text ?? "";
|
|
5475
|
-
for (const piece of pace ? text.match(/[\s\S]{1,14}/g) ?? [] : text ? [text] : []) {
|
|
5476
|
-
send({
|
|
5477
|
-
model: request.model,
|
|
5478
|
-
choices: [{ delta: { content: piece } }]
|
|
5479
|
-
});
|
|
5480
|
-
if (pace) await wait(pace);
|
|
6995
|
+
const t = setTimeout(done, ms);
|
|
6996
|
+
signal?.addEventListener("abort", () => {
|
|
6997
|
+
clearTimeout(t);
|
|
6998
|
+
stop(Object.assign(/* @__PURE__ */ new Error("aborted"), { name: "AbortError" }));
|
|
6999
|
+
});
|
|
7000
|
+
});
|
|
7001
|
+
const body = new ReadableStream({ async start(c) {
|
|
7002
|
+
const enc = new TextEncoder(), send = (o) => c.enqueue(enc.encode(data(o)));
|
|
7003
|
+
try {
|
|
7004
|
+
const text = reply.text ?? "";
|
|
7005
|
+
for (const piece of pace ? text.match(/[\s\S]{1,14}/g) ?? [] : text ? [text] : []) {
|
|
7006
|
+
send({
|
|
7007
|
+
model: request.model,
|
|
7008
|
+
choices: [{ delta: { content: piece } }]
|
|
7009
|
+
});
|
|
7010
|
+
if (pace) await wait(pace);
|
|
7011
|
+
}
|
|
7012
|
+
for (const [index, call] of (reply.calls ?? []).entries()) send({ choices: [{ delta: { tool_calls: [{
|
|
7013
|
+
index,
|
|
7014
|
+
id: `call_${index}_${Math.random().toString(36).slice(2, 8)}`,
|
|
7015
|
+
function: {
|
|
7016
|
+
name: call.name,
|
|
7017
|
+
arguments: JSON.stringify(call.arguments)
|
|
7018
|
+
}
|
|
7019
|
+
}] } }] });
|
|
7020
|
+
if (pace && reply.calls) await wait(pace * 20);
|
|
7021
|
+
const u = reply.usage ?? {
|
|
7022
|
+
in: 3e3,
|
|
7023
|
+
out: 200,
|
|
7024
|
+
usd: .008
|
|
7025
|
+
};
|
|
7026
|
+
send({ choices: [{
|
|
7027
|
+
delta: {},
|
|
7028
|
+
finish_reason: reply.calls ? "tool_calls" : "stop"
|
|
7029
|
+
}] });
|
|
7030
|
+
send({
|
|
7031
|
+
choices: [],
|
|
7032
|
+
usage: {
|
|
7033
|
+
prompt_tokens: u.in,
|
|
7034
|
+
completion_tokens: u.out,
|
|
7035
|
+
cost: u.usd,
|
|
7036
|
+
prompt_tokens_details: { cached_tokens: u.cached ?? 0 }
|
|
7037
|
+
}
|
|
7038
|
+
});
|
|
7039
|
+
c.enqueue(enc.encode("data: [DONE]\n\n"));
|
|
7040
|
+
c.close();
|
|
7041
|
+
} catch (e) {
|
|
7042
|
+
c.error(e);
|
|
7043
|
+
}
|
|
7044
|
+
} });
|
|
7045
|
+
return new Response(body, {
|
|
7046
|
+
status: 200,
|
|
7047
|
+
headers: { "content-type": "text/event-stream" }
|
|
7048
|
+
});
|
|
7049
|
+
});
|
|
7050
|
+
return { fetch };
|
|
7051
|
+
}
|
|
7052
|
+
//#endregion
|
|
7053
|
+
//#region packages/example/src/tutorial.ts
|
|
7054
|
+
const UNSCRIPTED$1 = "In this tutorial I answer only what it walks you through. With a provider key in your own isorite, I would answer this too.";
|
|
7055
|
+
const text = (r) => r.messages.filter((m) => m.role === "user").map((m) => String(m.content)).join("\n\n");
|
|
7056
|
+
const looked = (r) => r.messages.some((m) => m.role === "tool");
|
|
7057
|
+
const asked = (r) => (text(r).split(/# What they ask(?: now)?[^\n]*\n\n/).at(-1) ?? "").trim().toLowerCase();
|
|
7058
|
+
const INSERT = `To insert a body, start at the root and walk down: at each node, go into the one of its four squares that holds the body's position.
|
|
7059
|
+
|
|
7060
|
+
- **An empty leaf** takes the body, and is done.
|
|
7061
|
+
- **A leaf with a body already** is split: both bodies go down into its new children, which may split again if they land in the same square.
|
|
7062
|
+
- **An inner node** passes the body down to the child whose square holds it.
|
|
7063
|
+
|
|
7064
|
+
On the way down, every node passed adds the body to its total mass $M$ and to $M\\mathbf{R}$, so that each keeps its mass and centre of mass with no second pass. A node keeps its square (centre and side $s$), its mass, its centre of mass, and its four children or its one body.`;
|
|
7065
|
+
const THETA = `With $\\theta = 0.5$ the force error is about a percent for most bodies, and several percent for a few near dense nodes; it falls quickly as $\\theta$ shrinks. The error of a node used whole is of order $(s/d)^2$, the [centre of mass](/algorithms/centre-of-mass.md "uses")'s quadrupole, and $s/d < \\theta$ bounds it.
|
|
7066
|
+
|
|
7067
|
+
The cost: a body opens nodes only near it. Around it, each level of the tree adds a ring of about the same number of nodes used whole, and there are $\\log_4 N$ levels, so each body visits $O(\\log N)$ nodes, and all of them $O(N \\log N)$.`;
|
|
7068
|
+
const SPEED = `About ten times fewer interactions. Direct summation computes $N(N-1)/2 \\approx 1.25 \\times 10^7$ pairs at $N = 5000$. Barnes–Hut at $\\theta = 0.5$ uses some 200 nodes for each body, so about $10^6$ interactions in all.
|
|
7069
|
+
|
|
7070
|
+
Each of them costs more: the tree walk branches, and in plain Python it is slow enough to lose the whole factor. Measure it, as your note on [cost and accuracy](/algorithms/cost-and-accuracy.md) says, before counting on it.`;
|
|
7071
|
+
const OCTREE = `An octree is the same tree with eight children instead of four: each cube is split in half along each of the three axes.
|
|
7072
|
+
|
|
7073
|
+
What changes:
|
|
7074
|
+
- **Insertion** picks one of eight children, by the sign of the body's position against the cube's centre on each axis: three comparisons, one bit each.
|
|
7075
|
+
- **The opening rule** is the same, $s/d < \\theta$, with $s$ the cube's side.
|
|
7076
|
+
- **The cost** is still $O(N \\log N)$; the log is now base 8, but each opened node has twice as many children, so the work per body is about the same.
|
|
7077
|
+
|
|
7078
|
+
Nothing in the [main loop](/simulation/the-main-loop.md) changes: only \`accelerations\`.`;
|
|
7079
|
+
const OCTREE_NOTE = `---
|
|
7080
|
+
type: Concept
|
|
7081
|
+
title: The octree
|
|
7082
|
+
description: The quadtree in 3D: each cube split into eight.
|
|
7083
|
+
---
|
|
7084
|
+
|
|
7085
|
+
The [quadtree](/algorithms/the-quadtree.md "requires") in three dimensions, for [Barnes–Hut](/algorithms/barnes-hut.md "uses") on the 3D simulation.
|
|
7086
|
+
|
|
7087
|
+
${OCTREE}
|
|
7088
|
+
`;
|
|
7089
|
+
const CLOSE = `When two bodies pass within a small fraction of their usual distance, their acceleration grows as $1/r^2$, and a step of fixed length overshoots: the energy jumps.
|
|
7090
|
+
|
|
7091
|
+
Three ways out:
|
|
7092
|
+
- **Soften the force.** With $\\varepsilon$ a little larger than the closest passes you care about, the force stays finite. Right for a galaxy, whose bodies stand for many stars.
|
|
7093
|
+
- **A shorter step**, for everyone. Simple, and leapfrog stays symplectic, but every body pays for the one close pair.
|
|
7094
|
+
- **Steps that differ by body** (block timesteps). Efficient, but only a careful scheme keeps the energy from drifting.
|
|
7095
|
+
|
|
7096
|
+
For two galaxies colliding, the first is the usual answer.`;
|
|
7097
|
+
/** The answer to one request to the tutorial's Iso. */
|
|
7098
|
+
/** In a turn in Iso's pane (M10): which mode, from what it was told. */
|
|
7099
|
+
const modeOf = (r) => {
|
|
7100
|
+
const system = JSON.stringify(r.messages.find((m) => m.role === "system")?.content ?? "");
|
|
7101
|
+
return system.includes("# Write mode") ? "write" : system.includes("# Understand mode") ? "understand" : system.includes("# Ask mode") ? "ask" : null;
|
|
7102
|
+
};
|
|
7103
|
+
const tools = (r) => r.messages.filter((m) => m.role === "tool").length;
|
|
7104
|
+
const TODO = "TODO: how big an error does θ = 0.5 give, and why does the cost grow only as N log N?";
|
|
7105
|
+
/** Iso's pane (M10): Ask, Write and Understand mode, on the questions the tutorial walks through. */
|
|
7106
|
+
function turnAnswer(r, mode) {
|
|
7107
|
+
const q = asked(r), n = tools(r);
|
|
7108
|
+
if (mode === "ask" && q.includes("inserted")) {
|
|
7109
|
+
if (n === 0) return {
|
|
7110
|
+
calls: [{
|
|
7111
|
+
name: "search_notes",
|
|
7112
|
+
arguments: { query: "quadtree" }
|
|
7113
|
+
}, {
|
|
7114
|
+
name: "read_note",
|
|
7115
|
+
arguments: { id: "algorithms/centre-of-mass" }
|
|
7116
|
+
}],
|
|
7117
|
+
usage: {
|
|
7118
|
+
in: 5100,
|
|
7119
|
+
out: 50,
|
|
7120
|
+
usd: .0059
|
|
7121
|
+
}
|
|
7122
|
+
};
|
|
7123
|
+
if (n === 2) return {
|
|
7124
|
+
calls: [{
|
|
7125
|
+
name: "point_to_note",
|
|
7126
|
+
arguments: {
|
|
7127
|
+
id: "algorithms/centre-of-mass",
|
|
7128
|
+
why: "It is what each node keeps as bodies pass down."
|
|
7129
|
+
}
|
|
7130
|
+
}],
|
|
7131
|
+
usage: {
|
|
7132
|
+
in: 7800,
|
|
7133
|
+
out: 40,
|
|
7134
|
+
usd: .0061
|
|
7135
|
+
}
|
|
7136
|
+
};
|
|
7137
|
+
return {
|
|
7138
|
+
text: INSERT,
|
|
7139
|
+
usage: {
|
|
7140
|
+
in: 8400,
|
|
7141
|
+
out: 310,
|
|
7142
|
+
cached: 4800,
|
|
7143
|
+
usd: .0163
|
|
7144
|
+
}
|
|
7145
|
+
};
|
|
7146
|
+
}
|
|
7147
|
+
if (mode === "ask" && q.includes("faster")) {
|
|
7148
|
+
if (n === 0) return {
|
|
7149
|
+
calls: [{
|
|
7150
|
+
name: "read_note",
|
|
7151
|
+
arguments: { id: "algorithms/direct-summation" }
|
|
7152
|
+
}, {
|
|
7153
|
+
name: "read_note",
|
|
7154
|
+
arguments: { id: "algorithms/barnes-hut" }
|
|
7155
|
+
}],
|
|
7156
|
+
usage: {
|
|
7157
|
+
in: 5300,
|
|
7158
|
+
out: 50,
|
|
7159
|
+
usd: .006
|
|
7160
|
+
}
|
|
7161
|
+
};
|
|
7162
|
+
return {
|
|
7163
|
+
text: SPEED,
|
|
7164
|
+
usage: {
|
|
7165
|
+
in: 8800,
|
|
7166
|
+
out: 260,
|
|
7167
|
+
cached: 5e3,
|
|
7168
|
+
usd: .0158
|
|
7169
|
+
}
|
|
7170
|
+
};
|
|
7171
|
+
}
|
|
7172
|
+
if (mode === "write" && q.includes("octree")) {
|
|
7173
|
+
if (n === 0) return {
|
|
7174
|
+
calls: [{
|
|
7175
|
+
name: "read_note",
|
|
7176
|
+
arguments: { id: "algorithms/the-quadtree" }
|
|
7177
|
+
}, {
|
|
7178
|
+
name: "read_note",
|
|
7179
|
+
arguments: { id: "algorithms/barnes-hut" }
|
|
7180
|
+
}],
|
|
7181
|
+
usage: {
|
|
7182
|
+
in: 5200,
|
|
7183
|
+
out: 60,
|
|
7184
|
+
usd: .0058
|
|
7185
|
+
}
|
|
7186
|
+
};
|
|
7187
|
+
if (n === 2) return {
|
|
7188
|
+
calls: [{
|
|
7189
|
+
name: "write_note",
|
|
7190
|
+
arguments: {
|
|
7191
|
+
id: "algorithms/the-octree",
|
|
7192
|
+
text: OCTREE_NOTE
|
|
7193
|
+
}
|
|
7194
|
+
}],
|
|
7195
|
+
usage: {
|
|
7196
|
+
in: 9900,
|
|
7197
|
+
out: 380,
|
|
7198
|
+
cached: 4400,
|
|
7199
|
+
usd: .0192
|
|
7200
|
+
}
|
|
7201
|
+
};
|
|
7202
|
+
return {
|
|
7203
|
+
text: "I wrote The octree in algorithms, from the quadtree and Barnes–Hut, and linked it to both.",
|
|
7204
|
+
usage: {
|
|
7205
|
+
in: 10400,
|
|
7206
|
+
out: 40,
|
|
7207
|
+
cached: 9800,
|
|
7208
|
+
usd: .0043
|
|
7209
|
+
}
|
|
7210
|
+
};
|
|
7211
|
+
}
|
|
7212
|
+
if (mode === "write" && q.includes("todo")) {
|
|
7213
|
+
if (n === 0) return {
|
|
7214
|
+
calls: [{
|
|
7215
|
+
name: "read_note",
|
|
7216
|
+
arguments: { id: "algorithms/barnes-hut" }
|
|
7217
|
+
}, {
|
|
7218
|
+
name: "read_note",
|
|
7219
|
+
arguments: { id: "algorithms/the-quadtree" }
|
|
7220
|
+
}],
|
|
7221
|
+
usage: {
|
|
7222
|
+
in: 5600,
|
|
7223
|
+
out: 60,
|
|
7224
|
+
usd: .0061
|
|
7225
|
+
}
|
|
7226
|
+
};
|
|
7227
|
+
if (n === 2) return {
|
|
7228
|
+
calls: [{
|
|
7229
|
+
name: "edit_note",
|
|
7230
|
+
arguments: {
|
|
7231
|
+
id: "algorithms/barnes-hut",
|
|
7232
|
+
old: TODO,
|
|
7233
|
+
new: THETA
|
|
7234
|
+
}
|
|
7235
|
+
}],
|
|
7236
|
+
usage: {
|
|
7237
|
+
in: 8900,
|
|
7238
|
+
out: 290,
|
|
7239
|
+
cached: 5100,
|
|
7240
|
+
usd: .0154
|
|
7241
|
+
}
|
|
7242
|
+
};
|
|
7243
|
+
return {
|
|
7244
|
+
text: "I answered both questions in your TODO, from the quadtree's depth and the centre of mass, and took the TODO away.",
|
|
7245
|
+
usage: {
|
|
7246
|
+
in: 9400,
|
|
7247
|
+
out: 40,
|
|
7248
|
+
cached: 8800,
|
|
7249
|
+
usd: .0041
|
|
7250
|
+
}
|
|
7251
|
+
};
|
|
7252
|
+
}
|
|
7253
|
+
if (mode === "understand" && /backwards|retrace|reverse/.test(q)) {
|
|
7254
|
+
if (n === 0) return {
|
|
7255
|
+
calls: [{
|
|
7256
|
+
name: "record_understanding",
|
|
7257
|
+
arguments: {
|
|
7258
|
+
id: "integrators/leapfrog",
|
|
7259
|
+
by: .5,
|
|
7260
|
+
why: "You saw that running it backwards retraces it, and that this is why its error cannot pile up."
|
|
5481
7261
|
}
|
|
5482
|
-
|
|
5483
|
-
|
|
5484
|
-
|
|
5485
|
-
|
|
5486
|
-
|
|
5487
|
-
|
|
5488
|
-
|
|
5489
|
-
|
|
5490
|
-
|
|
5491
|
-
|
|
5492
|
-
|
|
5493
|
-
out: 200,
|
|
5494
|
-
usd: .008
|
|
5495
|
-
};
|
|
5496
|
-
send({ choices: [{
|
|
5497
|
-
delta: {},
|
|
5498
|
-
finish_reason: reply.calls ? "tool_calls" : "stop"
|
|
5499
|
-
}] });
|
|
5500
|
-
send({
|
|
5501
|
-
choices: [],
|
|
5502
|
-
usage: {
|
|
5503
|
-
prompt_tokens: u.in,
|
|
5504
|
-
completion_tokens: u.out,
|
|
5505
|
-
cost: u.usd,
|
|
5506
|
-
prompt_tokens_details: { cached_tokens: u.cached ?? 0 }
|
|
5507
|
-
}
|
|
5508
|
-
});
|
|
5509
|
-
c.enqueue(enc.encode("data: [DONE]\n\n"));
|
|
5510
|
-
c.close();
|
|
5511
|
-
} catch (e) {
|
|
5512
|
-
c.error(e);
|
|
7262
|
+
}, {
|
|
7263
|
+
name: "write_understanding",
|
|
7264
|
+
arguments: {
|
|
7265
|
+
id: "integrators/leapfrog",
|
|
7266
|
+
reading: "You can say why leapfrog's energy error stays bounded: it is time-reversible, so it cannot drift one way. Not yet shown: what being symplectic adds to that."
|
|
7267
|
+
}
|
|
7268
|
+
}],
|
|
7269
|
+
usage: {
|
|
7270
|
+
in: 6200,
|
|
7271
|
+
out: 120,
|
|
7272
|
+
usd: .0068
|
|
5513
7273
|
}
|
|
5514
|
-
}
|
|
5515
|
-
return
|
|
5516
|
-
|
|
5517
|
-
|
|
5518
|
-
|
|
5519
|
-
|
|
5520
|
-
|
|
7274
|
+
};
|
|
7275
|
+
return {
|
|
7276
|
+
text: "Yes: run backwards it retraces its steps, so an error it made going forward is undone coming back, and it cannot keep growing one way. That is reversibility. Being [symplectic](/integrators/symplectic-integrators.md) is the stronger reason, and the next thing to look at.",
|
|
7277
|
+
usage: {
|
|
7278
|
+
in: 7100,
|
|
7279
|
+
out: 90,
|
|
7280
|
+
cached: 5600,
|
|
7281
|
+
usd: .0049
|
|
7282
|
+
}
|
|
7283
|
+
};
|
|
7284
|
+
}
|
|
7285
|
+
if (mode === "understand" && /leapfrog|bounded|drift/.test(q)) {
|
|
7286
|
+
if (n === 0) return {
|
|
7287
|
+
calls: [{
|
|
7288
|
+
name: "read_note",
|
|
7289
|
+
arguments: {
|
|
7290
|
+
id: "integrators/leapfrog",
|
|
7291
|
+
section: "Why it does not drift"
|
|
7292
|
+
}
|
|
7293
|
+
}],
|
|
7294
|
+
usage: {
|
|
7295
|
+
in: 5200,
|
|
7296
|
+
out: 40,
|
|
7297
|
+
usd: .0056
|
|
7298
|
+
}
|
|
7299
|
+
};
|
|
7300
|
+
return {
|
|
7301
|
+
text: "Before I say why: what happens if you run leapfrog with $-\\Delta t$, from where it ended?",
|
|
7302
|
+
usage: {
|
|
7303
|
+
in: 6900,
|
|
7304
|
+
out: 30,
|
|
7305
|
+
cached: 4800,
|
|
7306
|
+
usd: .0044
|
|
7307
|
+
}
|
|
7308
|
+
};
|
|
7309
|
+
}
|
|
7310
|
+
return null;
|
|
5521
7311
|
}
|
|
5522
|
-
//#endregion
|
|
5523
|
-
//#region packages/example/src/tutorial.ts
|
|
5524
|
-
const UNSCRIPTED$1 = "In this tutorial I answer only what it walks you through. With a provider key in your own isorite, I would answer this too.";
|
|
5525
|
-
const text = (r) => r.messages.filter((m) => m.role === "user").map((m) => String(m.content)).join("\n\n");
|
|
5526
|
-
const looked = (r) => r.messages.some((m) => m.role === "tool");
|
|
5527
|
-
const asked = (r) => (text(r).split(/# What they ask(?: now)?[^\n]*\n\n/).at(-1) ?? "").trim().toLowerCase();
|
|
5528
|
-
const INSERT = `To insert a body, start at the root and walk down: at each node, go into the one of its four squares that holds the body's position.
|
|
5529
|
-
|
|
5530
|
-
- **An empty leaf** takes the body, and is done.
|
|
5531
|
-
- **A leaf with a body already** is split: both bodies go down into its new children, which may split again if they land in the same square.
|
|
5532
|
-
- **An inner node** passes the body down to the child whose square holds it.
|
|
5533
|
-
|
|
5534
|
-
On the way down, every node passed adds the body to its total mass $M$ and to $M\\mathbf{R}$, so that each keeps its mass and centre of mass with no second pass. A node keeps its square (centre and side $s$), its mass, its centre of mass, and its four children or its one body.`;
|
|
5535
|
-
const THETA = `With $\\theta = 0.5$ the force error is about a percent for most bodies, and several percent for a few near dense nodes; it falls quickly as $\\theta$ shrinks. The error of a node used whole is of order $(s/d)^2$, the [centre of mass](/algorithms/centre-of-mass.md "uses")'s quadrupole, and $s/d < \\theta$ bounds it.
|
|
5536
|
-
|
|
5537
|
-
The cost: a body opens nodes only near it. Around it, each level of the tree adds a ring of about the same number of nodes used whole, and there are $\\log_4 N$ levels, so each body visits $O(\\log N)$ nodes, and all of them $O(N \\log N)$.`;
|
|
5538
|
-
const SPEED = `About ten times fewer interactions. Direct summation computes $N(N-1)/2 \\approx 1.25 \\times 10^7$ pairs at $N = 5000$. Barnes–Hut at $\\theta = 0.5$ uses some 200 nodes for each body, so about $10^6$ interactions in all.
|
|
5539
|
-
|
|
5540
|
-
Each of them costs more: the tree walk branches, and in plain Python it is slow enough to lose the whole factor. Measure it, as your note on [cost and accuracy](/algorithms/cost-and-accuracy.md) says, before counting on it.`;
|
|
5541
|
-
const OCTREE = `An octree is the same tree with eight children instead of four: each cube is split in half along each of the three axes.
|
|
5542
|
-
|
|
5543
|
-
What changes:
|
|
5544
|
-
- **Insertion** picks one of eight children, by the sign of the body's position against the cube's centre on each axis: three comparisons, one bit each.
|
|
5545
|
-
- **The opening rule** is the same, $s/d < \\theta$, with $s$ the cube's side.
|
|
5546
|
-
- **The cost** is still $O(N \\log N)$; the log is now base 8, but each opened node has twice as many children, so the work per body is about the same.
|
|
5547
|
-
|
|
5548
|
-
Nothing in the [main loop](/simulation/the-main-loop.md) changes: only \`accelerations\`.`;
|
|
5549
|
-
const CLOSE = `When two bodies pass within a small fraction of their usual distance, their acceleration grows as $1/r^2$, and a step of fixed length overshoots: the energy jumps.
|
|
5550
|
-
|
|
5551
|
-
Three ways out:
|
|
5552
|
-
- **Soften the force.** With $\\varepsilon$ a little larger than the closest passes you care about, the force stays finite. Right for a galaxy, whose bodies stand for many stars.
|
|
5553
|
-
- **A shorter step**, for everyone. Simple, and leapfrog stays symplectic, but every body pays for the one close pair.
|
|
5554
|
-
- **Steps that differ by body** (block timesteps). Efficient, but only a careful scheme keeps the energy from drifting.
|
|
5555
|
-
|
|
5556
|
-
For two galaxies colliding, the first is the usual answer.`;
|
|
5557
|
-
/** The answer to one request to the tutorial's Iso. */
|
|
5558
7312
|
function tutorialAnswer(r) {
|
|
7313
|
+
const mode = modeOf(r);
|
|
7314
|
+
if (mode) return turnAnswer(r, mode) ?? {
|
|
7315
|
+
text: "In this tutorial I answer only what it walks you through. With a provider key in your own isorite, I would answer this too.",
|
|
7316
|
+
usage: {
|
|
7317
|
+
in: 4e3,
|
|
7318
|
+
out: 40,
|
|
7319
|
+
usd: .0049
|
|
7320
|
+
}
|
|
7321
|
+
};
|
|
5559
7322
|
const all = text(r);
|
|
5560
7323
|
if (all.includes("⟦answer⟧")) {
|
|
5561
7324
|
const answer = /⟦answer⟧([\s\S]*?)⟦\/answer⟧/.exec(all)?.[1] ?? "";
|
|
@@ -5721,6 +7484,70 @@ function tutorialAnswer(r) {
|
|
|
5721
7484
|
}
|
|
5722
7485
|
};
|
|
5723
7486
|
}
|
|
7487
|
+
if (q.includes("octree") && q.includes("barnes")) {
|
|
7488
|
+
if (!looked(r)) return {
|
|
7489
|
+
calls: [
|
|
7490
|
+
{
|
|
7491
|
+
name: "read_note",
|
|
7492
|
+
arguments: { id: "algorithms/the-quadtree" }
|
|
7493
|
+
},
|
|
7494
|
+
{
|
|
7495
|
+
name: "read_note",
|
|
7496
|
+
arguments: { id: "algorithms/barnes-hut" }
|
|
7497
|
+
},
|
|
7498
|
+
{
|
|
7499
|
+
name: "search_notes",
|
|
7500
|
+
arguments: { query: "octree" }
|
|
7501
|
+
}
|
|
7502
|
+
],
|
|
7503
|
+
usage: {
|
|
7504
|
+
in: 5200,
|
|
7505
|
+
out: 60,
|
|
7506
|
+
usd: .0058
|
|
7507
|
+
}
|
|
7508
|
+
};
|
|
7509
|
+
return {
|
|
7510
|
+
text: OCTREE,
|
|
7511
|
+
usage: {
|
|
7512
|
+
in: 9900,
|
|
7513
|
+
out: 340,
|
|
7514
|
+
cached: 4400,
|
|
7515
|
+
usd: .0188
|
|
7516
|
+
}
|
|
7517
|
+
};
|
|
7518
|
+
}
|
|
7519
|
+
if (/pass(es)? close|close (pass|encounter)/.test(q)) {
|
|
7520
|
+
if (!looked(r)) return {
|
|
7521
|
+
calls: [
|
|
7522
|
+
{
|
|
7523
|
+
name: "read_note",
|
|
7524
|
+
arguments: { id: "physics/softening" }
|
|
7525
|
+
},
|
|
7526
|
+
{
|
|
7527
|
+
name: "read_note",
|
|
7528
|
+
arguments: { id: "integrators/choosing-the-timestep" }
|
|
7529
|
+
},
|
|
7530
|
+
{
|
|
7531
|
+
name: "find_similar",
|
|
7532
|
+
arguments: { text: "an energy jump when two bodies pass close" }
|
|
7533
|
+
}
|
|
7534
|
+
],
|
|
7535
|
+
usage: {
|
|
7536
|
+
in: 4900,
|
|
7537
|
+
out: 60,
|
|
7538
|
+
usd: .0056
|
|
7539
|
+
}
|
|
7540
|
+
};
|
|
7541
|
+
return {
|
|
7542
|
+
text: CLOSE,
|
|
7543
|
+
usage: {
|
|
7544
|
+
in: 9600,
|
|
7545
|
+
out: 360,
|
|
7546
|
+
cached: 4100,
|
|
7547
|
+
usd: .0191
|
|
7548
|
+
}
|
|
7549
|
+
};
|
|
7550
|
+
}
|
|
5724
7551
|
if (/\b3d\b|three dimensions/.test(q)) return {
|
|
5725
7552
|
text: "In 3D the same tree has eight children a node: an octree.",
|
|
5726
7553
|
usage: {
|
|
@@ -6632,7 +8459,7 @@ function writeHome(dir, { gateway = null, plugin = pluginDir } = {}) {
|
|
|
6632
8459
|
mkdirSync(join(dir, "themes"), { recursive: true });
|
|
6633
8460
|
mkdirSync(join(dir, "plugins"), { recursive: true });
|
|
6634
8461
|
const json = (value) => JSON.stringify(value, null, 2) + "\n";
|
|
6635
|
-
writeFileSync(join(dir, "opencode.json"), json(openCodeConfig(gateway)));
|
|
8462
|
+
writeFileSync(join(dir, "opencode.json"), json(withLocal(openCodeConfig(gateway), join(dir, "local.json"))));
|
|
6636
8463
|
writeFileSync(join(dir, "cli.json"), json({
|
|
6637
8464
|
$schema: "https://opencode.ai/cli.json",
|
|
6638
8465
|
theme: {
|
|
@@ -6651,6 +8478,28 @@ function writeHome(dir, { gateway = null, plugin = pluginDir } = {}) {
|
|
|
6651
8478
|
}
|
|
6652
8479
|
return dir;
|
|
6653
8480
|
}
|
|
8481
|
+
/** Your local.json over what isorite writes: its providers added to isorite's, everything else of yours taking the
|
|
8482
|
+
* place of isorite's. A local.json that is not JSON is said, and left out. */
|
|
8483
|
+
function withLocal(config, local) {
|
|
8484
|
+
if (!existsSync(local)) return config;
|
|
8485
|
+
let mine;
|
|
8486
|
+
try {
|
|
8487
|
+
mine = JSON.parse(readFileSync(local, "utf8"));
|
|
8488
|
+
} catch (e) {
|
|
8489
|
+
console.error(`isorite: ${local} is not JSON, so it is left out: ${e.message}`);
|
|
8490
|
+
return config;
|
|
8491
|
+
}
|
|
8492
|
+
const ours = config;
|
|
8493
|
+
const providers = {
|
|
8494
|
+
...ours.providers,
|
|
8495
|
+
...mine.providers
|
|
8496
|
+
};
|
|
8497
|
+
return {
|
|
8498
|
+
...ours,
|
|
8499
|
+
...mine,
|
|
8500
|
+
...Object.keys(providers).length ? { providers } : {}
|
|
8501
|
+
};
|
|
8502
|
+
}
|
|
6654
8503
|
function linksTo(path, target) {
|
|
6655
8504
|
try {
|
|
6656
8505
|
return lstatSync(path).isSymbolicLink() && readlinkSync(path) === target;
|
|
@@ -6703,7 +8552,7 @@ function lastAsked(request) {
|
|
|
6703
8552
|
}
|
|
6704
8553
|
async function startScripted(script) {
|
|
6705
8554
|
const requests = [];
|
|
6706
|
-
const server = createServer((req, res) => {
|
|
8555
|
+
const server = createServer$1((req, res) => {
|
|
6707
8556
|
let body = "";
|
|
6708
8557
|
req.on("data", (chunk) => body += chunk);
|
|
6709
8558
|
req.on("end", () => {
|
|
@@ -6891,7 +8740,7 @@ function tutorialScript(notes) {
|
|
|
6891
8740
|
newString: NOW
|
|
6892
8741
|
}
|
|
6893
8742
|
}
|
|
6894
|
-
][done] ?? { text: "barnes_hut.py builds the quadtree and walks it with θ = 0.5 `[1]`. Your note on the goal now says where the code is
|
|
8743
|
+
][done] ?? { text: "barnes_hut.py builds the quadtree and walks it with θ = 0.5 `[1]`. Your note on the goal now says where the code is, marked as mine `[2]`.\n\n```isorite\n[1] algorithms/barnes-hut\n[2] simulation/two-galaxies-colliding\n```" };
|
|
6895
8744
|
return { text: UNSCRIPTED };
|
|
6896
8745
|
};
|
|
6897
8746
|
}
|
|
@@ -6969,23 +8818,147 @@ async function ensureServer(path, { port = 4767, env = process.env, wait = 12e4
|
|
|
6969
8818
|
}
|
|
6970
8819
|
};
|
|
6971
8820
|
}
|
|
6972
|
-
|
|
6973
|
-
|
|
6974
|
-
|
|
8821
|
+
/** A port nobody is listening on, on this machine. */
|
|
8822
|
+
async function freePort() {
|
|
8823
|
+
return new Promise((done, fail) => {
|
|
8824
|
+
const probe = createServer().once("error", fail).listen(0, "127.0.0.1", () => {
|
|
8825
|
+
const { port } = probe.address();
|
|
8826
|
+
probe.close(() => done(port));
|
|
8827
|
+
});
|
|
8828
|
+
});
|
|
8829
|
+
}
|
|
8830
|
+
/** A POST to the isorite server, made as its page makes one: from its origin, with the token it hands out. */
|
|
8831
|
+
async function tell(url, path, body) {
|
|
8832
|
+
const { token } = await (await fetch(`${url}/api/session`)).json();
|
|
8833
|
+
const response = await fetch(url + path, {
|
|
8834
|
+
method: "POST",
|
|
8835
|
+
headers: {
|
|
8836
|
+
"Content-Type": "application/json",
|
|
8837
|
+
"x-isorite-token": token,
|
|
8838
|
+
Origin: url
|
|
8839
|
+
},
|
|
8840
|
+
body: JSON.stringify(body)
|
|
8841
|
+
});
|
|
8842
|
+
if (!response.ok) throw new Error((await response.json().catch(() => ({}))).error ?? `${response.status} from ${path}`);
|
|
8843
|
+
}
|
|
8844
|
+
/** OpenCode's own server, on this machine only, with a password of its own; its output kept in a log. The terminal
|
|
8845
|
+
* attaches to it, and so does the isorite server, which shows its session in the Iso pane (M11). */
|
|
8846
|
+
async function startOpenCode(command, { cwd, env, wait = 6e4 }) {
|
|
8847
|
+
const port = await freePort(), password = randomBytes(24).toString("base64url"), url = `http://127.0.0.1:${port}`;
|
|
8848
|
+
const log = join(tmpdir(), `isorite-opencode-${port}.log`);
|
|
8849
|
+
const out = openSync(log, "w");
|
|
8850
|
+
const server = spawn(command, [
|
|
8851
|
+
"serve",
|
|
8852
|
+
"--port",
|
|
8853
|
+
String(port),
|
|
8854
|
+
"--hostname",
|
|
8855
|
+
"127.0.0.1"
|
|
8856
|
+
], {
|
|
8857
|
+
cwd,
|
|
8858
|
+
stdio: [
|
|
8859
|
+
"ignore",
|
|
8860
|
+
out,
|
|
8861
|
+
out
|
|
8862
|
+
],
|
|
8863
|
+
env: {
|
|
8864
|
+
...env,
|
|
8865
|
+
OPENCODE_PASSWORD: password
|
|
8866
|
+
}
|
|
8867
|
+
});
|
|
8868
|
+
const auth = { authorization: "Basic " + Buffer.from(`opencode:${password}`).toString("base64") };
|
|
8869
|
+
const started = Date.now();
|
|
8870
|
+
for (;;) {
|
|
8871
|
+
if (server.exitCode !== null || Date.now() - started > wait) {
|
|
8872
|
+
server.kill();
|
|
8873
|
+
throw new StudyError(`OpenCode's server did not start; its output is in ${log}`);
|
|
8874
|
+
}
|
|
8875
|
+
try {
|
|
8876
|
+
if ((await fetch(url, {
|
|
8877
|
+
headers: auth,
|
|
8878
|
+
signal: AbortSignal.timeout(1e3)
|
|
8879
|
+
})).status < 500) break;
|
|
8880
|
+
} catch {}
|
|
8881
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
8882
|
+
}
|
|
8883
|
+
return {
|
|
8884
|
+
url,
|
|
8885
|
+
password,
|
|
8886
|
+
stop: () => {
|
|
8887
|
+
server.kill();
|
|
8888
|
+
}
|
|
8889
|
+
};
|
|
8890
|
+
}
|
|
8891
|
+
/** The models the terminal uses: ISORITE_GATEWAY_URL and _MODELS when set; else Iso's own provider, as
|
|
8892
|
+
* ~/.config/isorite/config.toml has it (OpenRouter unless `[provider] url` names another), its models the tiers'
|
|
8893
|
+
* (mid, the default, then max and low), with the key it holds handed to OpenCode in the environment only. Null without a key: then
|
|
8894
|
+
* OpenCode's own models, or whatever your local.json names. */
|
|
8895
|
+
async function modelsFor(env) {
|
|
8896
|
+
const named = gatewayFrom(env);
|
|
8897
|
+
if (named) return {
|
|
8898
|
+
gateway: named,
|
|
8899
|
+
env
|
|
8900
|
+
};
|
|
8901
|
+
const path = configPath(env);
|
|
8902
|
+
const key = await new ConfigSecrets(path).get("provider.key");
|
|
8903
|
+
if (!key) return {
|
|
8904
|
+
gateway: null,
|
|
8905
|
+
env
|
|
8906
|
+
};
|
|
8907
|
+
const provider = providerConfig(path);
|
|
8908
|
+
const models = [.../* @__PURE__ */ new Set([
|
|
8909
|
+
provider.tiers.mid,
|
|
8910
|
+
provider.tiers.max,
|
|
8911
|
+
provider.tiers.low
|
|
8912
|
+
])];
|
|
8913
|
+
return {
|
|
8914
|
+
gateway: {
|
|
8915
|
+
url: provider.url,
|
|
8916
|
+
models
|
|
8917
|
+
},
|
|
8918
|
+
env: {
|
|
8919
|
+
...env,
|
|
8920
|
+
[GATEWAY_KEY]: key
|
|
8921
|
+
}
|
|
8922
|
+
};
|
|
8923
|
+
}
|
|
8924
|
+
async function term(path, { port = 4767, cwd = process.cwd(), env: given = process.env, wait = 12e4, onServer } = {}) {
|
|
8925
|
+
const opencode = findOpenCode(given);
|
|
8926
|
+
const { gateway, env } = await modelsFor(given);
|
|
6975
8927
|
const server = await ensureServer(path, {
|
|
6976
8928
|
port,
|
|
6977
|
-
env,
|
|
8929
|
+
env: given,
|
|
6978
8930
|
wait
|
|
6979
8931
|
});
|
|
6980
8932
|
onServer?.(server.url);
|
|
6981
8933
|
const home = writeHome(defaultHome(env), { gateway });
|
|
6982
|
-
const
|
|
8934
|
+
const shared = {
|
|
8935
|
+
...env,
|
|
8936
|
+
OPENCODE_CONFIG_DIR: home,
|
|
8937
|
+
ISORITE_URL: server.url
|
|
8938
|
+
};
|
|
8939
|
+
let backend;
|
|
8940
|
+
try {
|
|
8941
|
+
backend = await startOpenCode(opencode.command, {
|
|
8942
|
+
cwd,
|
|
8943
|
+
env: shared
|
|
8944
|
+
});
|
|
8945
|
+
} catch (e) {
|
|
8946
|
+
server.release();
|
|
8947
|
+
throw e;
|
|
8948
|
+
}
|
|
8949
|
+
await tell(server.url, "/api/terminal/attach", {
|
|
8950
|
+
url: backend.url,
|
|
8951
|
+
password: backend.password,
|
|
8952
|
+
directory: cwd
|
|
8953
|
+
}).catch((e) => console.error(`isorite: the Iso pane will not show the terminal: ${e.message}`));
|
|
8954
|
+
const child = spawn(opencode.command, ["--server", backend.url], {
|
|
6983
8955
|
cwd,
|
|
6984
8956
|
stdio: "inherit",
|
|
6985
8957
|
env: {
|
|
6986
|
-
...
|
|
8958
|
+
...given,
|
|
6987
8959
|
OPENCODE_CONFIG_DIR: home,
|
|
6988
|
-
ISORITE_URL: server.url
|
|
8960
|
+
ISORITE_URL: server.url,
|
|
8961
|
+
OPENCODE_PASSWORD: backend.password
|
|
6989
8962
|
}
|
|
6990
8963
|
});
|
|
6991
8964
|
const forward = (signal) => () => child.kill(signal);
|
|
@@ -6999,6 +8972,8 @@ async function term(path, { port = 4767, cwd = process.cwd(), env = process.env,
|
|
|
6999
8972
|
child.once("exit", (exit) => done(exit ?? 1));
|
|
7000
8973
|
});
|
|
7001
8974
|
for (const [signal, handler] of handlers) process.off(signal, handler);
|
|
8975
|
+
await tell(server.url, "/api/terminal/detach", {}).catch(() => void 0);
|
|
8976
|
+
backend.stop();
|
|
7002
8977
|
server.release();
|
|
7003
8978
|
return code;
|
|
7004
8979
|
}
|
|
@@ -7176,7 +9151,7 @@ async function mcp(path, { port = 4767, agent = "mcp", quiet = QUIET_MS, input =
|
|
|
7176
9151
|
}
|
|
7177
9152
|
//#endregion
|
|
7178
9153
|
//#region packages/server/src/main.ts
|
|
7179
|
-
const VERSION = "0.
|
|
9154
|
+
const VERSION = "0.2.0";
|
|
7180
9155
|
const USAGE = `isorite: an agentic harness for understanding
|
|
7181
9156
|
|
|
7182
9157
|
isorite check <notes> check a folder of notes: frontmatter, links, cycles
|
|
@@ -7191,7 +9166,8 @@ const USAGE = `isorite: an agentic harness for understanding
|
|
|
7191
9166
|
isorite mcp [<study base | notes>] [--port <n>] [--agent <name>]
|
|
7192
9167
|
the knowledge base's tools over MCP, on stdio, for Claude Code or Pi:
|
|
7193
9168
|
claude mcp add isorite -- isorite mcp <base> --agent claude-code
|
|
7194
|
-
isorite model
|
|
9169
|
+
isorite model [--from <folder>] fetch the model for search by meaning (53 MB, once): from npm, else
|
|
9170
|
+
Hugging Face; or from a folder its files were copied into
|
|
7195
9171
|
isorite --tutorial [<folder>] [--port <n>]
|
|
7196
9172
|
the tutorial, on the example base, with a scripted model (no key,
|
|
7197
9173
|
nothing sent anywhere): the terminal's tour in OpenCode 2, and the
|
|
@@ -7268,10 +9244,17 @@ function main(argv) {
|
|
|
7268
9244
|
return 2;
|
|
7269
9245
|
});
|
|
7270
9246
|
}
|
|
7271
|
-
if (command === "model")
|
|
7272
|
-
|
|
7273
|
-
|
|
7274
|
-
|
|
9247
|
+
if (command === "model") {
|
|
9248
|
+
const from = rest[rest.indexOf("--from") + 1];
|
|
9249
|
+
if (rest.includes("--from") && !from) {
|
|
9250
|
+
console.error("--from takes a folder");
|
|
9251
|
+
return 2;
|
|
9252
|
+
}
|
|
9253
|
+
return fetchModel(userModelDir(), console.log, from && rest.includes("--from") ? { from } : {}).then(() => 0, (e) => {
|
|
9254
|
+
console.error(e.message);
|
|
9255
|
+
return 1;
|
|
9256
|
+
});
|
|
9257
|
+
}
|
|
7275
9258
|
if (command === "--version" || command === "-v") {
|
|
7276
9259
|
console.log(VERSION);
|
|
7277
9260
|
return 0;
|