neko-harness 0.2.9 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/client/assets/index-BTHbU-2p.css +2 -0
- package/dist/client/assets/index-O88LHrzD.js +83 -0
- package/dist/client/index.html +2 -2
- package/dist/gateway.js +1572 -245
- package/dist/neko.js +3613 -1261
- package/package.json +1 -1
- package/dist/client/assets/index-BKYqZ61y.js +0 -82
- package/dist/client/assets/index-BwqDUlvt.css +0 -2
package/dist/gateway.js
CHANGED
|
@@ -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,17 @@ function truncateMiddle(text, maxWidth) {
|
|
|
516
516
|
function summarizeToolCall(name, args) {
|
|
517
517
|
const record = args ?? {};
|
|
518
518
|
switch (name) {
|
|
519
|
+
case "subagent":
|
|
520
|
+
return {
|
|
521
|
+
args: record.action === "list" ? "List agents" : `${record.agentId ?? record.type}: ${record.title}`,
|
|
522
|
+
detail: String(record.task ?? "")
|
|
523
|
+
};
|
|
524
|
+
case "find_files":
|
|
525
|
+
case "search_text":
|
|
526
|
+
return {
|
|
527
|
+
args: truncateMiddle(`${record.path ?? "."} ${record.pattern ?? record.query ?? ""}`, 100),
|
|
528
|
+
detail: JSON.stringify(args, null, 2)
|
|
529
|
+
};
|
|
519
530
|
case "bash": {
|
|
520
531
|
const command = typeof record.command === "string" ? record.command : "";
|
|
521
532
|
const firstLine = command.split(`
|
|
@@ -632,6 +643,7 @@ function projectChatView(view, modelLabel, options = {}) {
|
|
|
632
643
|
const defaultImagePrompt = entry.kind === NekoInput.kind ? savedDefaultImagePrompt(message.content, entry.data?.defaultImagePrompt) : undefined;
|
|
633
644
|
items.push({
|
|
634
645
|
type: "user",
|
|
646
|
+
...entry.data?.subagentRoundId ? { subagentRoundId: entry.data.subagentRoundId } : {},
|
|
635
647
|
key: `e${entry.id}`,
|
|
636
648
|
id: entry.id,
|
|
637
649
|
text: defaultImagePrompt && Array.isArray(message.content) ? userText(message.content.slice(1)) : userText(message.content),
|
|
@@ -653,6 +665,7 @@ function projectChatView(view, modelLabel, options = {}) {
|
|
|
653
665
|
items.push(withArgs(result, block.name, block.arguments));
|
|
654
666
|
} else {
|
|
655
667
|
const summary = summarizeToolCall(block.name, block.arguments);
|
|
668
|
+
const slot = live.tools?.find((s) => s.callId === block.id);
|
|
656
669
|
items.push({
|
|
657
670
|
type: "tool",
|
|
658
671
|
key: `t${block.id}`,
|
|
@@ -660,9 +673,9 @@ function projectChatView(view, modelLabel, options = {}) {
|
|
|
660
673
|
name: block.name,
|
|
661
674
|
summary: summary.args,
|
|
662
675
|
detail: summary.detail ?? null,
|
|
663
|
-
phase: "done",
|
|
676
|
+
phase: slot?.status ?? "done",
|
|
664
677
|
isError: false,
|
|
665
|
-
output: null,
|
|
678
|
+
output: slot?.output ?? null,
|
|
666
679
|
result: null,
|
|
667
680
|
durationMs: null,
|
|
668
681
|
note: ran ? null : "Not run: response interrupted."
|
|
@@ -821,23 +834,181 @@ var stripFrontmatter = (content) => extractFrontmatter(content).body;
|
|
|
821
834
|
// packages/core/src/runtime.ts
|
|
822
835
|
import { homedir as homedir4 } from "os";
|
|
823
836
|
import { join as join5 } from "path";
|
|
824
|
-
import { BACKGROUND_CONTEXT as
|
|
837
|
+
import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT3 } from "@earendil-works/chord/context";
|
|
825
838
|
import { clampThinkingLevel, createModels, getSupportedThinkingLevels as getSupportedThinkingLevels2 } from "@earendil-works/pi-ai/models";
|
|
826
839
|
import { deepseekProvider } from "@earendil-works/pi-ai/providers/deepseek";
|
|
827
840
|
import {
|
|
828
|
-
AssistantEntry,
|
|
841
|
+
AssistantEntry as AssistantEntry2,
|
|
829
842
|
createRegistry,
|
|
830
|
-
defineDoc as
|
|
843
|
+
defineDoc as defineDoc4,
|
|
831
844
|
Harness,
|
|
832
845
|
LiveDoc as importLiveDoc,
|
|
833
846
|
MemoryStorage,
|
|
834
|
-
UsageDoc,
|
|
847
|
+
UsageDoc as UsageDoc2,
|
|
835
848
|
watchEvents
|
|
836
849
|
} from "@earendil-works/pi-durable";
|
|
837
850
|
import { createNodePhotonImages } from "@earendil-works/pi-durable/images/node";
|
|
838
851
|
import { openNodeSqliteStorage } from "@earendil-works/pi-durable/storage/sqlite/node";
|
|
839
852
|
import { createCodingTools } from "@earendil-works/pi-durable/tools";
|
|
840
853
|
|
|
854
|
+
// packages/core/src/agent-file-search.ts
|
|
855
|
+
import { BACKGROUND_CONTEXT, withAbortSignal } from "@earendil-works/chord/context";
|
|
856
|
+
import { defineExtension, defineTool } from "@earendil-works/pi-durable";
|
|
857
|
+
import { getOrThrow } from "@earendil-works/pi-durable/env";
|
|
858
|
+
import { Type } from "typebox";
|
|
859
|
+
var SEARCH_LIMITS = {
|
|
860
|
+
entries: 1e4,
|
|
861
|
+
fileBytes: 2 * 1024 * 1024,
|
|
862
|
+
scanBytes: 32 * 1024 * 1024,
|
|
863
|
+
outputBytes: 32 * 1024,
|
|
864
|
+
timeoutMs: 1e4
|
|
865
|
+
};
|
|
866
|
+
var path = Type.Optional(Type.String({ description: "Directory relative to this conversation's cwd, or absolute path." }));
|
|
867
|
+
var limit = Type.Optional(Type.Integer({ minimum: 1, maximum: 500, description: "Maximum results (default 100)." }));
|
|
868
|
+
var text = (value) => ({ type: "text", text: value });
|
|
869
|
+
async function search(api, context, run) {
|
|
870
|
+
if (!api.env)
|
|
871
|
+
throw new Error("File environment unavailable");
|
|
872
|
+
const timer = new AbortController;
|
|
873
|
+
const timeout = setTimeout(() => timer.abort(new Error("File search timed out; narrow the directory.")), SEARCH_LIMITS.timeoutMs);
|
|
874
|
+
const signal = context.abortSignal ? AbortSignal.any([context.abortSignal, timer.signal]) : timer.signal;
|
|
875
|
+
try {
|
|
876
|
+
signal.throwIfAborted();
|
|
877
|
+
return await run(api.env, withAbortSignal(signal, context));
|
|
878
|
+
} finally {
|
|
879
|
+
clearTimeout(timeout);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
async function listing(env, path, ctx) {
|
|
883
|
+
if (!env.listFiles)
|
|
884
|
+
throw new Error("File discovery unavailable. Retry.");
|
|
885
|
+
const base = getOrThrow(await env.absolutePath(path, ctx));
|
|
886
|
+
const files = getOrThrow(await env.listFiles(base, {
|
|
887
|
+
hidden: true,
|
|
888
|
+
gitignore: true,
|
|
889
|
+
maxEntries: SEARCH_LIMITS.entries,
|
|
890
|
+
maxBytes: 1024 * 1024,
|
|
891
|
+
timeoutMs: SEARCH_LIMITS.timeoutMs
|
|
892
|
+
}, ctx));
|
|
893
|
+
return { base, ...files };
|
|
894
|
+
}
|
|
895
|
+
function output(lines, complete, skipped = 0) {
|
|
896
|
+
const status = complete ? "Search complete within Pi listing; unreadable entries may be omitted." : "Partial results; narrow the directory or filter and retry.";
|
|
897
|
+
return {
|
|
898
|
+
output: [text(`${lines.join(`
|
|
899
|
+
`)}
|
|
900
|
+
${status}${skipped ? ` Skipped/unreadable files: ${skipped}.` : ""}`)],
|
|
901
|
+
details: { complete, skipped }
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
var FindFiles = defineTool({
|
|
905
|
+
name: "find_files",
|
|
906
|
+
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.",
|
|
907
|
+
parameters: Type.Object({ path, query: Type.Optional(Type.String()), limit }),
|
|
908
|
+
replay: "safe",
|
|
909
|
+
execute: (args, api, context) => search(api, context, async (env, ctx) => {
|
|
910
|
+
const files = await listing(env, args.path ?? ".", ctx);
|
|
911
|
+
const lines = [];
|
|
912
|
+
let bytes = 0;
|
|
913
|
+
let complete = !files.truncated;
|
|
914
|
+
for (const file of files.entries) {
|
|
915
|
+
ctx.abortSignal?.throwIfAborted();
|
|
916
|
+
if (args.query && !file.path.includes(args.query))
|
|
917
|
+
continue;
|
|
918
|
+
const absolute = getOrThrow(await env.joinPath([files.base, file.path], ctx)).replace(/\\/g, "/");
|
|
919
|
+
const line = `${file.kind} ${absolute}`;
|
|
920
|
+
bytes += Buffer.byteLength(line) + 1;
|
|
921
|
+
if (lines.length >= (args.limit ?? 100) || bytes > SEARCH_LIMITS.outputBytes - 256) {
|
|
922
|
+
complete = false;
|
|
923
|
+
break;
|
|
924
|
+
}
|
|
925
|
+
lines.push(line);
|
|
926
|
+
}
|
|
927
|
+
return output(lines, complete);
|
|
928
|
+
})
|
|
929
|
+
});
|
|
930
|
+
var SearchText = defineTool({
|
|
931
|
+
name: "search_text",
|
|
932
|
+
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.",
|
|
933
|
+
parameters: Type.Object({
|
|
934
|
+
pattern: Type.String({ minLength: 1, maxLength: 4096 }),
|
|
935
|
+
path,
|
|
936
|
+
filePattern: Type.Optional(Type.String()),
|
|
937
|
+
caseSensitive: Type.Optional(Type.Boolean()),
|
|
938
|
+
contextLines: Type.Optional(Type.Integer({ minimum: 0, maximum: 5 })),
|
|
939
|
+
limit
|
|
940
|
+
}),
|
|
941
|
+
replay: "safe",
|
|
942
|
+
execute: (args, api, context) => search(api, context, async (env, ctx) => {
|
|
943
|
+
const files = await listing(env, args.path ?? ".", ctx);
|
|
944
|
+
const results = [];
|
|
945
|
+
let complete = !files.truncated;
|
|
946
|
+
let skipped = 0;
|
|
947
|
+
let scanned = 0;
|
|
948
|
+
let bytes = 0;
|
|
949
|
+
const needle = args.caseSensitive === false ? args.pattern.toLowerCase() : args.pattern;
|
|
950
|
+
outer:
|
|
951
|
+
for (const file of files.entries) {
|
|
952
|
+
ctx.abortSignal?.throwIfAborted();
|
|
953
|
+
if (file.kind !== "file" || args.filePattern && !file.path.includes(args.filePattern))
|
|
954
|
+
continue;
|
|
955
|
+
const absolute = getOrThrow(await env.joinPath([files.base, file.path], ctx));
|
|
956
|
+
const opened = await env.openBinaryReader(absolute, { noFollow: true }, ctx);
|
|
957
|
+
if (!opened.ok) {
|
|
958
|
+
ctx.abortSignal?.throwIfAborted();
|
|
959
|
+
skipped++;
|
|
960
|
+
complete = false;
|
|
961
|
+
continue;
|
|
962
|
+
}
|
|
963
|
+
try {
|
|
964
|
+
const info = getOrThrow(await opened.value.info(ctx));
|
|
965
|
+
if (info.size > SEARCH_LIMITS.fileBytes) {
|
|
966
|
+
skipped++;
|
|
967
|
+
complete = false;
|
|
968
|
+
continue;
|
|
969
|
+
}
|
|
970
|
+
if (scanned + info.size > SEARCH_LIMITS.scanBytes) {
|
|
971
|
+
complete = false;
|
|
972
|
+
break;
|
|
973
|
+
}
|
|
974
|
+
const data = getOrThrow(await opened.value.read(0, info.size + 1, ctx));
|
|
975
|
+
scanned += data.length;
|
|
976
|
+
if (data.length !== info.size || data.includes(0)) {
|
|
977
|
+
skipped++;
|
|
978
|
+
complete = false;
|
|
979
|
+
continue;
|
|
980
|
+
}
|
|
981
|
+
const lines = new TextDecoder("utf-8", { fatal: true }).decode(data).split(/\r?\n/);
|
|
982
|
+
for (let i = 0;i < lines.length; i++) {
|
|
983
|
+
ctx.abortSignal?.throwIfAborted();
|
|
984
|
+
const value = lines[i];
|
|
985
|
+
if (!(args.caseSensitive === false ? value.toLowerCase() : value).includes(needle))
|
|
986
|
+
continue;
|
|
987
|
+
const n = args.contextLines ?? 0;
|
|
988
|
+
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(`
|
|
989
|
+
`);
|
|
990
|
+
bytes += Buffer.byteLength(block) + 1;
|
|
991
|
+
if (results.length >= (args.limit ?? 100) || bytes > SEARCH_LIMITS.outputBytes - 256) {
|
|
992
|
+
complete = false;
|
|
993
|
+
break outer;
|
|
994
|
+
}
|
|
995
|
+
results.push(block);
|
|
996
|
+
}
|
|
997
|
+
} catch (error) {
|
|
998
|
+
ctx.abortSignal?.throwIfAborted();
|
|
999
|
+
if (!(error instanceof Error))
|
|
1000
|
+
throw error;
|
|
1001
|
+
skipped++;
|
|
1002
|
+
complete = false;
|
|
1003
|
+
} finally {
|
|
1004
|
+
await opened.value.close(BACKGROUND_CONTEXT);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
return output(results, complete, skipped);
|
|
1008
|
+
})
|
|
1009
|
+
});
|
|
1010
|
+
var AgentFileSearch = defineExtension({ name: "neko-file-search", tools: [FindFiles, SearchText] });
|
|
1011
|
+
|
|
841
1012
|
// packages/core/src/compaction.ts
|
|
842
1013
|
import {
|
|
843
1014
|
CompactionTask
|
|
@@ -870,9 +1041,9 @@ async function readCompaction(harness, storage, id, context) {
|
|
|
870
1041
|
}
|
|
871
1042
|
|
|
872
1043
|
// packages/core/src/image-metadata.ts
|
|
873
|
-
import { defineExtension, hook, ToolTask } from "@earendil-works/pi-durable";
|
|
1044
|
+
import { defineExtension as defineExtension2, hook, ToolTask } from "@earendil-works/pi-durable";
|
|
874
1045
|
import sharp from "sharp";
|
|
875
|
-
var ImageDetails =
|
|
1046
|
+
var ImageDetails = defineExtension2({
|
|
876
1047
|
name: "neko-image-details",
|
|
877
1048
|
hooks: [
|
|
878
1049
|
hook(ToolTask, {
|
|
@@ -1045,10 +1216,575 @@ ${args}` : block;
|
|
|
1045
1216
|
};
|
|
1046
1217
|
|
|
1047
1218
|
// packages/core/src/prompt/prompt.ts
|
|
1048
|
-
import { defineExtension as
|
|
1219
|
+
import { defineExtension as defineExtension4, section } from "@earendil-works/pi-durable";
|
|
1220
|
+
|
|
1221
|
+
// packages/core/src/subagents.ts
|
|
1222
|
+
import {
|
|
1223
|
+
AssistantEntry,
|
|
1224
|
+
configure,
|
|
1225
|
+
defineDoc,
|
|
1226
|
+
defineExtension as defineExtension3,
|
|
1227
|
+
defineTask,
|
|
1228
|
+
defineTool as defineTool2,
|
|
1229
|
+
LiveDoc,
|
|
1230
|
+
UsageDoc
|
|
1231
|
+
} from "@earendil-works/pi-durable";
|
|
1232
|
+
import { Type as Type2 } from "typebox";
|
|
1233
|
+
import { Value } from "typebox/value";
|
|
1234
|
+
|
|
1235
|
+
// packages/core/src/subagent-usage.ts
|
|
1236
|
+
var emptySubagentUsage = () => ({ models: {}, tools: {} });
|
|
1237
|
+
function subtractSubagentUsage(end, start) {
|
|
1238
|
+
const result = emptySubagentUsage();
|
|
1239
|
+
for (const bucket of ["models", "tools"]) {
|
|
1240
|
+
for (const [key, usage] of Object.entries(end[bucket])) {
|
|
1241
|
+
const before = start[bucket][key];
|
|
1242
|
+
const cost = usage.cost;
|
|
1243
|
+
const next = {
|
|
1244
|
+
input: usage.input - (before?.input ?? 0),
|
|
1245
|
+
output: usage.output - (before?.output ?? 0),
|
|
1246
|
+
cacheRead: usage.cacheRead - (before?.cacheRead ?? 0),
|
|
1247
|
+
cacheWrite: usage.cacheWrite - (before?.cacheWrite ?? 0),
|
|
1248
|
+
totalTokens: usage.totalTokens - (before?.totalTokens ?? 0),
|
|
1249
|
+
cost: {
|
|
1250
|
+
input: cost.input - (before?.cost.input ?? 0),
|
|
1251
|
+
output: cost.output - (before?.cost.output ?? 0),
|
|
1252
|
+
cacheRead: cost.cacheRead - (before?.cost.cacheRead ?? 0),
|
|
1253
|
+
cacheWrite: cost.cacheWrite - (before?.cost.cacheWrite ?? 0),
|
|
1254
|
+
total: cost.total - (before?.cost.total ?? 0)
|
|
1255
|
+
}
|
|
1256
|
+
};
|
|
1257
|
+
if (usage.reasoning !== undefined)
|
|
1258
|
+
next.reasoning = usage.reasoning - (before?.reasoning ?? 0);
|
|
1259
|
+
if (usage.cacheWrite1h !== undefined)
|
|
1260
|
+
next.cacheWrite1h = usage.cacheWrite1h - (before?.cacheWrite1h ?? 0);
|
|
1261
|
+
result[bucket][key] = next;
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
return result;
|
|
1265
|
+
}
|
|
1266
|
+
function subagentUsageTotals(state) {
|
|
1267
|
+
const totals = totalUsage(state);
|
|
1268
|
+
return {
|
|
1269
|
+
input: totals.input,
|
|
1270
|
+
output: totals.output,
|
|
1271
|
+
cacheRead: totals.cacheRead,
|
|
1272
|
+
cacheWrite: totals.cacheWrite,
|
|
1273
|
+
cost: totals.cost.total
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
// packages/core/src/subagents.ts
|
|
1278
|
+
var agentIdFor = (childId) => `agent-${childId}`;
|
|
1279
|
+
var DelegationsDoc = defineDoc({
|
|
1280
|
+
kind: "app.neko.delegations",
|
|
1281
|
+
version: 1,
|
|
1282
|
+
scope: "conversation",
|
|
1283
|
+
history: "latest",
|
|
1284
|
+
fork: "initial",
|
|
1285
|
+
initial: () => ({ items: [] })
|
|
1286
|
+
});
|
|
1287
|
+
var SubagentsDoc = defineDoc({
|
|
1288
|
+
kind: "app.neko.subagents",
|
|
1289
|
+
version: 1,
|
|
1290
|
+
scope: "conversation",
|
|
1291
|
+
history: "latest",
|
|
1292
|
+
fork: "initial",
|
|
1293
|
+
initial: () => ({ items: [] })
|
|
1294
|
+
});
|
|
1295
|
+
var SubagentDoc = defineDoc({
|
|
1296
|
+
kind: "app.neko.subagent",
|
|
1297
|
+
version: 1,
|
|
1298
|
+
scope: "conversation",
|
|
1299
|
+
history: "latest",
|
|
1300
|
+
fork: "initial",
|
|
1301
|
+
initial: () => ({ role: null, parentId: null })
|
|
1302
|
+
});
|
|
1303
|
+
var SlotsDoc = defineDoc({
|
|
1304
|
+
kind: "app.neko.subagent-slots",
|
|
1305
|
+
version: 1,
|
|
1306
|
+
scope: "session",
|
|
1307
|
+
initial: () => ({ items: [] })
|
|
1308
|
+
});
|
|
1309
|
+
var ROLE_PROMPTS = {
|
|
1310
|
+
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.",
|
|
1311
|
+
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.",
|
|
1312
|
+
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."
|
|
1313
|
+
};
|
|
1314
|
+
var SUBAGENT_DELIVERY = " Work from the assigned task, your own prior conversation context and 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.";
|
|
1315
|
+
var READ_TOOLS = new Set(["read", "find_files", "search_text", "web_search", "web_extract"]);
|
|
1316
|
+
async function refreshDelegations(tx, id) {
|
|
1317
|
+
const doc = await tx.doc(DelegationsDoc, id);
|
|
1318
|
+
const identities = await tx.doc(SubagentsDoc, id);
|
|
1319
|
+
for (const item of doc.items) {
|
|
1320
|
+
item.agentId ??= agentIdFor(item.childId);
|
|
1321
|
+
item.round ??= doc.items.filter((d) => d.childId === item.childId && d.taskId <= item.taskId).length;
|
|
1322
|
+
if (item.historyThrough !== undefined)
|
|
1323
|
+
continue;
|
|
1324
|
+
if (item.inputEntryId === undefined) {
|
|
1325
|
+
const submission = await tx.submissionByRequest(item.childId, `subagent:${item.taskId}`);
|
|
1326
|
+
if (submission?.type === "input" && submission.entry !== undefined)
|
|
1327
|
+
item.inputEntryId = submission.entry;
|
|
1328
|
+
}
|
|
1329
|
+
let identity = identities.items.find((a) => a.childId === item.childId);
|
|
1330
|
+
if (!identity) {
|
|
1331
|
+
const role = await tx.doc(SubagentDoc, item.childId);
|
|
1332
|
+
if (role.parentId !== id || role.role !== item.role)
|
|
1333
|
+
continue;
|
|
1334
|
+
identity = {
|
|
1335
|
+
agentId: item.agentId,
|
|
1336
|
+
parentId: id,
|
|
1337
|
+
childId: item.childId,
|
|
1338
|
+
role: item.role,
|
|
1339
|
+
cwd: "",
|
|
1340
|
+
title: item.title,
|
|
1341
|
+
createdAt: item.createdAt,
|
|
1342
|
+
activeRoundId: item.taskId
|
|
1343
|
+
};
|
|
1344
|
+
identities.items.push(identity);
|
|
1345
|
+
identity = identities.items.at(-1);
|
|
1346
|
+
}
|
|
1347
|
+
const task = await tx.task(item.taskId);
|
|
1348
|
+
const runner = item.runnerId === undefined ? undefined : await tx.task(item.runnerId);
|
|
1349
|
+
const live = await tx.doc(LiveDoc, item.childId);
|
|
1350
|
+
const idle = live.run === undefined && !live.compactions?.length;
|
|
1351
|
+
if (task?.state.status === "terminal") {
|
|
1352
|
+
if (item.status === "queued" || item.status === "running") {
|
|
1353
|
+
item.status = task.state.outcome.status === "aborted" ? "cancelled" : "failed";
|
|
1354
|
+
item.endedAt ??= Date.now();
|
|
1355
|
+
}
|
|
1356
|
+
if (identity.activeRoundId === item.taskId && idle && (!runner || runner.state.status === "terminal"))
|
|
1357
|
+
identity.activeRoundId = null;
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
return JSON.parse(JSON.stringify(doc.items));
|
|
1361
|
+
}
|
|
1362
|
+
function summarizeSubagents(rounds, identities = []) {
|
|
1363
|
+
const groups = new Map;
|
|
1364
|
+
for (const round of rounds) {
|
|
1365
|
+
const id = round.agentId ?? agentIdFor(round.childId);
|
|
1366
|
+
groups.set(id, [...groups.get(id) ?? [], round]);
|
|
1367
|
+
}
|
|
1368
|
+
return [...groups].map(([agentId, entries]) => {
|
|
1369
|
+
const first = entries[0];
|
|
1370
|
+
const last = entries.at(-1);
|
|
1371
|
+
const identity = identities.find((a) => a.agentId === agentId);
|
|
1372
|
+
const active = entries.find((r) => identity ? r.taskId === identity.activeRoundId : r.status === "running" || r.status === "queued");
|
|
1373
|
+
return {
|
|
1374
|
+
agentId,
|
|
1375
|
+
parentId: first.parentId,
|
|
1376
|
+
childId: first.childId,
|
|
1377
|
+
role: first.role,
|
|
1378
|
+
title: identity?.title ?? first.title,
|
|
1379
|
+
status: active ? active.status === "queued" ? "queued" : "running" : "idle",
|
|
1380
|
+
activeRoundId: active?.taskId ?? null,
|
|
1381
|
+
lastRoundId: last.taskId,
|
|
1382
|
+
lastReport: last.report?.slice(0, 512) ?? null,
|
|
1383
|
+
roundCount: entries.length,
|
|
1384
|
+
taskTitle: (active ?? last).title,
|
|
1385
|
+
approvalCount: active?.approvalCount ?? 0,
|
|
1386
|
+
createdAt: first.createdAt,
|
|
1387
|
+
...first.historyThrough !== undefined ? { historyThrough: first.historyThrough, historyUsage: first.historyUsage } : {}
|
|
1388
|
+
};
|
|
1389
|
+
});
|
|
1390
|
+
}
|
|
1391
|
+
async function acquireSlot(tx, round, cwd) {
|
|
1392
|
+
const slots = await tx.doc(SlotsDoc);
|
|
1393
|
+
const live = [];
|
|
1394
|
+
for (const slot of slots.items) {
|
|
1395
|
+
const task = await tx.task(slot.taskId);
|
|
1396
|
+
if (task && task.state.status !== "terminal")
|
|
1397
|
+
live.push(slot);
|
|
1398
|
+
}
|
|
1399
|
+
slots.items = live;
|
|
1400
|
+
if (live.some((slot) => slot.taskId === round.taskId))
|
|
1401
|
+
return true;
|
|
1402
|
+
if (live.filter((slot) => slot.parentId === round.parentId).length >= 3 || round.role === "coder" && live.some((slot) => slot.coder && slot.cwd === cwd))
|
|
1403
|
+
return false;
|
|
1404
|
+
slots.items.push({ taskId: round.taskId, parentId: round.parentId, cwd, coder: round.role === "coder" });
|
|
1405
|
+
return true;
|
|
1406
|
+
}
|
|
1407
|
+
var actionParameters = Type2.Union([
|
|
1408
|
+
Type2.Object({
|
|
1409
|
+
action: Type2.Optional(Type2.Literal("create")),
|
|
1410
|
+
type: Type2.Enum(["explore", "plan", "coder"]),
|
|
1411
|
+
title: Type2.String({ minLength: 1, maxLength: 80 }),
|
|
1412
|
+
task: Type2.String({ minLength: 1, maxLength: 32768 })
|
|
1413
|
+
}, { additionalProperties: false }),
|
|
1414
|
+
Type2.Object({
|
|
1415
|
+
action: Type2.Literal("continue"),
|
|
1416
|
+
agentId: Type2.String(),
|
|
1417
|
+
title: Type2.String({ minLength: 1, maxLength: 80 }),
|
|
1418
|
+
task: Type2.String({ minLength: 1, maxLength: 32768 })
|
|
1419
|
+
}, { additionalProperties: false }),
|
|
1420
|
+
Type2.Object({
|
|
1421
|
+
action: Type2.Literal("list"),
|
|
1422
|
+
cursor: Type2.Optional(Type2.Integer({ minimum: 0 })),
|
|
1423
|
+
limit: Type2.Optional(Type2.Integer({ minimum: 1, maximum: 100 }))
|
|
1424
|
+
}, { additionalProperties: false })
|
|
1425
|
+
]);
|
|
1426
|
+
var parameters = Type2.Object({
|
|
1427
|
+
action: Type2.Optional(Type2.String({ enum: ["create", "continue", "list"], description: "Defaults to create." })),
|
|
1428
|
+
type: Type2.Optional(Type2.String({ enum: ["explore", "plan", "coder"], description: "Required for create only." })),
|
|
1429
|
+
agentId: Type2.Optional(Type2.String({ description: "Required for continue only; use an ID returned by create/list." })),
|
|
1430
|
+
title: Type2.Optional(Type2.String({ minLength: 1, maxLength: 80, description: "Required for create/continue." })),
|
|
1431
|
+
task: Type2.Optional(Type2.String({ minLength: 1, maxLength: 32768, description: "Required for create/continue." })),
|
|
1432
|
+
cursor: Type2.Optional(Type2.Integer({ minimum: 0, description: "List pagination cursor only." })),
|
|
1433
|
+
limit: Type2.Optional(Type2.Integer({ minimum: 1, maximum: 100, description: "List page size only." }))
|
|
1434
|
+
}, { additionalProperties: false });
|
|
1435
|
+
var structuredOutputSchema = Type2.Union([
|
|
1436
|
+
Type2.Object({
|
|
1437
|
+
agentId: Type2.String(),
|
|
1438
|
+
roundId: Type2.Integer(),
|
|
1439
|
+
status: Type2.Enum(["completed", "failed", "cancelled"]),
|
|
1440
|
+
report: Type2.String()
|
|
1441
|
+
}, { additionalProperties: false }),
|
|
1442
|
+
Type2.Object({
|
|
1443
|
+
items: Type2.Array(Type2.Object({
|
|
1444
|
+
agentId: Type2.String(),
|
|
1445
|
+
parentId: Type2.Integer(),
|
|
1446
|
+
childId: Type2.Integer(),
|
|
1447
|
+
role: Type2.Enum(["explore", "plan", "coder"]),
|
|
1448
|
+
title: Type2.String(),
|
|
1449
|
+
status: Type2.Enum(["idle", "queued", "running"]),
|
|
1450
|
+
activeRoundId: Type2.Union([Type2.Integer(), Type2.Null()]),
|
|
1451
|
+
lastRoundId: Type2.Integer(),
|
|
1452
|
+
lastReport: Type2.Optional(Type2.Union([Type2.String(), Type2.Null()])),
|
|
1453
|
+
roundCount: Type2.Integer(),
|
|
1454
|
+
taskTitle: Type2.String(),
|
|
1455
|
+
approvalCount: Type2.Integer(),
|
|
1456
|
+
createdAt: Type2.Number()
|
|
1457
|
+
}, { additionalProperties: false })),
|
|
1458
|
+
next: Type2.Union([Type2.Integer(), Type2.Null()])
|
|
1459
|
+
}, { additionalProperties: false })
|
|
1460
|
+
]);
|
|
1461
|
+
function createSubagentExtension(prepare, discard) {
|
|
1462
|
+
const finish = async (tx, input, status, report) => {
|
|
1463
|
+
const round = (await tx.doc(DelegationsDoc, input.parentId)).items.find((r) => r.taskId === input.taskId);
|
|
1464
|
+
round.status = status;
|
|
1465
|
+
round.report = report;
|
|
1466
|
+
round.endedAt ??= Date.now();
|
|
1467
|
+
round.usageEnd = JSON.parse(JSON.stringify(await tx.doc(UsageDoc, input.childId)));
|
|
1468
|
+
round.roundUsage = round.usageStart ? subagentUsageTotals(subtractSubagentUsage(round.usageEnd, round.usageStart)) : round.startedAt === null ? subagentUsageTotals(emptySubagentUsage()) : null;
|
|
1469
|
+
};
|
|
1470
|
+
const RoundTask = defineTask({
|
|
1471
|
+
name: "app.neko.subagent-round",
|
|
1472
|
+
version: 1,
|
|
1473
|
+
initial: () => ({ phase: "admission" }),
|
|
1474
|
+
phases: {
|
|
1475
|
+
admission: async (task, runtime, ctx) => {
|
|
1476
|
+
const env = await runtime.env(ctx);
|
|
1477
|
+
const canonical = env && await env.canonicalPath(task.input.cwd, ctx);
|
|
1478
|
+
const slotCwd = canonical?.ok ? canonical.value : task.input.cwd;
|
|
1479
|
+
let admitted = false;
|
|
1480
|
+
while (!admitted) {
|
|
1481
|
+
await runtime.commit(async (tx) => {
|
|
1482
|
+
const round = (await tx.doc(DelegationsDoc, task.input.parentId)).items.find((r) => r.taskId === task.input.taskId);
|
|
1483
|
+
if (!await acquireSlot(tx, round, slotCwd))
|
|
1484
|
+
return;
|
|
1485
|
+
admitted = true;
|
|
1486
|
+
const duplicate = await tx.submissionByRequest(task.input.childId, `subagent:${round.taskId}`);
|
|
1487
|
+
if (!duplicate && !round.usageStart)
|
|
1488
|
+
round.usageStart = JSON.parse(JSON.stringify(await tx.doc(UsageDoc, task.input.childId)));
|
|
1489
|
+
round.status = "running";
|
|
1490
|
+
round.startedAt ??= Date.now();
|
|
1491
|
+
return { status: "running", checkpoint: { phase: "deliver" } };
|
|
1492
|
+
}, ctx);
|
|
1493
|
+
if (!admitted)
|
|
1494
|
+
await runtime.sleep(runtime.now() + 100, ctx);
|
|
1495
|
+
}
|
|
1496
|
+
},
|
|
1497
|
+
deliver: async (task, runtime, ctx) => {
|
|
1498
|
+
const input = task.input;
|
|
1499
|
+
const child = await runtime.conversation(input.childId, ctx);
|
|
1500
|
+
const requestId = `subagent:${input.taskId}`;
|
|
1501
|
+
try {
|
|
1502
|
+
let duplicate = false;
|
|
1503
|
+
await runtime.commit(async (tx) => {
|
|
1504
|
+
duplicate = !!await tx.submissionByRequest(input.childId, requestId);
|
|
1505
|
+
}, ctx);
|
|
1506
|
+
if (!duplicate)
|
|
1507
|
+
await prepare(input.childId, requestId, input.cwd);
|
|
1508
|
+
const submission = await child.submit({
|
|
1509
|
+
type: "input",
|
|
1510
|
+
content: input.task,
|
|
1511
|
+
requestId,
|
|
1512
|
+
whenBusy: "reject",
|
|
1513
|
+
entry: {
|
|
1514
|
+
kind: NekoInput.kind,
|
|
1515
|
+
data: { at: Date.now(), prompt: input.task, images: [], subagentRoundId: input.taskId }
|
|
1516
|
+
}
|
|
1517
|
+
}, ctx);
|
|
1518
|
+
const placed = await submission.status(ctx);
|
|
1519
|
+
await runtime.commit(async (tx) => {
|
|
1520
|
+
if (placed.type === "input" && placed.entry !== undefined)
|
|
1521
|
+
(await tx.doc(DelegationsDoc, input.parentId)).items.find((r) => r.taskId === input.taskId).inputEntryId = placed.entry;
|
|
1522
|
+
return { status: "running", checkpoint: { phase: "settle" } };
|
|
1523
|
+
}, ctx);
|
|
1524
|
+
} catch (error) {
|
|
1525
|
+
discard(input.childId, requestId);
|
|
1526
|
+
if (ctx.abortSignal?.aborted)
|
|
1527
|
+
throw error;
|
|
1528
|
+
await child.abort(ctx);
|
|
1529
|
+
const report = `Subagent failed: ${String(error)}`;
|
|
1530
|
+
await runtime.commit(async (tx) => {
|
|
1531
|
+
await finish(tx, input, "failed", report);
|
|
1532
|
+
return { status: "terminal", outcome: { status: "completed", result: { status: "failed", report } } };
|
|
1533
|
+
}, ctx);
|
|
1534
|
+
}
|
|
1535
|
+
},
|
|
1536
|
+
settle: async (task, runtime, ctx) => {
|
|
1537
|
+
const input = task.input;
|
|
1538
|
+
const child = await runtime.conversation(input.childId, ctx);
|
|
1539
|
+
const submission = await child.submit({ type: "input", content: input.task, requestId: `subagent:${input.taskId}` }, ctx);
|
|
1540
|
+
const settled = await submission.wait(ctx);
|
|
1541
|
+
await child.waitForIdle(ctx);
|
|
1542
|
+
await runtime.commit(async (tx) => {
|
|
1543
|
+
let report = "";
|
|
1544
|
+
let status = "failed";
|
|
1545
|
+
const round = (await tx.doc(DelegationsDoc, input.parentId)).items.find((r) => r.taskId === input.taskId);
|
|
1546
|
+
if (settled.type === "input" && settled.entry !== undefined)
|
|
1547
|
+
round.inputEntryId = settled.entry;
|
|
1548
|
+
if (settled.status === "done" && settled.type === "input") {
|
|
1549
|
+
round.answerEntryId = settled.answer;
|
|
1550
|
+
const message = (await tx.entry(AssistantEntry, settled.answer))?.model?.[0];
|
|
1551
|
+
report = message?.role === "assistant" ? message.content.flatMap((b) => b.type === "text" ? [b.text] : []).join("") : "";
|
|
1552
|
+
status = "completed";
|
|
1553
|
+
} else if (settled.status === "unanswered") {
|
|
1554
|
+
status = settled.reason === "aborted" ? "cancelled" : "failed";
|
|
1555
|
+
report = `Subagent ${status}: ${settled.reason}`;
|
|
1556
|
+
}
|
|
1557
|
+
await finish(tx, input, status, report);
|
|
1558
|
+
return { status: "terminal", outcome: { status: "completed", result: { status, report } } };
|
|
1559
|
+
}, ctx);
|
|
1560
|
+
}
|
|
1561
|
+
},
|
|
1562
|
+
abort: async (task, runtime, ctx) => {
|
|
1563
|
+
const input = task.input;
|
|
1564
|
+
let own = false;
|
|
1565
|
+
await runtime.commit(async (tx) => {
|
|
1566
|
+
own = (await tx.doc(SubagentsDoc, input.parentId)).items.some((a) => a.childId === input.childId && a.activeRoundId === input.taskId);
|
|
1567
|
+
}, ctx);
|
|
1568
|
+
if (own)
|
|
1569
|
+
await (await runtime.conversation(input.childId, ctx))?.abort(ctx);
|
|
1570
|
+
await runtime.commit(async (tx) => {
|
|
1571
|
+
await finish(tx, input, "cancelled", "Subagent cancelled");
|
|
1572
|
+
return { status: "terminal", outcome: { status: "aborted" } };
|
|
1573
|
+
}, ctx);
|
|
1574
|
+
}
|
|
1575
|
+
});
|
|
1576
|
+
const extension = defineExtension3({
|
|
1577
|
+
name: "neko-subagents",
|
|
1578
|
+
tasks: [RoundTask],
|
|
1579
|
+
tools: [
|
|
1580
|
+
defineTool2({
|
|
1581
|
+
name: "subagent",
|
|
1582
|
+
description: "Manage persistent foreground subagents. create (default): type explore/plan/coder, title, task. continue: agentId, title, task; reuses its own context, only while idle. list: discover this session's agents, optional cursor/limit. Explore Bash requires per-call user approval. Keep constraints explicit; verify and integrate final reports. Subagents cannot delegate.",
|
|
1583
|
+
parameters,
|
|
1584
|
+
structuredOutputSchema,
|
|
1585
|
+
replay: "safe",
|
|
1586
|
+
execute: async (input, api, context) => {
|
|
1587
|
+
if (!Value.Check(actionParameters, input))
|
|
1588
|
+
throw new Error("Invalid subagent arguments. create requires type/title/task; continue requires agentId/title/task; list accepts only cursor/limit. Do not mix fields from different actions.");
|
|
1589
|
+
const args = input;
|
|
1590
|
+
if ((await api.snapshot(SubagentDoc, api.conversationId, context))?.role)
|
|
1591
|
+
throw new Error("Subagents cannot delegate.");
|
|
1592
|
+
if (args.action === "list") {
|
|
1593
|
+
const list = await api.commit(async (tx) => {
|
|
1594
|
+
const rounds = await refreshDelegations(tx, api.conversationId);
|
|
1595
|
+
return summarizeSubagents(rounds, (await tx.doc(SubagentsDoc, api.conversationId)).items).filter((a) => a.historyThrough === undefined);
|
|
1596
|
+
}, context);
|
|
1597
|
+
const offset = args.cursor ?? 0;
|
|
1598
|
+
const items = list.slice(offset, offset + (args.limit ?? 50));
|
|
1599
|
+
const result = { items, next: offset + items.length < list.length ? offset + items.length : null };
|
|
1600
|
+
return { output: [{ type: "text", text: JSON.stringify(result) }], structuredOutput: result };
|
|
1601
|
+
}
|
|
1602
|
+
if (!args.task.trim() || !args.title.trim() || Buffer.byteLength(args.task) > 32768)
|
|
1603
|
+
throw new Error("Task/title must be nonempty; task limit is 32 KiB.");
|
|
1604
|
+
const agent = await api.agent(context);
|
|
1605
|
+
const round = await api.commit(async (tx) => {
|
|
1606
|
+
const rounds = await refreshDelegations(tx, api.conversationId);
|
|
1607
|
+
const receipt = rounds.find((r) => r.taskId === api.taskId);
|
|
1608
|
+
if (receipt?.runnerId !== undefined)
|
|
1609
|
+
return receipt;
|
|
1610
|
+
const identities = await tx.doc(SubagentsDoc, api.conversationId);
|
|
1611
|
+
let identity;
|
|
1612
|
+
if (args.action === "continue") {
|
|
1613
|
+
identity = identities.items.find((a) => a.agentId === args.agentId);
|
|
1614
|
+
if (!identity)
|
|
1615
|
+
throw new Error("Unknown subagent or it belongs to another conversation.");
|
|
1616
|
+
const info = await tx.doc(SubagentDoc, identity.childId);
|
|
1617
|
+
if (info.parentId !== api.conversationId || info.role !== identity.role)
|
|
1618
|
+
throw new Error("Subagent ownership mismatch.");
|
|
1619
|
+
if (identity.activeRoundId !== null)
|
|
1620
|
+
throw new Error("Subagent is busy; continue only when idle.");
|
|
1621
|
+
} else if (receipt) {
|
|
1622
|
+
identity = identities.items.find((a) => a.childId === receipt.childId);
|
|
1623
|
+
}
|
|
1624
|
+
const existing = identity ? undefined : (await tx.scanConversations({ ownerTaskId: api.taskId }, 1)).items[0];
|
|
1625
|
+
const child = identity ? await tx.conversation(identity.childId) : existing;
|
|
1626
|
+
if (!identity) {
|
|
1627
|
+
const created = child ?? await tx.createConversation({ ownership: { kind: "task", taskId: api.taskId } });
|
|
1628
|
+
const role = args.action === "continue" ? "explore" : args.type;
|
|
1629
|
+
await configure(tx, created.id, {
|
|
1630
|
+
model: agent.model,
|
|
1631
|
+
thinkingLevel: agent.thinkingLevel,
|
|
1632
|
+
cwd: agent.cwd,
|
|
1633
|
+
extensions: agent.extensions.filter((e) => e.name !== extension.name),
|
|
1634
|
+
tools: agent.tools.filter((t) => t.name !== "subagent" && (role === "coder" || READ_TOOLS.has(t.name) || role === "explore" && t.name === "bash"))
|
|
1635
|
+
});
|
|
1636
|
+
Object.assign(await tx.doc(SubagentDoc, created.id), { role, parentId: api.conversationId });
|
|
1637
|
+
identity = {
|
|
1638
|
+
agentId: agentIdFor(created.id),
|
|
1639
|
+
parentId: api.conversationId,
|
|
1640
|
+
childId: created.id,
|
|
1641
|
+
role,
|
|
1642
|
+
cwd: agent.cwd ?? "",
|
|
1643
|
+
title: args.title,
|
|
1644
|
+
createdAt: Date.now(),
|
|
1645
|
+
activeRoundId: null
|
|
1646
|
+
};
|
|
1647
|
+
identities.items.push(identity);
|
|
1648
|
+
}
|
|
1649
|
+
const record = receipt ?? {
|
|
1650
|
+
parentId: api.conversationId,
|
|
1651
|
+
taskId: api.taskId,
|
|
1652
|
+
callId: api.callId,
|
|
1653
|
+
childId: identity.childId,
|
|
1654
|
+
agentId: identity.agentId,
|
|
1655
|
+
round: rounds.filter((r) => r.childId === identity.childId).length + 1,
|
|
1656
|
+
role: identity.role,
|
|
1657
|
+
title: args.title,
|
|
1658
|
+
createdAt: Date.now(),
|
|
1659
|
+
startedAt: null,
|
|
1660
|
+
endedAt: null,
|
|
1661
|
+
status: "queued",
|
|
1662
|
+
report: null
|
|
1663
|
+
};
|
|
1664
|
+
identities.items.find((a) => a.agentId === identity.agentId).activeRoundId = api.taskId;
|
|
1665
|
+
const runner = await tx.createTask(RoundTask, {
|
|
1666
|
+
parentId: api.conversationId,
|
|
1667
|
+
childId: identity.childId,
|
|
1668
|
+
taskId: api.taskId,
|
|
1669
|
+
task: args.task,
|
|
1670
|
+
cwd: identity.cwd || agent.cwd || ""
|
|
1671
|
+
}, { ownership: { kind: "task", taskId: api.taskId } });
|
|
1672
|
+
record.runnerId = runner;
|
|
1673
|
+
const doc = await tx.doc(DelegationsDoc, api.conversationId);
|
|
1674
|
+
if (receipt)
|
|
1675
|
+
Object.assign(doc.items.find((r) => r.taskId === receipt.taskId), record);
|
|
1676
|
+
else
|
|
1677
|
+
doc.items.push(record);
|
|
1678
|
+
return record;
|
|
1679
|
+
}, context);
|
|
1680
|
+
await api.details({
|
|
1681
|
+
conversationId: round.childId,
|
|
1682
|
+
agentId: round.agentId,
|
|
1683
|
+
roundId: round.taskId,
|
|
1684
|
+
role: round.role,
|
|
1685
|
+
title: round.title
|
|
1686
|
+
}, context);
|
|
1687
|
+
const settled = await api.waitForTask(round.runnerId, context);
|
|
1688
|
+
const outcome = settled.state.outcome;
|
|
1689
|
+
const result = outcome.status === "completed" ? outcome.result : { status: outcome.status === "aborted" ? "cancelled" : "failed", report: `Subagent ${outcome.status}` };
|
|
1690
|
+
return {
|
|
1691
|
+
output: [
|
|
1692
|
+
{
|
|
1693
|
+
type: "text",
|
|
1694
|
+
text: `Agent: ${round.agentId}
|
|
1695
|
+
Status: ${result.status}
|
|
1696
|
+
${result.report || "Subagent completed without a text report."}`
|
|
1697
|
+
}
|
|
1698
|
+
],
|
|
1699
|
+
isError: result.status !== "completed",
|
|
1700
|
+
structuredOutput: { agentId: round.agentId, roundId: round.taskId, ...result },
|
|
1701
|
+
details: {
|
|
1702
|
+
conversationId: round.childId,
|
|
1703
|
+
agentId: round.agentId,
|
|
1704
|
+
roundId: round.taskId,
|
|
1705
|
+
role: round.role,
|
|
1706
|
+
title: round.title,
|
|
1707
|
+
status: result.status
|
|
1708
|
+
}
|
|
1709
|
+
};
|
|
1710
|
+
}
|
|
1711
|
+
})
|
|
1712
|
+
]
|
|
1713
|
+
});
|
|
1714
|
+
return extension;
|
|
1715
|
+
}
|
|
1716
|
+
async function listDelegations(harness, parent, context) {
|
|
1717
|
+
const rounds = await harness.commit((tx) => refreshDelegations(tx, parent), context);
|
|
1718
|
+
for (const round of rounds) {
|
|
1719
|
+
if (round.historyThrough !== undefined)
|
|
1720
|
+
continue;
|
|
1721
|
+
const task = await harness.getTask(round.taskId, context);
|
|
1722
|
+
if (task?.state.status !== "terminal")
|
|
1723
|
+
continue;
|
|
1724
|
+
const child = await harness.conversation(round.childId, context);
|
|
1725
|
+
const live = await harness.snapshot(LiveDoc, round.childId, context);
|
|
1726
|
+
const identity = (await harness.snapshot(SubagentsDoc, parent, context))?.items.find((a) => a.childId === round.childId);
|
|
1727
|
+
if (identity?.activeRoundId === round.taskId && live?.run !== undefined)
|
|
1728
|
+
await child?.abort(context);
|
|
1729
|
+
}
|
|
1730
|
+
return harness.commit((tx) => refreshDelegations(tx, parent), context);
|
|
1731
|
+
}
|
|
1732
|
+
async function installSubagentCoordinator(harness, parents, context) {
|
|
1733
|
+
const known = new Set(parents);
|
|
1734
|
+
let running = false;
|
|
1735
|
+
let dirty = false;
|
|
1736
|
+
const reconcile = async () => {
|
|
1737
|
+
if (running) {
|
|
1738
|
+
dirty = true;
|
|
1739
|
+
return;
|
|
1740
|
+
}
|
|
1741
|
+
running = true;
|
|
1742
|
+
try {
|
|
1743
|
+
do {
|
|
1744
|
+
dirty = false;
|
|
1745
|
+
for (const id of known)
|
|
1746
|
+
await listDelegations(harness, id, context);
|
|
1747
|
+
} while (dirty);
|
|
1748
|
+
} finally {
|
|
1749
|
+
running = false;
|
|
1750
|
+
}
|
|
1751
|
+
};
|
|
1752
|
+
for (const parent of parents) {
|
|
1753
|
+
await harness.commit((tx) => refreshDelegations(tx, parent), context);
|
|
1754
|
+
for (const identity of (await harness.snapshot(SubagentsDoc, parent, context))?.items ?? []) {
|
|
1755
|
+
if (identity.cwd)
|
|
1756
|
+
continue;
|
|
1757
|
+
const child = await harness.conversation(identity.childId, context);
|
|
1758
|
+
const cwd = (await child?.agent(context))?.cwd;
|
|
1759
|
+
if (cwd)
|
|
1760
|
+
await harness.commit(async (tx) => {
|
|
1761
|
+
(await tx.doc(SubagentsDoc, parent)).items.find((a) => a.agentId === identity.agentId).cwd = cwd;
|
|
1762
|
+
}, context);
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
const stop = harness.subscribeCommits(({ changes }) => {
|
|
1766
|
+
for (const c of changes)
|
|
1767
|
+
if (c.type === "document" && c.record.kind === "app.neko.delegations" && c.conversationId !== undefined)
|
|
1768
|
+
known.add(c.conversationId);
|
|
1769
|
+
if (changes.some((c) => c.type === "task" && c.value.state.status === "terminal"))
|
|
1770
|
+
reconcile().catch(() => {});
|
|
1771
|
+
});
|
|
1772
|
+
harness.closed.finally(stop).catch(() => {});
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
// packages/core/src/prompt/prompt.ts
|
|
1049
1776
|
var KEYS = ["preamble", "rules", "project_context", "skills", "cwd"];
|
|
1050
1777
|
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
1778
|
var TOOL_GUIDELINES = {
|
|
1779
|
+
subagent: [
|
|
1780
|
+
"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. For related follow-up work, list and continue a suitable idle subagent using its stable agentId; busy means no task was accepted. Do not change its role. Verify and integrate reports. Do not modify files concurrently with a coder assigned those files."
|
|
1781
|
+
],
|
|
1782
|
+
find_files: [
|
|
1783
|
+
"Use find_files to discover files; query is a literal substring. Narrow the directory when results are partial."
|
|
1784
|
+
],
|
|
1785
|
+
search_text: [
|
|
1786
|
+
"Use search_text for literal text search and read for surrounding evidence. Partial results do not prove absence."
|
|
1787
|
+
],
|
|
1052
1788
|
read: ["Use read to examine files instead of cat or sed."],
|
|
1053
1789
|
write: ["Use write only for new files or complete rewrites."],
|
|
1054
1790
|
edit: [
|
|
@@ -1084,11 +1820,12 @@ ${content}
|
|
|
1084
1820
|
`);
|
|
1085
1821
|
var createNekoPrompt = (loadResources) => {
|
|
1086
1822
|
const built = new WeakMap;
|
|
1087
|
-
const build = async (input) => {
|
|
1823
|
+
const build = async (input, context) => {
|
|
1088
1824
|
const cwd = input.agent.cwd;
|
|
1825
|
+
const role = (await input.read.snapshot(SubagentDoc, input.conversationId, context))?.role;
|
|
1089
1826
|
const resources = cwd ? await loadResources(input.conversationId, cwd) : { contextFiles: [], skills: [] };
|
|
1090
1827
|
const sections = {
|
|
1091
|
-
preamble: PREAMBLE,
|
|
1828
|
+
preamble: role ? ROLE_PROMPTS[role] + SUBAGENT_DELIVERY : PREAMBLE,
|
|
1092
1829
|
rules: buildRules(input.agent.tools.map((tool) => tool.name)),
|
|
1093
1830
|
project_context: renderProjectContext(resources.contextFiles),
|
|
1094
1831
|
skills: formatSkillsForPrompt(resources.skills) || undefined,
|
|
@@ -1096,12 +1833,12 @@ var createNekoPrompt = (loadResources) => {
|
|
|
1096
1833
|
};
|
|
1097
1834
|
return sections;
|
|
1098
1835
|
};
|
|
1099
|
-
return
|
|
1836
|
+
return defineExtension4({
|
|
1100
1837
|
name: "neko-prompt",
|
|
1101
|
-
sections: KEYS.map((key) => section(key, async (input) => {
|
|
1838
|
+
sections: KEYS.map((key) => section(key, async (input, context) => {
|
|
1102
1839
|
let pending = built.get(input);
|
|
1103
1840
|
if (!pending) {
|
|
1104
|
-
pending = build(input);
|
|
1841
|
+
pending = build(input, context);
|
|
1105
1842
|
built.set(input, pending);
|
|
1106
1843
|
}
|
|
1107
1844
|
const text = (await pending)[key];
|
|
@@ -1248,13 +1985,13 @@ var createResourceSnapshots = (load, submission) => {
|
|
|
1248
1985
|
import { stripVTControlCharacters } from "util";
|
|
1249
1986
|
import { getSupportedThinkingLevels } from "@earendil-works/pi-ai";
|
|
1250
1987
|
import {
|
|
1251
|
-
defineDoc,
|
|
1252
|
-
defineExtension as
|
|
1253
|
-
defineTask,
|
|
1988
|
+
defineDoc as defineDoc2,
|
|
1989
|
+
defineExtension as defineExtension5,
|
|
1990
|
+
defineTask as defineTask2,
|
|
1254
1991
|
InboxDoc,
|
|
1255
|
-
LiveDoc
|
|
1992
|
+
LiveDoc as LiveDoc2
|
|
1256
1993
|
} from "@earendil-works/pi-durable";
|
|
1257
|
-
var SessionMetadataDoc =
|
|
1994
|
+
var SessionMetadataDoc = defineDoc2({
|
|
1258
1995
|
kind: "app.neko.session-metadata",
|
|
1259
1996
|
version: 1,
|
|
1260
1997
|
scope: "conversation",
|
|
@@ -1317,7 +2054,7 @@ function mergeUsage(base, extra) {
|
|
|
1317
2054
|
function namingTask() {
|
|
1318
2055
|
let active = 0;
|
|
1319
2056
|
const waits = new Set;
|
|
1320
|
-
return
|
|
2057
|
+
return defineTask2({
|
|
1321
2058
|
name: "app.neko.name",
|
|
1322
2059
|
version: 1,
|
|
1323
2060
|
initial: () => ({ phase: "wait", until: Date.now() + 100 }),
|
|
@@ -1441,7 +2178,7 @@ class SessionMetadataManager {
|
|
|
1441
2178
|
this.storage = storage;
|
|
1442
2179
|
this.context = context;
|
|
1443
2180
|
this.report = report;
|
|
1444
|
-
registry.install(
|
|
2181
|
+
registry.install(defineExtension5({ name: "neko-naming", tasks: [this.task] }));
|
|
1445
2182
|
this.#unsubscribe = harness.subscribeCommits(({ changes }) => {
|
|
1446
2183
|
const ids = new Set;
|
|
1447
2184
|
const submissions = new Map;
|
|
@@ -1531,7 +2268,7 @@ class SessionMetadataManager {
|
|
|
1531
2268
|
const answer = record.status === "done" ? (await this.storage.entry(record.answer, this.context))?.entry.model?.[0] : undefined;
|
|
1532
2269
|
return { record, data, answer: answer?.role === "assistant" ? answer : undefined };
|
|
1533
2270
|
}));
|
|
1534
|
-
const live = await this.harness.snapshot(
|
|
2271
|
+
const live = await this.harness.snapshot(LiveDoc2, id, this.context);
|
|
1535
2272
|
await this.harness.commit(async (tx) => {
|
|
1536
2273
|
const d = await tx.doc(SessionMetadataDoc, id);
|
|
1537
2274
|
for (const { record, data, answer } of updates) {
|
|
@@ -1580,9 +2317,192 @@ class SessionMetadataManager {
|
|
|
1580
2317
|
}
|
|
1581
2318
|
}
|
|
1582
2319
|
|
|
2320
|
+
// packages/core/src/tool-approvals.ts
|
|
2321
|
+
import { randomUUID } from "crypto";
|
|
2322
|
+
import { realpath } from "fs/promises";
|
|
2323
|
+
import {
|
|
2324
|
+
configure as configure2,
|
|
2325
|
+
defineDoc as defineDoc3,
|
|
2326
|
+
defineExtension as defineExtension6,
|
|
2327
|
+
hook as hook2,
|
|
2328
|
+
ToolTask as ToolTask2,
|
|
2329
|
+
wrapTool
|
|
2330
|
+
} from "@earendil-works/pi-durable";
|
|
2331
|
+
import { createBashTool } from "@earendil-works/pi-durable/tools";
|
|
2332
|
+
var ApprovalsDoc = defineDoc3({
|
|
2333
|
+
kind: "app.neko.approvals",
|
|
2334
|
+
version: 1,
|
|
2335
|
+
scope: "conversation",
|
|
2336
|
+
history: "latest",
|
|
2337
|
+
fork: "initial",
|
|
2338
|
+
initial: () => ({ items: [] })
|
|
2339
|
+
});
|
|
2340
|
+
async function live(tx, id) {
|
|
2341
|
+
const task = await tx.task(id);
|
|
2342
|
+
return !!task && !task.abortRequested && task.state.status !== "terminal" && task.state.status !== "completing";
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
class ToolApprovals {
|
|
2346
|
+
harness;
|
|
2347
|
+
#parents = new Set;
|
|
2348
|
+
constructor(harness, context) {
|
|
2349
|
+
this.harness = harness;
|
|
2350
|
+
const stop = harness.subscribeCommits(({ changes }) => {
|
|
2351
|
+
if (!changes.some((c) => c.type === "task" && (c.value.abortRequested || c.value.state.status === "terminal")))
|
|
2352
|
+
return;
|
|
2353
|
+
for (const parent of this.#parents)
|
|
2354
|
+
this.list(parent, context).catch(() => {});
|
|
2355
|
+
});
|
|
2356
|
+
harness.closed.finally(stop).catch(() => {});
|
|
2357
|
+
}
|
|
2358
|
+
async list(parent, context) {
|
|
2359
|
+
this.#parents.add(parent);
|
|
2360
|
+
return this.harness.commit(async (tx) => {
|
|
2361
|
+
const doc = await tx.doc(ApprovalsDoc, parent);
|
|
2362
|
+
for (const item of doc.items) {
|
|
2363
|
+
if (item.status === "pending" && (!await live(tx, item.id) || !await live(tx, item.delegationTaskId))) {
|
|
2364
|
+
item.status = "cancelled";
|
|
2365
|
+
item.decidedAt = Date.now();
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
return JSON.parse(JSON.stringify(doc.items));
|
|
2369
|
+
}, context);
|
|
2370
|
+
}
|
|
2371
|
+
async decide(parent, delegationTaskId, id, version, decision, context) {
|
|
2372
|
+
return this.harness.commit(async (tx) => {
|
|
2373
|
+
const delegation = (await tx.doc(DelegationsDoc, parent)).items.find((d) => d.taskId === delegationTaskId);
|
|
2374
|
+
const item = (await tx.doc(ApprovalsDoc, parent)).items.find((a) => a.id === id);
|
|
2375
|
+
if (!delegation || delegation.historyThrough !== undefined || !item || item.delegationTaskId !== delegationTaskId || item.childId !== delegation.childId || item.version !== version)
|
|
2376
|
+
throw new Error("Approval does not belong to this task or has changed");
|
|
2377
|
+
if (item.status !== "pending")
|
|
2378
|
+
return item.status;
|
|
2379
|
+
item.status = await live(tx, id) && await live(tx, delegationTaskId) ? decision : "cancelled";
|
|
2380
|
+
item.decidedAt = Date.now();
|
|
2381
|
+
return item.status;
|
|
2382
|
+
}, context);
|
|
2383
|
+
}
|
|
2384
|
+
async wait(parent, id, context) {
|
|
2385
|
+
const state = await this.harness.documentState(ApprovalsDoc, parent, context);
|
|
2386
|
+
if (!state)
|
|
2387
|
+
throw new Error("Approval unavailable");
|
|
2388
|
+
try {
|
|
2389
|
+
return await new Promise((resolve, reject) => {
|
|
2390
|
+
let stop = () => {};
|
|
2391
|
+
const cleanup = () => {
|
|
2392
|
+
stop();
|
|
2393
|
+
context.abortSignal?.removeEventListener("abort", abort);
|
|
2394
|
+
};
|
|
2395
|
+
const abort = () => {
|
|
2396
|
+
cleanup();
|
|
2397
|
+
reject(context.abortSignal?.reason ?? new Error("Cancelled"));
|
|
2398
|
+
};
|
|
2399
|
+
const check = () => {
|
|
2400
|
+
const item = state.value?.items.find((a) => a.id === id);
|
|
2401
|
+
if (item && item.status !== "pending") {
|
|
2402
|
+
cleanup();
|
|
2403
|
+
resolve(structuredClone(item));
|
|
2404
|
+
}
|
|
2405
|
+
};
|
|
2406
|
+
stop = state.subscribe(check);
|
|
2407
|
+
context.abortSignal?.addEventListener("abort", abort, { once: true });
|
|
2408
|
+
if (context.abortSignal?.aborted)
|
|
2409
|
+
abort();
|
|
2410
|
+
else
|
|
2411
|
+
check();
|
|
2412
|
+
});
|
|
2413
|
+
} finally {
|
|
2414
|
+
state.dispose();
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
async ensure(parent, candidate, context) {
|
|
2418
|
+
this.#parents.add(parent);
|
|
2419
|
+
return this.harness.commit(async (tx) => {
|
|
2420
|
+
if (!await live(tx, candidate.id) || !await live(tx, candidate.delegationTaskId))
|
|
2421
|
+
throw new Error("Task stopped");
|
|
2422
|
+
const doc = await tx.doc(ApprovalsDoc, parent);
|
|
2423
|
+
const existing = doc.items.find((a) => a.id === candidate.id);
|
|
2424
|
+
if (existing) {
|
|
2425
|
+
if (existing.command !== candidate.command || existing.cwd !== candidate.cwd || existing.timeout !== candidate.timeout || existing.childId !== candidate.childId)
|
|
2426
|
+
throw new Error("Approval operation changed; a new call is required");
|
|
2427
|
+
return;
|
|
2428
|
+
}
|
|
2429
|
+
doc.items.push({
|
|
2430
|
+
...candidate,
|
|
2431
|
+
version: randomUUID(),
|
|
2432
|
+
status: "pending",
|
|
2433
|
+
createdAt: Date.now(),
|
|
2434
|
+
decidedAt: null
|
|
2435
|
+
});
|
|
2436
|
+
}, context);
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2439
|
+
function createApprovalExtension(service) {
|
|
2440
|
+
return defineExtension6({
|
|
2441
|
+
name: "neko-tool-approvals",
|
|
2442
|
+
hooks: [
|
|
2443
|
+
hook2(ToolTask2, {
|
|
2444
|
+
beforeTool: async (call, api, context) => {
|
|
2445
|
+
const role = await api.snapshot(SubagentDoc, api.conversationId, context);
|
|
2446
|
+
if (role?.role !== "explore" || call.name !== "bash")
|
|
2447
|
+
return;
|
|
2448
|
+
if (role.parentId === null)
|
|
2449
|
+
return { block: "Missing parent session" };
|
|
2450
|
+
const approvals = service(), parent = role.parentId;
|
|
2451
|
+
const child = await approvals.harness.conversation(api.conversationId, context);
|
|
2452
|
+
const agent = await child.agent(context);
|
|
2453
|
+
const cwd = await realpath(agent.cwd);
|
|
2454
|
+
const identity = (await api.snapshot(SubagentsDoc, parent, context))?.items.find((a) => a.childId === api.conversationId);
|
|
2455
|
+
const delegation = (await api.snapshot(DelegationsDoc, parent, context))?.items.find((d) => d.childId === api.conversationId && d.historyThrough === undefined && d.taskId === identity?.activeRoundId);
|
|
2456
|
+
if (!delegation)
|
|
2457
|
+
return { block: "Missing delegation" };
|
|
2458
|
+
await approvals.ensure(parent, {
|
|
2459
|
+
id: api.taskId,
|
|
2460
|
+
childId: api.conversationId,
|
|
2461
|
+
delegationTaskId: delegation.taskId,
|
|
2462
|
+
callId: call.id,
|
|
2463
|
+
command: String(call.arguments.command),
|
|
2464
|
+
cwd,
|
|
2465
|
+
timeout: typeof call.arguments.timeout === "number" ? call.arguments.timeout : null
|
|
2466
|
+
}, context);
|
|
2467
|
+
const result = await approvals.wait(parent, api.taskId, context);
|
|
2468
|
+
return result.status === "allowed" ? undefined : { block: "Command denied or cancelled by user. Use another approach or report the limitation." };
|
|
2469
|
+
}
|
|
2470
|
+
})
|
|
2471
|
+
],
|
|
2472
|
+
wraps: [
|
|
2473
|
+
wrapTool(createBashTool(), (bash) => ({
|
|
2474
|
+
...bash,
|
|
2475
|
+
execute: async (args, api, context) => {
|
|
2476
|
+
const role = await api.snapshot(SubagentDoc, api.conversationId, context);
|
|
2477
|
+
if (role?.role === "explore") {
|
|
2478
|
+
const item = role.parentId === null ? undefined : (await api.snapshot(ApprovalsDoc, role.parentId, context))?.items.find((a) => a.id === api.taskId);
|
|
2479
|
+
const cwd = api.env && await api.env.canonicalPath(api.env.cwd, context);
|
|
2480
|
+
if (!item || item.status !== "allowed" || item.command !== args.command || item.timeout !== (args.timeout ?? null) || !cwd?.ok || cwd.value !== item.cwd)
|
|
2481
|
+
throw new Error("Bash approval missing or operation changed");
|
|
2482
|
+
}
|
|
2483
|
+
context.abortSignal?.throwIfAborted();
|
|
2484
|
+
return bash.execute(args, api, context);
|
|
2485
|
+
}
|
|
2486
|
+
}))
|
|
2487
|
+
]
|
|
2488
|
+
});
|
|
2489
|
+
}
|
|
2490
|
+
async function prepareApprovalConversations(harness, ids, extension, context) {
|
|
2491
|
+
for (const id of ids) {
|
|
2492
|
+
if ((await harness.snapshot(SubagentDoc, id, context))?.role !== "explore")
|
|
2493
|
+
continue;
|
|
2494
|
+
const child = await harness.conversation(id, context);
|
|
2495
|
+
const agent = await child.agent(context);
|
|
2496
|
+
await harness.commit((tx) => configure2(tx, id, {
|
|
2497
|
+
tools: [...agent.tools.filter((t) => t.name !== "bash"), createBashTool()],
|
|
2498
|
+
extensions: { add: [extension] }
|
|
2499
|
+
}), context);
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
|
|
1583
2503
|
// packages/core/src/web-search.ts
|
|
1584
|
-
import { Type } from "@earendil-works/pi-ai";
|
|
1585
|
-
import { defineExtension as
|
|
2504
|
+
import { Type as Type3 } from "@earendil-works/pi-ai";
|
|
2505
|
+
import { defineExtension as defineExtension7, defineTool as defineTool3 } from "@earendil-works/pi-durable";
|
|
1586
2506
|
var SEARCH_URL = "https://api.tavily.com/search";
|
|
1587
2507
|
var EXTRACT_URL = "https://api.tavily.com/extract";
|
|
1588
2508
|
var REQUEST_TIMEOUT_MS = 30000;
|
|
@@ -1700,15 +2620,15 @@ function urlKey(value) {
|
|
|
1700
2620
|
}
|
|
1701
2621
|
}
|
|
1702
2622
|
function createWebSearchTools(resolveKey, fetchImpl = fetch) {
|
|
1703
|
-
return
|
|
2623
|
+
return defineExtension7({
|
|
1704
2624
|
name: "neko-web-search",
|
|
1705
2625
|
tools: [
|
|
1706
|
-
|
|
2626
|
+
defineTool3({
|
|
1707
2627
|
name: "web_search",
|
|
1708
2628
|
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:
|
|
2629
|
+
parameters: Type3.Object({
|
|
2630
|
+
query: Type3.String({ minLength: 1, maxLength: 4096, description: "The search query" }),
|
|
2631
|
+
maxResults: Type3.Optional(Type3.Number({ description: `Maximum number of results (1-${MAX_RESULTS}, default ${DEFAULT_RESULTS})` }))
|
|
1712
2632
|
}),
|
|
1713
2633
|
async execute({ query, maxResults }, _api, context) {
|
|
1714
2634
|
try {
|
|
@@ -1733,11 +2653,11 @@ ${truncate(String(item.url ?? ""), 300)}
|
|
|
1733
2653
|
}
|
|
1734
2654
|
}
|
|
1735
2655
|
}),
|
|
1736
|
-
|
|
2656
|
+
defineTool3({
|
|
1737
2657
|
name: "web_extract",
|
|
1738
2658
|
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:
|
|
2659
|
+
parameters: Type3.Object({
|
|
2660
|
+
urls: Type3.Array(Type3.String({ maxLength: 2048, description: "http(s) URL" }), {
|
|
1741
2661
|
minItems: 1,
|
|
1742
2662
|
maxItems: MAX_URLS,
|
|
1743
2663
|
description: `URLs to extract (1-${MAX_URLS})`
|
|
@@ -1782,10 +2702,10 @@ ${truncate(String(item.url ?? ""), 300)}
|
|
|
1782
2702
|
}
|
|
1783
2703
|
|
|
1784
2704
|
// packages/core/src/workspace-files.ts
|
|
1785
|
-
import { realpath, stat } from "fs/promises";
|
|
2705
|
+
import { realpath as realpath2, stat } from "fs/promises";
|
|
1786
2706
|
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";
|
|
2707
|
+
import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT2, withAbortSignal as withAbortSignal2 } from "@earendil-works/chord/context";
|
|
2708
|
+
import { getOrThrow as getOrThrow2 } from "@earendil-works/pi-durable/env";
|
|
1789
2709
|
import { Connection, LocalExecutionEnv, localDaemon } from "@earendil-works/pi-env";
|
|
1790
2710
|
function relativeQuery(query) {
|
|
1791
2711
|
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 +2713,8 @@ function relativeQuery(query) {
|
|
|
1793
2713
|
return query.replace(/^\.\//, "");
|
|
1794
2714
|
}
|
|
1795
2715
|
async function workspaceTarget(root, path) {
|
|
1796
|
-
const base = await
|
|
1797
|
-
const target = await
|
|
2716
|
+
const base = await realpath2(root);
|
|
2717
|
+
const target = await realpath2(resolve3(base, relativeQuery(path)));
|
|
1798
2718
|
const local = relative(base, target);
|
|
1799
2719
|
if (local === ".." || local.startsWith(`..${sep}`) || isAbsolute2(local))
|
|
1800
2720
|
throw new Error("Path is outside the workspace.");
|
|
@@ -1903,7 +2823,7 @@ class WorkspaceFiles {
|
|
|
1903
2823
|
}
|
|
1904
2824
|
#disposeWatch(watch) {
|
|
1905
2825
|
watch.controller.abort();
|
|
1906
|
-
const closing = watch.work.catch(() => {}).then(() => watch.watcher?.close(
|
|
2826
|
+
const closing = watch.work.catch(() => {}).then(() => watch.watcher?.close(BACKGROUND_CONTEXT2)).then(() => {}).catch(() => {}).finally(() => this.#disposing.delete(closing));
|
|
1907
2827
|
this.#disposing.add(closing);
|
|
1908
2828
|
}
|
|
1909
2829
|
#closeWatcher(item) {
|
|
@@ -1950,11 +2870,11 @@ class WorkspaceFiles {
|
|
|
1950
2870
|
for (const user of [...current.users])
|
|
1951
2871
|
this.#closeWatcher(user);
|
|
1952
2872
|
}
|
|
1953
|
-
},
|
|
2873
|
+
}, withAbortSignal2(current.controller.signal, BACKGROUND_CONTEXT2));
|
|
1954
2874
|
if (result.ok) {
|
|
1955
2875
|
current.watcher = result.value;
|
|
1956
2876
|
if (current.controller.signal.aborted)
|
|
1957
|
-
await result.value.close(
|
|
2877
|
+
await result.value.close(BACKGROUND_CONTEXT2);
|
|
1958
2878
|
} else {
|
|
1959
2879
|
forget();
|
|
1960
2880
|
}
|
|
@@ -1978,7 +2898,7 @@ class WorkspaceFiles {
|
|
|
1978
2898
|
async#scan(item) {
|
|
1979
2899
|
const controller = new AbortController;
|
|
1980
2900
|
item.controller = controller;
|
|
1981
|
-
const context =
|
|
2901
|
+
const context = withAbortSignal2(controller.signal, BACKGROUND_CONTEXT2);
|
|
1982
2902
|
while (this.#active >= this.limits.concurrent) {
|
|
1983
2903
|
await new Promise((resolve, reject) => {
|
|
1984
2904
|
const wake = () => {
|
|
@@ -2023,7 +2943,7 @@ class WorkspaceFiles {
|
|
|
2023
2943
|
i.generation++;
|
|
2024
2944
|
}
|
|
2025
2945
|
}
|
|
2026
|
-
const listing =
|
|
2946
|
+
const listing = getOrThrow2(result);
|
|
2027
2947
|
controller.signal.throwIfAborted();
|
|
2028
2948
|
if (this.#closed || item.generation !== generation)
|
|
2029
2949
|
throw new Error("Workspace changed during file search. Retry.");
|
|
@@ -2043,7 +2963,7 @@ class WorkspaceFiles {
|
|
|
2043
2963
|
throw new Error("File environment closed");
|
|
2044
2964
|
query = relativeQuery(query);
|
|
2045
2965
|
const slash = query.lastIndexOf("/"), prefix = slash < 0 ? "" : query.slice(0, slash + 1), needle = query.slice(slash + 1);
|
|
2046
|
-
root = await
|
|
2966
|
+
root = await realpath2(root);
|
|
2047
2967
|
const base = await workspaceTarget(root, prefix);
|
|
2048
2968
|
if (!(await stat(base)).isDirectory())
|
|
2049
2969
|
throw new Error("Workspace directory is unavailable.");
|
|
@@ -2144,7 +3064,7 @@ function cancellable(work, signal) {
|
|
|
2144
3064
|
// packages/core/src/runtime.ts
|
|
2145
3065
|
var DEFAULT_MODEL = { provider: "deepseek", modelId: "deepseek-flash" };
|
|
2146
3066
|
var DEFAULT_STORAGE_PATH = join5(homedir4(), ".neko", "neko.sqlite");
|
|
2147
|
-
var ArchivedDoc =
|
|
3067
|
+
var ArchivedDoc = defineDoc4({
|
|
2148
3068
|
kind: "app.neko.conversation",
|
|
2149
3069
|
version: 1,
|
|
2150
3070
|
scope: "conversation",
|
|
@@ -2152,7 +3072,7 @@ var ArchivedDoc = defineDoc2({
|
|
|
2152
3072
|
fork: "initial",
|
|
2153
3073
|
initial: () => ({ archived: false })
|
|
2154
3074
|
});
|
|
2155
|
-
var WorkspacesDoc =
|
|
3075
|
+
var WorkspacesDoc = defineDoc4({
|
|
2156
3076
|
kind: "app.neko.workspaces",
|
|
2157
3077
|
version: 1,
|
|
2158
3078
|
scope: "conversation",
|
|
@@ -2160,7 +3080,7 @@ var WorkspacesDoc = defineDoc2({
|
|
|
2160
3080
|
fork: "initial",
|
|
2161
3081
|
initial: () => ({ items: [] })
|
|
2162
3082
|
});
|
|
2163
|
-
var ActivityDoc =
|
|
3083
|
+
var ActivityDoc = defineDoc4({
|
|
2164
3084
|
kind: "app.neko.activity",
|
|
2165
3085
|
version: 1,
|
|
2166
3086
|
scope: "conversation",
|
|
@@ -2169,7 +3089,7 @@ var ActivityDoc = defineDoc2({
|
|
|
2169
3089
|
initial: () => ({ lastUsedAt: 0 })
|
|
2170
3090
|
});
|
|
2171
3091
|
async function createNekoRuntime(options = {}) {
|
|
2172
|
-
const context = options.context ??
|
|
3092
|
+
const context = options.context ?? BACKGROUND_CONTEXT3;
|
|
2173
3093
|
const defaultCwd = normalizeWorkspace(options.cwd);
|
|
2174
3094
|
const model = options.model ?? DEFAULT_MODEL;
|
|
2175
3095
|
const providers = options.providers ?? [];
|
|
@@ -2205,6 +3125,7 @@ async function createNekoRuntime(options = {}) {
|
|
|
2205
3125
|
const registry = createRegistry();
|
|
2206
3126
|
registry.install(createCodingTools({ images: await createNodePhotonImages() }));
|
|
2207
3127
|
registry.install(ImageDetails);
|
|
3128
|
+
registry.install(AgentFileSearch);
|
|
2208
3129
|
registry.install(createWebSearchTools(async () => {
|
|
2209
3130
|
const saved = await effectiveCredentials.read("tavily");
|
|
2210
3131
|
if (saved?.type === "api_key" && saved.key)
|
|
@@ -2214,8 +3135,14 @@ async function createNekoRuntime(options = {}) {
|
|
|
2214
3135
|
const loadResources = createResourceLoader(options.promptHome);
|
|
2215
3136
|
const resourceSnapshots = createResourceSnapshots(loadResources, (id, requestId) => storage.submissionByRequest(id, requestId, context));
|
|
2216
3137
|
registry.install(createNekoPrompt(resourceSnapshots.current));
|
|
3138
|
+
registry.install(createSubagentExtension(async (id, requestId, cwd) => {
|
|
3139
|
+
resourceSnapshots.stage(id, requestId, cwd, await loadResources(cwd));
|
|
3140
|
+
}, resourceSnapshots.discard));
|
|
2217
3141
|
for (const extension of options.extensions ?? [])
|
|
2218
3142
|
registry.install(extension);
|
|
3143
|
+
let approvals;
|
|
3144
|
+
const approvalExtension = createApprovalExtension(() => approvals);
|
|
3145
|
+
registry.install(approvalExtension);
|
|
2219
3146
|
const harness = await Harness.open(storage, {
|
|
2220
3147
|
models,
|
|
2221
3148
|
registry,
|
|
@@ -2235,8 +3162,11 @@ async function createNekoRuntime(options = {}) {
|
|
|
2235
3162
|
} while (cursor);
|
|
2236
3163
|
return records;
|
|
2237
3164
|
}, context);
|
|
3165
|
+
approvals = new ToolApprovals(harness, context);
|
|
2238
3166
|
const existing = await scanConversations();
|
|
2239
3167
|
try {
|
|
3168
|
+
await installSubagentCoordinator(harness, existing.map((r) => r.id), context);
|
|
3169
|
+
await prepareApprovalConversations(harness, existing.map((r) => r.id), approvalExtension, context);
|
|
2240
3170
|
for (const record of existing) {
|
|
2241
3171
|
const target = await harness.conversation(record.id, context);
|
|
2242
3172
|
if (target) {
|
|
@@ -2287,7 +3217,7 @@ async function createNekoRuntime(options = {}) {
|
|
|
2287
3217
|
if (existing.length === 0)
|
|
2288
3218
|
await metadata.initialize(conversation.id);
|
|
2289
3219
|
for (const record of existing)
|
|
2290
|
-
if (!(await harness.snapshot(ArchivedDoc, record.id, context))?.archived)
|
|
3220
|
+
if (!record.owner && !(await harness.snapshot(ArchivedDoc, record.id, context))?.archived)
|
|
2291
3221
|
await metadata.restore(record.id);
|
|
2292
3222
|
let closing;
|
|
2293
3223
|
const cleanup = () => {
|
|
@@ -2389,7 +3319,7 @@ async function createNekoRuntime(options = {}) {
|
|
|
2389
3319
|
await metadata.flush();
|
|
2390
3320
|
await metadata.reconcile(target.id);
|
|
2391
3321
|
if (settled.status === "done" && settled.type === "input") {
|
|
2392
|
-
const entry = await target.commit((tx) => tx.entry(
|
|
3322
|
+
const entry = await target.commit((tx) => tx.entry(AssistantEntry2, settled.answer), context);
|
|
2393
3323
|
const message = entry?.model?.[0];
|
|
2394
3324
|
const text = message?.role === "assistant" ? message.content.flatMap((block) => block.type === "text" ? [block.text] : []).join("") : "";
|
|
2395
3325
|
return { status: "done", text };
|
|
@@ -2420,7 +3350,7 @@ async function createNekoRuntime(options = {}) {
|
|
|
2420
3350
|
return watchEvents(harness, target.id, context);
|
|
2421
3351
|
},
|
|
2422
3352
|
async usage() {
|
|
2423
|
-
return mergeUsage(await harness.snapshot(
|
|
3353
|
+
return mergeUsage(await harness.snapshot(UsageDoc2, target.id, context) ?? { models: {}, tools: {} }, (await harness.snapshot(SessionMetadataDoc, target.id, context))?.namingUsage ?? { models: {}, tools: {} });
|
|
2424
3354
|
},
|
|
2425
3355
|
compactionRecord() {
|
|
2426
3356
|
return readCompaction(harness, storage, target.id, context);
|
|
@@ -2454,7 +3384,8 @@ async function createNekoRuntime(options = {}) {
|
|
|
2454
3384
|
if (target === undefined)
|
|
2455
3385
|
return;
|
|
2456
3386
|
const archived = (await harness.snapshot(ArchivedDoc, id, context))?.archived ?? false;
|
|
2457
|
-
|
|
3387
|
+
const record = await harness.commit((tx) => tx.conversation(id), context);
|
|
3388
|
+
return archived || record?.owner ? undefined : makeSession(target);
|
|
2458
3389
|
};
|
|
2459
3390
|
return {
|
|
2460
3391
|
context,
|
|
@@ -2468,7 +3399,7 @@ async function createNekoRuntime(options = {}) {
|
|
|
2468
3399
|
async globalUsage() {
|
|
2469
3400
|
let total = { models: {}, tools: {} };
|
|
2470
3401
|
for (const record of await scanConversations()) {
|
|
2471
|
-
total = mergeUsage(total, await harness.snapshot(
|
|
3402
|
+
total = mergeUsage(total, await harness.snapshot(UsageDoc2, record.id, context) ?? { models: {}, tools: {} });
|
|
2472
3403
|
total = mergeUsage(total, (await harness.snapshot(SessionMetadataDoc, record.id, context))?.namingUsage ?? { models: {}, tools: {} });
|
|
2473
3404
|
}
|
|
2474
3405
|
return total;
|
|
@@ -2485,24 +3416,70 @@ async function createNekoRuntime(options = {}) {
|
|
|
2485
3416
|
const newest = await source.conversation.entries({}, 1, undefined, context);
|
|
2486
3417
|
const at = newest.items[0]?.id;
|
|
2487
3418
|
const sourceMetadata = at === undefined ? undefined : await harness.snapshot(SessionMetadataDoc, id, context);
|
|
2488
|
-
const
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
3419
|
+
const childId = await harness.commit(async (tx) => {
|
|
3420
|
+
const inherited = [];
|
|
3421
|
+
if (at !== undefined) {
|
|
3422
|
+
const references = await refreshDelegations(tx, id);
|
|
3423
|
+
const calls = new Set;
|
|
3424
|
+
let cursor;
|
|
3425
|
+
do {
|
|
3426
|
+
const page = await tx.scanEntries({ conversationId: id, maxEntryId: at }, 100, cursor);
|
|
3427
|
+
for (const entry of page.items)
|
|
3428
|
+
for (const message of entry.model ?? []) {
|
|
3429
|
+
if (message.role === "assistant")
|
|
3430
|
+
for (const block of message.content) {
|
|
3431
|
+
if (block.type === "toolCall")
|
|
3432
|
+
calls.add(block.id);
|
|
3433
|
+
}
|
|
3434
|
+
}
|
|
3435
|
+
cursor = page.next;
|
|
3436
|
+
} while (cursor);
|
|
3437
|
+
const frozen = new Map;
|
|
3438
|
+
for (const reference of references) {
|
|
3439
|
+
if (!calls.has(reference.callId))
|
|
3440
|
+
continue;
|
|
3441
|
+
let snapshot = frozen.get(reference.childId);
|
|
3442
|
+
if (!snapshot) {
|
|
3443
|
+
const latest = await tx.scanEntries({ conversationId: reference.childId }, 1);
|
|
3444
|
+
snapshot = {
|
|
3445
|
+
through: latest.items[0]?.id ?? 0,
|
|
3446
|
+
state: JSON.parse(JSON.stringify(await tx.doc(UsageDoc2, reference.childId)))
|
|
3447
|
+
};
|
|
3448
|
+
frozen.set(reference.childId, snapshot);
|
|
3449
|
+
}
|
|
3450
|
+
const usage = totalUsage(snapshot.state);
|
|
3451
|
+
inherited.push({
|
|
3452
|
+
...reference,
|
|
3453
|
+
historyThrough: reference.historyThrough ?? snapshot.through,
|
|
3454
|
+
roundUsage: reference.roundUsage ?? (reference.usageStart ? subagentUsageTotals(subtractSubagentUsage(snapshot.state, reference.usageStart)) : null),
|
|
3455
|
+
historyUsage: reference.historyUsage ?? {
|
|
3456
|
+
input: usage.input,
|
|
3457
|
+
output: usage.output,
|
|
3458
|
+
cacheRead: usage.cacheRead,
|
|
3459
|
+
cacheWrite: usage.cacheWrite,
|
|
3460
|
+
cost: usage.cost.total
|
|
3461
|
+
}
|
|
3462
|
+
});
|
|
3463
|
+
}
|
|
3464
|
+
}
|
|
3465
|
+
const child = at === undefined ? await tx.createConversation({ ownership: { kind: "ownerless" } }) : await tx.forkConversation(id, at, { ownership: { kind: "ownerless" } });
|
|
3466
|
+
(await tx.doc(DelegationsDoc, child.id)).items = inherited;
|
|
3467
|
+
if (sourceMetadata) {
|
|
3468
|
+
const doc = await tx.doc(SessionMetadataDoc, child.id);
|
|
2495
3469
|
doc.title = sourceMetadata.title || sourceMetadata.firstPromptPreview;
|
|
2496
3470
|
doc.firstPromptPreview = sourceMetadata.firstPromptPreview;
|
|
2497
3471
|
doc.naming = "ready";
|
|
2498
3472
|
}
|
|
3473
|
+
return child.id;
|
|
2499
3474
|
}, context);
|
|
3475
|
+
const child = await harness.conversation(childId, context);
|
|
2500
3476
|
await child.configure({ model: agent.model, thinkingLevel: agent.thinkingLevel, cwd: savedWorkspace(agent.cwd) }, context);
|
|
2501
3477
|
await metadata.initialize(child.id);
|
|
2502
3478
|
await touchConversation(child.id);
|
|
2503
3479
|
return makeSession(child);
|
|
2504
3480
|
},
|
|
2505
3481
|
harness,
|
|
3482
|
+
approvals,
|
|
2506
3483
|
closed: harness.closed,
|
|
2507
3484
|
workspaceFiles,
|
|
2508
3485
|
conversation,
|
|
@@ -2528,6 +3505,8 @@ async function createNekoRuntime(options = {}) {
|
|
|
2528
3505
|
const records = await scanConversations();
|
|
2529
3506
|
const infos = [];
|
|
2530
3507
|
for (const record of records) {
|
|
3508
|
+
if (record.owner)
|
|
3509
|
+
continue;
|
|
2531
3510
|
const archived = (await harness.snapshot(ArchivedDoc, record.id, context))?.archived ?? false;
|
|
2532
3511
|
if (!archived) {
|
|
2533
3512
|
const target = await harness.conversation(record.id, context);
|
|
@@ -2566,6 +3545,7 @@ async function createNekoRuntime(options = {}) {
|
|
|
2566
3545
|
},
|
|
2567
3546
|
getConversation,
|
|
2568
3547
|
async archiveConversation(id) {
|
|
3548
|
+
await (await sessionFor(id)).abort();
|
|
2569
3549
|
await metadata.archive(id);
|
|
2570
3550
|
await harness.commit(async (tx) => {
|
|
2571
3551
|
(await tx.doc(ArchivedDoc, id)).archived = true;
|
|
@@ -2690,13 +3670,13 @@ async function removeGatewayPortFile(nekoDir, serverId) {
|
|
|
2690
3670
|
}
|
|
2691
3671
|
|
|
2692
3672
|
// packages/gateway/src/server-services.ts
|
|
2693
|
-
import { randomUUID as
|
|
3673
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
2694
3674
|
import {
|
|
2695
|
-
ChordError as
|
|
2696
|
-
cell as
|
|
2697
|
-
hostPrincipal as
|
|
2698
|
-
listener as
|
|
2699
|
-
local as
|
|
3675
|
+
ChordError as ChordError3,
|
|
3676
|
+
cell as cell3,
|
|
3677
|
+
hostPrincipal as hostPrincipal3,
|
|
3678
|
+
listener as listener3,
|
|
3679
|
+
local as local3,
|
|
2700
3680
|
method as method2
|
|
2701
3681
|
} from "@earendil-works/chord";
|
|
2702
3682
|
|
|
@@ -2707,8 +3687,8 @@ import { dirname as dirname5, join as join7 } from "path";
|
|
|
2707
3687
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2708
3688
|
|
|
2709
3689
|
// packages/core/src/application-version.ts
|
|
2710
|
-
var RUNNING_VERSION = "0.
|
|
2711
|
-
var APPLICATION_PROTOCOL =
|
|
3690
|
+
var RUNNING_VERSION = "0.3.1";
|
|
3691
|
+
var APPLICATION_PROTOCOL = 4;
|
|
2712
3692
|
function stableVersion(value) {
|
|
2713
3693
|
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
3694
|
}
|
|
@@ -2857,11 +3837,11 @@ class UpdateChecker {
|
|
|
2857
3837
|
}
|
|
2858
3838
|
|
|
2859
3839
|
// packages/gateway/src/server-services.ts
|
|
2860
|
-
import { Type as
|
|
3840
|
+
import { Type as Type7 } from "typebox";
|
|
2861
3841
|
|
|
2862
3842
|
// packages/gateway/src/contract.ts
|
|
2863
3843
|
import { method, remote } from "@earendil-works/chord";
|
|
2864
|
-
import { Type as
|
|
3844
|
+
import { Type as Type4 } from "typebox";
|
|
2865
3845
|
var rpc = (args, result) => [args, result];
|
|
2866
3846
|
function contract(name, def) {
|
|
2867
3847
|
const variants = Object.fromEntries(Object.entries(def.variants).map(([key, value]) => [
|
|
@@ -2873,19 +3853,19 @@ function contract(name, def) {
|
|
|
2873
3853
|
]));
|
|
2874
3854
|
return remote(name, { ...def, variants });
|
|
2875
3855
|
}
|
|
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 =
|
|
3856
|
+
var str = Type4.String();
|
|
3857
|
+
var num = Type4.Number();
|
|
3858
|
+
var bool = Type4.Boolean();
|
|
3859
|
+
var nil = Type4.Null();
|
|
3860
|
+
var integer = Type4.Integer({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER });
|
|
3861
|
+
var nullableString = Type4.Union([str, nil]);
|
|
3862
|
+
var phase = Type4.Enum(["pending", "running", "done"]);
|
|
3863
|
+
var level = Type4.Enum(["project", "user", "builtin"]);
|
|
3864
|
+
var thinking = Type4.Enum(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
|
|
3865
|
+
var model = Type4.Object({ provider: str, modelId: str });
|
|
3866
|
+
var change = Type4.Object({ model: Type4.Optional(model), thinkingLevel: Type4.Optional(thinking) }, { additionalProperties: false });
|
|
3867
|
+
var usage = Type4.Object({ input: num, output: num, cacheRead: num, cacheWrite: num, cost: num });
|
|
3868
|
+
var image = Type4.Object({
|
|
2889
3869
|
name: str,
|
|
2890
3870
|
mimeType: str,
|
|
2891
3871
|
width: integer,
|
|
@@ -2894,8 +3874,8 @@ var image = Type2.Object({
|
|
|
2894
3874
|
entryId: integer,
|
|
2895
3875
|
contentIndex: integer
|
|
2896
3876
|
});
|
|
2897
|
-
var skill =
|
|
2898
|
-
var call =
|
|
3877
|
+
var skill = Type4.Object({ name: str, location: str, body: str, args: str });
|
|
3878
|
+
var call = Type4.Object({
|
|
2899
3879
|
callId: str,
|
|
2900
3880
|
name: str,
|
|
2901
3881
|
summary: str,
|
|
@@ -2903,44 +3883,90 @@ var call = Type2.Object({
|
|
|
2903
3883
|
phase,
|
|
2904
3884
|
output: nullableString
|
|
2905
3885
|
});
|
|
2906
|
-
var
|
|
3886
|
+
var DelegationSchema = Type4.Object({
|
|
3887
|
+
parentId: integer,
|
|
3888
|
+
agentId: Type4.Optional(str),
|
|
3889
|
+
round: Type4.Optional(integer),
|
|
3890
|
+
runnerId: Type4.Optional(integer),
|
|
3891
|
+
inputEntryId: Type4.Optional(integer),
|
|
3892
|
+
answerEntryId: Type4.Optional(integer),
|
|
3893
|
+
roundUsage: Type4.Optional(Type4.Union([usage, nil])),
|
|
3894
|
+
taskId: integer,
|
|
3895
|
+
callId: str,
|
|
3896
|
+
childId: integer,
|
|
3897
|
+
role: Type4.Enum(["explore", "plan", "coder"]),
|
|
3898
|
+
title: str,
|
|
3899
|
+
createdAt: num,
|
|
3900
|
+
startedAt: Type4.Union([num, nil]),
|
|
3901
|
+
endedAt: Type4.Union([num, nil]),
|
|
3902
|
+
status: Type4.Enum(["queued", "running", "completed", "failed", "cancelled"]),
|
|
3903
|
+
report: nullableString,
|
|
3904
|
+
approvalCount: Type4.Optional(integer),
|
|
3905
|
+
historyThrough: Type4.Optional(integer),
|
|
3906
|
+
historyUsage: Type4.Optional(usage)
|
|
3907
|
+
});
|
|
3908
|
+
var SubagentSummarySchema = Type4.Object({
|
|
3909
|
+
agentId: str,
|
|
3910
|
+
parentId: integer,
|
|
3911
|
+
childId: integer,
|
|
3912
|
+
role: Type4.Enum(["explore", "plan", "coder"]),
|
|
3913
|
+
title: str,
|
|
3914
|
+
status: Type4.Enum(["idle", "queued", "running"]),
|
|
3915
|
+
activeRoundId: Type4.Union([integer, nil]),
|
|
3916
|
+
lastRoundId: integer,
|
|
3917
|
+
lastReport: Type4.Optional(nullableString),
|
|
3918
|
+
roundCount: integer,
|
|
3919
|
+
taskTitle: str,
|
|
3920
|
+
approvalCount: integer,
|
|
3921
|
+
createdAt: num,
|
|
3922
|
+
historyThrough: Type4.Optional(integer),
|
|
3923
|
+
historyUsage: Type4.Optional(usage)
|
|
3924
|
+
});
|
|
3925
|
+
var ChatViewSchema = Type4.Object({
|
|
3926
|
+
historyBefore: Type4.Optional(Type4.Union([integer, nil])),
|
|
3927
|
+
agents: Type4.Optional(Type4.Array(SubagentSummarySchema)),
|
|
3928
|
+
rounds: Type4.Optional(Type4.Array(DelegationSchema)),
|
|
3929
|
+
subagents: Type4.Optional(Type4.Array(DelegationSchema)),
|
|
3930
|
+
taskUsage: Type4.Optional(usage),
|
|
2907
3931
|
model: str,
|
|
2908
|
-
agent:
|
|
3932
|
+
agent: Type4.Object({ model: Type4.Union([model, nil]), thinkingLevel: Type4.Union([thinking, nil]) }),
|
|
2909
3933
|
cwd: str,
|
|
2910
3934
|
busy: bool,
|
|
2911
3935
|
status: nullableString,
|
|
2912
3936
|
usage,
|
|
2913
|
-
contextUsage:
|
|
2914
|
-
compaction:
|
|
2915
|
-
failure:
|
|
2916
|
-
active:
|
|
3937
|
+
contextUsage: Type4.Optional(Type4.Object({ tokens: Type4.Union([num, nil]), window: num, percent: Type4.Union([num, nil]) })),
|
|
3938
|
+
compaction: Type4.Optional(Type4.Object({
|
|
3939
|
+
failure: Type4.Optional(Type4.Object({ taskId: integer, reason: Type4.Enum(["manual", "threshold", "overflow"]), error: str })),
|
|
3940
|
+
active: Type4.Array(Type4.Object({
|
|
2917
3941
|
taskId: integer,
|
|
2918
|
-
reason:
|
|
3942
|
+
reason: Type4.Enum(["manual", "threshold", "overflow"]),
|
|
2919
3943
|
blocking: bool,
|
|
2920
3944
|
attempt: integer,
|
|
2921
|
-
retry:
|
|
3945
|
+
retry: Type4.Optional(Type4.Object({ at: num, error: str }))
|
|
2922
3946
|
})),
|
|
2923
|
-
latest:
|
|
3947
|
+
latest: Type4.Union([
|
|
2924
3948
|
nil,
|
|
2925
|
-
|
|
3949
|
+
Type4.Object({
|
|
2926
3950
|
taskId: integer,
|
|
2927
|
-
phase:
|
|
3951
|
+
phase: Type4.Enum(["summarizing", "waiting-to-apply", "applied", "nothing", "skipped", "cancelled", "failed"]),
|
|
2928
3952
|
at: num
|
|
2929
3953
|
})
|
|
2930
3954
|
])
|
|
2931
3955
|
})),
|
|
2932
|
-
items:
|
|
2933
|
-
|
|
2934
|
-
type:
|
|
3956
|
+
items: Type4.Array(Type4.Union([
|
|
3957
|
+
Type4.Object({
|
|
3958
|
+
type: Type4.Literal("user"),
|
|
3959
|
+
order: Type4.Optional(num),
|
|
2935
3960
|
key: str,
|
|
2936
3961
|
id: integer,
|
|
2937
3962
|
text: str,
|
|
2938
|
-
images:
|
|
2939
|
-
skill:
|
|
2940
|
-
defaultImagePrompt:
|
|
3963
|
+
images: Type4.Optional(Type4.Array(image)),
|
|
3964
|
+
skill: Type4.Optional(skill),
|
|
3965
|
+
defaultImagePrompt: Type4.Optional(Type4.Object({ version: Type4.Integer({ minimum: 1 }), text: str }))
|
|
2941
3966
|
}),
|
|
2942
|
-
|
|
2943
|
-
type:
|
|
3967
|
+
Type4.Object({
|
|
3968
|
+
type: Type4.Literal("assistant"),
|
|
3969
|
+
order: Type4.Optional(num),
|
|
2944
3970
|
key: str,
|
|
2945
3971
|
id: integer,
|
|
2946
3972
|
thinking: str,
|
|
@@ -2949,10 +3975,11 @@ var ChatViewSchema = Type2.Object({
|
|
|
2949
3975
|
errorMessage: nullableString,
|
|
2950
3976
|
note: nullableString
|
|
2951
3977
|
}),
|
|
2952
|
-
|
|
2953
|
-
type:
|
|
3978
|
+
Type4.Object({
|
|
3979
|
+
type: Type4.Literal("tool"),
|
|
3980
|
+
order: Type4.Optional(num),
|
|
2954
3981
|
key: str,
|
|
2955
|
-
images:
|
|
3982
|
+
images: Type4.Optional(Type4.Array(image)),
|
|
2956
3983
|
callId: str,
|
|
2957
3984
|
name: str,
|
|
2958
3985
|
summary: str,
|
|
@@ -2961,50 +3988,50 @@ var ChatViewSchema = Type2.Object({
|
|
|
2961
3988
|
isError: bool,
|
|
2962
3989
|
output: nullableString,
|
|
2963
3990
|
result: nullableString,
|
|
2964
|
-
durationMs:
|
|
3991
|
+
durationMs: Type4.Union([num, nil]),
|
|
2965
3992
|
note: nullableString
|
|
2966
3993
|
}),
|
|
2967
|
-
|
|
3994
|
+
Type4.Object({ type: Type4.Literal("system"), order: Type4.Optional(num), key: str, id: integer, text: str })
|
|
2968
3995
|
])),
|
|
2969
|
-
streaming:
|
|
3996
|
+
streaming: Type4.Union([nil, Type4.Object({ thinking: str, text: str, toolCalls: Type4.Array(call) })])
|
|
2970
3997
|
});
|
|
2971
|
-
var SessionSummarySchema =
|
|
3998
|
+
var SessionSummarySchema = Type4.Object({
|
|
2972
3999
|
workspaceRoot: str,
|
|
2973
4000
|
lastUsedAt: num,
|
|
2974
4001
|
title: str,
|
|
2975
4002
|
firstPromptPreview: str,
|
|
2976
4003
|
createdAt: num,
|
|
2977
|
-
lastMessageAt:
|
|
2978
|
-
model:
|
|
2979
|
-
status:
|
|
4004
|
+
lastMessageAt: Type4.Union([num, nil]),
|
|
4005
|
+
model: Type4.Union([model, nil]),
|
|
4006
|
+
status: Type4.Enum(["idle", "running", "error"]),
|
|
2980
4007
|
id: str
|
|
2981
4008
|
});
|
|
2982
|
-
var DirectoryStateSchema =
|
|
2983
|
-
var WorkspaceSummarySchema =
|
|
2984
|
-
var WorkspaceStateSchema =
|
|
2985
|
-
var credentialStatus =
|
|
4009
|
+
var DirectoryStateSchema = Type4.Object({ revision: integer, sessions: Type4.Array(SessionSummarySchema) });
|
|
4010
|
+
var WorkspaceSummarySchema = Type4.Object({ path: str, createdAt: num });
|
|
4011
|
+
var WorkspaceStateSchema = Type4.Object({ revision: integer, workspaces: Type4.Array(WorkspaceSummarySchema) });
|
|
4012
|
+
var credentialStatus = Type4.Object({
|
|
2986
4013
|
savedConfigured: bool,
|
|
2987
4014
|
environmentConfigured: bool,
|
|
2988
|
-
source:
|
|
4015
|
+
source: Type4.Enum(["saved", "environment", "runtime", "keyless", "none"])
|
|
2989
4016
|
});
|
|
2990
|
-
var CredentialStateSchema =
|
|
4017
|
+
var CredentialStateSchema = Type4.Object({
|
|
2991
4018
|
revision: integer,
|
|
2992
|
-
statuses:
|
|
4019
|
+
statuses: Type4.Object({ deepseek: credentialStatus, tavily: credentialStatus })
|
|
2993
4020
|
});
|
|
2994
|
-
var AttachmentSchema =
|
|
2995
|
-
var chatInput =
|
|
4021
|
+
var AttachmentSchema = Type4.Object({ sessionId: str, attachmentId: str });
|
|
4022
|
+
var chatInput = Type4.Object({ sessionId: str, attachmentId: str }, { additionalProperties: false });
|
|
2996
4023
|
var NekoSessionDirectory = contract("neko.session-directory", {
|
|
2997
4024
|
version: 1,
|
|
2998
|
-
input:
|
|
4025
|
+
input: Type4.Object({}, { additionalProperties: false }),
|
|
2999
4026
|
variants: { ready: { state: { state: DirectoryStateSchema } } }
|
|
3000
4027
|
});
|
|
3001
4028
|
var NekoSessionManagement = contract("neko.session-management", {
|
|
3002
4029
|
version: 1,
|
|
3003
|
-
input:
|
|
4030
|
+
input: Type4.Object({}, { additionalProperties: false }),
|
|
3004
4031
|
variants: {
|
|
3005
4032
|
ready: {
|
|
3006
4033
|
methods: {
|
|
3007
|
-
create: rpc(
|
|
4034
|
+
create: rpc(Type4.Object({ ...change.properties, cwd: Type4.Optional(str) }, { additionalProperties: false }), SessionSummarySchema),
|
|
3008
4035
|
archive: rpc(str, nil),
|
|
3009
4036
|
attach: rpc(str, AttachmentSchema),
|
|
3010
4037
|
detach: rpc(nil, nil),
|
|
@@ -3013,30 +4040,30 @@ var NekoSessionManagement = contract("neko.session-management", {
|
|
|
3013
4040
|
}
|
|
3014
4041
|
}
|
|
3015
4042
|
});
|
|
3016
|
-
var resize =
|
|
3017
|
-
maxWidth:
|
|
3018
|
-
maxHeight:
|
|
3019
|
-
maxBytes:
|
|
3020
|
-
jpegQuality:
|
|
4043
|
+
var resize = Type4.Object({
|
|
4044
|
+
maxWidth: Type4.Optional(integer),
|
|
4045
|
+
maxHeight: Type4.Optional(integer),
|
|
4046
|
+
maxBytes: Type4.Optional(integer),
|
|
4047
|
+
jpegQuality: Type4.Optional(integer)
|
|
3021
4048
|
});
|
|
3022
|
-
var inputLimits =
|
|
3023
|
-
maxRequestBytes:
|
|
3024
|
-
images:
|
|
3025
|
-
resize:
|
|
3026
|
-
maxPerMessage:
|
|
3027
|
-
maxPerRequest:
|
|
4049
|
+
var inputLimits = Type4.Object({
|
|
4050
|
+
maxRequestBytes: Type4.Optional(integer),
|
|
4051
|
+
images: Type4.Optional(Type4.Object({
|
|
4052
|
+
resize: Type4.Optional(resize),
|
|
4053
|
+
maxPerMessage: Type4.Optional(integer),
|
|
4054
|
+
maxPerRequest: Type4.Optional(integer)
|
|
3028
4055
|
}))
|
|
3029
4056
|
});
|
|
3030
|
-
var modelInfo =
|
|
4057
|
+
var modelInfo = Type4.Object({
|
|
3031
4058
|
provider: str,
|
|
3032
4059
|
modelId: str,
|
|
3033
4060
|
name: str,
|
|
3034
4061
|
contextWindow: num,
|
|
3035
|
-
input:
|
|
3036
|
-
inputLimits:
|
|
4062
|
+
input: Type4.Array(str),
|
|
4063
|
+
inputLimits: Type4.Optional(inputLimits),
|
|
3037
4064
|
reasoning: bool,
|
|
3038
|
-
supportedThinkingLevels:
|
|
3039
|
-
thinkingLevelAdjustments:
|
|
4065
|
+
supportedThinkingLevels: Type4.Array(thinking),
|
|
4066
|
+
thinkingLevelAdjustments: Type4.Object({
|
|
3040
4067
|
off: thinking,
|
|
3041
4068
|
minimal: thinking,
|
|
3042
4069
|
low: thinking,
|
|
@@ -3048,10 +4075,35 @@ var modelInfo = Type2.Object({
|
|
|
3048
4075
|
});
|
|
3049
4076
|
var NekoModels = contract("neko.models", {
|
|
3050
4077
|
version: 1,
|
|
3051
|
-
input:
|
|
3052
|
-
variants: { ready: { methods: { list: rpc(nil,
|
|
4078
|
+
input: Type4.Object({}, { additionalProperties: false }),
|
|
4079
|
+
variants: { ready: { methods: { list: rpc(nil, Type4.Array(modelInfo)) } } }
|
|
4080
|
+
});
|
|
4081
|
+
var imageChunk = Type4.Object({ data: str, total: integer, mimeType: str });
|
|
4082
|
+
var NekoSubagent = contract("neko.subagent", {
|
|
4083
|
+
version: 1,
|
|
4084
|
+
input: Type4.Object({
|
|
4085
|
+
sessionId: str,
|
|
4086
|
+
attachmentId: str,
|
|
4087
|
+
taskId: Type4.Optional(integer),
|
|
4088
|
+
agentId: Type4.Optional(str)
|
|
4089
|
+
}),
|
|
4090
|
+
variants: {
|
|
4091
|
+
ready: {
|
|
4092
|
+
state: { revision: integer },
|
|
4093
|
+
methods: {
|
|
4094
|
+
chunk: rpc(Type4.Object({
|
|
4095
|
+
kind: Type4.Enum(["view", "history"]),
|
|
4096
|
+
before: Type4.Optional(integer),
|
|
4097
|
+
token: Type4.Optional(integer),
|
|
4098
|
+
offset: integer
|
|
4099
|
+
}), Type4.Object({ token: integer, data: str, next: Type4.Union([integer, nil]) })),
|
|
4100
|
+
getImage: rpc(Type4.Object({ entryId: integer, contentIndex: integer, offset: integer }), imageChunk),
|
|
4101
|
+
decideApproval: rpc(Type4.Object({ id: integer, version: str, decision: Type4.Enum(["allowed", "denied"]) }), Type4.Enum(["pending", "allowed", "denied", "cancelled"])),
|
|
4102
|
+
stop: rpc(Type4.Union([nil, Type4.Object({ taskId: integer })]), nil)
|
|
4103
|
+
}
|
|
4104
|
+
}
|
|
4105
|
+
}
|
|
3053
4106
|
});
|
|
3054
|
-
var imageChunk = Type2.Object({ data: str, total: integer, mimeType: str });
|
|
3055
4107
|
var NekoChat = contract("neko.chat", {
|
|
3056
4108
|
version: 1,
|
|
3057
4109
|
input: chatInput,
|
|
@@ -3059,79 +4111,79 @@ var NekoChat = contract("neko.chat", {
|
|
|
3059
4111
|
ready: {
|
|
3060
4112
|
state: { view: ChatViewSchema },
|
|
3061
4113
|
methods: {
|
|
3062
|
-
submit: rpc(
|
|
4114
|
+
submit: rpc(Type4.Object({
|
|
3063
4115
|
content: str,
|
|
3064
|
-
images:
|
|
3065
|
-
requestId:
|
|
3066
|
-
whenBusy:
|
|
4116
|
+
images: Type4.Optional(Type4.Array(str, { maxItems: 4 })),
|
|
4117
|
+
requestId: Type4.Optional(str),
|
|
4118
|
+
whenBusy: Type4.Optional(Type4.Enum(["steer", "followUp"]))
|
|
3067
4119
|
}, { additionalProperties: false }), nil),
|
|
3068
4120
|
abort: rpc(nil, nil),
|
|
3069
4121
|
usage: rpc(nil, usage),
|
|
3070
|
-
skills: rpc(nil,
|
|
4122
|
+
skills: rpc(nil, Type4.Array(Type4.Object({ name: str, description: str, location: str, level }))),
|
|
3071
4123
|
configure: rpc(change, nil),
|
|
3072
|
-
compact: rpc(
|
|
3073
|
-
importImage: rpc(str,
|
|
3074
|
-
draftImage: rpc(
|
|
3075
|
-
beginImage: rpc(
|
|
3076
|
-
appendImage: rpc(
|
|
4124
|
+
compact: rpc(Type4.Object({ instructions: Type4.Optional(str) }, { additionalProperties: false }), nil),
|
|
4125
|
+
importImage: rpc(str, Type4.Object({ id: str, name: str })),
|
|
4126
|
+
draftImage: rpc(Type4.Object({ id: str, offset: integer }, { additionalProperties: false }), imageChunk),
|
|
4127
|
+
beginImage: rpc(Type4.Object({ name: str, mimeType: str, size: Type4.Integer({ minimum: 1, maximum: 20971520 }) }, { additionalProperties: false }), str),
|
|
4128
|
+
appendImage: rpc(Type4.Object({ id: str, offset: integer, data: Type4.String({ maxLength: 350000 }) }, { additionalProperties: false }), nil),
|
|
3077
4129
|
finishImage: rpc(str, nil),
|
|
3078
4130
|
releaseImage: rpc(str, nil),
|
|
3079
|
-
getImage: rpc(
|
|
4131
|
+
getImage: rpc(Type4.Object({ entryId: integer, contentIndex: integer, offset: integer }, { additionalProperties: false }), imageChunk)
|
|
3080
4132
|
}
|
|
3081
4133
|
}
|
|
3082
4134
|
}
|
|
3083
4135
|
});
|
|
3084
|
-
var browse =
|
|
3085
|
-
var page =
|
|
4136
|
+
var browse = Type4.Object({ path: str, query: Type4.Optional(str), showHidden: Type4.Optional(bool), cursor: Type4.Optional(str) }, { additionalProperties: false });
|
|
4137
|
+
var page = Type4.Object({
|
|
3086
4138
|
path: str,
|
|
3087
4139
|
parent: nullableString,
|
|
3088
|
-
entries:
|
|
4140
|
+
entries: Type4.Array(Type4.Object({ name: str, path: str, error: Type4.Optional(str) })),
|
|
3089
4141
|
next: nullableString
|
|
3090
4142
|
});
|
|
3091
4143
|
var NekoWorkspaces = contract("neko.workspaces", {
|
|
3092
4144
|
version: 1,
|
|
3093
|
-
input:
|
|
4145
|
+
input: Type4.Object({}, { additionalProperties: false }),
|
|
3094
4146
|
variants: {
|
|
3095
4147
|
ready: {
|
|
3096
4148
|
state: { state: WorkspaceStateSchema },
|
|
3097
4149
|
methods: {
|
|
3098
|
-
add: rpc(
|
|
4150
|
+
add: rpc(Type4.Object({ path: str }, { additionalProperties: false }), WorkspaceSummarySchema),
|
|
3099
4151
|
browse: rpc(browse, page)
|
|
3100
4152
|
}
|
|
3101
4153
|
}
|
|
3102
4154
|
}
|
|
3103
4155
|
});
|
|
3104
|
-
var provider =
|
|
4156
|
+
var provider = Type4.Enum(["deepseek", "tavily"]);
|
|
3105
4157
|
var NekoCredentials = contract("neko.credentials", {
|
|
3106
4158
|
version: 1,
|
|
3107
|
-
input:
|
|
4159
|
+
input: Type4.Object({}, { additionalProperties: false }),
|
|
3108
4160
|
variants: {
|
|
3109
4161
|
ready: {
|
|
3110
4162
|
state: { state: CredentialStateSchema },
|
|
3111
4163
|
methods: {
|
|
3112
|
-
save: rpc(
|
|
3113
|
-
remove: rpc(
|
|
4164
|
+
save: rpc(Type4.Object({ provider, key: Type4.String({ minLength: 1, maxLength: 8192 }) }, { additionalProperties: false }), nil),
|
|
4165
|
+
remove: rpc(Type4.Object({ provider }, { additionalProperties: false }), nil)
|
|
3114
4166
|
}
|
|
3115
4167
|
}
|
|
3116
4168
|
}
|
|
3117
4169
|
});
|
|
3118
|
-
var FileCandidatesSchema =
|
|
3119
|
-
entries:
|
|
4170
|
+
var FileCandidatesSchema = Type4.Object({
|
|
4171
|
+
entries: Type4.Array(Type4.Object({ name: str, path: str, kind: Type4.Enum(["file", "directory"]) }), { maxItems: 100 }),
|
|
3120
4172
|
limited: bool,
|
|
3121
4173
|
incomplete: bool
|
|
3122
4174
|
});
|
|
3123
4175
|
var NekoWorkspaceFiles = contract("neko.workspace-files", {
|
|
3124
4176
|
version: 1,
|
|
3125
|
-
input:
|
|
4177
|
+
input: Type4.Object({}, { additionalProperties: false }),
|
|
3126
4178
|
variants: {
|
|
3127
4179
|
ready: {
|
|
3128
4180
|
methods: {
|
|
3129
|
-
search: rpc(
|
|
4181
|
+
search: rpc(Type4.Object({ workspaceRoot: str, query: str }, { additionalProperties: false }), FileCandidatesSchema)
|
|
3130
4182
|
}
|
|
3131
4183
|
}
|
|
3132
4184
|
}
|
|
3133
4185
|
});
|
|
3134
|
-
var marketSkill =
|
|
4186
|
+
var marketSkill = Type4.Object({
|
|
3135
4187
|
name: str,
|
|
3136
4188
|
description: str,
|
|
3137
4189
|
category: str,
|
|
@@ -3141,38 +4193,38 @@ var marketSkill = Type2.Object({
|
|
|
3141
4193
|
sha256: str,
|
|
3142
4194
|
sizeBytes: integer,
|
|
3143
4195
|
uploadedAt: str,
|
|
3144
|
-
installed:
|
|
3145
|
-
activeLevel:
|
|
4196
|
+
installed: Type4.Object({ project: bool, user: bool, builtin: bool }),
|
|
4197
|
+
activeLevel: Type4.Union([level, nil])
|
|
3146
4198
|
});
|
|
3147
|
-
var preview =
|
|
3148
|
-
var scope =
|
|
3149
|
-
var installResult =
|
|
4199
|
+
var preview = Type4.Object({ body: str, files: Type4.Array(str) });
|
|
4200
|
+
var scope = Type4.Enum(["user", "project"]);
|
|
4201
|
+
var installResult = Type4.Object({ name: str, scope, path: str, shadowedBy: Type4.Optional(scope) });
|
|
3150
4202
|
var NekoSkillMarket = contract("neko.skill-market", {
|
|
3151
4203
|
version: 1,
|
|
3152
|
-
input:
|
|
4204
|
+
input: Type4.Object({}, { additionalProperties: false }),
|
|
3153
4205
|
variants: {
|
|
3154
4206
|
ready: {
|
|
3155
4207
|
methods: {
|
|
3156
|
-
list: rpc(
|
|
3157
|
-
detail: rpc(
|
|
3158
|
-
preview: rpc(
|
|
3159
|
-
install: rpc(
|
|
3160
|
-
uninstall: rpc(
|
|
4208
|
+
list: rpc(Type4.Object({ query: Type4.Optional(str), workspaceRoot: Type4.Optional(str) }, { additionalProperties: false }), Type4.Array(marketSkill)),
|
|
4209
|
+
detail: rpc(Type4.Object({ name: str, workspaceRoot: Type4.Optional(str) }, { additionalProperties: false }), marketSkill),
|
|
4210
|
+
preview: rpc(Type4.Object({ name: str }, { additionalProperties: false }), preview),
|
|
4211
|
+
install: rpc(Type4.Object({ name: str, scope, workspaceRoot: Type4.Optional(str), overwrite: Type4.Optional(bool) }, { additionalProperties: false }), installResult),
|
|
4212
|
+
uninstall: rpc(Type4.Object({ name: str, scope, workspaceRoot: Type4.Optional(str) }, { additionalProperties: false }), nil)
|
|
3161
4213
|
}
|
|
3162
4214
|
}
|
|
3163
4215
|
}
|
|
3164
4216
|
});
|
|
3165
|
-
var ApplicationInfoSchema =
|
|
4217
|
+
var ApplicationInfoSchema = Type4.Object({
|
|
3166
4218
|
runningVersion: str,
|
|
3167
4219
|
installedVersion: nullableString,
|
|
3168
|
-
protocolVersion:
|
|
4220
|
+
protocolVersion: Type4.Integer({ minimum: 1 }),
|
|
3169
4221
|
latestVersion: nullableString,
|
|
3170
|
-
checkedAt:
|
|
3171
|
-
checkState:
|
|
4222
|
+
checkedAt: Type4.Union([integer, nil]),
|
|
4223
|
+
checkState: Type4.Enum(["unknown", "checking", "ok", "failed", "development"])
|
|
3172
4224
|
});
|
|
3173
4225
|
var NekoApplicationInfo = contract("neko.application-info", {
|
|
3174
4226
|
version: 1,
|
|
3175
|
-
input:
|
|
4227
|
+
input: Type4.Object({}),
|
|
3176
4228
|
variants: { ready: { state: { info: ApplicationInfoSchema }, methods: { check: rpc(nil, ApplicationInfoSchema) } } }
|
|
3177
4229
|
});
|
|
3178
4230
|
|
|
@@ -3180,16 +4232,17 @@ var NekoApplicationInfo = contract("neko.application-info", {
|
|
|
3180
4232
|
import { open as open2 } from "fs/promises";
|
|
3181
4233
|
import { basename as basename3, extname } from "path";
|
|
3182
4234
|
import {
|
|
3183
|
-
ChordError,
|
|
3184
|
-
cell,
|
|
3185
|
-
hostPrincipal,
|
|
3186
|
-
listener,
|
|
3187
|
-
local,
|
|
3188
|
-
opaque,
|
|
3189
|
-
resource
|
|
4235
|
+
ChordError as ChordError2,
|
|
4236
|
+
cell as cell2,
|
|
4237
|
+
hostPrincipal as hostPrincipal2,
|
|
4238
|
+
listener as listener2,
|
|
4239
|
+
local as local2,
|
|
4240
|
+
opaque as opaque2,
|
|
4241
|
+
resource as resource2
|
|
3190
4242
|
} from "@earendil-works/chord";
|
|
3191
|
-
import { withAbortSignal as
|
|
3192
|
-
import {
|
|
4243
|
+
import { withAbortSignal as withAbortSignal3 } from "@earendil-works/chord/context";
|
|
4244
|
+
import { UsageDoc as UsageDoc4 } from "@earendil-works/pi-durable";
|
|
4245
|
+
import { Type as Type6 } from "typebox";
|
|
3193
4246
|
|
|
3194
4247
|
// packages/gateway/src/image-uploads.ts
|
|
3195
4248
|
var IMAGE_CHUNK_BYTES = 256 * 1024;
|
|
@@ -3285,19 +4338,259 @@ class ImageUploads {
|
|
|
3285
4338
|
}
|
|
3286
4339
|
}
|
|
3287
4340
|
|
|
4341
|
+
// packages/gateway/src/subagent-host.ts
|
|
4342
|
+
import {
|
|
4343
|
+
ChordError,
|
|
4344
|
+
cell,
|
|
4345
|
+
hostPrincipal,
|
|
4346
|
+
listener,
|
|
4347
|
+
local,
|
|
4348
|
+
opaque,
|
|
4349
|
+
resource
|
|
4350
|
+
} from "@earendil-works/chord";
|
|
4351
|
+
import { UsageDoc as UsageDoc3 } from "@earendil-works/pi-durable";
|
|
4352
|
+
import { Type as Type5 } from "typebox";
|
|
4353
|
+
async function openReader(runtime, childId, parentId, context, through, historyUsage) {
|
|
4354
|
+
const child = await runtime.harness.conversation(childId, context);
|
|
4355
|
+
if (!child)
|
|
4356
|
+
throw new ChordError("gone", "Subagent unavailable");
|
|
4357
|
+
const native = await child.viewState(context);
|
|
4358
|
+
if (through !== undefined)
|
|
4359
|
+
native.dispose();
|
|
4360
|
+
const project = (entries, streaming = true) => {
|
|
4361
|
+
const result = projectChatView({ ...native.value, entries: [...entries].sort((a, b) => a.id - b.id) }, "", {
|
|
4362
|
+
models: runtime.listModels(),
|
|
4363
|
+
contextEntries: native.value.entries
|
|
4364
|
+
});
|
|
4365
|
+
if (!streaming || through !== undefined) {
|
|
4366
|
+
result.streaming = null;
|
|
4367
|
+
result.busy = false;
|
|
4368
|
+
result.status = null;
|
|
4369
|
+
if (through !== undefined) {
|
|
4370
|
+
result.usage = historyUsage ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
|
|
4371
|
+
delete result.contextUsage;
|
|
4372
|
+
}
|
|
4373
|
+
}
|
|
4374
|
+
let order = 0;
|
|
4375
|
+
for (const item of result.items) {
|
|
4376
|
+
if (item.type !== "tool")
|
|
4377
|
+
order = item.id;
|
|
4378
|
+
item.order = order;
|
|
4379
|
+
}
|
|
4380
|
+
return result;
|
|
4381
|
+
};
|
|
4382
|
+
const pageAt = async (before) => {
|
|
4383
|
+
const max = Math.min(before === undefined ? Infinity : before - 1, through ?? Infinity);
|
|
4384
|
+
const page = await child.entries(Number.isFinite(max) ? { maxEntryId: max } : {}, 100, undefined, context);
|
|
4385
|
+
const entries = [...page.items];
|
|
4386
|
+
while (page.next && !["assistant", "user"].includes(entries.at(-1)?.model?.[0]?.role ?? "")) {
|
|
4387
|
+
const extra = await child.entries({ maxEntryId: entries.at(-1).id - 1 }, 1, undefined, context);
|
|
4388
|
+
entries.push(...extra.items);
|
|
4389
|
+
if (!extra.items.length || !extra.next)
|
|
4390
|
+
break;
|
|
4391
|
+
}
|
|
4392
|
+
const first = entries.at(-1)?.id;
|
|
4393
|
+
const older = first === undefined ? [] : (await child.entries({ maxEntryId: first - 1 }, 1, undefined, context)).items;
|
|
4394
|
+
return { entries, next: older.length ? first : null };
|
|
4395
|
+
};
|
|
4396
|
+
let revision = 0;
|
|
4397
|
+
let sequence = 0;
|
|
4398
|
+
const payloads = new Map;
|
|
4399
|
+
const remember = (value) => {
|
|
4400
|
+
const token = ++sequence;
|
|
4401
|
+
payloads.set(token, JSON.stringify(value));
|
|
4402
|
+
while (payloads.size > 4) {
|
|
4403
|
+
const oldest = [...payloads.keys()].find((key) => key !== revision);
|
|
4404
|
+
payloads.delete(oldest);
|
|
4405
|
+
}
|
|
4406
|
+
return token;
|
|
4407
|
+
};
|
|
4408
|
+
return {
|
|
4409
|
+
subscribe: (fn) => {
|
|
4410
|
+
if (through !== undefined)
|
|
4411
|
+
return () => {};
|
|
4412
|
+
const stop = native.subscribe(() => fn(null));
|
|
4413
|
+
const stopApprovals = runtime.harness.subscribeCommits(({ changes }) => {
|
|
4414
|
+
if (changes.some((c) => c.type === "document" && ["app.neko.approvals", "app.neko.delegations", "app.neko.subagents"].includes(c.record.kind) && c.conversationId === parentId))
|
|
4415
|
+
fn(null);
|
|
4416
|
+
});
|
|
4417
|
+
return () => {
|
|
4418
|
+
stop();
|
|
4419
|
+
stopApprovals();
|
|
4420
|
+
};
|
|
4421
|
+
},
|
|
4422
|
+
async refresh() {
|
|
4423
|
+
const page = await pageAt();
|
|
4424
|
+
const approvals = through === undefined ? (await runtime.approvals.list(parentId, context)).filter((a) => a.childId === childId) : [];
|
|
4425
|
+
const currentUsage = await runtime.harness.snapshot(UsageDoc3, childId, context);
|
|
4426
|
+
const rounds = (await listDelegations(runtime.harness, parentId, context)).filter((r) => r.childId === childId).map(({ usageStart, usageEnd: _end, ...round }) => ({
|
|
4427
|
+
...round,
|
|
4428
|
+
roundUsage: round.roundUsage ?? (through === undefined && usageStart && currentUsage ? subagentUsageTotals(subtractSubagentUsage(currentUsage, usageStart)) : null)
|
|
4429
|
+
}));
|
|
4430
|
+
revision = remember({ ...project(page.entries), rounds, approvals, historyBefore: page.next });
|
|
4431
|
+
return revision;
|
|
4432
|
+
},
|
|
4433
|
+
async chunk(input) {
|
|
4434
|
+
let token = input.token;
|
|
4435
|
+
if (token === undefined) {
|
|
4436
|
+
if (input.kind === "view")
|
|
4437
|
+
token = revision;
|
|
4438
|
+
else {
|
|
4439
|
+
const page = await pageAt(input.before);
|
|
4440
|
+
token = remember({ items: project(page.entries, false).items, next: page.next });
|
|
4441
|
+
}
|
|
4442
|
+
}
|
|
4443
|
+
const payload = payloads.get(token);
|
|
4444
|
+
if (payload === undefined)
|
|
4445
|
+
throw new ChordError("gone", "Transcript changed; reload");
|
|
4446
|
+
if (input.offset > payload.length)
|
|
4447
|
+
throw new ChordError("invalid", "Invalid transcript range");
|
|
4448
|
+
const end = Math.min(payload.length, input.offset + 32768);
|
|
4449
|
+
return { token, data: payload.slice(input.offset, end), next: end < payload.length ? end : null };
|
|
4450
|
+
},
|
|
4451
|
+
async history(before) {
|
|
4452
|
+
const page = await pageAt(before);
|
|
4453
|
+
return { items: project(page.entries, false).items, next: page.next };
|
|
4454
|
+
},
|
|
4455
|
+
async image(entryId, contentIndex, offset) {
|
|
4456
|
+
if (through !== undefined && entryId > through)
|
|
4457
|
+
throw new ChordError("forbidden", "Image outside fork history");
|
|
4458
|
+
const page = await child.entries({ minEntryId: entryId, maxEntryId: entryId }, 1, undefined, context);
|
|
4459
|
+
const content = page.items[0]?.model?.[0]?.content;
|
|
4460
|
+
const image = Array.isArray(content) ? content[contentIndex] : undefined;
|
|
4461
|
+
if (image?.type !== "image")
|
|
4462
|
+
throw new ChordError("invalid", "Image not in subagent conversation");
|
|
4463
|
+
const bytes = Buffer.from(image.data, "base64");
|
|
4464
|
+
if (offset > bytes.length)
|
|
4465
|
+
throw new ChordError("invalid", "Invalid image range");
|
|
4466
|
+
return {
|
|
4467
|
+
data: bytes.subarray(offset, offset + IMAGE_CHUNK_BYTES).toString("base64"),
|
|
4468
|
+
total: bytes.length,
|
|
4469
|
+
mimeType: image.mimeType
|
|
4470
|
+
};
|
|
4471
|
+
},
|
|
4472
|
+
close: () => native.dispose()
|
|
4473
|
+
};
|
|
4474
|
+
}
|
|
4475
|
+
var ChildProjection = local("neko.subagent-projection", {
|
|
4476
|
+
version: 1,
|
|
4477
|
+
input: Type5.Object({ parentId: Type5.Integer(), agentId: Type5.String() }),
|
|
4478
|
+
members: {
|
|
4479
|
+
revision: Type5.Integer(),
|
|
4480
|
+
reader: opaque("neko.subagent-reader@1")
|
|
4481
|
+
}
|
|
4482
|
+
});
|
|
4483
|
+
function installSubagentHost(host, runtime, context) {
|
|
4484
|
+
host.serve(ChildProjection, (input, _principal, self) => {
|
|
4485
|
+
const { revision } = self.state({ revision: cell(Type5.Integer(), 0) });
|
|
4486
|
+
const { reader } = self.resources({
|
|
4487
|
+
reader: resource({
|
|
4488
|
+
rev: 1,
|
|
4489
|
+
async open() {
|
|
4490
|
+
const entry = (await listDelegations(runtime.harness, input.parentId, context)).find((d) => input.agentId.startsWith("round-") ? d.taskId === Number(input.agentId.slice(6)) : (d.agentId ?? agentIdFor(d.childId)) === input.agentId);
|
|
4491
|
+
if (!entry)
|
|
4492
|
+
throw new ChordError("gone", "Subagent unavailable");
|
|
4493
|
+
const r = await openReader(runtime, entry.childId, input.parentId, context, entry.historyThrough, entry.historyUsage);
|
|
4494
|
+
try {
|
|
4495
|
+
const initial = await r.refresh();
|
|
4496
|
+
await host.root((ctx) => revision.replace(ctx, initial));
|
|
4497
|
+
return r;
|
|
4498
|
+
} catch (error) {
|
|
4499
|
+
r.close();
|
|
4500
|
+
throw error;
|
|
4501
|
+
}
|
|
4502
|
+
},
|
|
4503
|
+
close: (r) => r.close()
|
|
4504
|
+
})
|
|
4505
|
+
});
|
|
4506
|
+
self.listen({
|
|
4507
|
+
view: listener(reader, (r) => r, async (_event, ctx) => {
|
|
4508
|
+
revision.replace(ctx, await reader.value.refresh());
|
|
4509
|
+
})
|
|
4510
|
+
});
|
|
4511
|
+
return { revision, reader: self.expose(reader) };
|
|
4512
|
+
});
|
|
4513
|
+
host.serve(NekoSubagent, (input, principal, self) => {
|
|
4514
|
+
const owner = self.get(Owner, {}, { principal });
|
|
4515
|
+
const check = async () => {
|
|
4516
|
+
if (owner.owner?.active?.attachmentId !== input.attachmentId || owner.owner.active.sessionId !== input.sessionId)
|
|
4517
|
+
throw new ChordError("forbidden", "Attachment expired");
|
|
4518
|
+
if (!await runtime.getConversation(Number(input.sessionId)))
|
|
4519
|
+
throw new ChordError("gone", "Session unavailable");
|
|
4520
|
+
const entry = (await listDelegations(runtime.harness, Number(input.sessionId), context)).find((d) => input.agentId !== undefined ? (d.agentId ?? agentIdFor(d.childId)) === input.agentId : d.taskId === input.taskId);
|
|
4521
|
+
if (!entry)
|
|
4522
|
+
throw new ChordError("forbidden", "Subagent does not belong to this session");
|
|
4523
|
+
return entry;
|
|
4524
|
+
};
|
|
4525
|
+
const { target } = self.resources({ target: resource({ rev: 1, deps: [owner], open: check }) });
|
|
4526
|
+
const shared = self.get(ChildProjection, { parentId: Number(input.sessionId), agentId: input.agentId ?? `round-${input.taskId}` }, { principal: hostPrincipal });
|
|
4527
|
+
const { authorizedRevision } = self.state({ authorizedRevision: cell(Type5.Integer(), 0) });
|
|
4528
|
+
self.listen({
|
|
4529
|
+
authorizedView: listener(shared.revision, async (value, ctx) => {
|
|
4530
|
+
try {
|
|
4531
|
+
target.value;
|
|
4532
|
+
await check();
|
|
4533
|
+
if (value)
|
|
4534
|
+
authorizedRevision.replace(ctx, value);
|
|
4535
|
+
} catch {
|
|
4536
|
+
authorizedRevision.replace(ctx, 0);
|
|
4537
|
+
}
|
|
4538
|
+
})
|
|
4539
|
+
});
|
|
4540
|
+
return {
|
|
4541
|
+
kind: "ready",
|
|
4542
|
+
revision: authorizedRevision,
|
|
4543
|
+
chunk: async (request) => {
|
|
4544
|
+
await check();
|
|
4545
|
+
return shared.reader.chunk(request);
|
|
4546
|
+
},
|
|
4547
|
+
getImage: async ({ entryId, contentIndex, offset }) => {
|
|
4548
|
+
await check();
|
|
4549
|
+
return shared.reader.image(entryId, contentIndex, offset);
|
|
4550
|
+
},
|
|
4551
|
+
decideApproval: async ({ id, version, decision }) => {
|
|
4552
|
+
const entry = await check();
|
|
4553
|
+
if (entry.historyThrough !== undefined)
|
|
4554
|
+
throw new ChordError("forbidden", "Fork history is read-only");
|
|
4555
|
+
const approval = (await runtime.approvals.list(Number(input.sessionId), context)).find((a) => a.id === id && a.childId === entry.childId);
|
|
4556
|
+
if (!approval)
|
|
4557
|
+
throw new ChordError("forbidden", "Approval does not belong to this agent");
|
|
4558
|
+
return runtime.approvals.decide(Number(input.sessionId), approval.delegationTaskId, id, version, decision, context);
|
|
4559
|
+
},
|
|
4560
|
+
stop: async (request) => {
|
|
4561
|
+
const entry = await check();
|
|
4562
|
+
if (entry.historyThrough !== undefined)
|
|
4563
|
+
throw new ChordError("forbidden", "Fork history is read-only");
|
|
4564
|
+
const taskId = request?.taskId ?? input.taskId;
|
|
4565
|
+
if (taskId === undefined)
|
|
4566
|
+
throw new ChordError("invalid", "Select the exact active round to stop");
|
|
4567
|
+
const valid = await runtime.harness.commit(async (tx) => {
|
|
4568
|
+
const identity = (await tx.doc(SubagentsDoc, Number(input.sessionId))).items.find((a) => a.childId === entry.childId);
|
|
4569
|
+
return identity?.activeRoundId === taskId;
|
|
4570
|
+
}, context);
|
|
4571
|
+
if (!valid)
|
|
4572
|
+
return null;
|
|
4573
|
+
await runtime.harness.abortTask(taskId, context);
|
|
4574
|
+
await runtime.harness.waitForTask(taskId, context);
|
|
4575
|
+
return null;
|
|
4576
|
+
}
|
|
4577
|
+
};
|
|
4578
|
+
});
|
|
4579
|
+
}
|
|
4580
|
+
|
|
3288
4581
|
// packages/gateway/src/session-host.ts
|
|
3289
|
-
var OwnerInput =
|
|
3290
|
-
var Owner =
|
|
4582
|
+
var OwnerInput = Type6.Object({});
|
|
4583
|
+
var Owner = local2("neko.attachment-owner", {
|
|
3291
4584
|
version: 1,
|
|
3292
4585
|
input: OwnerInput,
|
|
3293
|
-
members: { owner:
|
|
4586
|
+
members: { owner: opaque2("neko.owner@1") }
|
|
3294
4587
|
});
|
|
3295
|
-
var Projection =
|
|
4588
|
+
var Projection = local2("neko.projection", {
|
|
3296
4589
|
version: 1,
|
|
3297
|
-
input:
|
|
4590
|
+
input: Type6.Object({ id: Type6.String() }),
|
|
3298
4591
|
members: {
|
|
3299
4592
|
view: ChatViewSchema,
|
|
3300
|
-
reader:
|
|
4593
|
+
reader: opaque2("neko.projection-reader@1")
|
|
3301
4594
|
}
|
|
3302
4595
|
});
|
|
3303
4596
|
var emptyView = {
|
|
@@ -3323,6 +4616,11 @@ async function openProjection(runtime, session, context) {
|
|
|
3323
4616
|
let changed;
|
|
3324
4617
|
const stop = view.subscribe(() => changed?.(null));
|
|
3325
4618
|
const stopMetadata = runtime.onDirectoryChange(() => changed?.(null));
|
|
4619
|
+
let childIds = new Set;
|
|
4620
|
+
const stopChildren = runtime.harness.subscribeCommits(({ changes }) => {
|
|
4621
|
+
if (changes.some((change) => change.type === "document" && (["app.neko.delegations", "app.neko.approvals", "app.neko.subagents"].includes(change.record.kind) && change.conversationId === session.id || change.record.kind === "pi.usage" && change.conversationId !== undefined && childIds.has(change.conversationId))))
|
|
4622
|
+
changed?.(null);
|
|
4623
|
+
});
|
|
3326
4624
|
return {
|
|
3327
4625
|
subscribe(fn) {
|
|
3328
4626
|
changed = fn;
|
|
@@ -3335,7 +4633,7 @@ async function openProjection(runtime, session, context) {
|
|
|
3335
4633
|
for (const entry of view.value.entries)
|
|
3336
4634
|
history.set(entry.id, entry);
|
|
3337
4635
|
const [naming, compaction] = await Promise.all([runtime.namingUsage(session.id), session.compactionRecord()]);
|
|
3338
|
-
|
|
4636
|
+
const projected = projectChatView({
|
|
3339
4637
|
...view.value,
|
|
3340
4638
|
entries: [...history.values()].sort((a, b) => a.id - b.id),
|
|
3341
4639
|
docs: {
|
|
@@ -3346,6 +4644,33 @@ async function openProjection(runtime, session, context) {
|
|
|
3346
4644
|
}, naming)
|
|
3347
4645
|
}
|
|
3348
4646
|
}, "", { models: runtime.listModels(), contextEntries: view.value.entries, compaction });
|
|
4647
|
+
const subagents = await listDelegations(runtime.harness, session.id, context);
|
|
4648
|
+
const approvals = await runtime.approvals.list(session.id, context);
|
|
4649
|
+
for (const entry of subagents)
|
|
4650
|
+
entry.approvalCount = entry.historyThrough === undefined ? approvals.filter((a) => a.delegationTaskId === entry.taskId && a.status === "pending").length : 0;
|
|
4651
|
+
childIds = new Set(subagents.filter((entry) => entry.historyThrough === undefined).map((entry) => entry.childId));
|
|
4652
|
+
let aggregate = await session.usage();
|
|
4653
|
+
for (const child of new Map(subagents.filter((entry) => entry.historyThrough === undefined).map((r) => [r.childId, r])).values())
|
|
4654
|
+
aggregate = mergeUsage(aggregate, await runtime.harness.snapshot(UsageDoc4, child.childId, context) ?? {
|
|
4655
|
+
models: {},
|
|
4656
|
+
tools: {}
|
|
4657
|
+
});
|
|
4658
|
+
const u = totalUsage(aggregate);
|
|
4659
|
+
return {
|
|
4660
|
+
...projected,
|
|
4661
|
+
agents: summarizeSubagents(subagents, (await runtime.harness.snapshot(SubagentsDoc, session.id, context))?.items),
|
|
4662
|
+
subagents: subagents.map(({ usageStart: _start, usageEnd: _end, ...entry }) => ({
|
|
4663
|
+
...entry,
|
|
4664
|
+
report: entry.report?.slice(0, 512) ?? null
|
|
4665
|
+
})),
|
|
4666
|
+
taskUsage: {
|
|
4667
|
+
input: u.input,
|
|
4668
|
+
output: u.output,
|
|
4669
|
+
cacheRead: u.cacheRead,
|
|
4670
|
+
cacheWrite: u.cacheWrite,
|
|
4671
|
+
cost: u.cost.total
|
|
4672
|
+
}
|
|
4673
|
+
};
|
|
3349
4674
|
},
|
|
3350
4675
|
async entry(id, ctx) {
|
|
3351
4676
|
ctx.abortSignal?.throwIfAborted();
|
|
@@ -3356,15 +4681,17 @@ async function openProjection(runtime, session, context) {
|
|
|
3356
4681
|
close() {
|
|
3357
4682
|
stop();
|
|
3358
4683
|
stopMetadata();
|
|
4684
|
+
stopChildren();
|
|
3359
4685
|
view.dispose();
|
|
3360
4686
|
history.clear();
|
|
3361
4687
|
}
|
|
3362
4688
|
};
|
|
3363
4689
|
}
|
|
3364
4690
|
function serveSessions(host, runtime, context) {
|
|
4691
|
+
installSubagentHost(host, runtime, context);
|
|
3365
4692
|
host.serve(Owner, (_input, _principal, self) => {
|
|
3366
4693
|
const { owner } = self.resources({
|
|
3367
|
-
owner:
|
|
4694
|
+
owner: resource2({
|
|
3368
4695
|
rev: 1,
|
|
3369
4696
|
open: () => ({ active: null }),
|
|
3370
4697
|
close: (value) => {
|
|
@@ -3375,14 +4702,14 @@ function serveSessions(host, runtime, context) {
|
|
|
3375
4702
|
return { owner: self.expose(owner) };
|
|
3376
4703
|
});
|
|
3377
4704
|
host.serve(Projection, (input, _principal, self) => {
|
|
3378
|
-
const { view } = self.state({ view:
|
|
4705
|
+
const { view } = self.state({ view: cell2(ChatViewSchema, emptyView) });
|
|
3379
4706
|
const { projection } = self.resources({
|
|
3380
|
-
projection:
|
|
4707
|
+
projection: resource2({
|
|
3381
4708
|
rev: 1,
|
|
3382
4709
|
async open({ signal }) {
|
|
3383
4710
|
const session = await runtime.getConversation(Number(input.id));
|
|
3384
4711
|
if (!session)
|
|
3385
|
-
throw new
|
|
4712
|
+
throw new ChordError2("gone", "Session unavailable");
|
|
3386
4713
|
const opened = await openProjection(runtime, session, context);
|
|
3387
4714
|
try {
|
|
3388
4715
|
const initial = await opened.project();
|
|
@@ -3397,7 +4724,7 @@ function serveSessions(host, runtime, context) {
|
|
|
3397
4724
|
})
|
|
3398
4725
|
});
|
|
3399
4726
|
self.listen({
|
|
3400
|
-
view:
|
|
4727
|
+
view: listener2(projection, (value) => value, async (_event, ctx) => {
|
|
3401
4728
|
view.replace(ctx, await projection.value.project());
|
|
3402
4729
|
})
|
|
3403
4730
|
});
|
|
@@ -3405,18 +4732,18 @@ function serveSessions(host, runtime, context) {
|
|
|
3405
4732
|
});
|
|
3406
4733
|
host.serve(NekoChat, (input, principal, self) => {
|
|
3407
4734
|
const owner = self.get(Owner, {}, { principal });
|
|
3408
|
-
const shared = self.get(Projection, { id: input.sessionId }, { principal:
|
|
4735
|
+
const shared = self.get(Projection, { id: input.sessionId }, { principal: hostPrincipal2 });
|
|
3409
4736
|
const { uploads, projection } = self.resources({
|
|
3410
|
-
uploads:
|
|
3411
|
-
projection:
|
|
4737
|
+
uploads: resource2({ rev: 1, open: () => new ImageUploads, close: (value) => value.close() }),
|
|
4738
|
+
projection: resource2({
|
|
3412
4739
|
rev: 1,
|
|
3413
4740
|
deps: [owner],
|
|
3414
4741
|
async open() {
|
|
3415
4742
|
if (owner.owner?.active?.attachmentId !== input.attachmentId || owner.owner.active.sessionId !== input.sessionId)
|
|
3416
|
-
throw new
|
|
4743
|
+
throw new ChordError2("forbidden", "Attachment does not belong to this connection");
|
|
3417
4744
|
const session = await runtime.getConversation(Number(input.sessionId));
|
|
3418
4745
|
if (!session)
|
|
3419
|
-
throw new
|
|
4746
|
+
throw new ChordError2("gone", "Session unavailable");
|
|
3420
4747
|
return {
|
|
3421
4748
|
session
|
|
3422
4749
|
};
|
|
@@ -3427,7 +4754,7 @@ function serveSessions(host, runtime, context) {
|
|
|
3427
4754
|
try {
|
|
3428
4755
|
return action();
|
|
3429
4756
|
} catch (error) {
|
|
3430
|
-
throw new
|
|
4757
|
+
throw new ChordError2("invalid", error instanceof Error ? error.message : "Image upload failed");
|
|
3431
4758
|
}
|
|
3432
4759
|
};
|
|
3433
4760
|
const behavior = {
|
|
@@ -3483,7 +4810,7 @@ function serveSessions(host, runtime, context) {
|
|
|
3483
4810
|
await projection.value.session.compact(instructions);
|
|
3484
4811
|
return null;
|
|
3485
4812
|
} catch (error) {
|
|
3486
|
-
throw new
|
|
4813
|
+
throw new ChordError2("invalid", error instanceof Error ? error.message : "Cannot compact");
|
|
3487
4814
|
}
|
|
3488
4815
|
},
|
|
3489
4816
|
beginImage: async (input) => safeUpload(() => uploads.value.begin(input)),
|
|
@@ -3501,15 +4828,15 @@ function serveSessions(host, runtime, context) {
|
|
|
3501
4828
|
},
|
|
3502
4829
|
getImage: async (input, ctx) => {
|
|
3503
4830
|
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,
|
|
4831
|
+
throw new ChordError2("invalid", "Invalid image range");
|
|
4832
|
+
const entry = await shared.reader.entry(input.entryId, withAbortSignal3(ctx.signal, context));
|
|
3506
4833
|
const content = entry?.model?.[0]?.content;
|
|
3507
4834
|
const image = Array.isArray(content) ? content[input.contentIndex] : undefined;
|
|
3508
4835
|
if (image?.type !== "image")
|
|
3509
|
-
throw new
|
|
4836
|
+
throw new ChordError2("invalid", "Image is not part of this conversation");
|
|
3510
4837
|
const bytes = Buffer.from(image.data, "base64");
|
|
3511
4838
|
if (input.offset > bytes.length)
|
|
3512
|
-
throw new
|
|
4839
|
+
throw new ChordError2("invalid", "Invalid image range");
|
|
3513
4840
|
return {
|
|
3514
4841
|
data: bytes.subarray(input.offset, input.offset + IMAGE_CHUNK_BYTES).toString("base64"),
|
|
3515
4842
|
total: bytes.length,
|
|
@@ -3529,8 +4856,8 @@ function serveSessions(host, runtime, context) {
|
|
|
3529
4856
|
uploads.value.release(id);
|
|
3530
4857
|
} catch (error) {
|
|
3531
4858
|
if (error instanceof Error && error.message.startsWith("DEEPSEEK_KEY_REQUIRED:"))
|
|
3532
|
-
throw new
|
|
3533
|
-
throw new
|
|
4859
|
+
throw new ChordError2("invalid", "DEEPSEEK_KEY_REQUIRED: Configure DeepSeek in /login or Web settings.");
|
|
4860
|
+
throw new ChordError2("invalid", error instanceof Error ? error.message : "Cannot submit message");
|
|
3534
4861
|
}
|
|
3535
4862
|
return null;
|
|
3536
4863
|
},
|
|
@@ -3552,16 +4879,16 @@ function serveSessions(host, runtime, context) {
|
|
|
3552
4879
|
configure: async (change) => {
|
|
3553
4880
|
const applied = await runtime.configureConversation(projection.value.session.id, change);
|
|
3554
4881
|
if (!applied) {
|
|
3555
|
-
throw new
|
|
4882
|
+
throw new ChordError2("invalid", `Session ${String(projection.value.session.id)} is unavailable or archived`);
|
|
3556
4883
|
}
|
|
3557
4884
|
return null;
|
|
3558
4885
|
}
|
|
3559
4886
|
};
|
|
3560
4887
|
const guard = async () => {
|
|
3561
4888
|
if (owner.owner?.active?.attachmentId !== input.attachmentId || owner.owner.active.sessionId !== input.sessionId)
|
|
3562
|
-
throw new
|
|
4889
|
+
throw new ChordError2("gone", "Attachment is no longer active");
|
|
3563
4890
|
if (!await runtime.getConversation(Number(input.sessionId)))
|
|
3564
|
-
throw new
|
|
4891
|
+
throw new ChordError2("gone", "Session unavailable or archived");
|
|
3565
4892
|
};
|
|
3566
4893
|
for (const name of Object.keys(NekoChat.def.variants.ready.methods)) {
|
|
3567
4894
|
const method = behavior[name];
|
|
@@ -3576,7 +4903,7 @@ function serveSessions(host, runtime, context) {
|
|
|
3576
4903
|
}
|
|
3577
4904
|
|
|
3578
4905
|
// packages/gateway/src/skill-market.ts
|
|
3579
|
-
import { createHash, randomUUID } from "crypto";
|
|
4906
|
+
import { createHash, randomUUID as randomUUID2 } from "crypto";
|
|
3580
4907
|
import { mkdir as mkdir3, readdir as readdir2, rename as rename3, rm as rm3, stat as stat2, writeFile as writeFile3 } from "fs/promises";
|
|
3581
4908
|
import { homedir as homedir6 } from "os";
|
|
3582
4909
|
import { dirname as dirname6, join as join8 } from "path";
|
|
@@ -4150,7 +5477,7 @@ function createSkillMarketClient(options) {
|
|
|
4150
5477
|
const skill = await detail(name);
|
|
4151
5478
|
const files = unpack(skill, await download(skill));
|
|
4152
5479
|
await sweepTempDirs(skillsDir);
|
|
4153
|
-
const temp = join8(dirname6(skillsDir), `.tmp-${
|
|
5480
|
+
const temp = join8(dirname6(skillsDir), `.tmp-${randomUUID2()}`);
|
|
4154
5481
|
try {
|
|
4155
5482
|
for (const [relative, bytes] of files) {
|
|
4156
5483
|
const dest = join8(temp, ...relative.split("/"));
|
|
@@ -4271,14 +5598,14 @@ async function listDrives() {
|
|
|
4271
5598
|
}
|
|
4272
5599
|
|
|
4273
5600
|
// packages/gateway/src/server-services.ts
|
|
4274
|
-
var ServerState =
|
|
5601
|
+
var ServerState = local3("neko.server-state", {
|
|
4275
5602
|
version: 1,
|
|
4276
|
-
input:
|
|
5603
|
+
input: Type7.Object({}),
|
|
4277
5604
|
members: {
|
|
4278
5605
|
directory: DirectoryStateSchema,
|
|
4279
5606
|
workspaces: WorkspaceStateSchema,
|
|
4280
5607
|
credentials: CredentialStateSchema,
|
|
4281
|
-
refreshCredentials: method2(
|
|
5608
|
+
refreshCredentials: method2(Type7.Null(), Type7.Null())
|
|
4282
5609
|
}
|
|
4283
5610
|
});
|
|
4284
5611
|
async function createNekoServerServices(options) {
|
|
@@ -4298,9 +5625,9 @@ async function createNekoServerServices(options) {
|
|
|
4298
5625
|
listener(updates.state);
|
|
4299
5626
|
};
|
|
4300
5627
|
host.serve(NekoApplicationInfo, (_input, _principal, self) => {
|
|
4301
|
-
const state = self.state({ info:
|
|
5628
|
+
const state = self.state({ info: cell3(ApplicationInfoSchema, updates.state) });
|
|
4302
5629
|
self.listen({
|
|
4303
|
-
updates:
|
|
5630
|
+
updates: listener3({
|
|
4304
5631
|
subscribe: (fn) => {
|
|
4305
5632
|
updateListeners.add(fn);
|
|
4306
5633
|
return () => updateListeners.delete(fn);
|
|
@@ -4332,7 +5659,7 @@ async function createNekoServerServices(options) {
|
|
|
4332
5659
|
const toRemote = (error) => {
|
|
4333
5660
|
if (error instanceof SkillMarketError) {
|
|
4334
5661
|
const code = error.code === "skill_not_found" || error.code === "skill_not_installed" ? "gone" : "invalid";
|
|
4335
|
-
throw new
|
|
5662
|
+
throw new ChordError3(code, error.message, { reason: error.code });
|
|
4336
5663
|
}
|
|
4337
5664
|
throw error;
|
|
4338
5665
|
};
|
|
@@ -4366,12 +5693,12 @@ async function createNekoServerServices(options) {
|
|
|
4366
5693
|
};
|
|
4367
5694
|
host.serve(ServerState, (_input, _principal, self) => {
|
|
4368
5695
|
const state = self.state({
|
|
4369
|
-
directory:
|
|
4370
|
-
workspaces:
|
|
4371
|
-
credentials:
|
|
5696
|
+
directory: cell3(DirectoryStateSchema, { revision: 1, sessions }),
|
|
5697
|
+
workspaces: cell3(WorkspaceStateSchema, { revision: 1, workspaces }),
|
|
5698
|
+
credentials: cell3(CredentialStateSchema, { revision: 1, statuses })
|
|
4372
5699
|
});
|
|
4373
5700
|
self.listen({
|
|
4374
|
-
directory:
|
|
5701
|
+
directory: listener3({ subscribe: runtime.harness.subscribeCommits.bind(runtime.harness) }, async (event, ctx) => {
|
|
4375
5702
|
if (!event.changes.some((change) => change.type === "conversation" || change.type === "document" && [
|
|
4376
5703
|
"app.neko.session-metadata",
|
|
4377
5704
|
"app.neko.activity",
|
|
@@ -4398,7 +5725,7 @@ async function createNekoServerServices(options) {
|
|
|
4398
5725
|
}
|
|
4399
5726
|
};
|
|
4400
5727
|
}, { eager: { input: {} } });
|
|
4401
|
-
const shared = (self) => self.get(ServerState, {}, { principal:
|
|
5728
|
+
const shared = (self) => self.get(ServerState, {}, { principal: hostPrincipal3 });
|
|
4402
5729
|
host.serve(NekoSessionDirectory, (_input, _principal, self) => ({ kind: "ready", state: shared(self).directory }));
|
|
4403
5730
|
host.serve(NekoWorkspaces, (_input, _principal, self) => ({
|
|
4404
5731
|
kind: "ready",
|
|
@@ -4434,7 +5761,7 @@ async function createNekoServerServices(options) {
|
|
|
4434
5761
|
search: async (input, ctx) => {
|
|
4435
5762
|
const roots = await runtime.listWorkspaces();
|
|
4436
5763
|
if (!roots.some((root) => root.path === input.workspaceRoot))
|
|
4437
|
-
throw new
|
|
5764
|
+
throw new ChordError3("invalid", "Unknown workspace.");
|
|
4438
5765
|
return runtime.workspaceFiles.search(input.workspaceRoot, input.query, ctx.signal);
|
|
4439
5766
|
}
|
|
4440
5767
|
}));
|
|
@@ -4444,7 +5771,7 @@ async function createNekoServerServices(options) {
|
|
|
4444
5771
|
const summary = async (id) => {
|
|
4445
5772
|
const found = (await list()).find((item) => item.id === String(id));
|
|
4446
5773
|
if (!found)
|
|
4447
|
-
throw new
|
|
5774
|
+
throw new ChordError3("gone", "Session unavailable");
|
|
4448
5775
|
return found;
|
|
4449
5776
|
};
|
|
4450
5777
|
return {
|
|
@@ -4457,9 +5784,9 @@ async function createNekoServerServices(options) {
|
|
|
4457
5784
|
},
|
|
4458
5785
|
attach: (id) => serialize(async () => {
|
|
4459
5786
|
if (!await runtime.getConversation(toConversationId(id)))
|
|
4460
|
-
throw new
|
|
5787
|
+
throw new ChordError3("gone", `Unknown session: ${id}`);
|
|
4461
5788
|
if (owner.owner.active?.sessionId !== id) {
|
|
4462
|
-
owner.owner.active = { sessionId: id, attachmentId:
|
|
5789
|
+
owner.owner.active = { sessionId: id, attachmentId: randomUUID3() };
|
|
4463
5790
|
await runtime.touchConversation(toConversationId(id));
|
|
4464
5791
|
}
|
|
4465
5792
|
return owner.owner.active;
|
|
@@ -4495,7 +5822,7 @@ async function createNekoServerServices(options) {
|
|
|
4495
5822
|
function toConversationId(sessionId) {
|
|
4496
5823
|
const numeric = Number(sessionId);
|
|
4497
5824
|
if (!Number.isSafeInteger(numeric) || numeric < 1 || String(numeric) !== sessionId)
|
|
4498
|
-
throw new
|
|
5825
|
+
throw new ChordError3("invalid", `Invalid neko session id: ${sessionId}`);
|
|
4499
5826
|
return numeric;
|
|
4500
5827
|
}
|
|
4501
5828
|
|
|
@@ -4792,7 +6119,7 @@ var DEFAULT_HOST = "127.0.0.1";
|
|
|
4792
6119
|
var DEFAULT_PORT = 8321;
|
|
4793
6120
|
var SERVER_ID_FILENAME = "gateway-server-id";
|
|
4794
6121
|
async function startGateway(options = {}) {
|
|
4795
|
-
const context = options.context ??
|
|
6122
|
+
const context = options.context ?? BACKGROUND_CONTEXT4;
|
|
4796
6123
|
const fileConfig = loadNekoConfig(options.configDir);
|
|
4797
6124
|
const model = options.model ?? fileConfig.defaultModel ?? DEFAULT_MODEL;
|
|
4798
6125
|
const storagePath = options.storagePath ?? fileConfig.storagePath ?? DEFAULT_STORAGE_PATH;
|
|
@@ -4862,7 +6189,7 @@ async function bindGateway(runtime, options) {
|
|
|
4862
6189
|
}
|
|
4863
6190
|
try {
|
|
4864
6191
|
for (const listener of listeners)
|
|
4865
|
-
await listener.start((link) => chord.accept(link, { principal: { connectionId:
|
|
6192
|
+
await listener.start((link) => chord.accept(link, { principal: { connectionId: randomUUID4() } }));
|
|
4866
6193
|
} catch (error) {
|
|
4867
6194
|
await Promise.allSettled(listeners.map((listener) => listener.close()));
|
|
4868
6195
|
await chord.close();
|
|
@@ -4932,7 +6259,7 @@ async function loadOrCreateServerId(nekoDir) {
|
|
|
4932
6259
|
if (existing !== "")
|
|
4933
6260
|
return existing;
|
|
4934
6261
|
} catch {}
|
|
4935
|
-
const created =
|
|
6262
|
+
const created = randomUUID4();
|
|
4936
6263
|
await mkdir5(nekoDir, { recursive: true, mode: 448 });
|
|
4937
6264
|
await writeFile4(file, `${created}
|
|
4938
6265
|
`, { mode: 384 });
|