neko-harness 0.3.0 → 0.3.2
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 +3 -1
- package/dist/client/assets/index-DhW2urBC.css +2 -0
- package/dist/client/assets/index-Dz-jYu0K.js +129 -0
- package/dist/client/index.html +2 -2
- package/dist/gateway.js +661 -157
- package/dist/neko.js +3149 -297
- package/package.json +1 -1
- package/dist/client/assets/index-DrXtf9P_.js +0 -83
- package/dist/client/assets/index-Dv47_XaY.css +0 -2
package/dist/gateway.js
CHANGED
|
@@ -517,7 +517,10 @@ function summarizeToolCall(name, args) {
|
|
|
517
517
|
const record = args ?? {};
|
|
518
518
|
switch (name) {
|
|
519
519
|
case "subagent":
|
|
520
|
-
return {
|
|
520
|
+
return {
|
|
521
|
+
args: record.action === "list" ? "List agents" : `${record.agentId ?? record.type}: ${record.title}`,
|
|
522
|
+
detail: String(record.task ?? "")
|
|
523
|
+
};
|
|
521
524
|
case "find_files":
|
|
522
525
|
case "search_text":
|
|
523
526
|
return {
|
|
@@ -557,6 +560,53 @@ function toolResultText(content) {
|
|
|
557
560
|
return content.filter((block) => block.type === "text" && typeof block.text === "string").map((block) => block.text ?? "").join(`
|
|
558
561
|
`);
|
|
559
562
|
}
|
|
563
|
+
var WRITE_DIFF_MAX_LINES = 2000;
|
|
564
|
+
var WRITE_DIFF_MAX_BYTES = 64 * 1024;
|
|
565
|
+
function countDiffLines(diff) {
|
|
566
|
+
let added = 0;
|
|
567
|
+
let removed = 0;
|
|
568
|
+
for (const line of diff.split(`
|
|
569
|
+
`)) {
|
|
570
|
+
if (line.startsWith("+") && !line.startsWith("+++"))
|
|
571
|
+
added++;
|
|
572
|
+
else if (line.startsWith("-") && !line.startsWith("---"))
|
|
573
|
+
removed++;
|
|
574
|
+
}
|
|
575
|
+
return { added, removed };
|
|
576
|
+
}
|
|
577
|
+
function editResultDiff(details) {
|
|
578
|
+
if (typeof details !== "object" || details === null)
|
|
579
|
+
return null;
|
|
580
|
+
const diff = details.diff;
|
|
581
|
+
return typeof diff === "string" ? diff : null;
|
|
582
|
+
}
|
|
583
|
+
function synthesizedWriteDiff(args) {
|
|
584
|
+
const record = args ?? {};
|
|
585
|
+
if (typeof record.content !== "string")
|
|
586
|
+
return null;
|
|
587
|
+
const content = record.content;
|
|
588
|
+
const shown = [];
|
|
589
|
+
let offset = 0;
|
|
590
|
+
let bytes = 0;
|
|
591
|
+
while (offset < content.length && shown.length < WRITE_DIFF_MAX_LINES) {
|
|
592
|
+
const newline = content.indexOf(`
|
|
593
|
+
`, offset);
|
|
594
|
+
const end = newline === -1 ? content.length : newline;
|
|
595
|
+
const overhead = shown.length === 0 ? 2 : 3;
|
|
596
|
+
const remaining = WRITE_DIFF_MAX_BYTES - bytes - overhead;
|
|
597
|
+
if (end - offset > remaining)
|
|
598
|
+
break;
|
|
599
|
+
const line = content.slice(offset, end);
|
|
600
|
+
const lineBytes = Buffer.byteLength(line, "utf8");
|
|
601
|
+
if (lineBytes > remaining)
|
|
602
|
+
break;
|
|
603
|
+
shown.push(`+ ${line}`);
|
|
604
|
+
bytes += lineBytes + overhead;
|
|
605
|
+
offset = newline === -1 ? content.length : end + 1;
|
|
606
|
+
}
|
|
607
|
+
return { diff: shown.join(`
|
|
608
|
+
`), added: shown.length, removed: 0, truncated: offset < content.length };
|
|
609
|
+
}
|
|
560
610
|
function totalUsage(state) {
|
|
561
611
|
const total = {
|
|
562
612
|
input: 0,
|
|
@@ -589,6 +639,8 @@ function projectChatView(view, modelLabel, options = {}) {
|
|
|
589
639
|
if (message.role !== "toolResult")
|
|
590
640
|
continue;
|
|
591
641
|
const diagnostics = entry.data?.diagnostics ?? [];
|
|
642
|
+
const diff = message.toolName === "edit" ? editResultDiff(message.details) : null;
|
|
643
|
+
const counts = diff === null ? null : countDiffLines(diff);
|
|
592
644
|
results.set(message.toolCallId, {
|
|
593
645
|
type: "tool",
|
|
594
646
|
key: `t${message.toolCallId}`,
|
|
@@ -605,7 +657,10 @@ function projectChatView(view, modelLabel, options = {}) {
|
|
|
605
657
|
].filter(Boolean).join(`
|
|
606
658
|
`),
|
|
607
659
|
durationMs: message.durationMs ?? null,
|
|
608
|
-
note: null
|
|
660
|
+
note: null,
|
|
661
|
+
diff,
|
|
662
|
+
diffAdded: counts?.added ?? null,
|
|
663
|
+
diffRemoved: counts?.removed ?? null
|
|
609
664
|
});
|
|
610
665
|
}
|
|
611
666
|
}
|
|
@@ -640,6 +695,7 @@ function projectChatView(view, modelLabel, options = {}) {
|
|
|
640
695
|
const defaultImagePrompt = entry.kind === NekoInput.kind ? savedDefaultImagePrompt(message.content, entry.data?.defaultImagePrompt) : undefined;
|
|
641
696
|
items.push({
|
|
642
697
|
type: "user",
|
|
698
|
+
...entry.data?.subagentRoundId ? { subagentRoundId: entry.data.subagentRoundId } : {},
|
|
643
699
|
key: `e${entry.id}`,
|
|
644
700
|
id: entry.id,
|
|
645
701
|
text: defaultImagePrompt && Array.isArray(message.content) ? userText(message.content.slice(1)) : userText(message.content),
|
|
@@ -662,6 +718,7 @@ function projectChatView(view, modelLabel, options = {}) {
|
|
|
662
718
|
} else {
|
|
663
719
|
const summary = summarizeToolCall(block.name, block.arguments);
|
|
664
720
|
const slot = live.tools?.find((s) => s.callId === block.id);
|
|
721
|
+
const synthesized = block.name === "write" ? synthesizedWriteDiff(block.arguments) : null;
|
|
665
722
|
items.push({
|
|
666
723
|
type: "tool",
|
|
667
724
|
key: `t${block.id}`,
|
|
@@ -674,7 +731,11 @@ function projectChatView(view, modelLabel, options = {}) {
|
|
|
674
731
|
output: slot?.output ?? null,
|
|
675
732
|
result: null,
|
|
676
733
|
durationMs: null,
|
|
677
|
-
note: ran ? null : "Not run: response interrupted."
|
|
734
|
+
note: ran ? null : "Not run: response interrupted.",
|
|
735
|
+
diff: synthesized?.diff ?? null,
|
|
736
|
+
diffAdded: synthesized?.added ?? null,
|
|
737
|
+
diffRemoved: synthesized?.removed ?? null,
|
|
738
|
+
diffTruncated: synthesized?.truncated ?? false
|
|
678
739
|
});
|
|
679
740
|
}
|
|
680
741
|
}
|
|
@@ -722,7 +783,17 @@ function projectChatView(view, modelLabel, options = {}) {
|
|
|
722
783
|
}
|
|
723
784
|
function withArgs(item, name, args) {
|
|
724
785
|
const summary = summarizeToolCall(name, args);
|
|
725
|
-
|
|
786
|
+
const synthesized = name === "write" ? synthesizedWriteDiff(args) : null;
|
|
787
|
+
return {
|
|
788
|
+
...item,
|
|
789
|
+
name,
|
|
790
|
+
summary: summary.args,
|
|
791
|
+
detail: summary.detail ?? null,
|
|
792
|
+
diff: synthesized?.diff ?? item.diff,
|
|
793
|
+
diffAdded: synthesized?.added ?? item.diffAdded,
|
|
794
|
+
diffRemoved: synthesized?.removed ?? item.diffRemoved,
|
|
795
|
+
diffTruncated: synthesized?.truncated ?? false
|
|
796
|
+
};
|
|
726
797
|
}
|
|
727
798
|
function assistantItem(message, id) {
|
|
728
799
|
let thinking = "";
|
|
@@ -775,13 +846,18 @@ ${block.text}`;
|
|
|
775
846
|
if (block.type === "toolCall") {
|
|
776
847
|
const slot = live.tools?.find((candidate) => candidate.callId === block.id);
|
|
777
848
|
const summary = summarizeToolCall(block.name, block.arguments);
|
|
849
|
+
const synthesized = block.name === "write" ? synthesizedWriteDiff(block.arguments) : null;
|
|
778
850
|
toolCalls.push({
|
|
779
851
|
callId: block.id,
|
|
780
852
|
name: block.name,
|
|
781
853
|
summary: summary.args,
|
|
782
854
|
detail: summary.detail ?? null,
|
|
783
855
|
phase: slot === undefined || slot.status === "pending" ? "pending" : slot.status,
|
|
784
|
-
output: slot?.status === "running" ? slot.output ?? null : null
|
|
856
|
+
output: slot?.status === "running" ? slot.output ?? null : null,
|
|
857
|
+
diff: synthesized?.diff ?? null,
|
|
858
|
+
diffAdded: synthesized?.added ?? null,
|
|
859
|
+
diffRemoved: synthesized?.removed ?? null,
|
|
860
|
+
diffTruncated: synthesized?.truncated ?? false
|
|
785
861
|
});
|
|
786
862
|
}
|
|
787
863
|
}
|
|
@@ -840,7 +916,7 @@ import {
|
|
|
840
916
|
Harness,
|
|
841
917
|
LiveDoc as importLiveDoc,
|
|
842
918
|
MemoryStorage,
|
|
843
|
-
UsageDoc,
|
|
919
|
+
UsageDoc as UsageDoc2,
|
|
844
920
|
watchEvents
|
|
845
921
|
} from "@earendil-works/pi-durable";
|
|
846
922
|
import { createNodePhotonImages } from "@earendil-works/pi-durable/images/node";
|
|
@@ -1220,9 +1296,58 @@ import {
|
|
|
1220
1296
|
configure,
|
|
1221
1297
|
defineDoc,
|
|
1222
1298
|
defineExtension as defineExtension3,
|
|
1223
|
-
|
|
1299
|
+
defineTask,
|
|
1300
|
+
defineTool as defineTool2,
|
|
1301
|
+
LiveDoc,
|
|
1302
|
+
UsageDoc
|
|
1224
1303
|
} from "@earendil-works/pi-durable";
|
|
1225
1304
|
import { Type as Type2 } from "typebox";
|
|
1305
|
+
import { Value } from "typebox/value";
|
|
1306
|
+
|
|
1307
|
+
// packages/core/src/subagent-usage.ts
|
|
1308
|
+
var emptySubagentUsage = () => ({ models: {}, tools: {} });
|
|
1309
|
+
function subtractSubagentUsage(end, start) {
|
|
1310
|
+
const result = emptySubagentUsage();
|
|
1311
|
+
for (const bucket of ["models", "tools"]) {
|
|
1312
|
+
for (const [key, usage] of Object.entries(end[bucket])) {
|
|
1313
|
+
const before = start[bucket][key];
|
|
1314
|
+
const cost = usage.cost;
|
|
1315
|
+
const next = {
|
|
1316
|
+
input: usage.input - (before?.input ?? 0),
|
|
1317
|
+
output: usage.output - (before?.output ?? 0),
|
|
1318
|
+
cacheRead: usage.cacheRead - (before?.cacheRead ?? 0),
|
|
1319
|
+
cacheWrite: usage.cacheWrite - (before?.cacheWrite ?? 0),
|
|
1320
|
+
totalTokens: usage.totalTokens - (before?.totalTokens ?? 0),
|
|
1321
|
+
cost: {
|
|
1322
|
+
input: cost.input - (before?.cost.input ?? 0),
|
|
1323
|
+
output: cost.output - (before?.cost.output ?? 0),
|
|
1324
|
+
cacheRead: cost.cacheRead - (before?.cost.cacheRead ?? 0),
|
|
1325
|
+
cacheWrite: cost.cacheWrite - (before?.cost.cacheWrite ?? 0),
|
|
1326
|
+
total: cost.total - (before?.cost.total ?? 0)
|
|
1327
|
+
}
|
|
1328
|
+
};
|
|
1329
|
+
if (usage.reasoning !== undefined)
|
|
1330
|
+
next.reasoning = usage.reasoning - (before?.reasoning ?? 0);
|
|
1331
|
+
if (usage.cacheWrite1h !== undefined)
|
|
1332
|
+
next.cacheWrite1h = usage.cacheWrite1h - (before?.cacheWrite1h ?? 0);
|
|
1333
|
+
result[bucket][key] = next;
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
return result;
|
|
1337
|
+
}
|
|
1338
|
+
function subagentUsageTotals(state) {
|
|
1339
|
+
const totals = totalUsage(state);
|
|
1340
|
+
return {
|
|
1341
|
+
input: totals.input,
|
|
1342
|
+
output: totals.output,
|
|
1343
|
+
cacheRead: totals.cacheRead,
|
|
1344
|
+
cacheWrite: totals.cacheWrite,
|
|
1345
|
+
cost: totals.cost.total
|
|
1346
|
+
};
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
// packages/core/src/subagents.ts
|
|
1350
|
+
var agentIdFor = (childId) => `agent-${childId}`;
|
|
1226
1351
|
var DelegationsDoc = defineDoc({
|
|
1227
1352
|
kind: "app.neko.delegations",
|
|
1228
1353
|
version: 1,
|
|
@@ -1231,6 +1356,14 @@ var DelegationsDoc = defineDoc({
|
|
|
1231
1356
|
fork: "initial",
|
|
1232
1357
|
initial: () => ({ items: [] })
|
|
1233
1358
|
});
|
|
1359
|
+
var SubagentsDoc = defineDoc({
|
|
1360
|
+
kind: "app.neko.subagents",
|
|
1361
|
+
version: 1,
|
|
1362
|
+
scope: "conversation",
|
|
1363
|
+
history: "latest",
|
|
1364
|
+
fork: "initial",
|
|
1365
|
+
initial: () => ({ items: [] })
|
|
1366
|
+
});
|
|
1234
1367
|
var SubagentDoc = defineDoc({
|
|
1235
1368
|
kind: "app.neko.subagent",
|
|
1236
1369
|
version: 1,
|
|
@@ -1250,157 +1383,401 @@ var ROLE_PROMPTS = {
|
|
|
1250
1383
|
plan: "You are neko's read-only planning subagent. Read evidence and propose implementation steps, trade-offs and verification. Do not modify files or execute commands.",
|
|
1251
1384
|
coder: "You are neko's coding subagent. Implement only the assigned scope and verify it. Coordinate through the main agent; do not overwrite unrelated changes. Report changed files and verification results."
|
|
1252
1385
|
};
|
|
1253
|
-
var SUBAGENT_DELIVERY = " Work
|
|
1386
|
+
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.";
|
|
1254
1387
|
var READ_TOOLS = new Set(["read", "find_files", "search_text", "web_search", "web_extract"]);
|
|
1255
1388
|
async function refreshDelegations(tx, id) {
|
|
1256
1389
|
const doc = await tx.doc(DelegationsDoc, id);
|
|
1390
|
+
const identities = await tx.doc(SubagentsDoc, id);
|
|
1257
1391
|
for (const item of doc.items) {
|
|
1392
|
+
item.agentId ??= agentIdFor(item.childId);
|
|
1393
|
+
item.round ??= doc.items.filter((d) => d.childId === item.childId && d.taskId <= item.taskId).length;
|
|
1258
1394
|
if (item.historyThrough !== undefined)
|
|
1259
1395
|
continue;
|
|
1260
|
-
if (item.
|
|
1261
|
-
|
|
1396
|
+
if (item.inputEntryId === undefined) {
|
|
1397
|
+
const submission = await tx.submissionByRequest(item.childId, `subagent:${item.taskId}`);
|
|
1398
|
+
if (submission?.type === "input" && submission.entry !== undefined)
|
|
1399
|
+
item.inputEntryId = submission.entry;
|
|
1400
|
+
}
|
|
1401
|
+
let identity = identities.items.find((a) => a.childId === item.childId);
|
|
1402
|
+
if (!identity) {
|
|
1403
|
+
const role = await tx.doc(SubagentDoc, item.childId);
|
|
1404
|
+
if (role.parentId !== id || role.role !== item.role)
|
|
1405
|
+
continue;
|
|
1406
|
+
identity = {
|
|
1407
|
+
agentId: item.agentId,
|
|
1408
|
+
parentId: id,
|
|
1409
|
+
childId: item.childId,
|
|
1410
|
+
role: item.role,
|
|
1411
|
+
cwd: "",
|
|
1412
|
+
title: item.title,
|
|
1413
|
+
createdAt: item.createdAt,
|
|
1414
|
+
activeRoundId: item.taskId
|
|
1415
|
+
};
|
|
1416
|
+
identities.items.push(identity);
|
|
1417
|
+
identity = identities.items.at(-1);
|
|
1418
|
+
}
|
|
1262
1419
|
const task = await tx.task(item.taskId);
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1420
|
+
const runner = item.runnerId === undefined ? undefined : await tx.task(item.runnerId);
|
|
1421
|
+
const live = await tx.doc(LiveDoc, item.childId);
|
|
1422
|
+
const idle = live.run === undefined && !live.compactions?.length;
|
|
1423
|
+
if (task?.state.status === "terminal") {
|
|
1424
|
+
if (item.status === "queued" || item.status === "running") {
|
|
1425
|
+
item.status = task.state.outcome.status === "aborted" ? "cancelled" : "failed";
|
|
1426
|
+
item.endedAt ??= Date.now();
|
|
1427
|
+
}
|
|
1428
|
+
if (identity.activeRoundId === item.taskId && idle && (!runner || runner.state.status === "terminal"))
|
|
1429
|
+
identity.activeRoundId = null;
|
|
1430
|
+
}
|
|
1267
1431
|
}
|
|
1268
1432
|
return JSON.parse(JSON.stringify(doc.items));
|
|
1269
1433
|
}
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
const
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
}
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
reject(context.abortSignal?.reason ?? new Error("Cancelled"));
|
|
1299
|
-
};
|
|
1300
|
-
const timer = setTimeout(() => {
|
|
1301
|
-
cleanup();
|
|
1302
|
-
resolve();
|
|
1303
|
-
}, 100);
|
|
1304
|
-
context.abortSignal?.addEventListener("abort", abort, { once: true });
|
|
1305
|
-
if (context.abortSignal?.aborted)
|
|
1306
|
-
abort();
|
|
1307
|
-
});
|
|
1308
|
-
}
|
|
1434
|
+
function summarizeSubagents(rounds, identities = []) {
|
|
1435
|
+
const groups = new Map;
|
|
1436
|
+
for (const round of rounds) {
|
|
1437
|
+
const id = round.agentId ?? agentIdFor(round.childId);
|
|
1438
|
+
groups.set(id, [...groups.get(id) ?? [], round]);
|
|
1439
|
+
}
|
|
1440
|
+
return [...groups].map(([agentId, entries]) => {
|
|
1441
|
+
const first = entries[0];
|
|
1442
|
+
const last = entries.at(-1);
|
|
1443
|
+
const identity = identities.find((a) => a.agentId === agentId);
|
|
1444
|
+
const active = entries.find((r) => identity ? r.taskId === identity.activeRoundId : r.status === "running" || r.status === "queued");
|
|
1445
|
+
return {
|
|
1446
|
+
agentId,
|
|
1447
|
+
parentId: first.parentId,
|
|
1448
|
+
childId: first.childId,
|
|
1449
|
+
role: first.role,
|
|
1450
|
+
title: identity?.title ?? first.title,
|
|
1451
|
+
status: active ? active.status === "queued" ? "queued" : "running" : "idle",
|
|
1452
|
+
activeRoundId: active?.taskId ?? null,
|
|
1453
|
+
lastRoundId: last.taskId,
|
|
1454
|
+
lastReport: last.report?.slice(0, 512) ?? null,
|
|
1455
|
+
roundCount: entries.length,
|
|
1456
|
+
taskTitle: (active ?? last).title,
|
|
1457
|
+
approvalCount: active?.approvalCount ?? 0,
|
|
1458
|
+
createdAt: first.createdAt,
|
|
1459
|
+
...first.historyThrough !== undefined ? { historyThrough: first.historyThrough, historyUsage: first.historyUsage } : {}
|
|
1460
|
+
};
|
|
1461
|
+
});
|
|
1309
1462
|
}
|
|
1463
|
+
async function acquireSlot(tx, round, cwd) {
|
|
1464
|
+
const slots = await tx.doc(SlotsDoc);
|
|
1465
|
+
const live = [];
|
|
1466
|
+
for (const slot of slots.items) {
|
|
1467
|
+
const task = await tx.task(slot.taskId);
|
|
1468
|
+
if (task && task.state.status !== "terminal")
|
|
1469
|
+
live.push(slot);
|
|
1470
|
+
}
|
|
1471
|
+
slots.items = live;
|
|
1472
|
+
if (live.some((slot) => slot.taskId === round.taskId))
|
|
1473
|
+
return true;
|
|
1474
|
+
if (live.filter((slot) => slot.parentId === round.parentId).length >= 3 || round.role === "coder" && live.some((slot) => slot.coder && slot.cwd === cwd))
|
|
1475
|
+
return false;
|
|
1476
|
+
slots.items.push({ taskId: round.taskId, parentId: round.parentId, cwd, coder: round.role === "coder" });
|
|
1477
|
+
return true;
|
|
1478
|
+
}
|
|
1479
|
+
var actionParameters = Type2.Union([
|
|
1480
|
+
Type2.Object({
|
|
1481
|
+
action: Type2.Optional(Type2.Literal("create")),
|
|
1482
|
+
type: Type2.Enum(["explore", "plan", "coder"]),
|
|
1483
|
+
title: Type2.String({ minLength: 1, maxLength: 80 }),
|
|
1484
|
+
task: Type2.String({ minLength: 1, maxLength: 32768 })
|
|
1485
|
+
}, { additionalProperties: false }),
|
|
1486
|
+
Type2.Object({
|
|
1487
|
+
action: Type2.Literal("continue"),
|
|
1488
|
+
agentId: Type2.String(),
|
|
1489
|
+
title: Type2.String({ minLength: 1, maxLength: 80 }),
|
|
1490
|
+
task: Type2.String({ minLength: 1, maxLength: 32768 })
|
|
1491
|
+
}, { additionalProperties: false }),
|
|
1492
|
+
Type2.Object({
|
|
1493
|
+
action: Type2.Literal("list"),
|
|
1494
|
+
cursor: Type2.Optional(Type2.Integer({ minimum: 0 })),
|
|
1495
|
+
limit: Type2.Optional(Type2.Integer({ minimum: 1, maximum: 100 }))
|
|
1496
|
+
}, { additionalProperties: false })
|
|
1497
|
+
]);
|
|
1498
|
+
var parameters = Type2.Object({
|
|
1499
|
+
action: Type2.Optional(Type2.String({ enum: ["create", "continue", "list"], description: "Defaults to create." })),
|
|
1500
|
+
type: Type2.Optional(Type2.String({ enum: ["explore", "plan", "coder"], description: "Required for create only." })),
|
|
1501
|
+
agentId: Type2.Optional(Type2.String({ description: "Required for continue only; use an ID returned by create/list." })),
|
|
1502
|
+
title: Type2.Optional(Type2.String({ minLength: 1, maxLength: 80, description: "Required for create/continue." })),
|
|
1503
|
+
task: Type2.Optional(Type2.String({ minLength: 1, maxLength: 32768, description: "Required for create/continue." })),
|
|
1504
|
+
cursor: Type2.Optional(Type2.Integer({ minimum: 0, description: "List pagination cursor only." })),
|
|
1505
|
+
limit: Type2.Optional(Type2.Integer({ minimum: 1, maximum: 100, description: "List page size only." }))
|
|
1506
|
+
}, { additionalProperties: false });
|
|
1507
|
+
var structuredOutputSchema = Type2.Union([
|
|
1508
|
+
Type2.Object({
|
|
1509
|
+
agentId: Type2.String(),
|
|
1510
|
+
roundId: Type2.Integer(),
|
|
1511
|
+
status: Type2.Enum(["completed", "failed", "cancelled"]),
|
|
1512
|
+
report: Type2.String()
|
|
1513
|
+
}, { additionalProperties: false }),
|
|
1514
|
+
Type2.Object({
|
|
1515
|
+
items: Type2.Array(Type2.Object({
|
|
1516
|
+
agentId: Type2.String(),
|
|
1517
|
+
parentId: Type2.Integer(),
|
|
1518
|
+
childId: Type2.Integer(),
|
|
1519
|
+
role: Type2.Enum(["explore", "plan", "coder"]),
|
|
1520
|
+
title: Type2.String(),
|
|
1521
|
+
status: Type2.Enum(["idle", "queued", "running"]),
|
|
1522
|
+
activeRoundId: Type2.Union([Type2.Integer(), Type2.Null()]),
|
|
1523
|
+
lastRoundId: Type2.Integer(),
|
|
1524
|
+
lastReport: Type2.Optional(Type2.Union([Type2.String(), Type2.Null()])),
|
|
1525
|
+
roundCount: Type2.Integer(),
|
|
1526
|
+
taskTitle: Type2.String(),
|
|
1527
|
+
approvalCount: Type2.Integer(),
|
|
1528
|
+
createdAt: Type2.Number()
|
|
1529
|
+
}, { additionalProperties: false })),
|
|
1530
|
+
next: Type2.Union([Type2.Integer(), Type2.Null()])
|
|
1531
|
+
}, { additionalProperties: false })
|
|
1532
|
+
]);
|
|
1310
1533
|
function createSubagentExtension(prepare, discard) {
|
|
1534
|
+
const finish = async (tx, input, status, report) => {
|
|
1535
|
+
const round = (await tx.doc(DelegationsDoc, input.parentId)).items.find((r) => r.taskId === input.taskId);
|
|
1536
|
+
round.status = status;
|
|
1537
|
+
round.report = report;
|
|
1538
|
+
round.endedAt ??= Date.now();
|
|
1539
|
+
round.usageEnd = JSON.parse(JSON.stringify(await tx.doc(UsageDoc, input.childId)));
|
|
1540
|
+
round.roundUsage = round.usageStart ? subagentUsageTotals(subtractSubagentUsage(round.usageEnd, round.usageStart)) : round.startedAt === null ? subagentUsageTotals(emptySubagentUsage()) : null;
|
|
1541
|
+
};
|
|
1542
|
+
const RoundTask = defineTask({
|
|
1543
|
+
name: "app.neko.subagent-round",
|
|
1544
|
+
version: 1,
|
|
1545
|
+
initial: () => ({ phase: "admission" }),
|
|
1546
|
+
phases: {
|
|
1547
|
+
admission: async (task, runtime, ctx) => {
|
|
1548
|
+
const env = await runtime.env(ctx);
|
|
1549
|
+
const canonical = env && await env.canonicalPath(task.input.cwd, ctx);
|
|
1550
|
+
const slotCwd = canonical?.ok ? canonical.value : task.input.cwd;
|
|
1551
|
+
let admitted = false;
|
|
1552
|
+
while (!admitted) {
|
|
1553
|
+
await runtime.commit(async (tx) => {
|
|
1554
|
+
const round = (await tx.doc(DelegationsDoc, task.input.parentId)).items.find((r) => r.taskId === task.input.taskId);
|
|
1555
|
+
if (!await acquireSlot(tx, round, slotCwd))
|
|
1556
|
+
return;
|
|
1557
|
+
admitted = true;
|
|
1558
|
+
const duplicate = await tx.submissionByRequest(task.input.childId, `subagent:${round.taskId}`);
|
|
1559
|
+
if (!duplicate && !round.usageStart)
|
|
1560
|
+
round.usageStart = JSON.parse(JSON.stringify(await tx.doc(UsageDoc, task.input.childId)));
|
|
1561
|
+
round.status = "running";
|
|
1562
|
+
round.startedAt ??= Date.now();
|
|
1563
|
+
return { status: "running", checkpoint: { phase: "deliver" } };
|
|
1564
|
+
}, ctx);
|
|
1565
|
+
if (!admitted)
|
|
1566
|
+
await runtime.sleep(runtime.now() + 100, ctx);
|
|
1567
|
+
}
|
|
1568
|
+
},
|
|
1569
|
+
deliver: async (task, runtime, ctx) => {
|
|
1570
|
+
const input = task.input;
|
|
1571
|
+
const child = await runtime.conversation(input.childId, ctx);
|
|
1572
|
+
const requestId = `subagent:${input.taskId}`;
|
|
1573
|
+
try {
|
|
1574
|
+
let duplicate = false;
|
|
1575
|
+
await runtime.commit(async (tx) => {
|
|
1576
|
+
duplicate = !!await tx.submissionByRequest(input.childId, requestId);
|
|
1577
|
+
}, ctx);
|
|
1578
|
+
if (!duplicate)
|
|
1579
|
+
await prepare(input.childId, requestId, input.cwd);
|
|
1580
|
+
const submission = await child.submit({
|
|
1581
|
+
type: "input",
|
|
1582
|
+
content: input.task,
|
|
1583
|
+
requestId,
|
|
1584
|
+
whenBusy: "reject",
|
|
1585
|
+
entry: {
|
|
1586
|
+
kind: NekoInput.kind,
|
|
1587
|
+
data: { at: Date.now(), prompt: input.task, images: [], subagentRoundId: input.taskId }
|
|
1588
|
+
}
|
|
1589
|
+
}, ctx);
|
|
1590
|
+
const placed = await submission.status(ctx);
|
|
1591
|
+
await runtime.commit(async (tx) => {
|
|
1592
|
+
if (placed.type === "input" && placed.entry !== undefined)
|
|
1593
|
+
(await tx.doc(DelegationsDoc, input.parentId)).items.find((r) => r.taskId === input.taskId).inputEntryId = placed.entry;
|
|
1594
|
+
return { status: "running", checkpoint: { phase: "settle" } };
|
|
1595
|
+
}, ctx);
|
|
1596
|
+
} catch (error) {
|
|
1597
|
+
discard(input.childId, requestId);
|
|
1598
|
+
if (ctx.abortSignal?.aborted)
|
|
1599
|
+
throw error;
|
|
1600
|
+
await child.abort(ctx);
|
|
1601
|
+
const report = `Subagent failed: ${String(error)}`;
|
|
1602
|
+
await runtime.commit(async (tx) => {
|
|
1603
|
+
await finish(tx, input, "failed", report);
|
|
1604
|
+
return { status: "terminal", outcome: { status: "completed", result: { status: "failed", report } } };
|
|
1605
|
+
}, ctx);
|
|
1606
|
+
}
|
|
1607
|
+
},
|
|
1608
|
+
settle: async (task, runtime, ctx) => {
|
|
1609
|
+
const input = task.input;
|
|
1610
|
+
const child = await runtime.conversation(input.childId, ctx);
|
|
1611
|
+
const submission = await child.submit({ type: "input", content: input.task, requestId: `subagent:${input.taskId}` }, ctx);
|
|
1612
|
+
const settled = await submission.wait(ctx);
|
|
1613
|
+
await child.waitForIdle(ctx);
|
|
1614
|
+
await runtime.commit(async (tx) => {
|
|
1615
|
+
let report = "";
|
|
1616
|
+
let status = "failed";
|
|
1617
|
+
const round = (await tx.doc(DelegationsDoc, input.parentId)).items.find((r) => r.taskId === input.taskId);
|
|
1618
|
+
if (settled.type === "input" && settled.entry !== undefined)
|
|
1619
|
+
round.inputEntryId = settled.entry;
|
|
1620
|
+
if (settled.status === "done" && settled.type === "input") {
|
|
1621
|
+
round.answerEntryId = settled.answer;
|
|
1622
|
+
const message = (await tx.entry(AssistantEntry, settled.answer))?.model?.[0];
|
|
1623
|
+
report = message?.role === "assistant" ? message.content.flatMap((b) => b.type === "text" ? [b.text] : []).join("") : "";
|
|
1624
|
+
status = "completed";
|
|
1625
|
+
} else if (settled.status === "unanswered") {
|
|
1626
|
+
status = settled.reason === "aborted" ? "cancelled" : "failed";
|
|
1627
|
+
report = `Subagent ${status}: ${settled.reason}`;
|
|
1628
|
+
}
|
|
1629
|
+
await finish(tx, input, status, report);
|
|
1630
|
+
return { status: "terminal", outcome: { status: "completed", result: { status, report } } };
|
|
1631
|
+
}, ctx);
|
|
1632
|
+
}
|
|
1633
|
+
},
|
|
1634
|
+
abort: async (task, runtime, ctx) => {
|
|
1635
|
+
const input = task.input;
|
|
1636
|
+
let own = false;
|
|
1637
|
+
await runtime.commit(async (tx) => {
|
|
1638
|
+
own = (await tx.doc(SubagentsDoc, input.parentId)).items.some((a) => a.childId === input.childId && a.activeRoundId === input.taskId);
|
|
1639
|
+
}, ctx);
|
|
1640
|
+
if (own)
|
|
1641
|
+
await (await runtime.conversation(input.childId, ctx))?.abort(ctx);
|
|
1642
|
+
await runtime.commit(async (tx) => {
|
|
1643
|
+
await finish(tx, input, "cancelled", "Subagent cancelled");
|
|
1644
|
+
return { status: "terminal", outcome: { status: "aborted" } };
|
|
1645
|
+
}, ctx);
|
|
1646
|
+
}
|
|
1647
|
+
});
|
|
1311
1648
|
const extension = defineExtension3({
|
|
1312
1649
|
name: "neko-subagents",
|
|
1650
|
+
tasks: [RoundTask],
|
|
1313
1651
|
tools: [
|
|
1314
1652
|
defineTool2({
|
|
1315
1653
|
name: "subagent",
|
|
1316
|
-
description: "
|
|
1317
|
-
parameters
|
|
1318
|
-
|
|
1319
|
-
title: Type2.String({ minLength: 1, maxLength: 80 }),
|
|
1320
|
-
task: Type2.String({ minLength: 1, maxLength: 32768 })
|
|
1321
|
-
}),
|
|
1654
|
+
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.",
|
|
1655
|
+
parameters,
|
|
1656
|
+
structuredOutputSchema,
|
|
1322
1657
|
replay: "safe",
|
|
1323
|
-
execute: async (
|
|
1324
|
-
if (!
|
|
1325
|
-
throw new Error("
|
|
1658
|
+
execute: async (input, api, context) => {
|
|
1659
|
+
if (!Value.Check(actionParameters, input))
|
|
1660
|
+
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.");
|
|
1661
|
+
const args = input;
|
|
1326
1662
|
if ((await api.snapshot(SubagentDoc, api.conversationId, context))?.role)
|
|
1327
1663
|
throw new Error("Subagents cannot delegate.");
|
|
1664
|
+
if (args.action === "list") {
|
|
1665
|
+
const list = await api.commit(async (tx) => {
|
|
1666
|
+
const rounds = await refreshDelegations(tx, api.conversationId);
|
|
1667
|
+
return summarizeSubagents(rounds, (await tx.doc(SubagentsDoc, api.conversationId)).items).filter((a) => a.historyThrough === undefined);
|
|
1668
|
+
}, context);
|
|
1669
|
+
const offset = args.cursor ?? 0;
|
|
1670
|
+
const items = list.slice(offset, offset + (args.limit ?? 50));
|
|
1671
|
+
const result = { items, next: offset + items.length < list.length ? offset + items.length : null };
|
|
1672
|
+
return { output: [{ type: "text", text: JSON.stringify(result) }], structuredOutput: result };
|
|
1673
|
+
}
|
|
1674
|
+
if (!args.task.trim() || !args.title.trim() || Buffer.byteLength(args.task) > 32768)
|
|
1675
|
+
throw new Error("Task/title must be nonempty; task limit is 32 KiB.");
|
|
1328
1676
|
const agent = await api.agent(context);
|
|
1329
|
-
const
|
|
1330
|
-
const
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
await
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1677
|
+
const round = await api.commit(async (tx) => {
|
|
1678
|
+
const rounds = await refreshDelegations(tx, api.conversationId);
|
|
1679
|
+
const receipt = rounds.find((r) => r.taskId === api.taskId);
|
|
1680
|
+
if (receipt?.runnerId !== undefined)
|
|
1681
|
+
return receipt;
|
|
1682
|
+
const identities = await tx.doc(SubagentsDoc, api.conversationId);
|
|
1683
|
+
let identity;
|
|
1684
|
+
if (args.action === "continue") {
|
|
1685
|
+
identity = identities.items.find((a) => a.agentId === args.agentId);
|
|
1686
|
+
if (!identity)
|
|
1687
|
+
throw new Error("Unknown subagent or it belongs to another conversation.");
|
|
1688
|
+
const info = await tx.doc(SubagentDoc, identity.childId);
|
|
1689
|
+
if (info.parentId !== api.conversationId || info.role !== identity.role)
|
|
1690
|
+
throw new Error("Subagent ownership mismatch.");
|
|
1691
|
+
if (identity.activeRoundId !== null)
|
|
1692
|
+
throw new Error("Subagent is busy; continue only when idle.");
|
|
1693
|
+
} else if (receipt) {
|
|
1694
|
+
identity = identities.items.find((a) => a.childId === receipt.childId);
|
|
1695
|
+
}
|
|
1696
|
+
const existing = identity ? undefined : (await tx.scanConversations({ ownerTaskId: api.taskId }, 1)).items[0];
|
|
1697
|
+
const child = identity ? await tx.conversation(identity.childId) : existing;
|
|
1698
|
+
if (!identity) {
|
|
1699
|
+
const created = child ?? await tx.createConversation({ ownership: { kind: "task", taskId: api.taskId } });
|
|
1700
|
+
const role = args.action === "continue" ? "explore" : args.type;
|
|
1701
|
+
await configure(tx, created.id, {
|
|
1702
|
+
model: agent.model,
|
|
1703
|
+
thinkingLevel: agent.thinkingLevel,
|
|
1704
|
+
cwd: agent.cwd,
|
|
1705
|
+
extensions: agent.extensions.filter((e) => e.name !== extension.name),
|
|
1706
|
+
tools: agent.tools.filter((t) => t.name !== "subagent" && (role === "coder" || READ_TOOLS.has(t.name) || role === "explore" && t.name === "bash"))
|
|
1707
|
+
});
|
|
1708
|
+
Object.assign(await tx.doc(SubagentDoc, created.id), { role, parentId: api.conversationId });
|
|
1709
|
+
identity = {
|
|
1710
|
+
agentId: agentIdFor(created.id),
|
|
1711
|
+
parentId: api.conversationId,
|
|
1712
|
+
childId: created.id,
|
|
1713
|
+
role,
|
|
1714
|
+
cwd: agent.cwd ?? "",
|
|
1715
|
+
title: args.title,
|
|
1716
|
+
createdAt: Date.now(),
|
|
1717
|
+
activeRoundId: null
|
|
1718
|
+
};
|
|
1719
|
+
identities.items.push(identity);
|
|
1720
|
+
}
|
|
1721
|
+
const record = receipt ?? {
|
|
1343
1722
|
parentId: api.conversationId,
|
|
1344
1723
|
taskId: api.taskId,
|
|
1345
1724
|
callId: api.callId,
|
|
1346
|
-
childId:
|
|
1347
|
-
|
|
1725
|
+
childId: identity.childId,
|
|
1726
|
+
agentId: identity.agentId,
|
|
1727
|
+
round: rounds.filter((r) => r.childId === identity.childId).length + 1,
|
|
1728
|
+
role: identity.role,
|
|
1348
1729
|
title: args.title,
|
|
1349
1730
|
createdAt: Date.now(),
|
|
1350
1731
|
startedAt: null,
|
|
1351
1732
|
endedAt: null,
|
|
1352
1733
|
status: "queued",
|
|
1353
1734
|
report: null
|
|
1354
|
-
}
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1735
|
+
};
|
|
1736
|
+
identities.items.find((a) => a.agentId === identity.agentId).activeRoundId = api.taskId;
|
|
1737
|
+
const runner = await tx.createTask(RoundTask, {
|
|
1738
|
+
parentId: api.conversationId,
|
|
1739
|
+
childId: identity.childId,
|
|
1740
|
+
taskId: api.taskId,
|
|
1741
|
+
task: args.task,
|
|
1742
|
+
cwd: identity.cwd || agent.cwd || ""
|
|
1743
|
+
}, { ownership: { kind: "task", taskId: api.taskId } });
|
|
1744
|
+
record.runnerId = runner;
|
|
1745
|
+
const doc = await tx.doc(DelegationsDoc, api.conversationId);
|
|
1746
|
+
if (receipt)
|
|
1747
|
+
Object.assign(doc.items.find((r) => r.taskId === receipt.taskId), record);
|
|
1748
|
+
else
|
|
1749
|
+
doc.items.push(record);
|
|
1750
|
+
return record;
|
|
1364
1751
|
}, context);
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
try {
|
|
1372
|
-
submission = await child.submit({
|
|
1373
|
-
type: "input",
|
|
1374
|
-
content: args.task,
|
|
1375
|
-
requestId,
|
|
1376
|
-
entry: { kind: NekoInput.kind, data: { at: Date.now(), prompt: args.task, images: [] } }
|
|
1377
|
-
}, context);
|
|
1378
|
-
} catch (error) {
|
|
1379
|
-
discard(childId, requestId);
|
|
1380
|
-
throw error;
|
|
1381
|
-
}
|
|
1382
|
-
const settled = await submission.wait(context);
|
|
1383
|
-
let report = "";
|
|
1384
|
-
let status = "failed";
|
|
1385
|
-
if (settled.status === "done" && settled.type === "input") {
|
|
1386
|
-
const entry = await api.commit((tx) => tx.entry(AssistantEntry, settled.answer), context);
|
|
1387
|
-
const message = entry?.model?.[0];
|
|
1388
|
-
report = message?.role === "assistant" ? message.content.flatMap((b) => b.type === "text" ? [b.text] : []).join("") : "";
|
|
1389
|
-
status = "completed";
|
|
1390
|
-
} else if (settled.status === "unanswered") {
|
|
1391
|
-
status = settled.reason === "aborted" ? "cancelled" : "failed";
|
|
1392
|
-
report = `Subagent ${status}: ${settled.reason}`;
|
|
1393
|
-
}
|
|
1394
|
-
await api.commit(async (tx) => {
|
|
1395
|
-
const item = (await tx.doc(DelegationsDoc, api.conversationId)).items.find((d) => d.taskId === api.taskId);
|
|
1396
|
-
item.status = status;
|
|
1397
|
-
item.report = report;
|
|
1398
|
-
item.endedAt = Date.now();
|
|
1752
|
+
await api.details({
|
|
1753
|
+
conversationId: round.childId,
|
|
1754
|
+
agentId: round.agentId,
|
|
1755
|
+
roundId: round.taskId,
|
|
1756
|
+
role: round.role,
|
|
1757
|
+
title: round.title
|
|
1399
1758
|
}, context);
|
|
1759
|
+
const settled = await api.waitForTask(round.runnerId, context);
|
|
1760
|
+
const outcome = settled.state.outcome;
|
|
1761
|
+
const result = outcome.status === "completed" ? outcome.result : { status: outcome.status === "aborted" ? "cancelled" : "failed", report: `Subagent ${outcome.status}` };
|
|
1400
1762
|
return {
|
|
1401
|
-
output: [
|
|
1402
|
-
|
|
1403
|
-
|
|
1763
|
+
output: [
|
|
1764
|
+
{
|
|
1765
|
+
type: "text",
|
|
1766
|
+
text: `Agent: ${round.agentId}
|
|
1767
|
+
Status: ${result.status}
|
|
1768
|
+
${result.report || "Subagent completed without a text report."}`
|
|
1769
|
+
}
|
|
1770
|
+
],
|
|
1771
|
+
isError: result.status !== "completed",
|
|
1772
|
+
structuredOutput: { agentId: round.agentId, roundId: round.taskId, ...result },
|
|
1773
|
+
details: {
|
|
1774
|
+
conversationId: round.childId,
|
|
1775
|
+
agentId: round.agentId,
|
|
1776
|
+
roundId: round.taskId,
|
|
1777
|
+
role: round.role,
|
|
1778
|
+
title: round.title,
|
|
1779
|
+
status: result.status
|
|
1780
|
+
}
|
|
1404
1781
|
};
|
|
1405
1782
|
}
|
|
1406
1783
|
})
|
|
@@ -1409,15 +1786,70 @@ function createSubagentExtension(prepare, discard) {
|
|
|
1409
1786
|
return extension;
|
|
1410
1787
|
}
|
|
1411
1788
|
async function listDelegations(harness, parent, context) {
|
|
1789
|
+
const rounds = await harness.commit((tx) => refreshDelegations(tx, parent), context);
|
|
1790
|
+
for (const round of rounds) {
|
|
1791
|
+
if (round.historyThrough !== undefined)
|
|
1792
|
+
continue;
|
|
1793
|
+
const task = await harness.getTask(round.taskId, context);
|
|
1794
|
+
if (task?.state.status !== "terminal")
|
|
1795
|
+
continue;
|
|
1796
|
+
const child = await harness.conversation(round.childId, context);
|
|
1797
|
+
const live = await harness.snapshot(LiveDoc, round.childId, context);
|
|
1798
|
+
const identity = (await harness.snapshot(SubagentsDoc, parent, context))?.items.find((a) => a.childId === round.childId);
|
|
1799
|
+
if (identity?.activeRoundId === round.taskId && live?.run !== undefined)
|
|
1800
|
+
await child?.abort(context);
|
|
1801
|
+
}
|
|
1412
1802
|
return harness.commit((tx) => refreshDelegations(tx, parent), context);
|
|
1413
1803
|
}
|
|
1804
|
+
async function installSubagentCoordinator(harness, parents, context) {
|
|
1805
|
+
const known = new Set(parents);
|
|
1806
|
+
let running = false;
|
|
1807
|
+
let dirty = false;
|
|
1808
|
+
const reconcile = async () => {
|
|
1809
|
+
if (running) {
|
|
1810
|
+
dirty = true;
|
|
1811
|
+
return;
|
|
1812
|
+
}
|
|
1813
|
+
running = true;
|
|
1814
|
+
try {
|
|
1815
|
+
do {
|
|
1816
|
+
dirty = false;
|
|
1817
|
+
for (const id of known)
|
|
1818
|
+
await listDelegations(harness, id, context);
|
|
1819
|
+
} while (dirty);
|
|
1820
|
+
} finally {
|
|
1821
|
+
running = false;
|
|
1822
|
+
}
|
|
1823
|
+
};
|
|
1824
|
+
for (const parent of parents) {
|
|
1825
|
+
await harness.commit((tx) => refreshDelegations(tx, parent), context);
|
|
1826
|
+
for (const identity of (await harness.snapshot(SubagentsDoc, parent, context))?.items ?? []) {
|
|
1827
|
+
if (identity.cwd)
|
|
1828
|
+
continue;
|
|
1829
|
+
const child = await harness.conversation(identity.childId, context);
|
|
1830
|
+
const cwd = (await child?.agent(context))?.cwd;
|
|
1831
|
+
if (cwd)
|
|
1832
|
+
await harness.commit(async (tx) => {
|
|
1833
|
+
(await tx.doc(SubagentsDoc, parent)).items.find((a) => a.agentId === identity.agentId).cwd = cwd;
|
|
1834
|
+
}, context);
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
const stop = harness.subscribeCommits(({ changes }) => {
|
|
1838
|
+
for (const c of changes)
|
|
1839
|
+
if (c.type === "document" && c.record.kind === "app.neko.delegations" && c.conversationId !== undefined)
|
|
1840
|
+
known.add(c.conversationId);
|
|
1841
|
+
if (changes.some((c) => c.type === "task" && c.value.state.status === "terminal"))
|
|
1842
|
+
reconcile().catch(() => {});
|
|
1843
|
+
});
|
|
1844
|
+
harness.closed.finally(stop).catch(() => {});
|
|
1845
|
+
}
|
|
1414
1846
|
|
|
1415
1847
|
// packages/core/src/prompt/prompt.ts
|
|
1416
1848
|
var KEYS = ["preamble", "rules", "project_context", "skills", "cwd"];
|
|
1417
1849
|
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.";
|
|
1418
1850
|
var TOOL_GUIDELINES = {
|
|
1419
1851
|
subagent: [
|
|
1420
|
-
"Delegate only independent, sufficiently scoped work when isolation or parallelism helps. Handle simple tasks directly. Pass necessary context and user constraints; research-only requests must not become coding tasks. Verify and integrate reports. Do not modify files concurrently with a coder assigned those files."
|
|
1852
|
+
"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."
|
|
1421
1853
|
],
|
|
1422
1854
|
find_files: [
|
|
1423
1855
|
"Use find_files to discover files; query is a literal substring. Narrow the directory when results are partial."
|
|
@@ -1627,9 +2059,9 @@ import { getSupportedThinkingLevels } from "@earendil-works/pi-ai";
|
|
|
1627
2059
|
import {
|
|
1628
2060
|
defineDoc as defineDoc2,
|
|
1629
2061
|
defineExtension as defineExtension5,
|
|
1630
|
-
defineTask,
|
|
2062
|
+
defineTask as defineTask2,
|
|
1631
2063
|
InboxDoc,
|
|
1632
|
-
LiveDoc
|
|
2064
|
+
LiveDoc as LiveDoc2
|
|
1633
2065
|
} from "@earendil-works/pi-durable";
|
|
1634
2066
|
var SessionMetadataDoc = defineDoc2({
|
|
1635
2067
|
kind: "app.neko.session-metadata",
|
|
@@ -1694,7 +2126,7 @@ function mergeUsage(base, extra) {
|
|
|
1694
2126
|
function namingTask() {
|
|
1695
2127
|
let active = 0;
|
|
1696
2128
|
const waits = new Set;
|
|
1697
|
-
return
|
|
2129
|
+
return defineTask2({
|
|
1698
2130
|
name: "app.neko.name",
|
|
1699
2131
|
version: 1,
|
|
1700
2132
|
initial: () => ({ phase: "wait", until: Date.now() + 100 }),
|
|
@@ -1908,7 +2340,7 @@ class SessionMetadataManager {
|
|
|
1908
2340
|
const answer = record.status === "done" ? (await this.storage.entry(record.answer, this.context))?.entry.model?.[0] : undefined;
|
|
1909
2341
|
return { record, data, answer: answer?.role === "assistant" ? answer : undefined };
|
|
1910
2342
|
}));
|
|
1911
|
-
const live = await this.harness.snapshot(
|
|
2343
|
+
const live = await this.harness.snapshot(LiveDoc2, id, this.context);
|
|
1912
2344
|
await this.harness.commit(async (tx) => {
|
|
1913
2345
|
const d = await tx.doc(SessionMetadataDoc, id);
|
|
1914
2346
|
for (const { record, data, answer } of updates) {
|
|
@@ -2091,7 +2523,8 @@ function createApprovalExtension(service) {
|
|
|
2091
2523
|
const child = await approvals.harness.conversation(api.conversationId, context);
|
|
2092
2524
|
const agent = await child.agent(context);
|
|
2093
2525
|
const cwd = await realpath(agent.cwd);
|
|
2094
|
-
const
|
|
2526
|
+
const identity = (await api.snapshot(SubagentsDoc, parent, context))?.items.find((a) => a.childId === api.conversationId);
|
|
2527
|
+
const delegation = (await api.snapshot(DelegationsDoc, parent, context))?.items.find((d) => d.childId === api.conversationId && d.historyThrough === undefined && d.taskId === identity?.activeRoundId);
|
|
2095
2528
|
if (!delegation)
|
|
2096
2529
|
return { block: "Missing delegation" };
|
|
2097
2530
|
await approvals.ensure(parent, {
|
|
@@ -2804,6 +3237,7 @@ async function createNekoRuntime(options = {}) {
|
|
|
2804
3237
|
approvals = new ToolApprovals(harness, context);
|
|
2805
3238
|
const existing = await scanConversations();
|
|
2806
3239
|
try {
|
|
3240
|
+
await installSubagentCoordinator(harness, existing.map((r) => r.id), context);
|
|
2807
3241
|
await prepareApprovalConversations(harness, existing.map((r) => r.id), approvalExtension, context);
|
|
2808
3242
|
for (const record of existing) {
|
|
2809
3243
|
const target = await harness.conversation(record.id, context);
|
|
@@ -2988,7 +3422,7 @@ async function createNekoRuntime(options = {}) {
|
|
|
2988
3422
|
return watchEvents(harness, target.id, context);
|
|
2989
3423
|
},
|
|
2990
3424
|
async usage() {
|
|
2991
|
-
return mergeUsage(await harness.snapshot(
|
|
3425
|
+
return mergeUsage(await harness.snapshot(UsageDoc2, target.id, context) ?? { models: {}, tools: {} }, (await harness.snapshot(SessionMetadataDoc, target.id, context))?.namingUsage ?? { models: {}, tools: {} });
|
|
2992
3426
|
},
|
|
2993
3427
|
compactionRecord() {
|
|
2994
3428
|
return readCompaction(harness, storage, target.id, context);
|
|
@@ -3037,7 +3471,7 @@ async function createNekoRuntime(options = {}) {
|
|
|
3037
3471
|
async globalUsage() {
|
|
3038
3472
|
let total = { models: {}, tools: {} };
|
|
3039
3473
|
for (const record of await scanConversations()) {
|
|
3040
|
-
total = mergeUsage(total, await harness.snapshot(
|
|
3474
|
+
total = mergeUsage(total, await harness.snapshot(UsageDoc2, record.id, context) ?? { models: {}, tools: {} });
|
|
3041
3475
|
total = mergeUsage(total, (await harness.snapshot(SessionMetadataDoc, record.id, context))?.namingUsage ?? { models: {}, tools: {} });
|
|
3042
3476
|
}
|
|
3043
3477
|
return total;
|
|
@@ -3072,14 +3506,24 @@ async function createNekoRuntime(options = {}) {
|
|
|
3072
3506
|
}
|
|
3073
3507
|
cursor = page.next;
|
|
3074
3508
|
} while (cursor);
|
|
3509
|
+
const frozen = new Map;
|
|
3075
3510
|
for (const reference of references) {
|
|
3076
3511
|
if (!calls.has(reference.callId))
|
|
3077
3512
|
continue;
|
|
3078
|
-
|
|
3079
|
-
|
|
3513
|
+
let snapshot = frozen.get(reference.childId);
|
|
3514
|
+
if (!snapshot) {
|
|
3515
|
+
const latest = await tx.scanEntries({ conversationId: reference.childId }, 1);
|
|
3516
|
+
snapshot = {
|
|
3517
|
+
through: latest.items[0]?.id ?? 0,
|
|
3518
|
+
state: JSON.parse(JSON.stringify(await tx.doc(UsageDoc2, reference.childId)))
|
|
3519
|
+
};
|
|
3520
|
+
frozen.set(reference.childId, snapshot);
|
|
3521
|
+
}
|
|
3522
|
+
const usage = totalUsage(snapshot.state);
|
|
3080
3523
|
inherited.push({
|
|
3081
3524
|
...reference,
|
|
3082
|
-
historyThrough: reference.historyThrough ??
|
|
3525
|
+
historyThrough: reference.historyThrough ?? snapshot.through,
|
|
3526
|
+
roundUsage: reference.roundUsage ?? (reference.usageStart ? subagentUsageTotals(subtractSubagentUsage(snapshot.state, reference.usageStart)) : null),
|
|
3083
3527
|
historyUsage: reference.historyUsage ?? {
|
|
3084
3528
|
input: usage.input,
|
|
3085
3529
|
output: usage.output,
|
|
@@ -3315,8 +3759,8 @@ import { dirname as dirname5, join as join7 } from "path";
|
|
|
3315
3759
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3316
3760
|
|
|
3317
3761
|
// packages/core/src/application-version.ts
|
|
3318
|
-
var RUNNING_VERSION = "0.3.
|
|
3319
|
-
var APPLICATION_PROTOCOL =
|
|
3762
|
+
var RUNNING_VERSION = "0.3.2";
|
|
3763
|
+
var APPLICATION_PROTOCOL = 4;
|
|
3320
3764
|
function stableVersion(value) {
|
|
3321
3765
|
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)));
|
|
3322
3766
|
}
|
|
@@ -3509,10 +3953,20 @@ var call = Type4.Object({
|
|
|
3509
3953
|
summary: str,
|
|
3510
3954
|
detail: nullableString,
|
|
3511
3955
|
phase,
|
|
3512
|
-
output: nullableString
|
|
3956
|
+
output: nullableString,
|
|
3957
|
+
diff: nullableString,
|
|
3958
|
+
diffAdded: Type4.Union([num, nil]),
|
|
3959
|
+
diffRemoved: Type4.Union([num, nil]),
|
|
3960
|
+
diffTruncated: Type4.Optional(bool)
|
|
3513
3961
|
});
|
|
3514
3962
|
var DelegationSchema = Type4.Object({
|
|
3515
3963
|
parentId: integer,
|
|
3964
|
+
agentId: Type4.Optional(str),
|
|
3965
|
+
round: Type4.Optional(integer),
|
|
3966
|
+
runnerId: Type4.Optional(integer),
|
|
3967
|
+
inputEntryId: Type4.Optional(integer),
|
|
3968
|
+
answerEntryId: Type4.Optional(integer),
|
|
3969
|
+
roundUsage: Type4.Optional(Type4.Union([usage, nil])),
|
|
3516
3970
|
taskId: integer,
|
|
3517
3971
|
callId: str,
|
|
3518
3972
|
childId: integer,
|
|
@@ -3527,8 +3981,27 @@ var DelegationSchema = Type4.Object({
|
|
|
3527
3981
|
historyThrough: Type4.Optional(integer),
|
|
3528
3982
|
historyUsage: Type4.Optional(usage)
|
|
3529
3983
|
});
|
|
3984
|
+
var SubagentSummarySchema = Type4.Object({
|
|
3985
|
+
agentId: str,
|
|
3986
|
+
parentId: integer,
|
|
3987
|
+
childId: integer,
|
|
3988
|
+
role: Type4.Enum(["explore", "plan", "coder"]),
|
|
3989
|
+
title: str,
|
|
3990
|
+
status: Type4.Enum(["idle", "queued", "running"]),
|
|
3991
|
+
activeRoundId: Type4.Union([integer, nil]),
|
|
3992
|
+
lastRoundId: integer,
|
|
3993
|
+
lastReport: Type4.Optional(nullableString),
|
|
3994
|
+
roundCount: integer,
|
|
3995
|
+
taskTitle: str,
|
|
3996
|
+
approvalCount: integer,
|
|
3997
|
+
createdAt: num,
|
|
3998
|
+
historyThrough: Type4.Optional(integer),
|
|
3999
|
+
historyUsage: Type4.Optional(usage)
|
|
4000
|
+
});
|
|
3530
4001
|
var ChatViewSchema = Type4.Object({
|
|
3531
4002
|
historyBefore: Type4.Optional(Type4.Union([integer, nil])),
|
|
4003
|
+
agents: Type4.Optional(Type4.Array(SubagentSummarySchema)),
|
|
4004
|
+
rounds: Type4.Optional(Type4.Array(DelegationSchema)),
|
|
3532
4005
|
subagents: Type4.Optional(Type4.Array(DelegationSchema)),
|
|
3533
4006
|
taskUsage: Type4.Optional(usage),
|
|
3534
4007
|
model: str,
|
|
@@ -3592,7 +4065,11 @@ var ChatViewSchema = Type4.Object({
|
|
|
3592
4065
|
output: nullableString,
|
|
3593
4066
|
result: nullableString,
|
|
3594
4067
|
durationMs: Type4.Union([num, nil]),
|
|
3595
|
-
note: nullableString
|
|
4068
|
+
note: nullableString,
|
|
4069
|
+
diff: nullableString,
|
|
4070
|
+
diffAdded: Type4.Union([num, nil]),
|
|
4071
|
+
diffRemoved: Type4.Union([num, nil]),
|
|
4072
|
+
diffTruncated: Type4.Optional(bool)
|
|
3596
4073
|
}),
|
|
3597
4074
|
Type4.Object({ type: Type4.Literal("system"), order: Type4.Optional(num), key: str, id: integer, text: str })
|
|
3598
4075
|
])),
|
|
@@ -3684,7 +4161,12 @@ var NekoModels = contract("neko.models", {
|
|
|
3684
4161
|
var imageChunk = Type4.Object({ data: str, total: integer, mimeType: str });
|
|
3685
4162
|
var NekoSubagent = contract("neko.subagent", {
|
|
3686
4163
|
version: 1,
|
|
3687
|
-
input: Type4.Object({
|
|
4164
|
+
input: Type4.Object({
|
|
4165
|
+
sessionId: str,
|
|
4166
|
+
attachmentId: str,
|
|
4167
|
+
taskId: Type4.Optional(integer),
|
|
4168
|
+
agentId: Type4.Optional(str)
|
|
4169
|
+
}),
|
|
3688
4170
|
variants: {
|
|
3689
4171
|
ready: {
|
|
3690
4172
|
state: { revision: integer },
|
|
@@ -3697,7 +4179,7 @@ var NekoSubagent = contract("neko.subagent", {
|
|
|
3697
4179
|
}), Type4.Object({ token: integer, data: str, next: Type4.Union([integer, nil]) })),
|
|
3698
4180
|
getImage: rpc(Type4.Object({ entryId: integer, contentIndex: integer, offset: integer }), imageChunk),
|
|
3699
4181
|
decideApproval: rpc(Type4.Object({ id: integer, version: str, decision: Type4.Enum(["allowed", "denied"]) }), Type4.Enum(["pending", "allowed", "denied", "cancelled"])),
|
|
3700
|
-
stop: rpc(nil, nil)
|
|
4182
|
+
stop: rpc(Type4.Union([nil, Type4.Object({ taskId: integer })]), nil)
|
|
3701
4183
|
}
|
|
3702
4184
|
}
|
|
3703
4185
|
}
|
|
@@ -3839,7 +4321,7 @@ import {
|
|
|
3839
4321
|
resource as resource2
|
|
3840
4322
|
} from "@earendil-works/chord";
|
|
3841
4323
|
import { withAbortSignal as withAbortSignal3 } from "@earendil-works/chord/context";
|
|
3842
|
-
import { UsageDoc as
|
|
4324
|
+
import { UsageDoc as UsageDoc4 } from "@earendil-works/pi-durable";
|
|
3843
4325
|
import { Type as Type6 } from "typebox";
|
|
3844
4326
|
|
|
3845
4327
|
// packages/gateway/src/image-uploads.ts
|
|
@@ -3946,6 +4428,7 @@ import {
|
|
|
3946
4428
|
opaque,
|
|
3947
4429
|
resource
|
|
3948
4430
|
} from "@earendil-works/chord";
|
|
4431
|
+
import { UsageDoc as UsageDoc3 } from "@earendil-works/pi-durable";
|
|
3949
4432
|
import { Type as Type5 } from "typebox";
|
|
3950
4433
|
async function openReader(runtime, childId, parentId, context, through, historyUsage) {
|
|
3951
4434
|
const child = await runtime.harness.conversation(childId, context);
|
|
@@ -4008,7 +4491,7 @@ async function openReader(runtime, childId, parentId, context, through, historyU
|
|
|
4008
4491
|
return () => {};
|
|
4009
4492
|
const stop = native.subscribe(() => fn(null));
|
|
4010
4493
|
const stopApprovals = runtime.harness.subscribeCommits(({ changes }) => {
|
|
4011
|
-
if (changes.some((c) => c.type === "document" &&
|
|
4494
|
+
if (changes.some((c) => c.type === "document" && ["app.neko.approvals", "app.neko.delegations", "app.neko.subagents"].includes(c.record.kind) && c.conversationId === parentId))
|
|
4012
4495
|
fn(null);
|
|
4013
4496
|
});
|
|
4014
4497
|
return () => {
|
|
@@ -4019,7 +4502,12 @@ async function openReader(runtime, childId, parentId, context, through, historyU
|
|
|
4019
4502
|
async refresh() {
|
|
4020
4503
|
const page = await pageAt();
|
|
4021
4504
|
const approvals = through === undefined ? (await runtime.approvals.list(parentId, context)).filter((a) => a.childId === childId) : [];
|
|
4022
|
-
|
|
4505
|
+
const currentUsage = await runtime.harness.snapshot(UsageDoc3, childId, context);
|
|
4506
|
+
const rounds = (await listDelegations(runtime.harness, parentId, context)).filter((r) => r.childId === childId).map(({ usageStart, usageEnd: _end, ...round }) => ({
|
|
4507
|
+
...round,
|
|
4508
|
+
roundUsage: round.roundUsage ?? (through === undefined && usageStart && currentUsage ? subagentUsageTotals(subtractSubagentUsage(currentUsage, usageStart)) : null)
|
|
4509
|
+
}));
|
|
4510
|
+
revision = remember({ ...project(page.entries), rounds, approvals, historyBefore: page.next });
|
|
4023
4511
|
return revision;
|
|
4024
4512
|
},
|
|
4025
4513
|
async chunk(input) {
|
|
@@ -4066,7 +4554,7 @@ async function openReader(runtime, childId, parentId, context, through, historyU
|
|
|
4066
4554
|
}
|
|
4067
4555
|
var ChildProjection = local("neko.subagent-projection", {
|
|
4068
4556
|
version: 1,
|
|
4069
|
-
input: Type5.Object({ parentId: Type5.Integer(),
|
|
4557
|
+
input: Type5.Object({ parentId: Type5.Integer(), agentId: Type5.String() }),
|
|
4070
4558
|
members: {
|
|
4071
4559
|
revision: Type5.Integer(),
|
|
4072
4560
|
reader: opaque("neko.subagent-reader@1")
|
|
@@ -4079,7 +4567,7 @@ function installSubagentHost(host, runtime, context) {
|
|
|
4079
4567
|
reader: resource({
|
|
4080
4568
|
rev: 1,
|
|
4081
4569
|
async open() {
|
|
4082
|
-
const entry = (await listDelegations(runtime.harness, input.parentId, context)).find((d) => d.taskId === input.
|
|
4570
|
+
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);
|
|
4083
4571
|
if (!entry)
|
|
4084
4572
|
throw new ChordError("gone", "Subagent unavailable");
|
|
4085
4573
|
const r = await openReader(runtime, entry.childId, input.parentId, context, entry.historyThrough, entry.historyUsage);
|
|
@@ -4109,13 +4597,13 @@ function installSubagentHost(host, runtime, context) {
|
|
|
4109
4597
|
throw new ChordError("forbidden", "Attachment expired");
|
|
4110
4598
|
if (!await runtime.getConversation(Number(input.sessionId)))
|
|
4111
4599
|
throw new ChordError("gone", "Session unavailable");
|
|
4112
|
-
const entry = (await listDelegations(runtime.harness, Number(input.sessionId), context)).find((d) => d.taskId === input.taskId);
|
|
4600
|
+
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);
|
|
4113
4601
|
if (!entry)
|
|
4114
4602
|
throw new ChordError("forbidden", "Subagent does not belong to this session");
|
|
4115
4603
|
return entry;
|
|
4116
4604
|
};
|
|
4117
4605
|
const { target } = self.resources({ target: resource({ rev: 1, deps: [owner], open: check }) });
|
|
4118
|
-
const shared = self.get(ChildProjection, { parentId: Number(input.sessionId),
|
|
4606
|
+
const shared = self.get(ChildProjection, { parentId: Number(input.sessionId), agentId: input.agentId ?? `round-${input.taskId}` }, { principal: hostPrincipal });
|
|
4119
4607
|
const { authorizedRevision } = self.state({ authorizedRevision: cell(Type5.Integer(), 0) });
|
|
4120
4608
|
self.listen({
|
|
4121
4609
|
authorizedView: listener(shared.revision, async (value, ctx) => {
|
|
@@ -4144,14 +4632,26 @@ function installSubagentHost(host, runtime, context) {
|
|
|
4144
4632
|
const entry = await check();
|
|
4145
4633
|
if (entry.historyThrough !== undefined)
|
|
4146
4634
|
throw new ChordError("forbidden", "Fork history is read-only");
|
|
4147
|
-
|
|
4635
|
+
const approval = (await runtime.approvals.list(Number(input.sessionId), context)).find((a) => a.id === id && a.childId === entry.childId);
|
|
4636
|
+
if (!approval)
|
|
4637
|
+
throw new ChordError("forbidden", "Approval does not belong to this agent");
|
|
4638
|
+
return runtime.approvals.decide(Number(input.sessionId), approval.delegationTaskId, id, version, decision, context);
|
|
4148
4639
|
},
|
|
4149
|
-
stop: async () => {
|
|
4640
|
+
stop: async (request) => {
|
|
4150
4641
|
const entry = await check();
|
|
4151
4642
|
if (entry.historyThrough !== undefined)
|
|
4152
4643
|
throw new ChordError("forbidden", "Fork history is read-only");
|
|
4153
|
-
|
|
4154
|
-
|
|
4644
|
+
const taskId = request?.taskId ?? input.taskId;
|
|
4645
|
+
if (taskId === undefined)
|
|
4646
|
+
throw new ChordError("invalid", "Select the exact active round to stop");
|
|
4647
|
+
const valid = await runtime.harness.commit(async (tx) => {
|
|
4648
|
+
const identity = (await tx.doc(SubagentsDoc, Number(input.sessionId))).items.find((a) => a.childId === entry.childId);
|
|
4649
|
+
return identity?.activeRoundId === taskId;
|
|
4650
|
+
}, context);
|
|
4651
|
+
if (!valid)
|
|
4652
|
+
return null;
|
|
4653
|
+
await runtime.harness.abortTask(taskId, context);
|
|
4654
|
+
await runtime.harness.waitForTask(taskId, context);
|
|
4155
4655
|
return null;
|
|
4156
4656
|
}
|
|
4157
4657
|
};
|
|
@@ -4198,7 +4698,7 @@ async function openProjection(runtime, session, context) {
|
|
|
4198
4698
|
const stopMetadata = runtime.onDirectoryChange(() => changed?.(null));
|
|
4199
4699
|
let childIds = new Set;
|
|
4200
4700
|
const stopChildren = runtime.harness.subscribeCommits(({ changes }) => {
|
|
4201
|
-
if (changes.some((change) => change.type === "document" && (["app.neko.delegations", "app.neko.approvals"].includes(change.record.kind) && change.conversationId === session.id || change.record.kind === "pi.usage" && change.conversationId !== undefined && childIds.has(change.conversationId))))
|
|
4701
|
+
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))))
|
|
4202
4702
|
changed?.(null);
|
|
4203
4703
|
});
|
|
4204
4704
|
return {
|
|
@@ -4230,15 +4730,19 @@ async function openProjection(runtime, session, context) {
|
|
|
4230
4730
|
entry.approvalCount = entry.historyThrough === undefined ? approvals.filter((a) => a.delegationTaskId === entry.taskId && a.status === "pending").length : 0;
|
|
4231
4731
|
childIds = new Set(subagents.filter((entry) => entry.historyThrough === undefined).map((entry) => entry.childId));
|
|
4232
4732
|
let aggregate = await session.usage();
|
|
4233
|
-
for (const child of subagents.filter((entry) => entry.historyThrough === undefined))
|
|
4234
|
-
aggregate = mergeUsage(aggregate, await runtime.harness.snapshot(
|
|
4733
|
+
for (const child of new Map(subagents.filter((entry) => entry.historyThrough === undefined).map((r) => [r.childId, r])).values())
|
|
4734
|
+
aggregate = mergeUsage(aggregate, await runtime.harness.snapshot(UsageDoc4, child.childId, context) ?? {
|
|
4235
4735
|
models: {},
|
|
4236
4736
|
tools: {}
|
|
4237
4737
|
});
|
|
4238
4738
|
const u = totalUsage(aggregate);
|
|
4239
4739
|
return {
|
|
4240
4740
|
...projected,
|
|
4241
|
-
|
|
4741
|
+
agents: summarizeSubagents(subagents, (await runtime.harness.snapshot(SubagentsDoc, session.id, context))?.items),
|
|
4742
|
+
subagents: subagents.map(({ usageStart: _start, usageEnd: _end, ...entry }) => ({
|
|
4743
|
+
...entry,
|
|
4744
|
+
report: entry.report?.slice(0, 512) ?? null
|
|
4745
|
+
})),
|
|
4242
4746
|
taskUsage: {
|
|
4243
4747
|
input: u.input,
|
|
4244
4748
|
output: u.output,
|