neko-harness 0.2.8 → 0.3.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/LICENSE +210 -0
- package/README.md +1 -0
- package/dist/client/assets/index-DrXtf9P_.js +83 -0
- package/dist/client/assets/index-Dv47_XaY.css +2 -0
- package/dist/client/index.html +2 -2
- package/dist/gateway.js +1143 -240
- package/dist/neko.js +3316 -1378
- package/package.json +1 -1
- package/dist/client/assets/index-BJ4XDkf5.js +0 -82
- package/dist/client/assets/index-BwqDUlvt.css +0 -2
package/dist/gateway.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// @bun
|
|
3
3
|
|
|
4
4
|
// packages/gateway/src/cli-args.ts
|
|
5
|
-
var GATEWAY_USAGE = `\u7528\u6CD5:
|
|
5
|
+
var GATEWAY_USAGE = `\u7528\u6CD5: neko serve [\u9009\u9879]
|
|
6
6
|
|
|
7
7
|
\u9009\u9879:
|
|
8
8
|
--host <ip> WebSocket \u76D1\u542C\u5730\u5740\uFF08\u9ED8\u8BA4 127.0.0.1\uFF0C\u4EC5\u672C\u673A\uFF09
|
|
@@ -105,11 +105,11 @@ function parseGatewayArgs(argv) {
|
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
// packages/gateway/src/gateway.ts
|
|
108
|
-
import { randomUUID as
|
|
108
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
109
109
|
import { mkdir as mkdir5, readFile as readFile6, writeFile as writeFile4 } from "fs/promises";
|
|
110
110
|
import { basename as basename4, dirname as dirname9, join as join10 } from "path";
|
|
111
111
|
import { createHost } from "@earendil-works/chord";
|
|
112
|
-
import { BACKGROUND_CONTEXT as
|
|
112
|
+
import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT4 } from "@earendil-works/chord/context";
|
|
113
113
|
|
|
114
114
|
// packages/core/src/config.ts
|
|
115
115
|
import { existsSync, readFileSync } from "fs";
|
|
@@ -516,6 +516,14 @@ function truncateMiddle(text, maxWidth) {
|
|
|
516
516
|
function summarizeToolCall(name, args) {
|
|
517
517
|
const record = args ?? {};
|
|
518
518
|
switch (name) {
|
|
519
|
+
case "subagent":
|
|
520
|
+
return { args: `${record.type}: ${record.title}`, detail: String(record.task ?? "") };
|
|
521
|
+
case "find_files":
|
|
522
|
+
case "search_text":
|
|
523
|
+
return {
|
|
524
|
+
args: truncateMiddle(`${record.path ?? "."} ${record.pattern ?? record.query ?? ""}`, 100),
|
|
525
|
+
detail: JSON.stringify(args, null, 2)
|
|
526
|
+
};
|
|
519
527
|
case "bash": {
|
|
520
528
|
const command = typeof record.command === "string" ? record.command : "";
|
|
521
529
|
const firstLine = command.split(`
|
|
@@ -653,6 +661,7 @@ function projectChatView(view, modelLabel, options = {}) {
|
|
|
653
661
|
items.push(withArgs(result, block.name, block.arguments));
|
|
654
662
|
} else {
|
|
655
663
|
const summary = summarizeToolCall(block.name, block.arguments);
|
|
664
|
+
const slot = live.tools?.find((s) => s.callId === block.id);
|
|
656
665
|
items.push({
|
|
657
666
|
type: "tool",
|
|
658
667
|
key: `t${block.id}`,
|
|
@@ -660,9 +669,9 @@ function projectChatView(view, modelLabel, options = {}) {
|
|
|
660
669
|
name: block.name,
|
|
661
670
|
summary: summary.args,
|
|
662
671
|
detail: summary.detail ?? null,
|
|
663
|
-
phase: "done",
|
|
672
|
+
phase: slot?.status ?? "done",
|
|
664
673
|
isError: false,
|
|
665
|
-
output: null,
|
|
674
|
+
output: slot?.output ?? null,
|
|
666
675
|
result: null,
|
|
667
676
|
durationMs: null,
|
|
668
677
|
note: ran ? null : "Not run: response interrupted."
|
|
@@ -821,13 +830,13 @@ var stripFrontmatter = (content) => extractFrontmatter(content).body;
|
|
|
821
830
|
// packages/core/src/runtime.ts
|
|
822
831
|
import { homedir as homedir4 } from "os";
|
|
823
832
|
import { join as join5 } from "path";
|
|
824
|
-
import { BACKGROUND_CONTEXT as
|
|
833
|
+
import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT3 } from "@earendil-works/chord/context";
|
|
825
834
|
import { clampThinkingLevel, createModels, getSupportedThinkingLevels as getSupportedThinkingLevels2 } from "@earendil-works/pi-ai/models";
|
|
826
835
|
import { deepseekProvider } from "@earendil-works/pi-ai/providers/deepseek";
|
|
827
836
|
import {
|
|
828
|
-
AssistantEntry,
|
|
837
|
+
AssistantEntry as AssistantEntry2,
|
|
829
838
|
createRegistry,
|
|
830
|
-
defineDoc as
|
|
839
|
+
defineDoc as defineDoc4,
|
|
831
840
|
Harness,
|
|
832
841
|
LiveDoc as importLiveDoc,
|
|
833
842
|
MemoryStorage,
|
|
@@ -838,6 +847,164 @@ import { createNodePhotonImages } from "@earendil-works/pi-durable/images/node";
|
|
|
838
847
|
import { openNodeSqliteStorage } from "@earendil-works/pi-durable/storage/sqlite/node";
|
|
839
848
|
import { createCodingTools } from "@earendil-works/pi-durable/tools";
|
|
840
849
|
|
|
850
|
+
// packages/core/src/agent-file-search.ts
|
|
851
|
+
import { BACKGROUND_CONTEXT, withAbortSignal } from "@earendil-works/chord/context";
|
|
852
|
+
import { defineExtension, defineTool } from "@earendil-works/pi-durable";
|
|
853
|
+
import { getOrThrow } from "@earendil-works/pi-durable/env";
|
|
854
|
+
import { Type } from "typebox";
|
|
855
|
+
var SEARCH_LIMITS = {
|
|
856
|
+
entries: 1e4,
|
|
857
|
+
fileBytes: 2 * 1024 * 1024,
|
|
858
|
+
scanBytes: 32 * 1024 * 1024,
|
|
859
|
+
outputBytes: 32 * 1024,
|
|
860
|
+
timeoutMs: 1e4
|
|
861
|
+
};
|
|
862
|
+
var path = Type.Optional(Type.String({ description: "Directory relative to this conversation's cwd, or absolute path." }));
|
|
863
|
+
var limit = Type.Optional(Type.Integer({ minimum: 1, maximum: 500, description: "Maximum results (default 100)." }));
|
|
864
|
+
var text = (value) => ({ type: "text", text: value });
|
|
865
|
+
async function search(api, context, run) {
|
|
866
|
+
if (!api.env)
|
|
867
|
+
throw new Error("File environment unavailable");
|
|
868
|
+
const timer = new AbortController;
|
|
869
|
+
const timeout = setTimeout(() => timer.abort(new Error("File search timed out; narrow the directory.")), SEARCH_LIMITS.timeoutMs);
|
|
870
|
+
const signal = context.abortSignal ? AbortSignal.any([context.abortSignal, timer.signal]) : timer.signal;
|
|
871
|
+
try {
|
|
872
|
+
signal.throwIfAborted();
|
|
873
|
+
return await run(api.env, withAbortSignal(signal, context));
|
|
874
|
+
} finally {
|
|
875
|
+
clearTimeout(timeout);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
async function listing(env, path, ctx) {
|
|
879
|
+
if (!env.listFiles)
|
|
880
|
+
throw new Error("File discovery unavailable. Retry.");
|
|
881
|
+
const base = getOrThrow(await env.absolutePath(path, ctx));
|
|
882
|
+
const files = getOrThrow(await env.listFiles(base, {
|
|
883
|
+
hidden: true,
|
|
884
|
+
gitignore: true,
|
|
885
|
+
maxEntries: SEARCH_LIMITS.entries,
|
|
886
|
+
maxBytes: 1024 * 1024,
|
|
887
|
+
timeoutMs: SEARCH_LIMITS.timeoutMs
|
|
888
|
+
}, ctx));
|
|
889
|
+
return { base, ...files };
|
|
890
|
+
}
|
|
891
|
+
function output(lines, complete, skipped = 0) {
|
|
892
|
+
const status = complete ? "Search complete within Pi listing; unreadable entries may be omitted." : "Partial results; narrow the directory or filter and retry.";
|
|
893
|
+
return {
|
|
894
|
+
output: [text(`${lines.join(`
|
|
895
|
+
`)}
|
|
896
|
+
${status}${skipped ? ` Skipped/unreadable files: ${skipped}.` : ""}`)],
|
|
897
|
+
details: { complete, skipped }
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
var FindFiles = defineTool({
|
|
901
|
+
name: "find_files",
|
|
902
|
+
description: "Discover files and directories without a shell. query is a literal path/name substring (not a glob). Respects gitignore; returns absolute paths usable by read. A partial result is not a complete absence of matches.",
|
|
903
|
+
parameters: Type.Object({ path, query: Type.Optional(Type.String()), limit }),
|
|
904
|
+
replay: "safe",
|
|
905
|
+
execute: (args, api, context) => search(api, context, async (env, ctx) => {
|
|
906
|
+
const files = await listing(env, args.path ?? ".", ctx);
|
|
907
|
+
const lines = [];
|
|
908
|
+
let bytes = 0;
|
|
909
|
+
let complete = !files.truncated;
|
|
910
|
+
for (const file of files.entries) {
|
|
911
|
+
ctx.abortSignal?.throwIfAborted();
|
|
912
|
+
if (args.query && !file.path.includes(args.query))
|
|
913
|
+
continue;
|
|
914
|
+
const absolute = getOrThrow(await env.joinPath([files.base, file.path], ctx)).replace(/\\/g, "/");
|
|
915
|
+
const line = `${file.kind} ${absolute}`;
|
|
916
|
+
bytes += Buffer.byteLength(line) + 1;
|
|
917
|
+
if (lines.length >= (args.limit ?? 100) || bytes > SEARCH_LIMITS.outputBytes - 256) {
|
|
918
|
+
complete = false;
|
|
919
|
+
break;
|
|
920
|
+
}
|
|
921
|
+
lines.push(line);
|
|
922
|
+
}
|
|
923
|
+
return output(lines, complete);
|
|
924
|
+
})
|
|
925
|
+
});
|
|
926
|
+
var SearchText = defineTool({
|
|
927
|
+
name: "search_text",
|
|
928
|
+
description: "Search UTF-8 file contents without a shell. pattern and filePattern are literal substrings, not regex/globs. Returns absolute paths, 1-based line numbers and context for read. Binary/large/unreadable files and exhausted budgets are explicitly reported as partial results.",
|
|
929
|
+
parameters: Type.Object({
|
|
930
|
+
pattern: Type.String({ minLength: 1, maxLength: 4096 }),
|
|
931
|
+
path,
|
|
932
|
+
filePattern: Type.Optional(Type.String()),
|
|
933
|
+
caseSensitive: Type.Optional(Type.Boolean()),
|
|
934
|
+
contextLines: Type.Optional(Type.Integer({ minimum: 0, maximum: 5 })),
|
|
935
|
+
limit
|
|
936
|
+
}),
|
|
937
|
+
replay: "safe",
|
|
938
|
+
execute: (args, api, context) => search(api, context, async (env, ctx) => {
|
|
939
|
+
const files = await listing(env, args.path ?? ".", ctx);
|
|
940
|
+
const results = [];
|
|
941
|
+
let complete = !files.truncated;
|
|
942
|
+
let skipped = 0;
|
|
943
|
+
let scanned = 0;
|
|
944
|
+
let bytes = 0;
|
|
945
|
+
const needle = args.caseSensitive === false ? args.pattern.toLowerCase() : args.pattern;
|
|
946
|
+
outer:
|
|
947
|
+
for (const file of files.entries) {
|
|
948
|
+
ctx.abortSignal?.throwIfAborted();
|
|
949
|
+
if (file.kind !== "file" || args.filePattern && !file.path.includes(args.filePattern))
|
|
950
|
+
continue;
|
|
951
|
+
const absolute = getOrThrow(await env.joinPath([files.base, file.path], ctx));
|
|
952
|
+
const opened = await env.openBinaryReader(absolute, { noFollow: true }, ctx);
|
|
953
|
+
if (!opened.ok) {
|
|
954
|
+
ctx.abortSignal?.throwIfAborted();
|
|
955
|
+
skipped++;
|
|
956
|
+
complete = false;
|
|
957
|
+
continue;
|
|
958
|
+
}
|
|
959
|
+
try {
|
|
960
|
+
const info = getOrThrow(await opened.value.info(ctx));
|
|
961
|
+
if (info.size > SEARCH_LIMITS.fileBytes) {
|
|
962
|
+
skipped++;
|
|
963
|
+
complete = false;
|
|
964
|
+
continue;
|
|
965
|
+
}
|
|
966
|
+
if (scanned + info.size > SEARCH_LIMITS.scanBytes) {
|
|
967
|
+
complete = false;
|
|
968
|
+
break;
|
|
969
|
+
}
|
|
970
|
+
const data = getOrThrow(await opened.value.read(0, info.size + 1, ctx));
|
|
971
|
+
scanned += data.length;
|
|
972
|
+
if (data.length !== info.size || data.includes(0)) {
|
|
973
|
+
skipped++;
|
|
974
|
+
complete = false;
|
|
975
|
+
continue;
|
|
976
|
+
}
|
|
977
|
+
const lines = new TextDecoder("utf-8", { fatal: true }).decode(data).split(/\r?\n/);
|
|
978
|
+
for (let i = 0;i < lines.length; i++) {
|
|
979
|
+
ctx.abortSignal?.throwIfAborted();
|
|
980
|
+
const value = lines[i];
|
|
981
|
+
if (!(args.caseSensitive === false ? value.toLowerCase() : value).includes(needle))
|
|
982
|
+
continue;
|
|
983
|
+
const n = args.contextLines ?? 0;
|
|
984
|
+
const block = lines.slice(Math.max(0, i - n), i + n + 1).map((line, offset) => `${absolute.replace(/\\/g, "/")}:${Math.max(0, i - n) + offset + 1}:${line}`).join(`
|
|
985
|
+
`);
|
|
986
|
+
bytes += Buffer.byteLength(block) + 1;
|
|
987
|
+
if (results.length >= (args.limit ?? 100) || bytes > SEARCH_LIMITS.outputBytes - 256) {
|
|
988
|
+
complete = false;
|
|
989
|
+
break outer;
|
|
990
|
+
}
|
|
991
|
+
results.push(block);
|
|
992
|
+
}
|
|
993
|
+
} catch (error) {
|
|
994
|
+
ctx.abortSignal?.throwIfAborted();
|
|
995
|
+
if (!(error instanceof Error))
|
|
996
|
+
throw error;
|
|
997
|
+
skipped++;
|
|
998
|
+
complete = false;
|
|
999
|
+
} finally {
|
|
1000
|
+
await opened.value.close(BACKGROUND_CONTEXT);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
return output(results, complete, skipped);
|
|
1004
|
+
})
|
|
1005
|
+
});
|
|
1006
|
+
var AgentFileSearch = defineExtension({ name: "neko-file-search", tools: [FindFiles, SearchText] });
|
|
1007
|
+
|
|
841
1008
|
// packages/core/src/compaction.ts
|
|
842
1009
|
import {
|
|
843
1010
|
CompactionTask
|
|
@@ -870,9 +1037,9 @@ async function readCompaction(harness, storage, id, context) {
|
|
|
870
1037
|
}
|
|
871
1038
|
|
|
872
1039
|
// packages/core/src/image-metadata.ts
|
|
873
|
-
import { defineExtension, hook, ToolTask } from "@earendil-works/pi-durable";
|
|
1040
|
+
import { defineExtension as defineExtension2, hook, ToolTask } from "@earendil-works/pi-durable";
|
|
874
1041
|
import sharp from "sharp";
|
|
875
|
-
var ImageDetails =
|
|
1042
|
+
var ImageDetails = defineExtension2({
|
|
876
1043
|
name: "neko-image-details",
|
|
877
1044
|
hooks: [
|
|
878
1045
|
hook(ToolTask, {
|
|
@@ -1045,10 +1212,219 @@ ${args}` : block;
|
|
|
1045
1212
|
};
|
|
1046
1213
|
|
|
1047
1214
|
// packages/core/src/prompt/prompt.ts
|
|
1048
|
-
import { defineExtension as
|
|
1215
|
+
import { defineExtension as defineExtension4, section } from "@earendil-works/pi-durable";
|
|
1216
|
+
|
|
1217
|
+
// packages/core/src/subagents.ts
|
|
1218
|
+
import {
|
|
1219
|
+
AssistantEntry,
|
|
1220
|
+
configure,
|
|
1221
|
+
defineDoc,
|
|
1222
|
+
defineExtension as defineExtension3,
|
|
1223
|
+
defineTool as defineTool2
|
|
1224
|
+
} from "@earendil-works/pi-durable";
|
|
1225
|
+
import { Type as Type2 } from "typebox";
|
|
1226
|
+
var DelegationsDoc = defineDoc({
|
|
1227
|
+
kind: "app.neko.delegations",
|
|
1228
|
+
version: 1,
|
|
1229
|
+
scope: "conversation",
|
|
1230
|
+
history: "latest",
|
|
1231
|
+
fork: "initial",
|
|
1232
|
+
initial: () => ({ items: [] })
|
|
1233
|
+
});
|
|
1234
|
+
var SubagentDoc = defineDoc({
|
|
1235
|
+
kind: "app.neko.subagent",
|
|
1236
|
+
version: 1,
|
|
1237
|
+
scope: "conversation",
|
|
1238
|
+
history: "latest",
|
|
1239
|
+
fork: "initial",
|
|
1240
|
+
initial: () => ({ role: null, parentId: null })
|
|
1241
|
+
});
|
|
1242
|
+
var SlotsDoc = defineDoc({
|
|
1243
|
+
kind: "app.neko.subagent-slots",
|
|
1244
|
+
version: 1,
|
|
1245
|
+
scope: "session",
|
|
1246
|
+
initial: () => ({ items: [] })
|
|
1247
|
+
});
|
|
1248
|
+
var ROLE_PROMPTS = {
|
|
1249
|
+
explore: "You are neko's exploration subagent. Read and search by default. Bash requires explicit user approval for each call and may have side effects. Respect the assigned constraints. After denial use another approach or report the limitation; do not bypass or repeatedly request denied operations. Return findings with paths and line numbers.",
|
|
1250
|
+
plan: "You are neko's read-only planning subagent. Read evidence and propose implementation steps, trade-offs and verification. Do not modify files or execute commands.",
|
|
1251
|
+
coder: "You are neko's coding subagent. Implement only the assigned scope and verify it. Coordinate through the main agent; do not overwrite unrelated changes. Report changed files and verification results."
|
|
1252
|
+
};
|
|
1253
|
+
var SUBAGENT_DELIVERY = " Work only from the assigned task and your project resources; you cannot see the main conversation. Do not delegate or ask the user directly. Your final message must be a self-contained delivery to the main agent, including evidence, results and limitations. Skills never expand your allowed tools.";
|
|
1254
|
+
var READ_TOOLS = new Set(["read", "find_files", "search_text", "web_search", "web_extract"]);
|
|
1255
|
+
async function refreshDelegations(tx, id) {
|
|
1256
|
+
const doc = await tx.doc(DelegationsDoc, id);
|
|
1257
|
+
for (const item of doc.items) {
|
|
1258
|
+
if (item.historyThrough !== undefined)
|
|
1259
|
+
continue;
|
|
1260
|
+
if (item.status === "completed" || item.status === "failed" || item.status === "cancelled")
|
|
1261
|
+
continue;
|
|
1262
|
+
const task = await tx.task(item.taskId);
|
|
1263
|
+
if (task?.state.status !== "terminal")
|
|
1264
|
+
continue;
|
|
1265
|
+
item.status = task.state.outcome.status === "aborted" ? "cancelled" : "failed";
|
|
1266
|
+
item.endedAt = Date.now();
|
|
1267
|
+
}
|
|
1268
|
+
return JSON.parse(JSON.stringify(doc.items));
|
|
1269
|
+
}
|
|
1270
|
+
async function admit(api, cwd, role, context) {
|
|
1271
|
+
for (;; ) {
|
|
1272
|
+
context.abortSignal?.throwIfAborted();
|
|
1273
|
+
const acquired = await api.commit(async (tx) => {
|
|
1274
|
+
const slots = await tx.doc(SlotsDoc);
|
|
1275
|
+
const live = [];
|
|
1276
|
+
for (const slot of slots.items) {
|
|
1277
|
+
const task = await tx.task(slot.taskId);
|
|
1278
|
+
if (task && task.state.status !== "terminal")
|
|
1279
|
+
live.push(slot);
|
|
1280
|
+
}
|
|
1281
|
+
slots.items = live;
|
|
1282
|
+
if (live.some((slot) => slot.taskId === api.taskId))
|
|
1283
|
+
return true;
|
|
1284
|
+
if (live.filter((slot) => slot.parentId === api.conversationId).length >= 3 || role === "coder" && live.some((slot) => slot.coder && slot.cwd === cwd))
|
|
1285
|
+
return false;
|
|
1286
|
+
slots.items.push({ taskId: api.taskId, parentId: api.conversationId, cwd, coder: role === "coder" });
|
|
1287
|
+
return true;
|
|
1288
|
+
}, context);
|
|
1289
|
+
if (acquired)
|
|
1290
|
+
return;
|
|
1291
|
+
await new Promise((resolve, reject) => {
|
|
1292
|
+
const cleanup = () => {
|
|
1293
|
+
clearTimeout(timer);
|
|
1294
|
+
context.abortSignal?.removeEventListener("abort", abort);
|
|
1295
|
+
};
|
|
1296
|
+
const abort = () => {
|
|
1297
|
+
cleanup();
|
|
1298
|
+
reject(context.abortSignal?.reason ?? new Error("Cancelled"));
|
|
1299
|
+
};
|
|
1300
|
+
const timer = setTimeout(() => {
|
|
1301
|
+
cleanup();
|
|
1302
|
+
resolve();
|
|
1303
|
+
}, 100);
|
|
1304
|
+
context.abortSignal?.addEventListener("abort", abort, { once: true });
|
|
1305
|
+
if (context.abortSignal?.aborted)
|
|
1306
|
+
abort();
|
|
1307
|
+
});
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
function createSubagentExtension(prepare, discard) {
|
|
1311
|
+
const extension = defineExtension3({
|
|
1312
|
+
name: "neko-subagents",
|
|
1313
|
+
tools: [
|
|
1314
|
+
defineTool2({
|
|
1315
|
+
name: "subagent",
|
|
1316
|
+
description: "Delegate a self-contained task to an independent foreground subagent. explore: investigation, Bash requires user approval; plan: read-only implementation planning; coder: scoped changes and verification. Include all needed context, constraints and expected delivery. Use only when isolation or parallel independent work helps; handle simple tasks directly. Returns the final report; the main agent waits and must verify/integrate it.",
|
|
1317
|
+
parameters: Type2.Object({
|
|
1318
|
+
type: Type2.Union([Type2.Literal("explore"), Type2.Literal("plan"), Type2.Literal("coder")]),
|
|
1319
|
+
title: Type2.String({ minLength: 1, maxLength: 80 }),
|
|
1320
|
+
task: Type2.String({ minLength: 1, maxLength: 32768 })
|
|
1321
|
+
}),
|
|
1322
|
+
replay: "safe",
|
|
1323
|
+
execute: async (args, api, context) => {
|
|
1324
|
+
if (!args.task.trim() || !args.title.trim() || Buffer.byteLength(args.task) > 32768)
|
|
1325
|
+
throw new Error("Task/title must be nonempty; task limit is 32 KiB.");
|
|
1326
|
+
if ((await api.snapshot(SubagentDoc, api.conversationId, context))?.role)
|
|
1327
|
+
throw new Error("Subagents cannot delegate.");
|
|
1328
|
+
const agent = await api.agent(context);
|
|
1329
|
+
const childId = await api.commit(async (tx) => {
|
|
1330
|
+
const existing = (await tx.scanConversations({ ownerTaskId: api.taskId }, 1)).items[0];
|
|
1331
|
+
if (existing)
|
|
1332
|
+
return existing.id;
|
|
1333
|
+
const child = await tx.createConversation({ ownership: { kind: "task", taskId: api.taskId } });
|
|
1334
|
+
await configure(tx, child.id, {
|
|
1335
|
+
model: agent.model,
|
|
1336
|
+
thinkingLevel: agent.thinkingLevel,
|
|
1337
|
+
cwd: agent.cwd,
|
|
1338
|
+
extensions: agent.extensions.filter((e) => e.name !== extension.name),
|
|
1339
|
+
tools: agent.tools.filter((tool) => tool.name !== "subagent" && (args.type === "coder" || READ_TOOLS.has(tool.name) || args.type === "explore" && tool.name === "bash"))
|
|
1340
|
+
});
|
|
1341
|
+
Object.assign(await tx.doc(SubagentDoc, child.id), { role: args.type, parentId: api.conversationId });
|
|
1342
|
+
(await tx.doc(DelegationsDoc, api.conversationId)).items.push({
|
|
1343
|
+
parentId: api.conversationId,
|
|
1344
|
+
taskId: api.taskId,
|
|
1345
|
+
callId: api.callId,
|
|
1346
|
+
childId: child.id,
|
|
1347
|
+
role: args.type,
|
|
1348
|
+
title: args.title,
|
|
1349
|
+
createdAt: Date.now(),
|
|
1350
|
+
startedAt: null,
|
|
1351
|
+
endedAt: null,
|
|
1352
|
+
status: "queued",
|
|
1353
|
+
report: null
|
|
1354
|
+
});
|
|
1355
|
+
return child.id;
|
|
1356
|
+
}, context);
|
|
1357
|
+
await api.details({ conversationId: childId, role: args.type, title: args.title }, context);
|
|
1358
|
+
const cwd = api.env ? await api.env.canonicalPath(agent.cwd ?? api.env.cwd, context) : undefined;
|
|
1359
|
+
await admit(api, cwd?.ok ? cwd.value : agent.cwd ?? "", args.type, context);
|
|
1360
|
+
await api.commit(async (tx) => {
|
|
1361
|
+
const item = (await tx.doc(DelegationsDoc, api.conversationId)).items.find((d) => d.taskId === api.taskId);
|
|
1362
|
+
item.status = "running";
|
|
1363
|
+
item.startedAt ??= Date.now();
|
|
1364
|
+
}, context);
|
|
1365
|
+
const child = await api.conversation(childId, context);
|
|
1366
|
+
const requestId = `subagent:${api.taskId}`;
|
|
1367
|
+
const duplicate = await api.commit((tx) => tx.submissionByRequest(childId, requestId), context);
|
|
1368
|
+
if (!duplicate)
|
|
1369
|
+
await prepare(childId, requestId, agent.cwd ?? "");
|
|
1370
|
+
let submission;
|
|
1371
|
+
try {
|
|
1372
|
+
submission = await child.submit({
|
|
1373
|
+
type: "input",
|
|
1374
|
+
content: args.task,
|
|
1375
|
+
requestId,
|
|
1376
|
+
entry: { kind: NekoInput.kind, data: { at: Date.now(), prompt: args.task, images: [] } }
|
|
1377
|
+
}, context);
|
|
1378
|
+
} catch (error) {
|
|
1379
|
+
discard(childId, requestId);
|
|
1380
|
+
throw error;
|
|
1381
|
+
}
|
|
1382
|
+
const settled = await submission.wait(context);
|
|
1383
|
+
let report = "";
|
|
1384
|
+
let status = "failed";
|
|
1385
|
+
if (settled.status === "done" && settled.type === "input") {
|
|
1386
|
+
const entry = await api.commit((tx) => tx.entry(AssistantEntry, settled.answer), context);
|
|
1387
|
+
const message = entry?.model?.[0];
|
|
1388
|
+
report = message?.role === "assistant" ? message.content.flatMap((b) => b.type === "text" ? [b.text] : []).join("") : "";
|
|
1389
|
+
status = "completed";
|
|
1390
|
+
} else if (settled.status === "unanswered") {
|
|
1391
|
+
status = settled.reason === "aborted" ? "cancelled" : "failed";
|
|
1392
|
+
report = `Subagent ${status}: ${settled.reason}`;
|
|
1393
|
+
}
|
|
1394
|
+
await api.commit(async (tx) => {
|
|
1395
|
+
const item = (await tx.doc(DelegationsDoc, api.conversationId)).items.find((d) => d.taskId === api.taskId);
|
|
1396
|
+
item.status = status;
|
|
1397
|
+
item.report = report;
|
|
1398
|
+
item.endedAt = Date.now();
|
|
1399
|
+
}, context);
|
|
1400
|
+
return {
|
|
1401
|
+
output: [{ type: "text", text: report || "Subagent completed without a text report." }],
|
|
1402
|
+
isError: status !== "completed",
|
|
1403
|
+
details: { conversationId: childId, role: args.type, title: args.title, status }
|
|
1404
|
+
};
|
|
1405
|
+
}
|
|
1406
|
+
})
|
|
1407
|
+
]
|
|
1408
|
+
});
|
|
1409
|
+
return extension;
|
|
1410
|
+
}
|
|
1411
|
+
async function listDelegations(harness, parent, context) {
|
|
1412
|
+
return harness.commit((tx) => refreshDelegations(tx, parent), context);
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
// packages/core/src/prompt/prompt.ts
|
|
1049
1416
|
var KEYS = ["preamble", "rules", "project_context", "skills", "cwd"];
|
|
1050
1417
|
var PREAMBLE = "You are an expert coding assistant operating inside neko, a coding agent harness. You help users by reading files, executing commands, editing code, and writing new files.";
|
|
1051
1418
|
var TOOL_GUIDELINES = {
|
|
1419
|
+
subagent: [
|
|
1420
|
+
"Delegate only independent, sufficiently scoped work when isolation or parallelism helps. Handle simple tasks directly. Pass necessary context and user constraints; research-only requests must not become coding tasks. Verify and integrate reports. Do not modify files concurrently with a coder assigned those files."
|
|
1421
|
+
],
|
|
1422
|
+
find_files: [
|
|
1423
|
+
"Use find_files to discover files; query is a literal substring. Narrow the directory when results are partial."
|
|
1424
|
+
],
|
|
1425
|
+
search_text: [
|
|
1426
|
+
"Use search_text for literal text search and read for surrounding evidence. Partial results do not prove absence."
|
|
1427
|
+
],
|
|
1052
1428
|
read: ["Use read to examine files instead of cat or sed."],
|
|
1053
1429
|
write: ["Use write only for new files or complete rewrites."],
|
|
1054
1430
|
edit: [
|
|
@@ -1084,11 +1460,12 @@ ${content}
|
|
|
1084
1460
|
`);
|
|
1085
1461
|
var createNekoPrompt = (loadResources) => {
|
|
1086
1462
|
const built = new WeakMap;
|
|
1087
|
-
const build = async (input) => {
|
|
1463
|
+
const build = async (input, context) => {
|
|
1088
1464
|
const cwd = input.agent.cwd;
|
|
1465
|
+
const role = (await input.read.snapshot(SubagentDoc, input.conversationId, context))?.role;
|
|
1089
1466
|
const resources = cwd ? await loadResources(input.conversationId, cwd) : { contextFiles: [], skills: [] };
|
|
1090
1467
|
const sections = {
|
|
1091
|
-
preamble: PREAMBLE,
|
|
1468
|
+
preamble: role ? ROLE_PROMPTS[role] + SUBAGENT_DELIVERY : PREAMBLE,
|
|
1092
1469
|
rules: buildRules(input.agent.tools.map((tool) => tool.name)),
|
|
1093
1470
|
project_context: renderProjectContext(resources.contextFiles),
|
|
1094
1471
|
skills: formatSkillsForPrompt(resources.skills) || undefined,
|
|
@@ -1096,12 +1473,12 @@ var createNekoPrompt = (loadResources) => {
|
|
|
1096
1473
|
};
|
|
1097
1474
|
return sections;
|
|
1098
1475
|
};
|
|
1099
|
-
return
|
|
1476
|
+
return defineExtension4({
|
|
1100
1477
|
name: "neko-prompt",
|
|
1101
|
-
sections: KEYS.map((key) => section(key, async (input) => {
|
|
1478
|
+
sections: KEYS.map((key) => section(key, async (input, context) => {
|
|
1102
1479
|
let pending = built.get(input);
|
|
1103
1480
|
if (!pending) {
|
|
1104
|
-
pending = build(input);
|
|
1481
|
+
pending = build(input, context);
|
|
1105
1482
|
built.set(input, pending);
|
|
1106
1483
|
}
|
|
1107
1484
|
const text = (await pending)[key];
|
|
@@ -1248,13 +1625,13 @@ var createResourceSnapshots = (load, submission) => {
|
|
|
1248
1625
|
import { stripVTControlCharacters } from "util";
|
|
1249
1626
|
import { getSupportedThinkingLevels } from "@earendil-works/pi-ai";
|
|
1250
1627
|
import {
|
|
1251
|
-
defineDoc,
|
|
1252
|
-
defineExtension as
|
|
1628
|
+
defineDoc as defineDoc2,
|
|
1629
|
+
defineExtension as defineExtension5,
|
|
1253
1630
|
defineTask,
|
|
1254
1631
|
InboxDoc,
|
|
1255
1632
|
LiveDoc
|
|
1256
1633
|
} from "@earendil-works/pi-durable";
|
|
1257
|
-
var SessionMetadataDoc =
|
|
1634
|
+
var SessionMetadataDoc = defineDoc2({
|
|
1258
1635
|
kind: "app.neko.session-metadata",
|
|
1259
1636
|
version: 1,
|
|
1260
1637
|
scope: "conversation",
|
|
@@ -1441,7 +1818,7 @@ class SessionMetadataManager {
|
|
|
1441
1818
|
this.storage = storage;
|
|
1442
1819
|
this.context = context;
|
|
1443
1820
|
this.report = report;
|
|
1444
|
-
registry.install(
|
|
1821
|
+
registry.install(defineExtension5({ name: "neko-naming", tasks: [this.task] }));
|
|
1445
1822
|
this.#unsubscribe = harness.subscribeCommits(({ changes }) => {
|
|
1446
1823
|
const ids = new Set;
|
|
1447
1824
|
const submissions = new Map;
|
|
@@ -1580,9 +1957,191 @@ class SessionMetadataManager {
|
|
|
1580
1957
|
}
|
|
1581
1958
|
}
|
|
1582
1959
|
|
|
1960
|
+
// packages/core/src/tool-approvals.ts
|
|
1961
|
+
import { randomUUID } from "crypto";
|
|
1962
|
+
import { realpath } from "fs/promises";
|
|
1963
|
+
import {
|
|
1964
|
+
configure as configure2,
|
|
1965
|
+
defineDoc as defineDoc3,
|
|
1966
|
+
defineExtension as defineExtension6,
|
|
1967
|
+
hook as hook2,
|
|
1968
|
+
ToolTask as ToolTask2,
|
|
1969
|
+
wrapTool
|
|
1970
|
+
} from "@earendil-works/pi-durable";
|
|
1971
|
+
import { createBashTool } from "@earendil-works/pi-durable/tools";
|
|
1972
|
+
var ApprovalsDoc = defineDoc3({
|
|
1973
|
+
kind: "app.neko.approvals",
|
|
1974
|
+
version: 1,
|
|
1975
|
+
scope: "conversation",
|
|
1976
|
+
history: "latest",
|
|
1977
|
+
fork: "initial",
|
|
1978
|
+
initial: () => ({ items: [] })
|
|
1979
|
+
});
|
|
1980
|
+
async function live(tx, id) {
|
|
1981
|
+
const task = await tx.task(id);
|
|
1982
|
+
return !!task && !task.abortRequested && task.state.status !== "terminal" && task.state.status !== "completing";
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
class ToolApprovals {
|
|
1986
|
+
harness;
|
|
1987
|
+
#parents = new Set;
|
|
1988
|
+
constructor(harness, context) {
|
|
1989
|
+
this.harness = harness;
|
|
1990
|
+
const stop = harness.subscribeCommits(({ changes }) => {
|
|
1991
|
+
if (!changes.some((c) => c.type === "task" && (c.value.abortRequested || c.value.state.status === "terminal")))
|
|
1992
|
+
return;
|
|
1993
|
+
for (const parent of this.#parents)
|
|
1994
|
+
this.list(parent, context).catch(() => {});
|
|
1995
|
+
});
|
|
1996
|
+
harness.closed.finally(stop).catch(() => {});
|
|
1997
|
+
}
|
|
1998
|
+
async list(parent, context) {
|
|
1999
|
+
this.#parents.add(parent);
|
|
2000
|
+
return this.harness.commit(async (tx) => {
|
|
2001
|
+
const doc = await tx.doc(ApprovalsDoc, parent);
|
|
2002
|
+
for (const item of doc.items) {
|
|
2003
|
+
if (item.status === "pending" && (!await live(tx, item.id) || !await live(tx, item.delegationTaskId))) {
|
|
2004
|
+
item.status = "cancelled";
|
|
2005
|
+
item.decidedAt = Date.now();
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
return JSON.parse(JSON.stringify(doc.items));
|
|
2009
|
+
}, context);
|
|
2010
|
+
}
|
|
2011
|
+
async decide(parent, delegationTaskId, id, version, decision, context) {
|
|
2012
|
+
return this.harness.commit(async (tx) => {
|
|
2013
|
+
const delegation = (await tx.doc(DelegationsDoc, parent)).items.find((d) => d.taskId === delegationTaskId);
|
|
2014
|
+
const item = (await tx.doc(ApprovalsDoc, parent)).items.find((a) => a.id === id);
|
|
2015
|
+
if (!delegation || delegation.historyThrough !== undefined || !item || item.delegationTaskId !== delegationTaskId || item.childId !== delegation.childId || item.version !== version)
|
|
2016
|
+
throw new Error("Approval does not belong to this task or has changed");
|
|
2017
|
+
if (item.status !== "pending")
|
|
2018
|
+
return item.status;
|
|
2019
|
+
item.status = await live(tx, id) && await live(tx, delegationTaskId) ? decision : "cancelled";
|
|
2020
|
+
item.decidedAt = Date.now();
|
|
2021
|
+
return item.status;
|
|
2022
|
+
}, context);
|
|
2023
|
+
}
|
|
2024
|
+
async wait(parent, id, context) {
|
|
2025
|
+
const state = await this.harness.documentState(ApprovalsDoc, parent, context);
|
|
2026
|
+
if (!state)
|
|
2027
|
+
throw new Error("Approval unavailable");
|
|
2028
|
+
try {
|
|
2029
|
+
return await new Promise((resolve, reject) => {
|
|
2030
|
+
let stop = () => {};
|
|
2031
|
+
const cleanup = () => {
|
|
2032
|
+
stop();
|
|
2033
|
+
context.abortSignal?.removeEventListener("abort", abort);
|
|
2034
|
+
};
|
|
2035
|
+
const abort = () => {
|
|
2036
|
+
cleanup();
|
|
2037
|
+
reject(context.abortSignal?.reason ?? new Error("Cancelled"));
|
|
2038
|
+
};
|
|
2039
|
+
const check = () => {
|
|
2040
|
+
const item = state.value?.items.find((a) => a.id === id);
|
|
2041
|
+
if (item && item.status !== "pending") {
|
|
2042
|
+
cleanup();
|
|
2043
|
+
resolve(structuredClone(item));
|
|
2044
|
+
}
|
|
2045
|
+
};
|
|
2046
|
+
stop = state.subscribe(check);
|
|
2047
|
+
context.abortSignal?.addEventListener("abort", abort, { once: true });
|
|
2048
|
+
if (context.abortSignal?.aborted)
|
|
2049
|
+
abort();
|
|
2050
|
+
else
|
|
2051
|
+
check();
|
|
2052
|
+
});
|
|
2053
|
+
} finally {
|
|
2054
|
+
state.dispose();
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
async ensure(parent, candidate, context) {
|
|
2058
|
+
this.#parents.add(parent);
|
|
2059
|
+
return this.harness.commit(async (tx) => {
|
|
2060
|
+
if (!await live(tx, candidate.id) || !await live(tx, candidate.delegationTaskId))
|
|
2061
|
+
throw new Error("Task stopped");
|
|
2062
|
+
const doc = await tx.doc(ApprovalsDoc, parent);
|
|
2063
|
+
const existing = doc.items.find((a) => a.id === candidate.id);
|
|
2064
|
+
if (existing) {
|
|
2065
|
+
if (existing.command !== candidate.command || existing.cwd !== candidate.cwd || existing.timeout !== candidate.timeout || existing.childId !== candidate.childId)
|
|
2066
|
+
throw new Error("Approval operation changed; a new call is required");
|
|
2067
|
+
return;
|
|
2068
|
+
}
|
|
2069
|
+
doc.items.push({
|
|
2070
|
+
...candidate,
|
|
2071
|
+
version: randomUUID(),
|
|
2072
|
+
status: "pending",
|
|
2073
|
+
createdAt: Date.now(),
|
|
2074
|
+
decidedAt: null
|
|
2075
|
+
});
|
|
2076
|
+
}, context);
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
function createApprovalExtension(service) {
|
|
2080
|
+
return defineExtension6({
|
|
2081
|
+
name: "neko-tool-approvals",
|
|
2082
|
+
hooks: [
|
|
2083
|
+
hook2(ToolTask2, {
|
|
2084
|
+
beforeTool: async (call, api, context) => {
|
|
2085
|
+
const role = await api.snapshot(SubagentDoc, api.conversationId, context);
|
|
2086
|
+
if (role?.role !== "explore" || call.name !== "bash")
|
|
2087
|
+
return;
|
|
2088
|
+
if (role.parentId === null)
|
|
2089
|
+
return { block: "Missing parent session" };
|
|
2090
|
+
const approvals = service(), parent = role.parentId;
|
|
2091
|
+
const child = await approvals.harness.conversation(api.conversationId, context);
|
|
2092
|
+
const agent = await child.agent(context);
|
|
2093
|
+
const cwd = await realpath(agent.cwd);
|
|
2094
|
+
const delegation = (await api.snapshot(DelegationsDoc, parent, context))?.items.find((d) => d.childId === api.conversationId && d.historyThrough === undefined);
|
|
2095
|
+
if (!delegation)
|
|
2096
|
+
return { block: "Missing delegation" };
|
|
2097
|
+
await approvals.ensure(parent, {
|
|
2098
|
+
id: api.taskId,
|
|
2099
|
+
childId: api.conversationId,
|
|
2100
|
+
delegationTaskId: delegation.taskId,
|
|
2101
|
+
callId: call.id,
|
|
2102
|
+
command: String(call.arguments.command),
|
|
2103
|
+
cwd,
|
|
2104
|
+
timeout: typeof call.arguments.timeout === "number" ? call.arguments.timeout : null
|
|
2105
|
+
}, context);
|
|
2106
|
+
const result = await approvals.wait(parent, api.taskId, context);
|
|
2107
|
+
return result.status === "allowed" ? undefined : { block: "Command denied or cancelled by user. Use another approach or report the limitation." };
|
|
2108
|
+
}
|
|
2109
|
+
})
|
|
2110
|
+
],
|
|
2111
|
+
wraps: [
|
|
2112
|
+
wrapTool(createBashTool(), (bash) => ({
|
|
2113
|
+
...bash,
|
|
2114
|
+
execute: async (args, api, context) => {
|
|
2115
|
+
const role = await api.snapshot(SubagentDoc, api.conversationId, context);
|
|
2116
|
+
if (role?.role === "explore") {
|
|
2117
|
+
const item = role.parentId === null ? undefined : (await api.snapshot(ApprovalsDoc, role.parentId, context))?.items.find((a) => a.id === api.taskId);
|
|
2118
|
+
const cwd = api.env && await api.env.canonicalPath(api.env.cwd, context);
|
|
2119
|
+
if (!item || item.status !== "allowed" || item.command !== args.command || item.timeout !== (args.timeout ?? null) || !cwd?.ok || cwd.value !== item.cwd)
|
|
2120
|
+
throw new Error("Bash approval missing or operation changed");
|
|
2121
|
+
}
|
|
2122
|
+
context.abortSignal?.throwIfAborted();
|
|
2123
|
+
return bash.execute(args, api, context);
|
|
2124
|
+
}
|
|
2125
|
+
}))
|
|
2126
|
+
]
|
|
2127
|
+
});
|
|
2128
|
+
}
|
|
2129
|
+
async function prepareApprovalConversations(harness, ids, extension, context) {
|
|
2130
|
+
for (const id of ids) {
|
|
2131
|
+
if ((await harness.snapshot(SubagentDoc, id, context))?.role !== "explore")
|
|
2132
|
+
continue;
|
|
2133
|
+
const child = await harness.conversation(id, context);
|
|
2134
|
+
const agent = await child.agent(context);
|
|
2135
|
+
await harness.commit((tx) => configure2(tx, id, {
|
|
2136
|
+
tools: [...agent.tools.filter((t) => t.name !== "bash"), createBashTool()],
|
|
2137
|
+
extensions: { add: [extension] }
|
|
2138
|
+
}), context);
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
|
|
1583
2142
|
// packages/core/src/web-search.ts
|
|
1584
|
-
import { Type } from "@earendil-works/pi-ai";
|
|
1585
|
-
import { defineExtension as
|
|
2143
|
+
import { Type as Type3 } from "@earendil-works/pi-ai";
|
|
2144
|
+
import { defineExtension as defineExtension7, defineTool as defineTool3 } from "@earendil-works/pi-durable";
|
|
1586
2145
|
var SEARCH_URL = "https://api.tavily.com/search";
|
|
1587
2146
|
var EXTRACT_URL = "https://api.tavily.com/extract";
|
|
1588
2147
|
var REQUEST_TIMEOUT_MS = 30000;
|
|
@@ -1700,15 +2259,15 @@ function urlKey(value) {
|
|
|
1700
2259
|
}
|
|
1701
2260
|
}
|
|
1702
2261
|
function createWebSearchTools(resolveKey, fetchImpl = fetch) {
|
|
1703
|
-
return
|
|
2262
|
+
return defineExtension7({
|
|
1704
2263
|
name: "neko-web-search",
|
|
1705
2264
|
tools: [
|
|
1706
|
-
|
|
2265
|
+
defineTool3({
|
|
1707
2266
|
name: "web_search",
|
|
1708
2267
|
description: "Search the live web for current or unknown information. Returns ranked sources (title, URL, bounded excerpts). Use only when up-to-date or external information is needed; use web_extract to read a known URL.",
|
|
1709
|
-
parameters:
|
|
1710
|
-
query:
|
|
1711
|
-
maxResults:
|
|
2268
|
+
parameters: Type3.Object({
|
|
2269
|
+
query: Type3.String({ minLength: 1, maxLength: 4096, description: "The search query" }),
|
|
2270
|
+
maxResults: Type3.Optional(Type3.Number({ description: `Maximum number of results (1-${MAX_RESULTS}, default ${DEFAULT_RESULTS})` }))
|
|
1712
2271
|
}),
|
|
1713
2272
|
async execute({ query, maxResults }, _api, context) {
|
|
1714
2273
|
try {
|
|
@@ -1733,11 +2292,11 @@ ${truncate(String(item.url ?? ""), 300)}
|
|
|
1733
2292
|
}
|
|
1734
2293
|
}
|
|
1735
2294
|
}),
|
|
1736
|
-
|
|
2295
|
+
defineTool3({
|
|
1737
2296
|
name: "web_extract",
|
|
1738
2297
|
description: "Extract bounded markdown excerpts from web pages by URL. Every requested URL has a result or failure entry; long content is truncated.",
|
|
1739
|
-
parameters:
|
|
1740
|
-
urls:
|
|
2298
|
+
parameters: Type3.Object({
|
|
2299
|
+
urls: Type3.Array(Type3.String({ maxLength: 2048, description: "http(s) URL" }), {
|
|
1741
2300
|
minItems: 1,
|
|
1742
2301
|
maxItems: MAX_URLS,
|
|
1743
2302
|
description: `URLs to extract (1-${MAX_URLS})`
|
|
@@ -1782,10 +2341,10 @@ ${truncate(String(item.url ?? ""), 300)}
|
|
|
1782
2341
|
}
|
|
1783
2342
|
|
|
1784
2343
|
// packages/core/src/workspace-files.ts
|
|
1785
|
-
import { realpath, stat } from "fs/promises";
|
|
2344
|
+
import { realpath as realpath2, stat } from "fs/promises";
|
|
1786
2345
|
import { basename as basename2, isAbsolute as isAbsolute2, relative, resolve as resolve3, sep } from "path";
|
|
1787
|
-
import { BACKGROUND_CONTEXT, withAbortSignal } from "@earendil-works/chord/context";
|
|
1788
|
-
import { getOrThrow } from "@earendil-works/pi-durable/env";
|
|
2346
|
+
import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT2, withAbortSignal as withAbortSignal2 } from "@earendil-works/chord/context";
|
|
2347
|
+
import { getOrThrow as getOrThrow2 } from "@earendil-works/pi-durable/env";
|
|
1789
2348
|
import { Connection, LocalExecutionEnv, localDaemon } from "@earendil-works/pi-env";
|
|
1790
2349
|
function relativeQuery(query) {
|
|
1791
2350
|
if (query.length > 2048 || [...query].some((c) => c.charCodeAt(0) < 32 || c === '"' || c === "\\") || isAbsolute2(query) || query.startsWith("~") || query.split("/").some((part) => part === ".." || part === ".git"))
|
|
@@ -1793,8 +2352,8 @@ function relativeQuery(query) {
|
|
|
1793
2352
|
return query.replace(/^\.\//, "");
|
|
1794
2353
|
}
|
|
1795
2354
|
async function workspaceTarget(root, path) {
|
|
1796
|
-
const base = await
|
|
1797
|
-
const target = await
|
|
2355
|
+
const base = await realpath2(root);
|
|
2356
|
+
const target = await realpath2(resolve3(base, relativeQuery(path)));
|
|
1798
2357
|
const local = relative(base, target);
|
|
1799
2358
|
if (local === ".." || local.startsWith(`..${sep}`) || isAbsolute2(local))
|
|
1800
2359
|
throw new Error("Path is outside the workspace.");
|
|
@@ -1903,7 +2462,7 @@ class WorkspaceFiles {
|
|
|
1903
2462
|
}
|
|
1904
2463
|
#disposeWatch(watch) {
|
|
1905
2464
|
watch.controller.abort();
|
|
1906
|
-
const closing = watch.work.catch(() => {}).then(() => watch.watcher?.close(
|
|
2465
|
+
const closing = watch.work.catch(() => {}).then(() => watch.watcher?.close(BACKGROUND_CONTEXT2)).then(() => {}).catch(() => {}).finally(() => this.#disposing.delete(closing));
|
|
1907
2466
|
this.#disposing.add(closing);
|
|
1908
2467
|
}
|
|
1909
2468
|
#closeWatcher(item) {
|
|
@@ -1950,11 +2509,11 @@ class WorkspaceFiles {
|
|
|
1950
2509
|
for (const user of [...current.users])
|
|
1951
2510
|
this.#closeWatcher(user);
|
|
1952
2511
|
}
|
|
1953
|
-
},
|
|
2512
|
+
}, withAbortSignal2(current.controller.signal, BACKGROUND_CONTEXT2));
|
|
1954
2513
|
if (result.ok) {
|
|
1955
2514
|
current.watcher = result.value;
|
|
1956
2515
|
if (current.controller.signal.aborted)
|
|
1957
|
-
await result.value.close(
|
|
2516
|
+
await result.value.close(BACKGROUND_CONTEXT2);
|
|
1958
2517
|
} else {
|
|
1959
2518
|
forget();
|
|
1960
2519
|
}
|
|
@@ -1978,7 +2537,7 @@ class WorkspaceFiles {
|
|
|
1978
2537
|
async#scan(item) {
|
|
1979
2538
|
const controller = new AbortController;
|
|
1980
2539
|
item.controller = controller;
|
|
1981
|
-
const context =
|
|
2540
|
+
const context = withAbortSignal2(controller.signal, BACKGROUND_CONTEXT2);
|
|
1982
2541
|
while (this.#active >= this.limits.concurrent) {
|
|
1983
2542
|
await new Promise((resolve, reject) => {
|
|
1984
2543
|
const wake = () => {
|
|
@@ -2023,7 +2582,7 @@ class WorkspaceFiles {
|
|
|
2023
2582
|
i.generation++;
|
|
2024
2583
|
}
|
|
2025
2584
|
}
|
|
2026
|
-
const listing =
|
|
2585
|
+
const listing = getOrThrow2(result);
|
|
2027
2586
|
controller.signal.throwIfAborted();
|
|
2028
2587
|
if (this.#closed || item.generation !== generation)
|
|
2029
2588
|
throw new Error("Workspace changed during file search. Retry.");
|
|
@@ -2043,7 +2602,7 @@ class WorkspaceFiles {
|
|
|
2043
2602
|
throw new Error("File environment closed");
|
|
2044
2603
|
query = relativeQuery(query);
|
|
2045
2604
|
const slash = query.lastIndexOf("/"), prefix = slash < 0 ? "" : query.slice(0, slash + 1), needle = query.slice(slash + 1);
|
|
2046
|
-
root = await
|
|
2605
|
+
root = await realpath2(root);
|
|
2047
2606
|
const base = await workspaceTarget(root, prefix);
|
|
2048
2607
|
if (!(await stat(base)).isDirectory())
|
|
2049
2608
|
throw new Error("Workspace directory is unavailable.");
|
|
@@ -2144,7 +2703,7 @@ function cancellable(work, signal) {
|
|
|
2144
2703
|
// packages/core/src/runtime.ts
|
|
2145
2704
|
var DEFAULT_MODEL = { provider: "deepseek", modelId: "deepseek-flash" };
|
|
2146
2705
|
var DEFAULT_STORAGE_PATH = join5(homedir4(), ".neko", "neko.sqlite");
|
|
2147
|
-
var ArchivedDoc =
|
|
2706
|
+
var ArchivedDoc = defineDoc4({
|
|
2148
2707
|
kind: "app.neko.conversation",
|
|
2149
2708
|
version: 1,
|
|
2150
2709
|
scope: "conversation",
|
|
@@ -2152,7 +2711,7 @@ var ArchivedDoc = defineDoc2({
|
|
|
2152
2711
|
fork: "initial",
|
|
2153
2712
|
initial: () => ({ archived: false })
|
|
2154
2713
|
});
|
|
2155
|
-
var WorkspacesDoc =
|
|
2714
|
+
var WorkspacesDoc = defineDoc4({
|
|
2156
2715
|
kind: "app.neko.workspaces",
|
|
2157
2716
|
version: 1,
|
|
2158
2717
|
scope: "conversation",
|
|
@@ -2160,7 +2719,7 @@ var WorkspacesDoc = defineDoc2({
|
|
|
2160
2719
|
fork: "initial",
|
|
2161
2720
|
initial: () => ({ items: [] })
|
|
2162
2721
|
});
|
|
2163
|
-
var ActivityDoc =
|
|
2722
|
+
var ActivityDoc = defineDoc4({
|
|
2164
2723
|
kind: "app.neko.activity",
|
|
2165
2724
|
version: 1,
|
|
2166
2725
|
scope: "conversation",
|
|
@@ -2169,7 +2728,7 @@ var ActivityDoc = defineDoc2({
|
|
|
2169
2728
|
initial: () => ({ lastUsedAt: 0 })
|
|
2170
2729
|
});
|
|
2171
2730
|
async function createNekoRuntime(options = {}) {
|
|
2172
|
-
const context = options.context ??
|
|
2731
|
+
const context = options.context ?? BACKGROUND_CONTEXT3;
|
|
2173
2732
|
const defaultCwd = normalizeWorkspace(options.cwd);
|
|
2174
2733
|
const model = options.model ?? DEFAULT_MODEL;
|
|
2175
2734
|
const providers = options.providers ?? [];
|
|
@@ -2205,6 +2764,7 @@ async function createNekoRuntime(options = {}) {
|
|
|
2205
2764
|
const registry = createRegistry();
|
|
2206
2765
|
registry.install(createCodingTools({ images: await createNodePhotonImages() }));
|
|
2207
2766
|
registry.install(ImageDetails);
|
|
2767
|
+
registry.install(AgentFileSearch);
|
|
2208
2768
|
registry.install(createWebSearchTools(async () => {
|
|
2209
2769
|
const saved = await effectiveCredentials.read("tavily");
|
|
2210
2770
|
if (saved?.type === "api_key" && saved.key)
|
|
@@ -2214,8 +2774,14 @@ async function createNekoRuntime(options = {}) {
|
|
|
2214
2774
|
const loadResources = createResourceLoader(options.promptHome);
|
|
2215
2775
|
const resourceSnapshots = createResourceSnapshots(loadResources, (id, requestId) => storage.submissionByRequest(id, requestId, context));
|
|
2216
2776
|
registry.install(createNekoPrompt(resourceSnapshots.current));
|
|
2777
|
+
registry.install(createSubagentExtension(async (id, requestId, cwd) => {
|
|
2778
|
+
resourceSnapshots.stage(id, requestId, cwd, await loadResources(cwd));
|
|
2779
|
+
}, resourceSnapshots.discard));
|
|
2217
2780
|
for (const extension of options.extensions ?? [])
|
|
2218
2781
|
registry.install(extension);
|
|
2782
|
+
let approvals;
|
|
2783
|
+
const approvalExtension = createApprovalExtension(() => approvals);
|
|
2784
|
+
registry.install(approvalExtension);
|
|
2219
2785
|
const harness = await Harness.open(storage, {
|
|
2220
2786
|
models,
|
|
2221
2787
|
registry,
|
|
@@ -2235,8 +2801,10 @@ async function createNekoRuntime(options = {}) {
|
|
|
2235
2801
|
} while (cursor);
|
|
2236
2802
|
return records;
|
|
2237
2803
|
}, context);
|
|
2804
|
+
approvals = new ToolApprovals(harness, context);
|
|
2238
2805
|
const existing = await scanConversations();
|
|
2239
2806
|
try {
|
|
2807
|
+
await prepareApprovalConversations(harness, existing.map((r) => r.id), approvalExtension, context);
|
|
2240
2808
|
for (const record of existing) {
|
|
2241
2809
|
const target = await harness.conversation(record.id, context);
|
|
2242
2810
|
if (target) {
|
|
@@ -2287,7 +2855,7 @@ async function createNekoRuntime(options = {}) {
|
|
|
2287
2855
|
if (existing.length === 0)
|
|
2288
2856
|
await metadata.initialize(conversation.id);
|
|
2289
2857
|
for (const record of existing)
|
|
2290
|
-
if (!(await harness.snapshot(ArchivedDoc, record.id, context))?.archived)
|
|
2858
|
+
if (!record.owner && !(await harness.snapshot(ArchivedDoc, record.id, context))?.archived)
|
|
2291
2859
|
await metadata.restore(record.id);
|
|
2292
2860
|
let closing;
|
|
2293
2861
|
const cleanup = () => {
|
|
@@ -2389,7 +2957,7 @@ async function createNekoRuntime(options = {}) {
|
|
|
2389
2957
|
await metadata.flush();
|
|
2390
2958
|
await metadata.reconcile(target.id);
|
|
2391
2959
|
if (settled.status === "done" && settled.type === "input") {
|
|
2392
|
-
const entry = await target.commit((tx) => tx.entry(
|
|
2960
|
+
const entry = await target.commit((tx) => tx.entry(AssistantEntry2, settled.answer), context);
|
|
2393
2961
|
const message = entry?.model?.[0];
|
|
2394
2962
|
const text = message?.role === "assistant" ? message.content.flatMap((block) => block.type === "text" ? [block.text] : []).join("") : "";
|
|
2395
2963
|
return { status: "done", text };
|
|
@@ -2454,7 +3022,8 @@ async function createNekoRuntime(options = {}) {
|
|
|
2454
3022
|
if (target === undefined)
|
|
2455
3023
|
return;
|
|
2456
3024
|
const archived = (await harness.snapshot(ArchivedDoc, id, context))?.archived ?? false;
|
|
2457
|
-
|
|
3025
|
+
const record = await harness.commit((tx) => tx.conversation(id), context);
|
|
3026
|
+
return archived || record?.owner ? undefined : makeSession(target);
|
|
2458
3027
|
};
|
|
2459
3028
|
return {
|
|
2460
3029
|
context,
|
|
@@ -2485,24 +3054,60 @@ async function createNekoRuntime(options = {}) {
|
|
|
2485
3054
|
const newest = await source.conversation.entries({}, 1, undefined, context);
|
|
2486
3055
|
const at = newest.items[0]?.id;
|
|
2487
3056
|
const sourceMetadata = at === undefined ? undefined : await harness.snapshot(SessionMetadataDoc, id, context);
|
|
2488
|
-
const
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
3057
|
+
const childId = await harness.commit(async (tx) => {
|
|
3058
|
+
const inherited = [];
|
|
3059
|
+
if (at !== undefined) {
|
|
3060
|
+
const references = await refreshDelegations(tx, id);
|
|
3061
|
+
const calls = new Set;
|
|
3062
|
+
let cursor;
|
|
3063
|
+
do {
|
|
3064
|
+
const page = await tx.scanEntries({ conversationId: id, maxEntryId: at }, 100, cursor);
|
|
3065
|
+
for (const entry of page.items)
|
|
3066
|
+
for (const message of entry.model ?? []) {
|
|
3067
|
+
if (message.role === "assistant")
|
|
3068
|
+
for (const block of message.content) {
|
|
3069
|
+
if (block.type === "toolCall")
|
|
3070
|
+
calls.add(block.id);
|
|
3071
|
+
}
|
|
3072
|
+
}
|
|
3073
|
+
cursor = page.next;
|
|
3074
|
+
} while (cursor);
|
|
3075
|
+
for (const reference of references) {
|
|
3076
|
+
if (!calls.has(reference.callId))
|
|
3077
|
+
continue;
|
|
3078
|
+
const latest = await tx.scanEntries({ conversationId: reference.childId }, 1);
|
|
3079
|
+
const usage = totalUsage(await tx.doc(UsageDoc, reference.childId));
|
|
3080
|
+
inherited.push({
|
|
3081
|
+
...reference,
|
|
3082
|
+
historyThrough: reference.historyThrough ?? latest.items[0]?.id ?? 0,
|
|
3083
|
+
historyUsage: reference.historyUsage ?? {
|
|
3084
|
+
input: usage.input,
|
|
3085
|
+
output: usage.output,
|
|
3086
|
+
cacheRead: usage.cacheRead,
|
|
3087
|
+
cacheWrite: usage.cacheWrite,
|
|
3088
|
+
cost: usage.cost.total
|
|
3089
|
+
}
|
|
3090
|
+
});
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
3093
|
+
const child = at === undefined ? await tx.createConversation({ ownership: { kind: "ownerless" } }) : await tx.forkConversation(id, at, { ownership: { kind: "ownerless" } });
|
|
3094
|
+
(await tx.doc(DelegationsDoc, child.id)).items = inherited;
|
|
3095
|
+
if (sourceMetadata) {
|
|
3096
|
+
const doc = await tx.doc(SessionMetadataDoc, child.id);
|
|
2495
3097
|
doc.title = sourceMetadata.title || sourceMetadata.firstPromptPreview;
|
|
2496
3098
|
doc.firstPromptPreview = sourceMetadata.firstPromptPreview;
|
|
2497
3099
|
doc.naming = "ready";
|
|
2498
3100
|
}
|
|
3101
|
+
return child.id;
|
|
2499
3102
|
}, context);
|
|
3103
|
+
const child = await harness.conversation(childId, context);
|
|
2500
3104
|
await child.configure({ model: agent.model, thinkingLevel: agent.thinkingLevel, cwd: savedWorkspace(agent.cwd) }, context);
|
|
2501
3105
|
await metadata.initialize(child.id);
|
|
2502
3106
|
await touchConversation(child.id);
|
|
2503
3107
|
return makeSession(child);
|
|
2504
3108
|
},
|
|
2505
3109
|
harness,
|
|
3110
|
+
approvals,
|
|
2506
3111
|
closed: harness.closed,
|
|
2507
3112
|
workspaceFiles,
|
|
2508
3113
|
conversation,
|
|
@@ -2528,6 +3133,8 @@ async function createNekoRuntime(options = {}) {
|
|
|
2528
3133
|
const records = await scanConversations();
|
|
2529
3134
|
const infos = [];
|
|
2530
3135
|
for (const record of records) {
|
|
3136
|
+
if (record.owner)
|
|
3137
|
+
continue;
|
|
2531
3138
|
const archived = (await harness.snapshot(ArchivedDoc, record.id, context))?.archived ?? false;
|
|
2532
3139
|
if (!archived) {
|
|
2533
3140
|
const target = await harness.conversation(record.id, context);
|
|
@@ -2566,6 +3173,7 @@ async function createNekoRuntime(options = {}) {
|
|
|
2566
3173
|
},
|
|
2567
3174
|
getConversation,
|
|
2568
3175
|
async archiveConversation(id) {
|
|
3176
|
+
await (await sessionFor(id)).abort();
|
|
2569
3177
|
await metadata.archive(id);
|
|
2570
3178
|
await harness.commit(async (tx) => {
|
|
2571
3179
|
(await tx.doc(ArchivedDoc, id)).archived = true;
|
|
@@ -2690,13 +3298,13 @@ async function removeGatewayPortFile(nekoDir, serverId) {
|
|
|
2690
3298
|
}
|
|
2691
3299
|
|
|
2692
3300
|
// packages/gateway/src/server-services.ts
|
|
2693
|
-
import { randomUUID as
|
|
3301
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
2694
3302
|
import {
|
|
2695
|
-
ChordError as
|
|
2696
|
-
cell as
|
|
2697
|
-
hostPrincipal as
|
|
2698
|
-
listener as
|
|
2699
|
-
local as
|
|
3303
|
+
ChordError as ChordError3,
|
|
3304
|
+
cell as cell3,
|
|
3305
|
+
hostPrincipal as hostPrincipal3,
|
|
3306
|
+
listener as listener3,
|
|
3307
|
+
local as local3,
|
|
2700
3308
|
method as method2
|
|
2701
3309
|
} from "@earendil-works/chord";
|
|
2702
3310
|
|
|
@@ -2707,8 +3315,8 @@ import { dirname as dirname5, join as join7 } from "path";
|
|
|
2707
3315
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2708
3316
|
|
|
2709
3317
|
// packages/core/src/application-version.ts
|
|
2710
|
-
var RUNNING_VERSION = "0.
|
|
2711
|
-
var APPLICATION_PROTOCOL =
|
|
3318
|
+
var RUNNING_VERSION = "0.3.0";
|
|
3319
|
+
var APPLICATION_PROTOCOL = 3;
|
|
2712
3320
|
function stableVersion(value) {
|
|
2713
3321
|
return typeof value === "string" && /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(value) && value.split(".").every((n) => Number.isSafeInteger(Number(n)));
|
|
2714
3322
|
}
|
|
@@ -2857,11 +3465,11 @@ class UpdateChecker {
|
|
|
2857
3465
|
}
|
|
2858
3466
|
|
|
2859
3467
|
// packages/gateway/src/server-services.ts
|
|
2860
|
-
import { Type as
|
|
3468
|
+
import { Type as Type7 } from "typebox";
|
|
2861
3469
|
|
|
2862
3470
|
// packages/gateway/src/contract.ts
|
|
2863
3471
|
import { method, remote } from "@earendil-works/chord";
|
|
2864
|
-
import { Type as
|
|
3472
|
+
import { Type as Type4 } from "typebox";
|
|
2865
3473
|
var rpc = (args, result) => [args, result];
|
|
2866
3474
|
function contract(name, def) {
|
|
2867
3475
|
const variants = Object.fromEntries(Object.entries(def.variants).map(([key, value]) => [
|
|
@@ -2873,19 +3481,19 @@ function contract(name, def) {
|
|
|
2873
3481
|
]));
|
|
2874
3482
|
return remote(name, { ...def, variants });
|
|
2875
3483
|
}
|
|
2876
|
-
var str =
|
|
2877
|
-
var num =
|
|
2878
|
-
var bool =
|
|
2879
|
-
var nil =
|
|
2880
|
-
var integer =
|
|
2881
|
-
var nullableString =
|
|
2882
|
-
var phase =
|
|
2883
|
-
var level =
|
|
2884
|
-
var thinking =
|
|
2885
|
-
var model =
|
|
2886
|
-
var change =
|
|
2887
|
-
var usage =
|
|
2888
|
-
var image =
|
|
3484
|
+
var str = Type4.String();
|
|
3485
|
+
var num = Type4.Number();
|
|
3486
|
+
var bool = Type4.Boolean();
|
|
3487
|
+
var nil = Type4.Null();
|
|
3488
|
+
var integer = Type4.Integer({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER });
|
|
3489
|
+
var nullableString = Type4.Union([str, nil]);
|
|
3490
|
+
var phase = Type4.Enum(["pending", "running", "done"]);
|
|
3491
|
+
var level = Type4.Enum(["project", "user", "builtin"]);
|
|
3492
|
+
var thinking = Type4.Enum(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
|
|
3493
|
+
var model = Type4.Object({ provider: str, modelId: str });
|
|
3494
|
+
var change = Type4.Object({ model: Type4.Optional(model), thinkingLevel: Type4.Optional(thinking) }, { additionalProperties: false });
|
|
3495
|
+
var usage = Type4.Object({ input: num, output: num, cacheRead: num, cacheWrite: num, cost: num });
|
|
3496
|
+
var image = Type4.Object({
|
|
2889
3497
|
name: str,
|
|
2890
3498
|
mimeType: str,
|
|
2891
3499
|
width: integer,
|
|
@@ -2894,8 +3502,8 @@ var image = Type2.Object({
|
|
|
2894
3502
|
entryId: integer,
|
|
2895
3503
|
contentIndex: integer
|
|
2896
3504
|
});
|
|
2897
|
-
var skill =
|
|
2898
|
-
var call =
|
|
3505
|
+
var skill = Type4.Object({ name: str, location: str, body: str, args: str });
|
|
3506
|
+
var call = Type4.Object({
|
|
2899
3507
|
callId: str,
|
|
2900
3508
|
name: str,
|
|
2901
3509
|
summary: str,
|
|
@@ -2903,44 +3511,65 @@ var call = Type2.Object({
|
|
|
2903
3511
|
phase,
|
|
2904
3512
|
output: nullableString
|
|
2905
3513
|
});
|
|
2906
|
-
var
|
|
3514
|
+
var DelegationSchema = Type4.Object({
|
|
3515
|
+
parentId: integer,
|
|
3516
|
+
taskId: integer,
|
|
3517
|
+
callId: str,
|
|
3518
|
+
childId: integer,
|
|
3519
|
+
role: Type4.Enum(["explore", "plan", "coder"]),
|
|
3520
|
+
title: str,
|
|
3521
|
+
createdAt: num,
|
|
3522
|
+
startedAt: Type4.Union([num, nil]),
|
|
3523
|
+
endedAt: Type4.Union([num, nil]),
|
|
3524
|
+
status: Type4.Enum(["queued", "running", "completed", "failed", "cancelled"]),
|
|
3525
|
+
report: nullableString,
|
|
3526
|
+
approvalCount: Type4.Optional(integer),
|
|
3527
|
+
historyThrough: Type4.Optional(integer),
|
|
3528
|
+
historyUsage: Type4.Optional(usage)
|
|
3529
|
+
});
|
|
3530
|
+
var ChatViewSchema = Type4.Object({
|
|
3531
|
+
historyBefore: Type4.Optional(Type4.Union([integer, nil])),
|
|
3532
|
+
subagents: Type4.Optional(Type4.Array(DelegationSchema)),
|
|
3533
|
+
taskUsage: Type4.Optional(usage),
|
|
2907
3534
|
model: str,
|
|
2908
|
-
agent:
|
|
3535
|
+
agent: Type4.Object({ model: Type4.Union([model, nil]), thinkingLevel: Type4.Union([thinking, nil]) }),
|
|
2909
3536
|
cwd: str,
|
|
2910
3537
|
busy: bool,
|
|
2911
3538
|
status: nullableString,
|
|
2912
3539
|
usage,
|
|
2913
|
-
contextUsage:
|
|
2914
|
-
compaction:
|
|
2915
|
-
failure:
|
|
2916
|
-
active:
|
|
3540
|
+
contextUsage: Type4.Optional(Type4.Object({ tokens: Type4.Union([num, nil]), window: num, percent: Type4.Union([num, nil]) })),
|
|
3541
|
+
compaction: Type4.Optional(Type4.Object({
|
|
3542
|
+
failure: Type4.Optional(Type4.Object({ taskId: integer, reason: Type4.Enum(["manual", "threshold", "overflow"]), error: str })),
|
|
3543
|
+
active: Type4.Array(Type4.Object({
|
|
2917
3544
|
taskId: integer,
|
|
2918
|
-
reason:
|
|
3545
|
+
reason: Type4.Enum(["manual", "threshold", "overflow"]),
|
|
2919
3546
|
blocking: bool,
|
|
2920
3547
|
attempt: integer,
|
|
2921
|
-
retry:
|
|
3548
|
+
retry: Type4.Optional(Type4.Object({ at: num, error: str }))
|
|
2922
3549
|
})),
|
|
2923
|
-
latest:
|
|
3550
|
+
latest: Type4.Union([
|
|
2924
3551
|
nil,
|
|
2925
|
-
|
|
3552
|
+
Type4.Object({
|
|
2926
3553
|
taskId: integer,
|
|
2927
|
-
phase:
|
|
3554
|
+
phase: Type4.Enum(["summarizing", "waiting-to-apply", "applied", "nothing", "skipped", "cancelled", "failed"]),
|
|
2928
3555
|
at: num
|
|
2929
3556
|
})
|
|
2930
3557
|
])
|
|
2931
3558
|
})),
|
|
2932
|
-
items:
|
|
2933
|
-
|
|
2934
|
-
type:
|
|
3559
|
+
items: Type4.Array(Type4.Union([
|
|
3560
|
+
Type4.Object({
|
|
3561
|
+
type: Type4.Literal("user"),
|
|
3562
|
+
order: Type4.Optional(num),
|
|
2935
3563
|
key: str,
|
|
2936
3564
|
id: integer,
|
|
2937
3565
|
text: str,
|
|
2938
|
-
images:
|
|
2939
|
-
skill:
|
|
2940
|
-
defaultImagePrompt:
|
|
3566
|
+
images: Type4.Optional(Type4.Array(image)),
|
|
3567
|
+
skill: Type4.Optional(skill),
|
|
3568
|
+
defaultImagePrompt: Type4.Optional(Type4.Object({ version: Type4.Integer({ minimum: 1 }), text: str }))
|
|
2941
3569
|
}),
|
|
2942
|
-
|
|
2943
|
-
type:
|
|
3570
|
+
Type4.Object({
|
|
3571
|
+
type: Type4.Literal("assistant"),
|
|
3572
|
+
order: Type4.Optional(num),
|
|
2944
3573
|
key: str,
|
|
2945
3574
|
id: integer,
|
|
2946
3575
|
thinking: str,
|
|
@@ -2949,10 +3578,11 @@ var ChatViewSchema = Type2.Object({
|
|
|
2949
3578
|
errorMessage: nullableString,
|
|
2950
3579
|
note: nullableString
|
|
2951
3580
|
}),
|
|
2952
|
-
|
|
2953
|
-
type:
|
|
3581
|
+
Type4.Object({
|
|
3582
|
+
type: Type4.Literal("tool"),
|
|
3583
|
+
order: Type4.Optional(num),
|
|
2954
3584
|
key: str,
|
|
2955
|
-
images:
|
|
3585
|
+
images: Type4.Optional(Type4.Array(image)),
|
|
2956
3586
|
callId: str,
|
|
2957
3587
|
name: str,
|
|
2958
3588
|
summary: str,
|
|
@@ -2961,50 +3591,50 @@ var ChatViewSchema = Type2.Object({
|
|
|
2961
3591
|
isError: bool,
|
|
2962
3592
|
output: nullableString,
|
|
2963
3593
|
result: nullableString,
|
|
2964
|
-
durationMs:
|
|
3594
|
+
durationMs: Type4.Union([num, nil]),
|
|
2965
3595
|
note: nullableString
|
|
2966
3596
|
}),
|
|
2967
|
-
|
|
3597
|
+
Type4.Object({ type: Type4.Literal("system"), order: Type4.Optional(num), key: str, id: integer, text: str })
|
|
2968
3598
|
])),
|
|
2969
|
-
streaming:
|
|
3599
|
+
streaming: Type4.Union([nil, Type4.Object({ thinking: str, text: str, toolCalls: Type4.Array(call) })])
|
|
2970
3600
|
});
|
|
2971
|
-
var SessionSummarySchema =
|
|
3601
|
+
var SessionSummarySchema = Type4.Object({
|
|
2972
3602
|
workspaceRoot: str,
|
|
2973
3603
|
lastUsedAt: num,
|
|
2974
3604
|
title: str,
|
|
2975
3605
|
firstPromptPreview: str,
|
|
2976
3606
|
createdAt: num,
|
|
2977
|
-
lastMessageAt:
|
|
2978
|
-
model:
|
|
2979
|
-
status:
|
|
3607
|
+
lastMessageAt: Type4.Union([num, nil]),
|
|
3608
|
+
model: Type4.Union([model, nil]),
|
|
3609
|
+
status: Type4.Enum(["idle", "running", "error"]),
|
|
2980
3610
|
id: str
|
|
2981
3611
|
});
|
|
2982
|
-
var DirectoryStateSchema =
|
|
2983
|
-
var WorkspaceSummarySchema =
|
|
2984
|
-
var WorkspaceStateSchema =
|
|
2985
|
-
var credentialStatus =
|
|
3612
|
+
var DirectoryStateSchema = Type4.Object({ revision: integer, sessions: Type4.Array(SessionSummarySchema) });
|
|
3613
|
+
var WorkspaceSummarySchema = Type4.Object({ path: str, createdAt: num });
|
|
3614
|
+
var WorkspaceStateSchema = Type4.Object({ revision: integer, workspaces: Type4.Array(WorkspaceSummarySchema) });
|
|
3615
|
+
var credentialStatus = Type4.Object({
|
|
2986
3616
|
savedConfigured: bool,
|
|
2987
3617
|
environmentConfigured: bool,
|
|
2988
|
-
source:
|
|
3618
|
+
source: Type4.Enum(["saved", "environment", "runtime", "keyless", "none"])
|
|
2989
3619
|
});
|
|
2990
|
-
var CredentialStateSchema =
|
|
3620
|
+
var CredentialStateSchema = Type4.Object({
|
|
2991
3621
|
revision: integer,
|
|
2992
|
-
statuses:
|
|
3622
|
+
statuses: Type4.Object({ deepseek: credentialStatus, tavily: credentialStatus })
|
|
2993
3623
|
});
|
|
2994
|
-
var AttachmentSchema =
|
|
2995
|
-
var chatInput =
|
|
3624
|
+
var AttachmentSchema = Type4.Object({ sessionId: str, attachmentId: str });
|
|
3625
|
+
var chatInput = Type4.Object({ sessionId: str, attachmentId: str }, { additionalProperties: false });
|
|
2996
3626
|
var NekoSessionDirectory = contract("neko.session-directory", {
|
|
2997
3627
|
version: 1,
|
|
2998
|
-
input:
|
|
3628
|
+
input: Type4.Object({}, { additionalProperties: false }),
|
|
2999
3629
|
variants: { ready: { state: { state: DirectoryStateSchema } } }
|
|
3000
3630
|
});
|
|
3001
3631
|
var NekoSessionManagement = contract("neko.session-management", {
|
|
3002
3632
|
version: 1,
|
|
3003
|
-
input:
|
|
3633
|
+
input: Type4.Object({}, { additionalProperties: false }),
|
|
3004
3634
|
variants: {
|
|
3005
3635
|
ready: {
|
|
3006
3636
|
methods: {
|
|
3007
|
-
create: rpc(
|
|
3637
|
+
create: rpc(Type4.Object({ ...change.properties, cwd: Type4.Optional(str) }, { additionalProperties: false }), SessionSummarySchema),
|
|
3008
3638
|
archive: rpc(str, nil),
|
|
3009
3639
|
attach: rpc(str, AttachmentSchema),
|
|
3010
3640
|
detach: rpc(nil, nil),
|
|
@@ -3013,30 +3643,30 @@ var NekoSessionManagement = contract("neko.session-management", {
|
|
|
3013
3643
|
}
|
|
3014
3644
|
}
|
|
3015
3645
|
});
|
|
3016
|
-
var resize =
|
|
3017
|
-
maxWidth:
|
|
3018
|
-
maxHeight:
|
|
3019
|
-
maxBytes:
|
|
3020
|
-
jpegQuality:
|
|
3646
|
+
var resize = Type4.Object({
|
|
3647
|
+
maxWidth: Type4.Optional(integer),
|
|
3648
|
+
maxHeight: Type4.Optional(integer),
|
|
3649
|
+
maxBytes: Type4.Optional(integer),
|
|
3650
|
+
jpegQuality: Type4.Optional(integer)
|
|
3021
3651
|
});
|
|
3022
|
-
var inputLimits =
|
|
3023
|
-
maxRequestBytes:
|
|
3024
|
-
images:
|
|
3025
|
-
resize:
|
|
3026
|
-
maxPerMessage:
|
|
3027
|
-
maxPerRequest:
|
|
3652
|
+
var inputLimits = Type4.Object({
|
|
3653
|
+
maxRequestBytes: Type4.Optional(integer),
|
|
3654
|
+
images: Type4.Optional(Type4.Object({
|
|
3655
|
+
resize: Type4.Optional(resize),
|
|
3656
|
+
maxPerMessage: Type4.Optional(integer),
|
|
3657
|
+
maxPerRequest: Type4.Optional(integer)
|
|
3028
3658
|
}))
|
|
3029
3659
|
});
|
|
3030
|
-
var modelInfo =
|
|
3660
|
+
var modelInfo = Type4.Object({
|
|
3031
3661
|
provider: str,
|
|
3032
3662
|
modelId: str,
|
|
3033
3663
|
name: str,
|
|
3034
3664
|
contextWindow: num,
|
|
3035
|
-
input:
|
|
3036
|
-
inputLimits:
|
|
3665
|
+
input: Type4.Array(str),
|
|
3666
|
+
inputLimits: Type4.Optional(inputLimits),
|
|
3037
3667
|
reasoning: bool,
|
|
3038
|
-
supportedThinkingLevels:
|
|
3039
|
-
thinkingLevelAdjustments:
|
|
3668
|
+
supportedThinkingLevels: Type4.Array(thinking),
|
|
3669
|
+
thinkingLevelAdjustments: Type4.Object({
|
|
3040
3670
|
off: thinking,
|
|
3041
3671
|
minimal: thinking,
|
|
3042
3672
|
low: thinking,
|
|
@@ -3048,10 +3678,30 @@ var modelInfo = Type2.Object({
|
|
|
3048
3678
|
});
|
|
3049
3679
|
var NekoModels = contract("neko.models", {
|
|
3050
3680
|
version: 1,
|
|
3051
|
-
input:
|
|
3052
|
-
variants: { ready: { methods: { list: rpc(nil,
|
|
3681
|
+
input: Type4.Object({}, { additionalProperties: false }),
|
|
3682
|
+
variants: { ready: { methods: { list: rpc(nil, Type4.Array(modelInfo)) } } }
|
|
3683
|
+
});
|
|
3684
|
+
var imageChunk = Type4.Object({ data: str, total: integer, mimeType: str });
|
|
3685
|
+
var NekoSubagent = contract("neko.subagent", {
|
|
3686
|
+
version: 1,
|
|
3687
|
+
input: Type4.Object({ sessionId: str, attachmentId: str, taskId: integer }),
|
|
3688
|
+
variants: {
|
|
3689
|
+
ready: {
|
|
3690
|
+
state: { revision: integer },
|
|
3691
|
+
methods: {
|
|
3692
|
+
chunk: rpc(Type4.Object({
|
|
3693
|
+
kind: Type4.Enum(["view", "history"]),
|
|
3694
|
+
before: Type4.Optional(integer),
|
|
3695
|
+
token: Type4.Optional(integer),
|
|
3696
|
+
offset: integer
|
|
3697
|
+
}), Type4.Object({ token: integer, data: str, next: Type4.Union([integer, nil]) })),
|
|
3698
|
+
getImage: rpc(Type4.Object({ entryId: integer, contentIndex: integer, offset: integer }), imageChunk),
|
|
3699
|
+
decideApproval: rpc(Type4.Object({ id: integer, version: str, decision: Type4.Enum(["allowed", "denied"]) }), Type4.Enum(["pending", "allowed", "denied", "cancelled"])),
|
|
3700
|
+
stop: rpc(nil, nil)
|
|
3701
|
+
}
|
|
3702
|
+
}
|
|
3703
|
+
}
|
|
3053
3704
|
});
|
|
3054
|
-
var imageChunk = Type2.Object({ data: str, total: integer, mimeType: str });
|
|
3055
3705
|
var NekoChat = contract("neko.chat", {
|
|
3056
3706
|
version: 1,
|
|
3057
3707
|
input: chatInput,
|
|
@@ -3059,79 +3709,79 @@ var NekoChat = contract("neko.chat", {
|
|
|
3059
3709
|
ready: {
|
|
3060
3710
|
state: { view: ChatViewSchema },
|
|
3061
3711
|
methods: {
|
|
3062
|
-
submit: rpc(
|
|
3712
|
+
submit: rpc(Type4.Object({
|
|
3063
3713
|
content: str,
|
|
3064
|
-
images:
|
|
3065
|
-
requestId:
|
|
3066
|
-
whenBusy:
|
|
3714
|
+
images: Type4.Optional(Type4.Array(str, { maxItems: 4 })),
|
|
3715
|
+
requestId: Type4.Optional(str),
|
|
3716
|
+
whenBusy: Type4.Optional(Type4.Enum(["steer", "followUp"]))
|
|
3067
3717
|
}, { additionalProperties: false }), nil),
|
|
3068
3718
|
abort: rpc(nil, nil),
|
|
3069
3719
|
usage: rpc(nil, usage),
|
|
3070
|
-
skills: rpc(nil,
|
|
3720
|
+
skills: rpc(nil, Type4.Array(Type4.Object({ name: str, description: str, location: str, level }))),
|
|
3071
3721
|
configure: rpc(change, nil),
|
|
3072
|
-
compact: rpc(
|
|
3073
|
-
importImage: rpc(str,
|
|
3074
|
-
draftImage: rpc(
|
|
3075
|
-
beginImage: rpc(
|
|
3076
|
-
appendImage: rpc(
|
|
3722
|
+
compact: rpc(Type4.Object({ instructions: Type4.Optional(str) }, { additionalProperties: false }), nil),
|
|
3723
|
+
importImage: rpc(str, Type4.Object({ id: str, name: str })),
|
|
3724
|
+
draftImage: rpc(Type4.Object({ id: str, offset: integer }, { additionalProperties: false }), imageChunk),
|
|
3725
|
+
beginImage: rpc(Type4.Object({ name: str, mimeType: str, size: Type4.Integer({ minimum: 1, maximum: 20971520 }) }, { additionalProperties: false }), str),
|
|
3726
|
+
appendImage: rpc(Type4.Object({ id: str, offset: integer, data: Type4.String({ maxLength: 350000 }) }, { additionalProperties: false }), nil),
|
|
3077
3727
|
finishImage: rpc(str, nil),
|
|
3078
3728
|
releaseImage: rpc(str, nil),
|
|
3079
|
-
getImage: rpc(
|
|
3729
|
+
getImage: rpc(Type4.Object({ entryId: integer, contentIndex: integer, offset: integer }, { additionalProperties: false }), imageChunk)
|
|
3080
3730
|
}
|
|
3081
3731
|
}
|
|
3082
3732
|
}
|
|
3083
3733
|
});
|
|
3084
|
-
var browse =
|
|
3085
|
-
var page =
|
|
3734
|
+
var browse = Type4.Object({ path: str, query: Type4.Optional(str), showHidden: Type4.Optional(bool), cursor: Type4.Optional(str) }, { additionalProperties: false });
|
|
3735
|
+
var page = Type4.Object({
|
|
3086
3736
|
path: str,
|
|
3087
3737
|
parent: nullableString,
|
|
3088
|
-
entries:
|
|
3738
|
+
entries: Type4.Array(Type4.Object({ name: str, path: str, error: Type4.Optional(str) })),
|
|
3089
3739
|
next: nullableString
|
|
3090
3740
|
});
|
|
3091
3741
|
var NekoWorkspaces = contract("neko.workspaces", {
|
|
3092
3742
|
version: 1,
|
|
3093
|
-
input:
|
|
3743
|
+
input: Type4.Object({}, { additionalProperties: false }),
|
|
3094
3744
|
variants: {
|
|
3095
3745
|
ready: {
|
|
3096
3746
|
state: { state: WorkspaceStateSchema },
|
|
3097
3747
|
methods: {
|
|
3098
|
-
add: rpc(
|
|
3748
|
+
add: rpc(Type4.Object({ path: str }, { additionalProperties: false }), WorkspaceSummarySchema),
|
|
3099
3749
|
browse: rpc(browse, page)
|
|
3100
3750
|
}
|
|
3101
3751
|
}
|
|
3102
3752
|
}
|
|
3103
3753
|
});
|
|
3104
|
-
var provider =
|
|
3754
|
+
var provider = Type4.Enum(["deepseek", "tavily"]);
|
|
3105
3755
|
var NekoCredentials = contract("neko.credentials", {
|
|
3106
3756
|
version: 1,
|
|
3107
|
-
input:
|
|
3757
|
+
input: Type4.Object({}, { additionalProperties: false }),
|
|
3108
3758
|
variants: {
|
|
3109
3759
|
ready: {
|
|
3110
3760
|
state: { state: CredentialStateSchema },
|
|
3111
3761
|
methods: {
|
|
3112
|
-
save: rpc(
|
|
3113
|
-
remove: rpc(
|
|
3762
|
+
save: rpc(Type4.Object({ provider, key: Type4.String({ minLength: 1, maxLength: 8192 }) }, { additionalProperties: false }), nil),
|
|
3763
|
+
remove: rpc(Type4.Object({ provider }, { additionalProperties: false }), nil)
|
|
3114
3764
|
}
|
|
3115
3765
|
}
|
|
3116
3766
|
}
|
|
3117
3767
|
});
|
|
3118
|
-
var FileCandidatesSchema =
|
|
3119
|
-
entries:
|
|
3768
|
+
var FileCandidatesSchema = Type4.Object({
|
|
3769
|
+
entries: Type4.Array(Type4.Object({ name: str, path: str, kind: Type4.Enum(["file", "directory"]) }), { maxItems: 100 }),
|
|
3120
3770
|
limited: bool,
|
|
3121
3771
|
incomplete: bool
|
|
3122
3772
|
});
|
|
3123
3773
|
var NekoWorkspaceFiles = contract("neko.workspace-files", {
|
|
3124
3774
|
version: 1,
|
|
3125
|
-
input:
|
|
3775
|
+
input: Type4.Object({}, { additionalProperties: false }),
|
|
3126
3776
|
variants: {
|
|
3127
3777
|
ready: {
|
|
3128
3778
|
methods: {
|
|
3129
|
-
search: rpc(
|
|
3779
|
+
search: rpc(Type4.Object({ workspaceRoot: str, query: str }, { additionalProperties: false }), FileCandidatesSchema)
|
|
3130
3780
|
}
|
|
3131
3781
|
}
|
|
3132
3782
|
}
|
|
3133
3783
|
});
|
|
3134
|
-
var marketSkill =
|
|
3784
|
+
var marketSkill = Type4.Object({
|
|
3135
3785
|
name: str,
|
|
3136
3786
|
description: str,
|
|
3137
3787
|
category: str,
|
|
@@ -3141,38 +3791,38 @@ var marketSkill = Type2.Object({
|
|
|
3141
3791
|
sha256: str,
|
|
3142
3792
|
sizeBytes: integer,
|
|
3143
3793
|
uploadedAt: str,
|
|
3144
|
-
installed:
|
|
3145
|
-
activeLevel:
|
|
3794
|
+
installed: Type4.Object({ project: bool, user: bool, builtin: bool }),
|
|
3795
|
+
activeLevel: Type4.Union([level, nil])
|
|
3146
3796
|
});
|
|
3147
|
-
var preview =
|
|
3148
|
-
var scope =
|
|
3149
|
-
var installResult =
|
|
3797
|
+
var preview = Type4.Object({ body: str, files: Type4.Array(str) });
|
|
3798
|
+
var scope = Type4.Enum(["user", "project"]);
|
|
3799
|
+
var installResult = Type4.Object({ name: str, scope, path: str, shadowedBy: Type4.Optional(scope) });
|
|
3150
3800
|
var NekoSkillMarket = contract("neko.skill-market", {
|
|
3151
3801
|
version: 1,
|
|
3152
|
-
input:
|
|
3802
|
+
input: Type4.Object({}, { additionalProperties: false }),
|
|
3153
3803
|
variants: {
|
|
3154
3804
|
ready: {
|
|
3155
3805
|
methods: {
|
|
3156
|
-
list: rpc(
|
|
3157
|
-
detail: rpc(
|
|
3158
|
-
preview: rpc(
|
|
3159
|
-
install: rpc(
|
|
3160
|
-
uninstall: rpc(
|
|
3806
|
+
list: rpc(Type4.Object({ query: Type4.Optional(str), workspaceRoot: Type4.Optional(str) }, { additionalProperties: false }), Type4.Array(marketSkill)),
|
|
3807
|
+
detail: rpc(Type4.Object({ name: str, workspaceRoot: Type4.Optional(str) }, { additionalProperties: false }), marketSkill),
|
|
3808
|
+
preview: rpc(Type4.Object({ name: str }, { additionalProperties: false }), preview),
|
|
3809
|
+
install: rpc(Type4.Object({ name: str, scope, workspaceRoot: Type4.Optional(str), overwrite: Type4.Optional(bool) }, { additionalProperties: false }), installResult),
|
|
3810
|
+
uninstall: rpc(Type4.Object({ name: str, scope, workspaceRoot: Type4.Optional(str) }, { additionalProperties: false }), nil)
|
|
3161
3811
|
}
|
|
3162
3812
|
}
|
|
3163
3813
|
}
|
|
3164
3814
|
});
|
|
3165
|
-
var ApplicationInfoSchema =
|
|
3815
|
+
var ApplicationInfoSchema = Type4.Object({
|
|
3166
3816
|
runningVersion: str,
|
|
3167
3817
|
installedVersion: nullableString,
|
|
3168
|
-
protocolVersion:
|
|
3818
|
+
protocolVersion: Type4.Integer({ minimum: 1 }),
|
|
3169
3819
|
latestVersion: nullableString,
|
|
3170
|
-
checkedAt:
|
|
3171
|
-
checkState:
|
|
3820
|
+
checkedAt: Type4.Union([integer, nil]),
|
|
3821
|
+
checkState: Type4.Enum(["unknown", "checking", "ok", "failed", "development"])
|
|
3172
3822
|
});
|
|
3173
3823
|
var NekoApplicationInfo = contract("neko.application-info", {
|
|
3174
3824
|
version: 1,
|
|
3175
|
-
input:
|
|
3825
|
+
input: Type4.Object({}),
|
|
3176
3826
|
variants: { ready: { state: { info: ApplicationInfoSchema }, methods: { check: rpc(nil, ApplicationInfoSchema) } } }
|
|
3177
3827
|
});
|
|
3178
3828
|
|
|
@@ -3180,16 +3830,17 @@ var NekoApplicationInfo = contract("neko.application-info", {
|
|
|
3180
3830
|
import { open as open2 } from "fs/promises";
|
|
3181
3831
|
import { basename as basename3, extname } from "path";
|
|
3182
3832
|
import {
|
|
3183
|
-
ChordError,
|
|
3184
|
-
cell,
|
|
3185
|
-
hostPrincipal,
|
|
3186
|
-
listener,
|
|
3187
|
-
local,
|
|
3188
|
-
opaque,
|
|
3189
|
-
resource
|
|
3833
|
+
ChordError as ChordError2,
|
|
3834
|
+
cell as cell2,
|
|
3835
|
+
hostPrincipal as hostPrincipal2,
|
|
3836
|
+
listener as listener2,
|
|
3837
|
+
local as local2,
|
|
3838
|
+
opaque as opaque2,
|
|
3839
|
+
resource as resource2
|
|
3190
3840
|
} from "@earendil-works/chord";
|
|
3191
|
-
import { withAbortSignal as
|
|
3192
|
-
import {
|
|
3841
|
+
import { withAbortSignal as withAbortSignal3 } from "@earendil-works/chord/context";
|
|
3842
|
+
import { UsageDoc as UsageDoc2 } from "@earendil-works/pi-durable";
|
|
3843
|
+
import { Type as Type6 } from "typebox";
|
|
3193
3844
|
|
|
3194
3845
|
// packages/gateway/src/image-uploads.ts
|
|
3195
3846
|
var IMAGE_CHUNK_BYTES = 256 * 1024;
|
|
@@ -3285,19 +3936,241 @@ class ImageUploads {
|
|
|
3285
3936
|
}
|
|
3286
3937
|
}
|
|
3287
3938
|
|
|
3939
|
+
// packages/gateway/src/subagent-host.ts
|
|
3940
|
+
import {
|
|
3941
|
+
ChordError,
|
|
3942
|
+
cell,
|
|
3943
|
+
hostPrincipal,
|
|
3944
|
+
listener,
|
|
3945
|
+
local,
|
|
3946
|
+
opaque,
|
|
3947
|
+
resource
|
|
3948
|
+
} from "@earendil-works/chord";
|
|
3949
|
+
import { Type as Type5 } from "typebox";
|
|
3950
|
+
async function openReader(runtime, childId, parentId, context, through, historyUsage) {
|
|
3951
|
+
const child = await runtime.harness.conversation(childId, context);
|
|
3952
|
+
if (!child)
|
|
3953
|
+
throw new ChordError("gone", "Subagent unavailable");
|
|
3954
|
+
const native = await child.viewState(context);
|
|
3955
|
+
if (through !== undefined)
|
|
3956
|
+
native.dispose();
|
|
3957
|
+
const project = (entries, streaming = true) => {
|
|
3958
|
+
const result = projectChatView({ ...native.value, entries: [...entries].sort((a, b) => a.id - b.id) }, "", {
|
|
3959
|
+
models: runtime.listModels(),
|
|
3960
|
+
contextEntries: native.value.entries
|
|
3961
|
+
});
|
|
3962
|
+
if (!streaming || through !== undefined) {
|
|
3963
|
+
result.streaming = null;
|
|
3964
|
+
result.busy = false;
|
|
3965
|
+
result.status = null;
|
|
3966
|
+
if (through !== undefined) {
|
|
3967
|
+
result.usage = historyUsage ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
|
|
3968
|
+
delete result.contextUsage;
|
|
3969
|
+
}
|
|
3970
|
+
}
|
|
3971
|
+
let order = 0;
|
|
3972
|
+
for (const item of result.items) {
|
|
3973
|
+
if (item.type !== "tool")
|
|
3974
|
+
order = item.id;
|
|
3975
|
+
item.order = order;
|
|
3976
|
+
}
|
|
3977
|
+
return result;
|
|
3978
|
+
};
|
|
3979
|
+
const pageAt = async (before) => {
|
|
3980
|
+
const max = Math.min(before === undefined ? Infinity : before - 1, through ?? Infinity);
|
|
3981
|
+
const page = await child.entries(Number.isFinite(max) ? { maxEntryId: max } : {}, 100, undefined, context);
|
|
3982
|
+
const entries = [...page.items];
|
|
3983
|
+
while (page.next && !["assistant", "user"].includes(entries.at(-1)?.model?.[0]?.role ?? "")) {
|
|
3984
|
+
const extra = await child.entries({ maxEntryId: entries.at(-1).id - 1 }, 1, undefined, context);
|
|
3985
|
+
entries.push(...extra.items);
|
|
3986
|
+
if (!extra.items.length || !extra.next)
|
|
3987
|
+
break;
|
|
3988
|
+
}
|
|
3989
|
+
const first = entries.at(-1)?.id;
|
|
3990
|
+
const older = first === undefined ? [] : (await child.entries({ maxEntryId: first - 1 }, 1, undefined, context)).items;
|
|
3991
|
+
return { entries, next: older.length ? first : null };
|
|
3992
|
+
};
|
|
3993
|
+
let revision = 0;
|
|
3994
|
+
let sequence = 0;
|
|
3995
|
+
const payloads = new Map;
|
|
3996
|
+
const remember = (value) => {
|
|
3997
|
+
const token = ++sequence;
|
|
3998
|
+
payloads.set(token, JSON.stringify(value));
|
|
3999
|
+
while (payloads.size > 4) {
|
|
4000
|
+
const oldest = [...payloads.keys()].find((key) => key !== revision);
|
|
4001
|
+
payloads.delete(oldest);
|
|
4002
|
+
}
|
|
4003
|
+
return token;
|
|
4004
|
+
};
|
|
4005
|
+
return {
|
|
4006
|
+
subscribe: (fn) => {
|
|
4007
|
+
if (through !== undefined)
|
|
4008
|
+
return () => {};
|
|
4009
|
+
const stop = native.subscribe(() => fn(null));
|
|
4010
|
+
const stopApprovals = runtime.harness.subscribeCommits(({ changes }) => {
|
|
4011
|
+
if (changes.some((c) => c.type === "document" && c.record.kind === "app.neko.approvals" && c.conversationId === parentId))
|
|
4012
|
+
fn(null);
|
|
4013
|
+
});
|
|
4014
|
+
return () => {
|
|
4015
|
+
stop();
|
|
4016
|
+
stopApprovals();
|
|
4017
|
+
};
|
|
4018
|
+
},
|
|
4019
|
+
async refresh() {
|
|
4020
|
+
const page = await pageAt();
|
|
4021
|
+
const approvals = through === undefined ? (await runtime.approvals.list(parentId, context)).filter((a) => a.childId === childId) : [];
|
|
4022
|
+
revision = remember({ ...project(page.entries), approvals, historyBefore: page.next });
|
|
4023
|
+
return revision;
|
|
4024
|
+
},
|
|
4025
|
+
async chunk(input) {
|
|
4026
|
+
let token = input.token;
|
|
4027
|
+
if (token === undefined) {
|
|
4028
|
+
if (input.kind === "view")
|
|
4029
|
+
token = revision;
|
|
4030
|
+
else {
|
|
4031
|
+
const page = await pageAt(input.before);
|
|
4032
|
+
token = remember({ items: project(page.entries, false).items, next: page.next });
|
|
4033
|
+
}
|
|
4034
|
+
}
|
|
4035
|
+
const payload = payloads.get(token);
|
|
4036
|
+
if (payload === undefined)
|
|
4037
|
+
throw new ChordError("gone", "Transcript changed; reload");
|
|
4038
|
+
if (input.offset > payload.length)
|
|
4039
|
+
throw new ChordError("invalid", "Invalid transcript range");
|
|
4040
|
+
const end = Math.min(payload.length, input.offset + 32768);
|
|
4041
|
+
return { token, data: payload.slice(input.offset, end), next: end < payload.length ? end : null };
|
|
4042
|
+
},
|
|
4043
|
+
async history(before) {
|
|
4044
|
+
const page = await pageAt(before);
|
|
4045
|
+
return { items: project(page.entries, false).items, next: page.next };
|
|
4046
|
+
},
|
|
4047
|
+
async image(entryId, contentIndex, offset) {
|
|
4048
|
+
if (through !== undefined && entryId > through)
|
|
4049
|
+
throw new ChordError("forbidden", "Image outside fork history");
|
|
4050
|
+
const page = await child.entries({ minEntryId: entryId, maxEntryId: entryId }, 1, undefined, context);
|
|
4051
|
+
const content = page.items[0]?.model?.[0]?.content;
|
|
4052
|
+
const image = Array.isArray(content) ? content[contentIndex] : undefined;
|
|
4053
|
+
if (image?.type !== "image")
|
|
4054
|
+
throw new ChordError("invalid", "Image not in subagent conversation");
|
|
4055
|
+
const bytes = Buffer.from(image.data, "base64");
|
|
4056
|
+
if (offset > bytes.length)
|
|
4057
|
+
throw new ChordError("invalid", "Invalid image range");
|
|
4058
|
+
return {
|
|
4059
|
+
data: bytes.subarray(offset, offset + IMAGE_CHUNK_BYTES).toString("base64"),
|
|
4060
|
+
total: bytes.length,
|
|
4061
|
+
mimeType: image.mimeType
|
|
4062
|
+
};
|
|
4063
|
+
},
|
|
4064
|
+
close: () => native.dispose()
|
|
4065
|
+
};
|
|
4066
|
+
}
|
|
4067
|
+
var ChildProjection = local("neko.subagent-projection", {
|
|
4068
|
+
version: 1,
|
|
4069
|
+
input: Type5.Object({ parentId: Type5.Integer(), taskId: Type5.Integer() }),
|
|
4070
|
+
members: {
|
|
4071
|
+
revision: Type5.Integer(),
|
|
4072
|
+
reader: opaque("neko.subagent-reader@1")
|
|
4073
|
+
}
|
|
4074
|
+
});
|
|
4075
|
+
function installSubagentHost(host, runtime, context) {
|
|
4076
|
+
host.serve(ChildProjection, (input, _principal, self) => {
|
|
4077
|
+
const { revision } = self.state({ revision: cell(Type5.Integer(), 0) });
|
|
4078
|
+
const { reader } = self.resources({
|
|
4079
|
+
reader: resource({
|
|
4080
|
+
rev: 1,
|
|
4081
|
+
async open() {
|
|
4082
|
+
const entry = (await listDelegations(runtime.harness, input.parentId, context)).find((d) => d.taskId === input.taskId);
|
|
4083
|
+
if (!entry)
|
|
4084
|
+
throw new ChordError("gone", "Subagent unavailable");
|
|
4085
|
+
const r = await openReader(runtime, entry.childId, input.parentId, context, entry.historyThrough, entry.historyUsage);
|
|
4086
|
+
try {
|
|
4087
|
+
const initial = await r.refresh();
|
|
4088
|
+
await host.root((ctx) => revision.replace(ctx, initial));
|
|
4089
|
+
return r;
|
|
4090
|
+
} catch (error) {
|
|
4091
|
+
r.close();
|
|
4092
|
+
throw error;
|
|
4093
|
+
}
|
|
4094
|
+
},
|
|
4095
|
+
close: (r) => r.close()
|
|
4096
|
+
})
|
|
4097
|
+
});
|
|
4098
|
+
self.listen({
|
|
4099
|
+
view: listener(reader, (r) => r, async (_event, ctx) => {
|
|
4100
|
+
revision.replace(ctx, await reader.value.refresh());
|
|
4101
|
+
})
|
|
4102
|
+
});
|
|
4103
|
+
return { revision, reader: self.expose(reader) };
|
|
4104
|
+
});
|
|
4105
|
+
host.serve(NekoSubagent, (input, principal, self) => {
|
|
4106
|
+
const owner = self.get(Owner, {}, { principal });
|
|
4107
|
+
const check = async () => {
|
|
4108
|
+
if (owner.owner?.active?.attachmentId !== input.attachmentId || owner.owner.active.sessionId !== input.sessionId)
|
|
4109
|
+
throw new ChordError("forbidden", "Attachment expired");
|
|
4110
|
+
if (!await runtime.getConversation(Number(input.sessionId)))
|
|
4111
|
+
throw new ChordError("gone", "Session unavailable");
|
|
4112
|
+
const entry = (await listDelegations(runtime.harness, Number(input.sessionId), context)).find((d) => d.taskId === input.taskId);
|
|
4113
|
+
if (!entry)
|
|
4114
|
+
throw new ChordError("forbidden", "Subagent does not belong to this session");
|
|
4115
|
+
return entry;
|
|
4116
|
+
};
|
|
4117
|
+
const { target } = self.resources({ target: resource({ rev: 1, deps: [owner], open: check }) });
|
|
4118
|
+
const shared = self.get(ChildProjection, { parentId: Number(input.sessionId), taskId: input.taskId }, { principal: hostPrincipal });
|
|
4119
|
+
const { authorizedRevision } = self.state({ authorizedRevision: cell(Type5.Integer(), 0) });
|
|
4120
|
+
self.listen({
|
|
4121
|
+
authorizedView: listener(shared.revision, async (value, ctx) => {
|
|
4122
|
+
try {
|
|
4123
|
+
target.value;
|
|
4124
|
+
await check();
|
|
4125
|
+
if (value)
|
|
4126
|
+
authorizedRevision.replace(ctx, value);
|
|
4127
|
+
} catch {
|
|
4128
|
+
authorizedRevision.replace(ctx, 0);
|
|
4129
|
+
}
|
|
4130
|
+
})
|
|
4131
|
+
});
|
|
4132
|
+
return {
|
|
4133
|
+
kind: "ready",
|
|
4134
|
+
revision: authorizedRevision,
|
|
4135
|
+
chunk: async (request) => {
|
|
4136
|
+
await check();
|
|
4137
|
+
return shared.reader.chunk(request);
|
|
4138
|
+
},
|
|
4139
|
+
getImage: async ({ entryId, contentIndex, offset }) => {
|
|
4140
|
+
await check();
|
|
4141
|
+
return shared.reader.image(entryId, contentIndex, offset);
|
|
4142
|
+
},
|
|
4143
|
+
decideApproval: async ({ id, version, decision }) => {
|
|
4144
|
+
const entry = await check();
|
|
4145
|
+
if (entry.historyThrough !== undefined)
|
|
4146
|
+
throw new ChordError("forbidden", "Fork history is read-only");
|
|
4147
|
+
return runtime.approvals.decide(Number(input.sessionId), entry.taskId, id, version, decision, context);
|
|
4148
|
+
},
|
|
4149
|
+
stop: async () => {
|
|
4150
|
+
const entry = await check();
|
|
4151
|
+
if (entry.historyThrough !== undefined)
|
|
4152
|
+
throw new ChordError("forbidden", "Fork history is read-only");
|
|
4153
|
+
await runtime.harness.abortTask(entry.taskId, context);
|
|
4154
|
+
await runtime.harness.waitForTask(entry.taskId, context);
|
|
4155
|
+
return null;
|
|
4156
|
+
}
|
|
4157
|
+
};
|
|
4158
|
+
});
|
|
4159
|
+
}
|
|
4160
|
+
|
|
3288
4161
|
// packages/gateway/src/session-host.ts
|
|
3289
|
-
var OwnerInput =
|
|
3290
|
-
var Owner =
|
|
4162
|
+
var OwnerInput = Type6.Object({});
|
|
4163
|
+
var Owner = local2("neko.attachment-owner", {
|
|
3291
4164
|
version: 1,
|
|
3292
4165
|
input: OwnerInput,
|
|
3293
|
-
members: { owner:
|
|
4166
|
+
members: { owner: opaque2("neko.owner@1") }
|
|
3294
4167
|
});
|
|
3295
|
-
var Projection =
|
|
4168
|
+
var Projection = local2("neko.projection", {
|
|
3296
4169
|
version: 1,
|
|
3297
|
-
input:
|
|
4170
|
+
input: Type6.Object({ id: Type6.String() }),
|
|
3298
4171
|
members: {
|
|
3299
4172
|
view: ChatViewSchema,
|
|
3300
|
-
reader:
|
|
4173
|
+
reader: opaque2("neko.projection-reader@1")
|
|
3301
4174
|
}
|
|
3302
4175
|
});
|
|
3303
4176
|
var emptyView = {
|
|
@@ -3323,6 +4196,11 @@ async function openProjection(runtime, session, context) {
|
|
|
3323
4196
|
let changed;
|
|
3324
4197
|
const stop = view.subscribe(() => changed?.(null));
|
|
3325
4198
|
const stopMetadata = runtime.onDirectoryChange(() => changed?.(null));
|
|
4199
|
+
let childIds = new Set;
|
|
4200
|
+
const stopChildren = runtime.harness.subscribeCommits(({ changes }) => {
|
|
4201
|
+
if (changes.some((change) => change.type === "document" && (["app.neko.delegations", "app.neko.approvals"].includes(change.record.kind) && change.conversationId === session.id || change.record.kind === "pi.usage" && change.conversationId !== undefined && childIds.has(change.conversationId))))
|
|
4202
|
+
changed?.(null);
|
|
4203
|
+
});
|
|
3326
4204
|
return {
|
|
3327
4205
|
subscribe(fn) {
|
|
3328
4206
|
changed = fn;
|
|
@@ -3335,7 +4213,7 @@ async function openProjection(runtime, session, context) {
|
|
|
3335
4213
|
for (const entry of view.value.entries)
|
|
3336
4214
|
history.set(entry.id, entry);
|
|
3337
4215
|
const [naming, compaction] = await Promise.all([runtime.namingUsage(session.id), session.compactionRecord()]);
|
|
3338
|
-
|
|
4216
|
+
const projected = projectChatView({
|
|
3339
4217
|
...view.value,
|
|
3340
4218
|
entries: [...history.values()].sort((a, b) => a.id - b.id),
|
|
3341
4219
|
docs: {
|
|
@@ -3346,6 +4224,29 @@ async function openProjection(runtime, session, context) {
|
|
|
3346
4224
|
}, naming)
|
|
3347
4225
|
}
|
|
3348
4226
|
}, "", { models: runtime.listModels(), contextEntries: view.value.entries, compaction });
|
|
4227
|
+
const subagents = await listDelegations(runtime.harness, session.id, context);
|
|
4228
|
+
const approvals = await runtime.approvals.list(session.id, context);
|
|
4229
|
+
for (const entry of subagents)
|
|
4230
|
+
entry.approvalCount = entry.historyThrough === undefined ? approvals.filter((a) => a.delegationTaskId === entry.taskId && a.status === "pending").length : 0;
|
|
4231
|
+
childIds = new Set(subagents.filter((entry) => entry.historyThrough === undefined).map((entry) => entry.childId));
|
|
4232
|
+
let aggregate = await session.usage();
|
|
4233
|
+
for (const child of subagents.filter((entry) => entry.historyThrough === undefined))
|
|
4234
|
+
aggregate = mergeUsage(aggregate, await runtime.harness.snapshot(UsageDoc2, child.childId, context) ?? {
|
|
4235
|
+
models: {},
|
|
4236
|
+
tools: {}
|
|
4237
|
+
});
|
|
4238
|
+
const u = totalUsage(aggregate);
|
|
4239
|
+
return {
|
|
4240
|
+
...projected,
|
|
4241
|
+
subagents: subagents.map((entry) => ({ ...entry, report: entry.report?.slice(0, 512) ?? null })),
|
|
4242
|
+
taskUsage: {
|
|
4243
|
+
input: u.input,
|
|
4244
|
+
output: u.output,
|
|
4245
|
+
cacheRead: u.cacheRead,
|
|
4246
|
+
cacheWrite: u.cacheWrite,
|
|
4247
|
+
cost: u.cost.total
|
|
4248
|
+
}
|
|
4249
|
+
};
|
|
3349
4250
|
},
|
|
3350
4251
|
async entry(id, ctx) {
|
|
3351
4252
|
ctx.abortSignal?.throwIfAborted();
|
|
@@ -3356,15 +4257,17 @@ async function openProjection(runtime, session, context) {
|
|
|
3356
4257
|
close() {
|
|
3357
4258
|
stop();
|
|
3358
4259
|
stopMetadata();
|
|
4260
|
+
stopChildren();
|
|
3359
4261
|
view.dispose();
|
|
3360
4262
|
history.clear();
|
|
3361
4263
|
}
|
|
3362
4264
|
};
|
|
3363
4265
|
}
|
|
3364
4266
|
function serveSessions(host, runtime, context) {
|
|
4267
|
+
installSubagentHost(host, runtime, context);
|
|
3365
4268
|
host.serve(Owner, (_input, _principal, self) => {
|
|
3366
4269
|
const { owner } = self.resources({
|
|
3367
|
-
owner:
|
|
4270
|
+
owner: resource2({
|
|
3368
4271
|
rev: 1,
|
|
3369
4272
|
open: () => ({ active: null }),
|
|
3370
4273
|
close: (value) => {
|
|
@@ -3375,14 +4278,14 @@ function serveSessions(host, runtime, context) {
|
|
|
3375
4278
|
return { owner: self.expose(owner) };
|
|
3376
4279
|
});
|
|
3377
4280
|
host.serve(Projection, (input, _principal, self) => {
|
|
3378
|
-
const { view } = self.state({ view:
|
|
4281
|
+
const { view } = self.state({ view: cell2(ChatViewSchema, emptyView) });
|
|
3379
4282
|
const { projection } = self.resources({
|
|
3380
|
-
projection:
|
|
4283
|
+
projection: resource2({
|
|
3381
4284
|
rev: 1,
|
|
3382
4285
|
async open({ signal }) {
|
|
3383
4286
|
const session = await runtime.getConversation(Number(input.id));
|
|
3384
4287
|
if (!session)
|
|
3385
|
-
throw new
|
|
4288
|
+
throw new ChordError2("gone", "Session unavailable");
|
|
3386
4289
|
const opened = await openProjection(runtime, session, context);
|
|
3387
4290
|
try {
|
|
3388
4291
|
const initial = await opened.project();
|
|
@@ -3397,7 +4300,7 @@ function serveSessions(host, runtime, context) {
|
|
|
3397
4300
|
})
|
|
3398
4301
|
});
|
|
3399
4302
|
self.listen({
|
|
3400
|
-
view:
|
|
4303
|
+
view: listener2(projection, (value) => value, async (_event, ctx) => {
|
|
3401
4304
|
view.replace(ctx, await projection.value.project());
|
|
3402
4305
|
})
|
|
3403
4306
|
});
|
|
@@ -3405,18 +4308,18 @@ function serveSessions(host, runtime, context) {
|
|
|
3405
4308
|
});
|
|
3406
4309
|
host.serve(NekoChat, (input, principal, self) => {
|
|
3407
4310
|
const owner = self.get(Owner, {}, { principal });
|
|
3408
|
-
const shared = self.get(Projection, { id: input.sessionId }, { principal:
|
|
4311
|
+
const shared = self.get(Projection, { id: input.sessionId }, { principal: hostPrincipal2 });
|
|
3409
4312
|
const { uploads, projection } = self.resources({
|
|
3410
|
-
uploads:
|
|
3411
|
-
projection:
|
|
4313
|
+
uploads: resource2({ rev: 1, open: () => new ImageUploads, close: (value) => value.close() }),
|
|
4314
|
+
projection: resource2({
|
|
3412
4315
|
rev: 1,
|
|
3413
4316
|
deps: [owner],
|
|
3414
4317
|
async open() {
|
|
3415
4318
|
if (owner.owner?.active?.attachmentId !== input.attachmentId || owner.owner.active.sessionId !== input.sessionId)
|
|
3416
|
-
throw new
|
|
4319
|
+
throw new ChordError2("forbidden", "Attachment does not belong to this connection");
|
|
3417
4320
|
const session = await runtime.getConversation(Number(input.sessionId));
|
|
3418
4321
|
if (!session)
|
|
3419
|
-
throw new
|
|
4322
|
+
throw new ChordError2("gone", "Session unavailable");
|
|
3420
4323
|
return {
|
|
3421
4324
|
session
|
|
3422
4325
|
};
|
|
@@ -3427,7 +4330,7 @@ function serveSessions(host, runtime, context) {
|
|
|
3427
4330
|
try {
|
|
3428
4331
|
return action();
|
|
3429
4332
|
} catch (error) {
|
|
3430
|
-
throw new
|
|
4333
|
+
throw new ChordError2("invalid", error instanceof Error ? error.message : "Image upload failed");
|
|
3431
4334
|
}
|
|
3432
4335
|
};
|
|
3433
4336
|
const behavior = {
|
|
@@ -3483,7 +4386,7 @@ function serveSessions(host, runtime, context) {
|
|
|
3483
4386
|
await projection.value.session.compact(instructions);
|
|
3484
4387
|
return null;
|
|
3485
4388
|
} catch (error) {
|
|
3486
|
-
throw new
|
|
4389
|
+
throw new ChordError2("invalid", error instanceof Error ? error.message : "Cannot compact");
|
|
3487
4390
|
}
|
|
3488
4391
|
},
|
|
3489
4392
|
beginImage: async (input) => safeUpload(() => uploads.value.begin(input)),
|
|
@@ -3501,15 +4404,15 @@ function serveSessions(host, runtime, context) {
|
|
|
3501
4404
|
},
|
|
3502
4405
|
getImage: async (input, ctx) => {
|
|
3503
4406
|
if (!Number.isSafeInteger(input.offset) || input.offset < 0 || !Number.isSafeInteger(input.contentIndex) || input.contentIndex < 0)
|
|
3504
|
-
throw new
|
|
3505
|
-
const entry = await shared.reader.entry(input.entryId,
|
|
4407
|
+
throw new ChordError2("invalid", "Invalid image range");
|
|
4408
|
+
const entry = await shared.reader.entry(input.entryId, withAbortSignal3(ctx.signal, context));
|
|
3506
4409
|
const content = entry?.model?.[0]?.content;
|
|
3507
4410
|
const image = Array.isArray(content) ? content[input.contentIndex] : undefined;
|
|
3508
4411
|
if (image?.type !== "image")
|
|
3509
|
-
throw new
|
|
4412
|
+
throw new ChordError2("invalid", "Image is not part of this conversation");
|
|
3510
4413
|
const bytes = Buffer.from(image.data, "base64");
|
|
3511
4414
|
if (input.offset > bytes.length)
|
|
3512
|
-
throw new
|
|
4415
|
+
throw new ChordError2("invalid", "Invalid image range");
|
|
3513
4416
|
return {
|
|
3514
4417
|
data: bytes.subarray(input.offset, input.offset + IMAGE_CHUNK_BYTES).toString("base64"),
|
|
3515
4418
|
total: bytes.length,
|
|
@@ -3529,8 +4432,8 @@ function serveSessions(host, runtime, context) {
|
|
|
3529
4432
|
uploads.value.release(id);
|
|
3530
4433
|
} catch (error) {
|
|
3531
4434
|
if (error instanceof Error && error.message.startsWith("DEEPSEEK_KEY_REQUIRED:"))
|
|
3532
|
-
throw new
|
|
3533
|
-
throw new
|
|
4435
|
+
throw new ChordError2("invalid", "DEEPSEEK_KEY_REQUIRED: Configure DeepSeek in /login or Web settings.");
|
|
4436
|
+
throw new ChordError2("invalid", error instanceof Error ? error.message : "Cannot submit message");
|
|
3534
4437
|
}
|
|
3535
4438
|
return null;
|
|
3536
4439
|
},
|
|
@@ -3552,16 +4455,16 @@ function serveSessions(host, runtime, context) {
|
|
|
3552
4455
|
configure: async (change) => {
|
|
3553
4456
|
const applied = await runtime.configureConversation(projection.value.session.id, change);
|
|
3554
4457
|
if (!applied) {
|
|
3555
|
-
throw new
|
|
4458
|
+
throw new ChordError2("invalid", `Session ${String(projection.value.session.id)} is unavailable or archived`);
|
|
3556
4459
|
}
|
|
3557
4460
|
return null;
|
|
3558
4461
|
}
|
|
3559
4462
|
};
|
|
3560
4463
|
const guard = async () => {
|
|
3561
4464
|
if (owner.owner?.active?.attachmentId !== input.attachmentId || owner.owner.active.sessionId !== input.sessionId)
|
|
3562
|
-
throw new
|
|
4465
|
+
throw new ChordError2("gone", "Attachment is no longer active");
|
|
3563
4466
|
if (!await runtime.getConversation(Number(input.sessionId)))
|
|
3564
|
-
throw new
|
|
4467
|
+
throw new ChordError2("gone", "Session unavailable or archived");
|
|
3565
4468
|
};
|
|
3566
4469
|
for (const name of Object.keys(NekoChat.def.variants.ready.methods)) {
|
|
3567
4470
|
const method = behavior[name];
|
|
@@ -3576,7 +4479,7 @@ function serveSessions(host, runtime, context) {
|
|
|
3576
4479
|
}
|
|
3577
4480
|
|
|
3578
4481
|
// packages/gateway/src/skill-market.ts
|
|
3579
|
-
import { createHash, randomUUID } from "crypto";
|
|
4482
|
+
import { createHash, randomUUID as randomUUID2 } from "crypto";
|
|
3580
4483
|
import { mkdir as mkdir3, readdir as readdir2, rename as rename3, rm as rm3, stat as stat2, writeFile as writeFile3 } from "fs/promises";
|
|
3581
4484
|
import { homedir as homedir6 } from "os";
|
|
3582
4485
|
import { dirname as dirname6, join as join8 } from "path";
|
|
@@ -4150,7 +5053,7 @@ function createSkillMarketClient(options) {
|
|
|
4150
5053
|
const skill = await detail(name);
|
|
4151
5054
|
const files = unpack(skill, await download(skill));
|
|
4152
5055
|
await sweepTempDirs(skillsDir);
|
|
4153
|
-
const temp = join8(dirname6(skillsDir), `.tmp-${
|
|
5056
|
+
const temp = join8(dirname6(skillsDir), `.tmp-${randomUUID2()}`);
|
|
4154
5057
|
try {
|
|
4155
5058
|
for (const [relative, bytes] of files) {
|
|
4156
5059
|
const dest = join8(temp, ...relative.split("/"));
|
|
@@ -4271,14 +5174,14 @@ async function listDrives() {
|
|
|
4271
5174
|
}
|
|
4272
5175
|
|
|
4273
5176
|
// packages/gateway/src/server-services.ts
|
|
4274
|
-
var ServerState =
|
|
5177
|
+
var ServerState = local3("neko.server-state", {
|
|
4275
5178
|
version: 1,
|
|
4276
|
-
input:
|
|
5179
|
+
input: Type7.Object({}),
|
|
4277
5180
|
members: {
|
|
4278
5181
|
directory: DirectoryStateSchema,
|
|
4279
5182
|
workspaces: WorkspaceStateSchema,
|
|
4280
5183
|
credentials: CredentialStateSchema,
|
|
4281
|
-
refreshCredentials: method2(
|
|
5184
|
+
refreshCredentials: method2(Type7.Null(), Type7.Null())
|
|
4282
5185
|
}
|
|
4283
5186
|
});
|
|
4284
5187
|
async function createNekoServerServices(options) {
|
|
@@ -4298,9 +5201,9 @@ async function createNekoServerServices(options) {
|
|
|
4298
5201
|
listener(updates.state);
|
|
4299
5202
|
};
|
|
4300
5203
|
host.serve(NekoApplicationInfo, (_input, _principal, self) => {
|
|
4301
|
-
const state = self.state({ info:
|
|
5204
|
+
const state = self.state({ info: cell3(ApplicationInfoSchema, updates.state) });
|
|
4302
5205
|
self.listen({
|
|
4303
|
-
updates:
|
|
5206
|
+
updates: listener3({
|
|
4304
5207
|
subscribe: (fn) => {
|
|
4305
5208
|
updateListeners.add(fn);
|
|
4306
5209
|
return () => updateListeners.delete(fn);
|
|
@@ -4332,7 +5235,7 @@ async function createNekoServerServices(options) {
|
|
|
4332
5235
|
const toRemote = (error) => {
|
|
4333
5236
|
if (error instanceof SkillMarketError) {
|
|
4334
5237
|
const code = error.code === "skill_not_found" || error.code === "skill_not_installed" ? "gone" : "invalid";
|
|
4335
|
-
throw new
|
|
5238
|
+
throw new ChordError3(code, error.message, { reason: error.code });
|
|
4336
5239
|
}
|
|
4337
5240
|
throw error;
|
|
4338
5241
|
};
|
|
@@ -4366,12 +5269,12 @@ async function createNekoServerServices(options) {
|
|
|
4366
5269
|
};
|
|
4367
5270
|
host.serve(ServerState, (_input, _principal, self) => {
|
|
4368
5271
|
const state = self.state({
|
|
4369
|
-
directory:
|
|
4370
|
-
workspaces:
|
|
4371
|
-
credentials:
|
|
5272
|
+
directory: cell3(DirectoryStateSchema, { revision: 1, sessions }),
|
|
5273
|
+
workspaces: cell3(WorkspaceStateSchema, { revision: 1, workspaces }),
|
|
5274
|
+
credentials: cell3(CredentialStateSchema, { revision: 1, statuses })
|
|
4372
5275
|
});
|
|
4373
5276
|
self.listen({
|
|
4374
|
-
directory:
|
|
5277
|
+
directory: listener3({ subscribe: runtime.harness.subscribeCommits.bind(runtime.harness) }, async (event, ctx) => {
|
|
4375
5278
|
if (!event.changes.some((change) => change.type === "conversation" || change.type === "document" && [
|
|
4376
5279
|
"app.neko.session-metadata",
|
|
4377
5280
|
"app.neko.activity",
|
|
@@ -4398,7 +5301,7 @@ async function createNekoServerServices(options) {
|
|
|
4398
5301
|
}
|
|
4399
5302
|
};
|
|
4400
5303
|
}, { eager: { input: {} } });
|
|
4401
|
-
const shared = (self) => self.get(ServerState, {}, { principal:
|
|
5304
|
+
const shared = (self) => self.get(ServerState, {}, { principal: hostPrincipal3 });
|
|
4402
5305
|
host.serve(NekoSessionDirectory, (_input, _principal, self) => ({ kind: "ready", state: shared(self).directory }));
|
|
4403
5306
|
host.serve(NekoWorkspaces, (_input, _principal, self) => ({
|
|
4404
5307
|
kind: "ready",
|
|
@@ -4434,7 +5337,7 @@ async function createNekoServerServices(options) {
|
|
|
4434
5337
|
search: async (input, ctx) => {
|
|
4435
5338
|
const roots = await runtime.listWorkspaces();
|
|
4436
5339
|
if (!roots.some((root) => root.path === input.workspaceRoot))
|
|
4437
|
-
throw new
|
|
5340
|
+
throw new ChordError3("invalid", "Unknown workspace.");
|
|
4438
5341
|
return runtime.workspaceFiles.search(input.workspaceRoot, input.query, ctx.signal);
|
|
4439
5342
|
}
|
|
4440
5343
|
}));
|
|
@@ -4444,7 +5347,7 @@ async function createNekoServerServices(options) {
|
|
|
4444
5347
|
const summary = async (id) => {
|
|
4445
5348
|
const found = (await list()).find((item) => item.id === String(id));
|
|
4446
5349
|
if (!found)
|
|
4447
|
-
throw new
|
|
5350
|
+
throw new ChordError3("gone", "Session unavailable");
|
|
4448
5351
|
return found;
|
|
4449
5352
|
};
|
|
4450
5353
|
return {
|
|
@@ -4457,9 +5360,9 @@ async function createNekoServerServices(options) {
|
|
|
4457
5360
|
},
|
|
4458
5361
|
attach: (id) => serialize(async () => {
|
|
4459
5362
|
if (!await runtime.getConversation(toConversationId(id)))
|
|
4460
|
-
throw new
|
|
5363
|
+
throw new ChordError3("gone", `Unknown session: ${id}`);
|
|
4461
5364
|
if (owner.owner.active?.sessionId !== id) {
|
|
4462
|
-
owner.owner.active = { sessionId: id, attachmentId:
|
|
5365
|
+
owner.owner.active = { sessionId: id, attachmentId: randomUUID3() };
|
|
4463
5366
|
await runtime.touchConversation(toConversationId(id));
|
|
4464
5367
|
}
|
|
4465
5368
|
return owner.owner.active;
|
|
@@ -4495,7 +5398,7 @@ async function createNekoServerServices(options) {
|
|
|
4495
5398
|
function toConversationId(sessionId) {
|
|
4496
5399
|
const numeric = Number(sessionId);
|
|
4497
5400
|
if (!Number.isSafeInteger(numeric) || numeric < 1 || String(numeric) !== sessionId)
|
|
4498
|
-
throw new
|
|
5401
|
+
throw new ChordError3("invalid", `Invalid neko session id: ${sessionId}`);
|
|
4499
5402
|
return numeric;
|
|
4500
5403
|
}
|
|
4501
5404
|
|
|
@@ -4792,7 +5695,7 @@ var DEFAULT_HOST = "127.0.0.1";
|
|
|
4792
5695
|
var DEFAULT_PORT = 8321;
|
|
4793
5696
|
var SERVER_ID_FILENAME = "gateway-server-id";
|
|
4794
5697
|
async function startGateway(options = {}) {
|
|
4795
|
-
const context = options.context ??
|
|
5698
|
+
const context = options.context ?? BACKGROUND_CONTEXT4;
|
|
4796
5699
|
const fileConfig = loadNekoConfig(options.configDir);
|
|
4797
5700
|
const model = options.model ?? fileConfig.defaultModel ?? DEFAULT_MODEL;
|
|
4798
5701
|
const storagePath = options.storagePath ?? fileConfig.storagePath ?? DEFAULT_STORAGE_PATH;
|
|
@@ -4862,7 +5765,7 @@ async function bindGateway(runtime, options) {
|
|
|
4862
5765
|
}
|
|
4863
5766
|
try {
|
|
4864
5767
|
for (const listener of listeners)
|
|
4865
|
-
await listener.start((link) => chord.accept(link, { principal: { connectionId:
|
|
5768
|
+
await listener.start((link) => chord.accept(link, { principal: { connectionId: randomUUID4() } }));
|
|
4866
5769
|
} catch (error) {
|
|
4867
5770
|
await Promise.allSettled(listeners.map((listener) => listener.close()));
|
|
4868
5771
|
await chord.close();
|
|
@@ -4932,7 +5835,7 @@ async function loadOrCreateServerId(nekoDir) {
|
|
|
4932
5835
|
if (existing !== "")
|
|
4933
5836
|
return existing;
|
|
4934
5837
|
} catch {}
|
|
4935
|
-
const created =
|
|
5838
|
+
const created = randomUUID4();
|
|
4936
5839
|
await mkdir5(nekoDir, { recursive: true, mode: 448 });
|
|
4937
5840
|
await writeFile4(file, `${created}
|
|
4938
5841
|
`, { mode: 384 });
|
|
@@ -4955,7 +5858,7 @@ if (gateway.socketPath !== undefined)
|
|
|
4955
5858
|
console.log(` Unix socket: ${gateway.socketPath}`);
|
|
4956
5859
|
if (gateway.wsUrl !== undefined)
|
|
4957
5860
|
console.log(` WebSocket: ${gateway.wsUrl}`);
|
|
4958
|
-
console.log("Ctrl
|
|
5861
|
+
console.log("\u6309 Ctrl+C \u505C\u6B62\u3002");
|
|
4959
5862
|
var stopping = false;
|
|
4960
5863
|
var stop = () => {
|
|
4961
5864
|
if (stopping)
|