neko-harness 0.3.0 → 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 -1
- 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 +575 -151
- package/dist/neko.js +728 -198
- 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 {
|
|
@@ -640,6 +643,7 @@ function projectChatView(view, modelLabel, options = {}) {
|
|
|
640
643
|
const defaultImagePrompt = entry.kind === NekoInput.kind ? savedDefaultImagePrompt(message.content, entry.data?.defaultImagePrompt) : undefined;
|
|
641
644
|
items.push({
|
|
642
645
|
type: "user",
|
|
646
|
+
...entry.data?.subagentRoundId ? { subagentRoundId: entry.data.subagentRoundId } : {},
|
|
643
647
|
key: `e${entry.id}`,
|
|
644
648
|
id: entry.id,
|
|
645
649
|
text: defaultImagePrompt && Array.isArray(message.content) ? userText(message.content.slice(1)) : userText(message.content),
|
|
@@ -840,7 +844,7 @@ import {
|
|
|
840
844
|
Harness,
|
|
841
845
|
LiveDoc as importLiveDoc,
|
|
842
846
|
MemoryStorage,
|
|
843
|
-
UsageDoc,
|
|
847
|
+
UsageDoc as UsageDoc2,
|
|
844
848
|
watchEvents
|
|
845
849
|
} from "@earendil-works/pi-durable";
|
|
846
850
|
import { createNodePhotonImages } from "@earendil-works/pi-durable/images/node";
|
|
@@ -1220,9 +1224,58 @@ import {
|
|
|
1220
1224
|
configure,
|
|
1221
1225
|
defineDoc,
|
|
1222
1226
|
defineExtension as defineExtension3,
|
|
1223
|
-
|
|
1227
|
+
defineTask,
|
|
1228
|
+
defineTool as defineTool2,
|
|
1229
|
+
LiveDoc,
|
|
1230
|
+
UsageDoc
|
|
1224
1231
|
} from "@earendil-works/pi-durable";
|
|
1225
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}`;
|
|
1226
1279
|
var DelegationsDoc = defineDoc({
|
|
1227
1280
|
kind: "app.neko.delegations",
|
|
1228
1281
|
version: 1,
|
|
@@ -1231,6 +1284,14 @@ var DelegationsDoc = defineDoc({
|
|
|
1231
1284
|
fork: "initial",
|
|
1232
1285
|
initial: () => ({ items: [] })
|
|
1233
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
|
+
});
|
|
1234
1295
|
var SubagentDoc = defineDoc({
|
|
1235
1296
|
kind: "app.neko.subagent",
|
|
1236
1297
|
version: 1,
|
|
@@ -1250,157 +1311,401 @@ var ROLE_PROMPTS = {
|
|
|
1250
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.",
|
|
1251
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."
|
|
1252
1313
|
};
|
|
1253
|
-
var SUBAGENT_DELIVERY = " Work
|
|
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.";
|
|
1254
1315
|
var READ_TOOLS = new Set(["read", "find_files", "search_text", "web_search", "web_extract"]);
|
|
1255
1316
|
async function refreshDelegations(tx, id) {
|
|
1256
1317
|
const doc = await tx.doc(DelegationsDoc, id);
|
|
1318
|
+
const identities = await tx.doc(SubagentsDoc, id);
|
|
1257
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;
|
|
1258
1322
|
if (item.historyThrough !== undefined)
|
|
1259
1323
|
continue;
|
|
1260
|
-
if (item.
|
|
1261
|
-
|
|
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
|
+
}
|
|
1262
1347
|
const task = await tx.task(item.taskId);
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
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
|
+
}
|
|
1267
1359
|
}
|
|
1268
1360
|
return JSON.parse(JSON.stringify(doc.items));
|
|
1269
1361
|
}
|
|
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
|
-
}
|
|
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
|
+
});
|
|
1309
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
|
+
]);
|
|
1310
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
|
+
});
|
|
1311
1576
|
const extension = defineExtension3({
|
|
1312
1577
|
name: "neko-subagents",
|
|
1578
|
+
tasks: [RoundTask],
|
|
1313
1579
|
tools: [
|
|
1314
1580
|
defineTool2({
|
|
1315
1581
|
name: "subagent",
|
|
1316
|
-
description: "
|
|
1317
|
-
parameters
|
|
1318
|
-
|
|
1319
|
-
title: Type2.String({ minLength: 1, maxLength: 80 }),
|
|
1320
|
-
task: Type2.String({ minLength: 1, maxLength: 32768 })
|
|
1321
|
-
}),
|
|
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,
|
|
1322
1585
|
replay: "safe",
|
|
1323
|
-
execute: async (
|
|
1324
|
-
if (!
|
|
1325
|
-
throw new Error("
|
|
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;
|
|
1326
1590
|
if ((await api.snapshot(SubagentDoc, api.conversationId, context))?.role)
|
|
1327
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.");
|
|
1328
1604
|
const agent = await api.agent(context);
|
|
1329
|
-
const
|
|
1330
|
-
const
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
await
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
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 ?? {
|
|
1343
1650
|
parentId: api.conversationId,
|
|
1344
1651
|
taskId: api.taskId,
|
|
1345
1652
|
callId: api.callId,
|
|
1346
|
-
childId:
|
|
1347
|
-
|
|
1653
|
+
childId: identity.childId,
|
|
1654
|
+
agentId: identity.agentId,
|
|
1655
|
+
round: rounds.filter((r) => r.childId === identity.childId).length + 1,
|
|
1656
|
+
role: identity.role,
|
|
1348
1657
|
title: args.title,
|
|
1349
1658
|
createdAt: Date.now(),
|
|
1350
1659
|
startedAt: null,
|
|
1351
1660
|
endedAt: null,
|
|
1352
1661
|
status: "queued",
|
|
1353
1662
|
report: null
|
|
1354
|
-
}
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
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;
|
|
1364
1679
|
}, 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();
|
|
1680
|
+
await api.details({
|
|
1681
|
+
conversationId: round.childId,
|
|
1682
|
+
agentId: round.agentId,
|
|
1683
|
+
roundId: round.taskId,
|
|
1684
|
+
role: round.role,
|
|
1685
|
+
title: round.title
|
|
1399
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}` };
|
|
1400
1690
|
return {
|
|
1401
|
-
output: [
|
|
1402
|
-
|
|
1403
|
-
|
|
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
|
+
}
|
|
1404
1709
|
};
|
|
1405
1710
|
}
|
|
1406
1711
|
})
|
|
@@ -1409,15 +1714,70 @@ function createSubagentExtension(prepare, discard) {
|
|
|
1409
1714
|
return extension;
|
|
1410
1715
|
}
|
|
1411
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
|
+
}
|
|
1412
1730
|
return harness.commit((tx) => refreshDelegations(tx, parent), context);
|
|
1413
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
|
+
}
|
|
1414
1774
|
|
|
1415
1775
|
// packages/core/src/prompt/prompt.ts
|
|
1416
1776
|
var KEYS = ["preamble", "rules", "project_context", "skills", "cwd"];
|
|
1417
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.";
|
|
1418
1778
|
var TOOL_GUIDELINES = {
|
|
1419
1779
|
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."
|
|
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."
|
|
1421
1781
|
],
|
|
1422
1782
|
find_files: [
|
|
1423
1783
|
"Use find_files to discover files; query is a literal substring. Narrow the directory when results are partial."
|
|
@@ -1627,9 +1987,9 @@ import { getSupportedThinkingLevels } from "@earendil-works/pi-ai";
|
|
|
1627
1987
|
import {
|
|
1628
1988
|
defineDoc as defineDoc2,
|
|
1629
1989
|
defineExtension as defineExtension5,
|
|
1630
|
-
defineTask,
|
|
1990
|
+
defineTask as defineTask2,
|
|
1631
1991
|
InboxDoc,
|
|
1632
|
-
LiveDoc
|
|
1992
|
+
LiveDoc as LiveDoc2
|
|
1633
1993
|
} from "@earendil-works/pi-durable";
|
|
1634
1994
|
var SessionMetadataDoc = defineDoc2({
|
|
1635
1995
|
kind: "app.neko.session-metadata",
|
|
@@ -1694,7 +2054,7 @@ function mergeUsage(base, extra) {
|
|
|
1694
2054
|
function namingTask() {
|
|
1695
2055
|
let active = 0;
|
|
1696
2056
|
const waits = new Set;
|
|
1697
|
-
return
|
|
2057
|
+
return defineTask2({
|
|
1698
2058
|
name: "app.neko.name",
|
|
1699
2059
|
version: 1,
|
|
1700
2060
|
initial: () => ({ phase: "wait", until: Date.now() + 100 }),
|
|
@@ -1908,7 +2268,7 @@ class SessionMetadataManager {
|
|
|
1908
2268
|
const answer = record.status === "done" ? (await this.storage.entry(record.answer, this.context))?.entry.model?.[0] : undefined;
|
|
1909
2269
|
return { record, data, answer: answer?.role === "assistant" ? answer : undefined };
|
|
1910
2270
|
}));
|
|
1911
|
-
const live = await this.harness.snapshot(
|
|
2271
|
+
const live = await this.harness.snapshot(LiveDoc2, id, this.context);
|
|
1912
2272
|
await this.harness.commit(async (tx) => {
|
|
1913
2273
|
const d = await tx.doc(SessionMetadataDoc, id);
|
|
1914
2274
|
for (const { record, data, answer } of updates) {
|
|
@@ -2091,7 +2451,8 @@ function createApprovalExtension(service) {
|
|
|
2091
2451
|
const child = await approvals.harness.conversation(api.conversationId, context);
|
|
2092
2452
|
const agent = await child.agent(context);
|
|
2093
2453
|
const cwd = await realpath(agent.cwd);
|
|
2094
|
-
const
|
|
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);
|
|
2095
2456
|
if (!delegation)
|
|
2096
2457
|
return { block: "Missing delegation" };
|
|
2097
2458
|
await approvals.ensure(parent, {
|
|
@@ -2804,6 +3165,7 @@ async function createNekoRuntime(options = {}) {
|
|
|
2804
3165
|
approvals = new ToolApprovals(harness, context);
|
|
2805
3166
|
const existing = await scanConversations();
|
|
2806
3167
|
try {
|
|
3168
|
+
await installSubagentCoordinator(harness, existing.map((r) => r.id), context);
|
|
2807
3169
|
await prepareApprovalConversations(harness, existing.map((r) => r.id), approvalExtension, context);
|
|
2808
3170
|
for (const record of existing) {
|
|
2809
3171
|
const target = await harness.conversation(record.id, context);
|
|
@@ -2988,7 +3350,7 @@ async function createNekoRuntime(options = {}) {
|
|
|
2988
3350
|
return watchEvents(harness, target.id, context);
|
|
2989
3351
|
},
|
|
2990
3352
|
async usage() {
|
|
2991
|
-
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: {} });
|
|
2992
3354
|
},
|
|
2993
3355
|
compactionRecord() {
|
|
2994
3356
|
return readCompaction(harness, storage, target.id, context);
|
|
@@ -3037,7 +3399,7 @@ async function createNekoRuntime(options = {}) {
|
|
|
3037
3399
|
async globalUsage() {
|
|
3038
3400
|
let total = { models: {}, tools: {} };
|
|
3039
3401
|
for (const record of await scanConversations()) {
|
|
3040
|
-
total = mergeUsage(total, await harness.snapshot(
|
|
3402
|
+
total = mergeUsage(total, await harness.snapshot(UsageDoc2, record.id, context) ?? { models: {}, tools: {} });
|
|
3041
3403
|
total = mergeUsage(total, (await harness.snapshot(SessionMetadataDoc, record.id, context))?.namingUsage ?? { models: {}, tools: {} });
|
|
3042
3404
|
}
|
|
3043
3405
|
return total;
|
|
@@ -3072,14 +3434,24 @@ async function createNekoRuntime(options = {}) {
|
|
|
3072
3434
|
}
|
|
3073
3435
|
cursor = page.next;
|
|
3074
3436
|
} while (cursor);
|
|
3437
|
+
const frozen = new Map;
|
|
3075
3438
|
for (const reference of references) {
|
|
3076
3439
|
if (!calls.has(reference.callId))
|
|
3077
3440
|
continue;
|
|
3078
|
-
|
|
3079
|
-
|
|
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);
|
|
3080
3451
|
inherited.push({
|
|
3081
3452
|
...reference,
|
|
3082
|
-
historyThrough: reference.historyThrough ??
|
|
3453
|
+
historyThrough: reference.historyThrough ?? snapshot.through,
|
|
3454
|
+
roundUsage: reference.roundUsage ?? (reference.usageStart ? subagentUsageTotals(subtractSubagentUsage(snapshot.state, reference.usageStart)) : null),
|
|
3083
3455
|
historyUsage: reference.historyUsage ?? {
|
|
3084
3456
|
input: usage.input,
|
|
3085
3457
|
output: usage.output,
|
|
@@ -3315,8 +3687,8 @@ import { dirname as dirname5, join as join7 } from "path";
|
|
|
3315
3687
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3316
3688
|
|
|
3317
3689
|
// packages/core/src/application-version.ts
|
|
3318
|
-
var RUNNING_VERSION = "0.3.
|
|
3319
|
-
var APPLICATION_PROTOCOL =
|
|
3690
|
+
var RUNNING_VERSION = "0.3.1";
|
|
3691
|
+
var APPLICATION_PROTOCOL = 4;
|
|
3320
3692
|
function stableVersion(value) {
|
|
3321
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)));
|
|
3322
3694
|
}
|
|
@@ -3513,6 +3885,12 @@ var call = Type4.Object({
|
|
|
3513
3885
|
});
|
|
3514
3886
|
var DelegationSchema = Type4.Object({
|
|
3515
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])),
|
|
3516
3894
|
taskId: integer,
|
|
3517
3895
|
callId: str,
|
|
3518
3896
|
childId: integer,
|
|
@@ -3527,8 +3905,27 @@ var DelegationSchema = Type4.Object({
|
|
|
3527
3905
|
historyThrough: Type4.Optional(integer),
|
|
3528
3906
|
historyUsage: Type4.Optional(usage)
|
|
3529
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
|
+
});
|
|
3530
3925
|
var ChatViewSchema = Type4.Object({
|
|
3531
3926
|
historyBefore: Type4.Optional(Type4.Union([integer, nil])),
|
|
3927
|
+
agents: Type4.Optional(Type4.Array(SubagentSummarySchema)),
|
|
3928
|
+
rounds: Type4.Optional(Type4.Array(DelegationSchema)),
|
|
3532
3929
|
subagents: Type4.Optional(Type4.Array(DelegationSchema)),
|
|
3533
3930
|
taskUsage: Type4.Optional(usage),
|
|
3534
3931
|
model: str,
|
|
@@ -3684,7 +4081,12 @@ var NekoModels = contract("neko.models", {
|
|
|
3684
4081
|
var imageChunk = Type4.Object({ data: str, total: integer, mimeType: str });
|
|
3685
4082
|
var NekoSubagent = contract("neko.subagent", {
|
|
3686
4083
|
version: 1,
|
|
3687
|
-
input: Type4.Object({
|
|
4084
|
+
input: Type4.Object({
|
|
4085
|
+
sessionId: str,
|
|
4086
|
+
attachmentId: str,
|
|
4087
|
+
taskId: Type4.Optional(integer),
|
|
4088
|
+
agentId: Type4.Optional(str)
|
|
4089
|
+
}),
|
|
3688
4090
|
variants: {
|
|
3689
4091
|
ready: {
|
|
3690
4092
|
state: { revision: integer },
|
|
@@ -3697,7 +4099,7 @@ var NekoSubagent = contract("neko.subagent", {
|
|
|
3697
4099
|
}), Type4.Object({ token: integer, data: str, next: Type4.Union([integer, nil]) })),
|
|
3698
4100
|
getImage: rpc(Type4.Object({ entryId: integer, contentIndex: integer, offset: integer }), imageChunk),
|
|
3699
4101
|
decideApproval: rpc(Type4.Object({ id: integer, version: str, decision: Type4.Enum(["allowed", "denied"]) }), Type4.Enum(["pending", "allowed", "denied", "cancelled"])),
|
|
3700
|
-
stop: rpc(nil, nil)
|
|
4102
|
+
stop: rpc(Type4.Union([nil, Type4.Object({ taskId: integer })]), nil)
|
|
3701
4103
|
}
|
|
3702
4104
|
}
|
|
3703
4105
|
}
|
|
@@ -3839,7 +4241,7 @@ import {
|
|
|
3839
4241
|
resource as resource2
|
|
3840
4242
|
} from "@earendil-works/chord";
|
|
3841
4243
|
import { withAbortSignal as withAbortSignal3 } from "@earendil-works/chord/context";
|
|
3842
|
-
import { UsageDoc as
|
|
4244
|
+
import { UsageDoc as UsageDoc4 } from "@earendil-works/pi-durable";
|
|
3843
4245
|
import { Type as Type6 } from "typebox";
|
|
3844
4246
|
|
|
3845
4247
|
// packages/gateway/src/image-uploads.ts
|
|
@@ -3946,6 +4348,7 @@ import {
|
|
|
3946
4348
|
opaque,
|
|
3947
4349
|
resource
|
|
3948
4350
|
} from "@earendil-works/chord";
|
|
4351
|
+
import { UsageDoc as UsageDoc3 } from "@earendil-works/pi-durable";
|
|
3949
4352
|
import { Type as Type5 } from "typebox";
|
|
3950
4353
|
async function openReader(runtime, childId, parentId, context, through, historyUsage) {
|
|
3951
4354
|
const child = await runtime.harness.conversation(childId, context);
|
|
@@ -4008,7 +4411,7 @@ async function openReader(runtime, childId, parentId, context, through, historyU
|
|
|
4008
4411
|
return () => {};
|
|
4009
4412
|
const stop = native.subscribe(() => fn(null));
|
|
4010
4413
|
const stopApprovals = runtime.harness.subscribeCommits(({ changes }) => {
|
|
4011
|
-
if (changes.some((c) => c.type === "document" &&
|
|
4414
|
+
if (changes.some((c) => c.type === "document" && ["app.neko.approvals", "app.neko.delegations", "app.neko.subagents"].includes(c.record.kind) && c.conversationId === parentId))
|
|
4012
4415
|
fn(null);
|
|
4013
4416
|
});
|
|
4014
4417
|
return () => {
|
|
@@ -4019,7 +4422,12 @@ async function openReader(runtime, childId, parentId, context, through, historyU
|
|
|
4019
4422
|
async refresh() {
|
|
4020
4423
|
const page = await pageAt();
|
|
4021
4424
|
const approvals = through === undefined ? (await runtime.approvals.list(parentId, context)).filter((a) => a.childId === childId) : [];
|
|
4022
|
-
|
|
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 });
|
|
4023
4431
|
return revision;
|
|
4024
4432
|
},
|
|
4025
4433
|
async chunk(input) {
|
|
@@ -4066,7 +4474,7 @@ async function openReader(runtime, childId, parentId, context, through, historyU
|
|
|
4066
4474
|
}
|
|
4067
4475
|
var ChildProjection = local("neko.subagent-projection", {
|
|
4068
4476
|
version: 1,
|
|
4069
|
-
input: Type5.Object({ parentId: Type5.Integer(),
|
|
4477
|
+
input: Type5.Object({ parentId: Type5.Integer(), agentId: Type5.String() }),
|
|
4070
4478
|
members: {
|
|
4071
4479
|
revision: Type5.Integer(),
|
|
4072
4480
|
reader: opaque("neko.subagent-reader@1")
|
|
@@ -4079,7 +4487,7 @@ function installSubagentHost(host, runtime, context) {
|
|
|
4079
4487
|
reader: resource({
|
|
4080
4488
|
rev: 1,
|
|
4081
4489
|
async open() {
|
|
4082
|
-
const entry = (await listDelegations(runtime.harness, input.parentId, context)).find((d) => d.taskId === input.
|
|
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);
|
|
4083
4491
|
if (!entry)
|
|
4084
4492
|
throw new ChordError("gone", "Subagent unavailable");
|
|
4085
4493
|
const r = await openReader(runtime, entry.childId, input.parentId, context, entry.historyThrough, entry.historyUsage);
|
|
@@ -4109,13 +4517,13 @@ function installSubagentHost(host, runtime, context) {
|
|
|
4109
4517
|
throw new ChordError("forbidden", "Attachment expired");
|
|
4110
4518
|
if (!await runtime.getConversation(Number(input.sessionId)))
|
|
4111
4519
|
throw new ChordError("gone", "Session unavailable");
|
|
4112
|
-
const entry = (await listDelegations(runtime.harness, Number(input.sessionId), context)).find((d) => d.taskId === input.taskId);
|
|
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);
|
|
4113
4521
|
if (!entry)
|
|
4114
4522
|
throw new ChordError("forbidden", "Subagent does not belong to this session");
|
|
4115
4523
|
return entry;
|
|
4116
4524
|
};
|
|
4117
4525
|
const { target } = self.resources({ target: resource({ rev: 1, deps: [owner], open: check }) });
|
|
4118
|
-
const shared = self.get(ChildProjection, { parentId: Number(input.sessionId),
|
|
4526
|
+
const shared = self.get(ChildProjection, { parentId: Number(input.sessionId), agentId: input.agentId ?? `round-${input.taskId}` }, { principal: hostPrincipal });
|
|
4119
4527
|
const { authorizedRevision } = self.state({ authorizedRevision: cell(Type5.Integer(), 0) });
|
|
4120
4528
|
self.listen({
|
|
4121
4529
|
authorizedView: listener(shared.revision, async (value, ctx) => {
|
|
@@ -4144,14 +4552,26 @@ function installSubagentHost(host, runtime, context) {
|
|
|
4144
4552
|
const entry = await check();
|
|
4145
4553
|
if (entry.historyThrough !== undefined)
|
|
4146
4554
|
throw new ChordError("forbidden", "Fork history is read-only");
|
|
4147
|
-
|
|
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);
|
|
4148
4559
|
},
|
|
4149
|
-
stop: async () => {
|
|
4560
|
+
stop: async (request) => {
|
|
4150
4561
|
const entry = await check();
|
|
4151
4562
|
if (entry.historyThrough !== undefined)
|
|
4152
4563
|
throw new ChordError("forbidden", "Fork history is read-only");
|
|
4153
|
-
|
|
4154
|
-
|
|
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);
|
|
4155
4575
|
return null;
|
|
4156
4576
|
}
|
|
4157
4577
|
};
|
|
@@ -4198,7 +4618,7 @@ async function openProjection(runtime, session, context) {
|
|
|
4198
4618
|
const stopMetadata = runtime.onDirectoryChange(() => changed?.(null));
|
|
4199
4619
|
let childIds = new Set;
|
|
4200
4620
|
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))))
|
|
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))))
|
|
4202
4622
|
changed?.(null);
|
|
4203
4623
|
});
|
|
4204
4624
|
return {
|
|
@@ -4230,15 +4650,19 @@ async function openProjection(runtime, session, context) {
|
|
|
4230
4650
|
entry.approvalCount = entry.historyThrough === undefined ? approvals.filter((a) => a.delegationTaskId === entry.taskId && a.status === "pending").length : 0;
|
|
4231
4651
|
childIds = new Set(subagents.filter((entry) => entry.historyThrough === undefined).map((entry) => entry.childId));
|
|
4232
4652
|
let aggregate = await session.usage();
|
|
4233
|
-
for (const child of subagents.filter((entry) => entry.historyThrough === undefined))
|
|
4234
|
-
aggregate = mergeUsage(aggregate, await runtime.harness.snapshot(
|
|
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) ?? {
|
|
4235
4655
|
models: {},
|
|
4236
4656
|
tools: {}
|
|
4237
4657
|
});
|
|
4238
4658
|
const u = totalUsage(aggregate);
|
|
4239
4659
|
return {
|
|
4240
4660
|
...projected,
|
|
4241
|
-
|
|
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
|
+
})),
|
|
4242
4666
|
taskUsage: {
|
|
4243
4667
|
input: u.input,
|
|
4244
4668
|
output: u.output,
|