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/neko.js
CHANGED
|
@@ -79,7 +79,7 @@ function updateAction(input) {
|
|
|
79
79
|
return "install";
|
|
80
80
|
return "none";
|
|
81
81
|
}
|
|
82
|
-
var RUNNING_VERSION = "0.3.
|
|
82
|
+
var RUNNING_VERSION = "0.3.1", APPLICATION_PROTOCOL = 4, ApplicationProtocolError;
|
|
83
83
|
var init_application_version = __esm(() => {
|
|
84
84
|
ApplicationProtocolError = class ApplicationProtocolError extends Error {
|
|
85
85
|
constructor() {
|
|
@@ -803,7 +803,10 @@ function summarizeToolCall(name, args) {
|
|
|
803
803
|
const record = args ?? {};
|
|
804
804
|
switch (name) {
|
|
805
805
|
case "subagent":
|
|
806
|
-
return {
|
|
806
|
+
return {
|
|
807
|
+
args: record.action === "list" ? "List agents" : `${record.agentId ?? record.type}: ${record.title}`,
|
|
808
|
+
detail: String(record.task ?? "")
|
|
809
|
+
};
|
|
807
810
|
case "find_files":
|
|
808
811
|
case "search_text":
|
|
809
812
|
return {
|
|
@@ -926,6 +929,7 @@ function projectChatView(view, modelLabel, options = {}) {
|
|
|
926
929
|
const defaultImagePrompt = entry.kind === NekoInput.kind ? savedDefaultImagePrompt(message.content, entry.data?.defaultImagePrompt) : undefined;
|
|
927
930
|
items.push({
|
|
928
931
|
type: "user",
|
|
932
|
+
...entry.data?.subagentRoundId ? { subagentRoundId: entry.data.subagentRoundId } : {},
|
|
929
933
|
key: `e${entry.id}`,
|
|
930
934
|
id: entry.id,
|
|
931
935
|
text: defaultImagePrompt && Array.isArray(message.content) ? userText(message.content.slice(1)) : userText(message.content),
|
|
@@ -1485,164 +1489,403 @@ var init_invoke = __esm(() => {
|
|
|
1485
1489
|
init_skills();
|
|
1486
1490
|
});
|
|
1487
1491
|
|
|
1492
|
+
// packages/core/src/subagent-usage.ts
|
|
1493
|
+
function subtractSubagentUsage(end, start) {
|
|
1494
|
+
const result = emptySubagentUsage();
|
|
1495
|
+
for (const bucket of ["models", "tools"]) {
|
|
1496
|
+
for (const [key, usage] of Object.entries(end[bucket])) {
|
|
1497
|
+
const before = start[bucket][key];
|
|
1498
|
+
const cost = usage.cost;
|
|
1499
|
+
const next = {
|
|
1500
|
+
input: usage.input - (before?.input ?? 0),
|
|
1501
|
+
output: usage.output - (before?.output ?? 0),
|
|
1502
|
+
cacheRead: usage.cacheRead - (before?.cacheRead ?? 0),
|
|
1503
|
+
cacheWrite: usage.cacheWrite - (before?.cacheWrite ?? 0),
|
|
1504
|
+
totalTokens: usage.totalTokens - (before?.totalTokens ?? 0),
|
|
1505
|
+
cost: {
|
|
1506
|
+
input: cost.input - (before?.cost.input ?? 0),
|
|
1507
|
+
output: cost.output - (before?.cost.output ?? 0),
|
|
1508
|
+
cacheRead: cost.cacheRead - (before?.cost.cacheRead ?? 0),
|
|
1509
|
+
cacheWrite: cost.cacheWrite - (before?.cost.cacheWrite ?? 0),
|
|
1510
|
+
total: cost.total - (before?.cost.total ?? 0)
|
|
1511
|
+
}
|
|
1512
|
+
};
|
|
1513
|
+
if (usage.reasoning !== undefined)
|
|
1514
|
+
next.reasoning = usage.reasoning - (before?.reasoning ?? 0);
|
|
1515
|
+
if (usage.cacheWrite1h !== undefined)
|
|
1516
|
+
next.cacheWrite1h = usage.cacheWrite1h - (before?.cacheWrite1h ?? 0);
|
|
1517
|
+
result[bucket][key] = next;
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
return result;
|
|
1521
|
+
}
|
|
1522
|
+
function subagentUsageTotals(state) {
|
|
1523
|
+
const totals = totalUsage(state);
|
|
1524
|
+
return {
|
|
1525
|
+
input: totals.input,
|
|
1526
|
+
output: totals.output,
|
|
1527
|
+
cacheRead: totals.cacheRead,
|
|
1528
|
+
cacheWrite: totals.cacheWrite,
|
|
1529
|
+
cost: totals.cost.total
|
|
1530
|
+
};
|
|
1531
|
+
}
|
|
1532
|
+
var emptySubagentUsage = () => ({ models: {}, tools: {} });
|
|
1533
|
+
var init_subagent_usage = __esm(() => {
|
|
1534
|
+
init_projection();
|
|
1535
|
+
});
|
|
1536
|
+
|
|
1488
1537
|
// packages/core/src/subagents.ts
|
|
1489
1538
|
import {
|
|
1490
1539
|
AssistantEntry,
|
|
1491
1540
|
configure,
|
|
1492
1541
|
defineDoc,
|
|
1493
1542
|
defineExtension as defineExtension3,
|
|
1494
|
-
|
|
1543
|
+
defineTask,
|
|
1544
|
+
defineTool as defineTool2,
|
|
1545
|
+
LiveDoc,
|
|
1546
|
+
UsageDoc
|
|
1495
1547
|
} from "@earendil-works/pi-durable";
|
|
1496
1548
|
import { Type as Type2 } from "typebox";
|
|
1549
|
+
import { Value } from "typebox/value";
|
|
1497
1550
|
async function refreshDelegations(tx, id) {
|
|
1498
1551
|
const doc = await tx.doc(DelegationsDoc, id);
|
|
1552
|
+
const identities = await tx.doc(SubagentsDoc, id);
|
|
1499
1553
|
for (const item of doc.items) {
|
|
1554
|
+
item.agentId ??= agentIdFor(item.childId);
|
|
1555
|
+
item.round ??= doc.items.filter((d) => d.childId === item.childId && d.taskId <= item.taskId).length;
|
|
1500
1556
|
if (item.historyThrough !== undefined)
|
|
1501
1557
|
continue;
|
|
1502
|
-
if (item.
|
|
1503
|
-
|
|
1558
|
+
if (item.inputEntryId === undefined) {
|
|
1559
|
+
const submission = await tx.submissionByRequest(item.childId, `subagent:${item.taskId}`);
|
|
1560
|
+
if (submission?.type === "input" && submission.entry !== undefined)
|
|
1561
|
+
item.inputEntryId = submission.entry;
|
|
1562
|
+
}
|
|
1563
|
+
let identity = identities.items.find((a) => a.childId === item.childId);
|
|
1564
|
+
if (!identity) {
|
|
1565
|
+
const role = await tx.doc(SubagentDoc, item.childId);
|
|
1566
|
+
if (role.parentId !== id || role.role !== item.role)
|
|
1567
|
+
continue;
|
|
1568
|
+
identity = {
|
|
1569
|
+
agentId: item.agentId,
|
|
1570
|
+
parentId: id,
|
|
1571
|
+
childId: item.childId,
|
|
1572
|
+
role: item.role,
|
|
1573
|
+
cwd: "",
|
|
1574
|
+
title: item.title,
|
|
1575
|
+
createdAt: item.createdAt,
|
|
1576
|
+
activeRoundId: item.taskId
|
|
1577
|
+
};
|
|
1578
|
+
identities.items.push(identity);
|
|
1579
|
+
identity = identities.items.at(-1);
|
|
1580
|
+
}
|
|
1504
1581
|
const task = await tx.task(item.taskId);
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1582
|
+
const runner = item.runnerId === undefined ? undefined : await tx.task(item.runnerId);
|
|
1583
|
+
const live = await tx.doc(LiveDoc, item.childId);
|
|
1584
|
+
const idle = live.run === undefined && !live.compactions?.length;
|
|
1585
|
+
if (task?.state.status === "terminal") {
|
|
1586
|
+
if (item.status === "queued" || item.status === "running") {
|
|
1587
|
+
item.status = task.state.outcome.status === "aborted" ? "cancelled" : "failed";
|
|
1588
|
+
item.endedAt ??= Date.now();
|
|
1589
|
+
}
|
|
1590
|
+
if (identity.activeRoundId === item.taskId && idle && (!runner || runner.state.status === "terminal"))
|
|
1591
|
+
identity.activeRoundId = null;
|
|
1592
|
+
}
|
|
1509
1593
|
}
|
|
1510
1594
|
return JSON.parse(JSON.stringify(doc.items));
|
|
1511
1595
|
}
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
const
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
}
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1596
|
+
function summarizeSubagents(rounds, identities = []) {
|
|
1597
|
+
const groups = new Map;
|
|
1598
|
+
for (const round of rounds) {
|
|
1599
|
+
const id = round.agentId ?? agentIdFor(round.childId);
|
|
1600
|
+
groups.set(id, [...groups.get(id) ?? [], round]);
|
|
1601
|
+
}
|
|
1602
|
+
return [...groups].map(([agentId, entries]) => {
|
|
1603
|
+
const first = entries[0];
|
|
1604
|
+
const last = entries.at(-1);
|
|
1605
|
+
const identity = identities.find((a) => a.agentId === agentId);
|
|
1606
|
+
const active = entries.find((r) => identity ? r.taskId === identity.activeRoundId : r.status === "running" || r.status === "queued");
|
|
1607
|
+
return {
|
|
1608
|
+
agentId,
|
|
1609
|
+
parentId: first.parentId,
|
|
1610
|
+
childId: first.childId,
|
|
1611
|
+
role: first.role,
|
|
1612
|
+
title: identity?.title ?? first.title,
|
|
1613
|
+
status: active ? active.status === "queued" ? "queued" : "running" : "idle",
|
|
1614
|
+
activeRoundId: active?.taskId ?? null,
|
|
1615
|
+
lastRoundId: last.taskId,
|
|
1616
|
+
lastReport: last.report?.slice(0, 512) ?? null,
|
|
1617
|
+
roundCount: entries.length,
|
|
1618
|
+
taskTitle: (active ?? last).title,
|
|
1619
|
+
approvalCount: active?.approvalCount ?? 0,
|
|
1620
|
+
createdAt: first.createdAt,
|
|
1621
|
+
...first.historyThrough !== undefined ? { historyThrough: first.historyThrough, historyUsage: first.historyUsage } : {}
|
|
1622
|
+
};
|
|
1623
|
+
});
|
|
1624
|
+
}
|
|
1625
|
+
async function acquireSlot(tx, round, cwd) {
|
|
1626
|
+
const slots = await tx.doc(SlotsDoc);
|
|
1627
|
+
const live = [];
|
|
1628
|
+
for (const slot of slots.items) {
|
|
1629
|
+
const task = await tx.task(slot.taskId);
|
|
1630
|
+
if (task && task.state.status !== "terminal")
|
|
1631
|
+
live.push(slot);
|
|
1632
|
+
}
|
|
1633
|
+
slots.items = live;
|
|
1634
|
+
if (live.some((slot) => slot.taskId === round.taskId))
|
|
1635
|
+
return true;
|
|
1636
|
+
if (live.filter((slot) => slot.parentId === round.parentId).length >= 3 || round.role === "coder" && live.some((slot) => slot.coder && slot.cwd === cwd))
|
|
1637
|
+
return false;
|
|
1638
|
+
slots.items.push({ taskId: round.taskId, parentId: round.parentId, cwd, coder: round.role === "coder" });
|
|
1639
|
+
return true;
|
|
1551
1640
|
}
|
|
1552
1641
|
function createSubagentExtension(prepare, discard) {
|
|
1642
|
+
const finish = async (tx, input, status, report) => {
|
|
1643
|
+
const round = (await tx.doc(DelegationsDoc, input.parentId)).items.find((r) => r.taskId === input.taskId);
|
|
1644
|
+
round.status = status;
|
|
1645
|
+
round.report = report;
|
|
1646
|
+
round.endedAt ??= Date.now();
|
|
1647
|
+
round.usageEnd = JSON.parse(JSON.stringify(await tx.doc(UsageDoc, input.childId)));
|
|
1648
|
+
round.roundUsage = round.usageStart ? subagentUsageTotals(subtractSubagentUsage(round.usageEnd, round.usageStart)) : round.startedAt === null ? subagentUsageTotals(emptySubagentUsage()) : null;
|
|
1649
|
+
};
|
|
1650
|
+
const RoundTask = defineTask({
|
|
1651
|
+
name: "app.neko.subagent-round",
|
|
1652
|
+
version: 1,
|
|
1653
|
+
initial: () => ({ phase: "admission" }),
|
|
1654
|
+
phases: {
|
|
1655
|
+
admission: async (task, runtime, ctx) => {
|
|
1656
|
+
const env = await runtime.env(ctx);
|
|
1657
|
+
const canonical = env && await env.canonicalPath(task.input.cwd, ctx);
|
|
1658
|
+
const slotCwd = canonical?.ok ? canonical.value : task.input.cwd;
|
|
1659
|
+
let admitted = false;
|
|
1660
|
+
while (!admitted) {
|
|
1661
|
+
await runtime.commit(async (tx) => {
|
|
1662
|
+
const round = (await tx.doc(DelegationsDoc, task.input.parentId)).items.find((r) => r.taskId === task.input.taskId);
|
|
1663
|
+
if (!await acquireSlot(tx, round, slotCwd))
|
|
1664
|
+
return;
|
|
1665
|
+
admitted = true;
|
|
1666
|
+
const duplicate = await tx.submissionByRequest(task.input.childId, `subagent:${round.taskId}`);
|
|
1667
|
+
if (!duplicate && !round.usageStart)
|
|
1668
|
+
round.usageStart = JSON.parse(JSON.stringify(await tx.doc(UsageDoc, task.input.childId)));
|
|
1669
|
+
round.status = "running";
|
|
1670
|
+
round.startedAt ??= Date.now();
|
|
1671
|
+
return { status: "running", checkpoint: { phase: "deliver" } };
|
|
1672
|
+
}, ctx);
|
|
1673
|
+
if (!admitted)
|
|
1674
|
+
await runtime.sleep(runtime.now() + 100, ctx);
|
|
1675
|
+
}
|
|
1676
|
+
},
|
|
1677
|
+
deliver: async (task, runtime, ctx) => {
|
|
1678
|
+
const input = task.input;
|
|
1679
|
+
const child = await runtime.conversation(input.childId, ctx);
|
|
1680
|
+
const requestId = `subagent:${input.taskId}`;
|
|
1681
|
+
try {
|
|
1682
|
+
let duplicate = false;
|
|
1683
|
+
await runtime.commit(async (tx) => {
|
|
1684
|
+
duplicate = !!await tx.submissionByRequest(input.childId, requestId);
|
|
1685
|
+
}, ctx);
|
|
1686
|
+
if (!duplicate)
|
|
1687
|
+
await prepare(input.childId, requestId, input.cwd);
|
|
1688
|
+
const submission = await child.submit({
|
|
1689
|
+
type: "input",
|
|
1690
|
+
content: input.task,
|
|
1691
|
+
requestId,
|
|
1692
|
+
whenBusy: "reject",
|
|
1693
|
+
entry: {
|
|
1694
|
+
kind: NekoInput.kind,
|
|
1695
|
+
data: { at: Date.now(), prompt: input.task, images: [], subagentRoundId: input.taskId }
|
|
1696
|
+
}
|
|
1697
|
+
}, ctx);
|
|
1698
|
+
const placed = await submission.status(ctx);
|
|
1699
|
+
await runtime.commit(async (tx) => {
|
|
1700
|
+
if (placed.type === "input" && placed.entry !== undefined)
|
|
1701
|
+
(await tx.doc(DelegationsDoc, input.parentId)).items.find((r) => r.taskId === input.taskId).inputEntryId = placed.entry;
|
|
1702
|
+
return { status: "running", checkpoint: { phase: "settle" } };
|
|
1703
|
+
}, ctx);
|
|
1704
|
+
} catch (error) {
|
|
1705
|
+
discard(input.childId, requestId);
|
|
1706
|
+
if (ctx.abortSignal?.aborted)
|
|
1707
|
+
throw error;
|
|
1708
|
+
await child.abort(ctx);
|
|
1709
|
+
const report = `Subagent failed: ${String(error)}`;
|
|
1710
|
+
await runtime.commit(async (tx) => {
|
|
1711
|
+
await finish(tx, input, "failed", report);
|
|
1712
|
+
return { status: "terminal", outcome: { status: "completed", result: { status: "failed", report } } };
|
|
1713
|
+
}, ctx);
|
|
1714
|
+
}
|
|
1715
|
+
},
|
|
1716
|
+
settle: async (task, runtime, ctx) => {
|
|
1717
|
+
const input = task.input;
|
|
1718
|
+
const child = await runtime.conversation(input.childId, ctx);
|
|
1719
|
+
const submission = await child.submit({ type: "input", content: input.task, requestId: `subagent:${input.taskId}` }, ctx);
|
|
1720
|
+
const settled = await submission.wait(ctx);
|
|
1721
|
+
await child.waitForIdle(ctx);
|
|
1722
|
+
await runtime.commit(async (tx) => {
|
|
1723
|
+
let report = "";
|
|
1724
|
+
let status = "failed";
|
|
1725
|
+
const round = (await tx.doc(DelegationsDoc, input.parentId)).items.find((r) => r.taskId === input.taskId);
|
|
1726
|
+
if (settled.type === "input" && settled.entry !== undefined)
|
|
1727
|
+
round.inputEntryId = settled.entry;
|
|
1728
|
+
if (settled.status === "done" && settled.type === "input") {
|
|
1729
|
+
round.answerEntryId = settled.answer;
|
|
1730
|
+
const message = (await tx.entry(AssistantEntry, settled.answer))?.model?.[0];
|
|
1731
|
+
report = message?.role === "assistant" ? message.content.flatMap((b) => b.type === "text" ? [b.text] : []).join("") : "";
|
|
1732
|
+
status = "completed";
|
|
1733
|
+
} else if (settled.status === "unanswered") {
|
|
1734
|
+
status = settled.reason === "aborted" ? "cancelled" : "failed";
|
|
1735
|
+
report = `Subagent ${status}: ${settled.reason}`;
|
|
1736
|
+
}
|
|
1737
|
+
await finish(tx, input, status, report);
|
|
1738
|
+
return { status: "terminal", outcome: { status: "completed", result: { status, report } } };
|
|
1739
|
+
}, ctx);
|
|
1740
|
+
}
|
|
1741
|
+
},
|
|
1742
|
+
abort: async (task, runtime, ctx) => {
|
|
1743
|
+
const input = task.input;
|
|
1744
|
+
let own = false;
|
|
1745
|
+
await runtime.commit(async (tx) => {
|
|
1746
|
+
own = (await tx.doc(SubagentsDoc, input.parentId)).items.some((a) => a.childId === input.childId && a.activeRoundId === input.taskId);
|
|
1747
|
+
}, ctx);
|
|
1748
|
+
if (own)
|
|
1749
|
+
await (await runtime.conversation(input.childId, ctx))?.abort(ctx);
|
|
1750
|
+
await runtime.commit(async (tx) => {
|
|
1751
|
+
await finish(tx, input, "cancelled", "Subagent cancelled");
|
|
1752
|
+
return { status: "terminal", outcome: { status: "aborted" } };
|
|
1753
|
+
}, ctx);
|
|
1754
|
+
}
|
|
1755
|
+
});
|
|
1553
1756
|
const extension = defineExtension3({
|
|
1554
1757
|
name: "neko-subagents",
|
|
1758
|
+
tasks: [RoundTask],
|
|
1555
1759
|
tools: [
|
|
1556
1760
|
defineTool2({
|
|
1557
1761
|
name: "subagent",
|
|
1558
|
-
description: "
|
|
1559
|
-
parameters
|
|
1560
|
-
|
|
1561
|
-
title: Type2.String({ minLength: 1, maxLength: 80 }),
|
|
1562
|
-
task: Type2.String({ minLength: 1, maxLength: 32768 })
|
|
1563
|
-
}),
|
|
1762
|
+
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.",
|
|
1763
|
+
parameters,
|
|
1764
|
+
structuredOutputSchema,
|
|
1564
1765
|
replay: "safe",
|
|
1565
|
-
execute: async (
|
|
1566
|
-
if (!
|
|
1567
|
-
throw new Error("
|
|
1766
|
+
execute: async (input, api, context) => {
|
|
1767
|
+
if (!Value.Check(actionParameters, input))
|
|
1768
|
+
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.");
|
|
1769
|
+
const args = input;
|
|
1568
1770
|
if ((await api.snapshot(SubagentDoc, api.conversationId, context))?.role)
|
|
1569
1771
|
throw new Error("Subagents cannot delegate.");
|
|
1772
|
+
if (args.action === "list") {
|
|
1773
|
+
const list = await api.commit(async (tx) => {
|
|
1774
|
+
const rounds = await refreshDelegations(tx, api.conversationId);
|
|
1775
|
+
return summarizeSubagents(rounds, (await tx.doc(SubagentsDoc, api.conversationId)).items).filter((a) => a.historyThrough === undefined);
|
|
1776
|
+
}, context);
|
|
1777
|
+
const offset = args.cursor ?? 0;
|
|
1778
|
+
const items = list.slice(offset, offset + (args.limit ?? 50));
|
|
1779
|
+
const result = { items, next: offset + items.length < list.length ? offset + items.length : null };
|
|
1780
|
+
return { output: [{ type: "text", text: JSON.stringify(result) }], structuredOutput: result };
|
|
1781
|
+
}
|
|
1782
|
+
if (!args.task.trim() || !args.title.trim() || Buffer.byteLength(args.task) > 32768)
|
|
1783
|
+
throw new Error("Task/title must be nonempty; task limit is 32 KiB.");
|
|
1570
1784
|
const agent = await api.agent(context);
|
|
1571
|
-
const
|
|
1572
|
-
const
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
await
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1785
|
+
const round = await api.commit(async (tx) => {
|
|
1786
|
+
const rounds = await refreshDelegations(tx, api.conversationId);
|
|
1787
|
+
const receipt = rounds.find((r) => r.taskId === api.taskId);
|
|
1788
|
+
if (receipt?.runnerId !== undefined)
|
|
1789
|
+
return receipt;
|
|
1790
|
+
const identities = await tx.doc(SubagentsDoc, api.conversationId);
|
|
1791
|
+
let identity;
|
|
1792
|
+
if (args.action === "continue") {
|
|
1793
|
+
identity = identities.items.find((a) => a.agentId === args.agentId);
|
|
1794
|
+
if (!identity)
|
|
1795
|
+
throw new Error("Unknown subagent or it belongs to another conversation.");
|
|
1796
|
+
const info = await tx.doc(SubagentDoc, identity.childId);
|
|
1797
|
+
if (info.parentId !== api.conversationId || info.role !== identity.role)
|
|
1798
|
+
throw new Error("Subagent ownership mismatch.");
|
|
1799
|
+
if (identity.activeRoundId !== null)
|
|
1800
|
+
throw new Error("Subagent is busy; continue only when idle.");
|
|
1801
|
+
} else if (receipt) {
|
|
1802
|
+
identity = identities.items.find((a) => a.childId === receipt.childId);
|
|
1803
|
+
}
|
|
1804
|
+
const existing = identity ? undefined : (await tx.scanConversations({ ownerTaskId: api.taskId }, 1)).items[0];
|
|
1805
|
+
const child = identity ? await tx.conversation(identity.childId) : existing;
|
|
1806
|
+
if (!identity) {
|
|
1807
|
+
const created = child ?? await tx.createConversation({ ownership: { kind: "task", taskId: api.taskId } });
|
|
1808
|
+
const role = args.action === "continue" ? "explore" : args.type;
|
|
1809
|
+
await configure(tx, created.id, {
|
|
1810
|
+
model: agent.model,
|
|
1811
|
+
thinkingLevel: agent.thinkingLevel,
|
|
1812
|
+
cwd: agent.cwd,
|
|
1813
|
+
extensions: agent.extensions.filter((e) => e.name !== extension.name),
|
|
1814
|
+
tools: agent.tools.filter((t) => t.name !== "subagent" && (role === "coder" || READ_TOOLS.has(t.name) || role === "explore" && t.name === "bash"))
|
|
1815
|
+
});
|
|
1816
|
+
Object.assign(await tx.doc(SubagentDoc, created.id), { role, parentId: api.conversationId });
|
|
1817
|
+
identity = {
|
|
1818
|
+
agentId: agentIdFor(created.id),
|
|
1819
|
+
parentId: api.conversationId,
|
|
1820
|
+
childId: created.id,
|
|
1821
|
+
role,
|
|
1822
|
+
cwd: agent.cwd ?? "",
|
|
1823
|
+
title: args.title,
|
|
1824
|
+
createdAt: Date.now(),
|
|
1825
|
+
activeRoundId: null
|
|
1826
|
+
};
|
|
1827
|
+
identities.items.push(identity);
|
|
1828
|
+
}
|
|
1829
|
+
const record = receipt ?? {
|
|
1585
1830
|
parentId: api.conversationId,
|
|
1586
1831
|
taskId: api.taskId,
|
|
1587
1832
|
callId: api.callId,
|
|
1588
|
-
childId:
|
|
1589
|
-
|
|
1833
|
+
childId: identity.childId,
|
|
1834
|
+
agentId: identity.agentId,
|
|
1835
|
+
round: rounds.filter((r) => r.childId === identity.childId).length + 1,
|
|
1836
|
+
role: identity.role,
|
|
1590
1837
|
title: args.title,
|
|
1591
1838
|
createdAt: Date.now(),
|
|
1592
1839
|
startedAt: null,
|
|
1593
1840
|
endedAt: null,
|
|
1594
1841
|
status: "queued",
|
|
1595
1842
|
report: null
|
|
1596
|
-
}
|
|
1597
|
-
|
|
1843
|
+
};
|
|
1844
|
+
identities.items.find((a) => a.agentId === identity.agentId).activeRoundId = api.taskId;
|
|
1845
|
+
const runner = await tx.createTask(RoundTask, {
|
|
1846
|
+
parentId: api.conversationId,
|
|
1847
|
+
childId: identity.childId,
|
|
1848
|
+
taskId: api.taskId,
|
|
1849
|
+
task: args.task,
|
|
1850
|
+
cwd: identity.cwd || agent.cwd || ""
|
|
1851
|
+
}, { ownership: { kind: "task", taskId: api.taskId } });
|
|
1852
|
+
record.runnerId = runner;
|
|
1853
|
+
const doc = await tx.doc(DelegationsDoc, api.conversationId);
|
|
1854
|
+
if (receipt)
|
|
1855
|
+
Object.assign(doc.items.find((r) => r.taskId === receipt.taskId), record);
|
|
1856
|
+
else
|
|
1857
|
+
doc.items.push(record);
|
|
1858
|
+
return record;
|
|
1598
1859
|
}, context);
|
|
1599
|
-
await api.details({
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
item.startedAt ??= Date.now();
|
|
1606
|
-
}, context);
|
|
1607
|
-
const child = await api.conversation(childId, context);
|
|
1608
|
-
const requestId = `subagent:${api.taskId}`;
|
|
1609
|
-
const duplicate = await api.commit((tx) => tx.submissionByRequest(childId, requestId), context);
|
|
1610
|
-
if (!duplicate)
|
|
1611
|
-
await prepare(childId, requestId, agent.cwd ?? "");
|
|
1612
|
-
let submission;
|
|
1613
|
-
try {
|
|
1614
|
-
submission = await child.submit({
|
|
1615
|
-
type: "input",
|
|
1616
|
-
content: args.task,
|
|
1617
|
-
requestId,
|
|
1618
|
-
entry: { kind: NekoInput.kind, data: { at: Date.now(), prompt: args.task, images: [] } }
|
|
1619
|
-
}, context);
|
|
1620
|
-
} catch (error) {
|
|
1621
|
-
discard(childId, requestId);
|
|
1622
|
-
throw error;
|
|
1623
|
-
}
|
|
1624
|
-
const settled = await submission.wait(context);
|
|
1625
|
-
let report = "";
|
|
1626
|
-
let status = "failed";
|
|
1627
|
-
if (settled.status === "done" && settled.type === "input") {
|
|
1628
|
-
const entry = await api.commit((tx) => tx.entry(AssistantEntry, settled.answer), context);
|
|
1629
|
-
const message = entry?.model?.[0];
|
|
1630
|
-
report = message?.role === "assistant" ? message.content.flatMap((b) => b.type === "text" ? [b.text] : []).join("") : "";
|
|
1631
|
-
status = "completed";
|
|
1632
|
-
} else if (settled.status === "unanswered") {
|
|
1633
|
-
status = settled.reason === "aborted" ? "cancelled" : "failed";
|
|
1634
|
-
report = `Subagent ${status}: ${settled.reason}`;
|
|
1635
|
-
}
|
|
1636
|
-
await api.commit(async (tx) => {
|
|
1637
|
-
const item = (await tx.doc(DelegationsDoc, api.conversationId)).items.find((d) => d.taskId === api.taskId);
|
|
1638
|
-
item.status = status;
|
|
1639
|
-
item.report = report;
|
|
1640
|
-
item.endedAt = Date.now();
|
|
1860
|
+
await api.details({
|
|
1861
|
+
conversationId: round.childId,
|
|
1862
|
+
agentId: round.agentId,
|
|
1863
|
+
roundId: round.taskId,
|
|
1864
|
+
role: round.role,
|
|
1865
|
+
title: round.title
|
|
1641
1866
|
}, context);
|
|
1867
|
+
const settled = await api.waitForTask(round.runnerId, context);
|
|
1868
|
+
const outcome = settled.state.outcome;
|
|
1869
|
+
const result = outcome.status === "completed" ? outcome.result : { status: outcome.status === "aborted" ? "cancelled" : "failed", report: `Subagent ${outcome.status}` };
|
|
1642
1870
|
return {
|
|
1643
|
-
output: [
|
|
1644
|
-
|
|
1645
|
-
|
|
1871
|
+
output: [
|
|
1872
|
+
{
|
|
1873
|
+
type: "text",
|
|
1874
|
+
text: `Agent: ${round.agentId}
|
|
1875
|
+
Status: ${result.status}
|
|
1876
|
+
${result.report || "Subagent completed without a text report."}`
|
|
1877
|
+
}
|
|
1878
|
+
],
|
|
1879
|
+
isError: result.status !== "completed",
|
|
1880
|
+
structuredOutput: { agentId: round.agentId, roundId: round.taskId, ...result },
|
|
1881
|
+
details: {
|
|
1882
|
+
conversationId: round.childId,
|
|
1883
|
+
agentId: round.agentId,
|
|
1884
|
+
roundId: round.taskId,
|
|
1885
|
+
role: round.role,
|
|
1886
|
+
title: round.title,
|
|
1887
|
+
status: result.status
|
|
1888
|
+
}
|
|
1646
1889
|
};
|
|
1647
1890
|
}
|
|
1648
1891
|
})
|
|
@@ -1651,11 +1894,67 @@ function createSubagentExtension(prepare, discard) {
|
|
|
1651
1894
|
return extension;
|
|
1652
1895
|
}
|
|
1653
1896
|
async function listDelegations(harness, parent, context) {
|
|
1897
|
+
const rounds = await harness.commit((tx) => refreshDelegations(tx, parent), context);
|
|
1898
|
+
for (const round of rounds) {
|
|
1899
|
+
if (round.historyThrough !== undefined)
|
|
1900
|
+
continue;
|
|
1901
|
+
const task = await harness.getTask(round.taskId, context);
|
|
1902
|
+
if (task?.state.status !== "terminal")
|
|
1903
|
+
continue;
|
|
1904
|
+
const child = await harness.conversation(round.childId, context);
|
|
1905
|
+
const live = await harness.snapshot(LiveDoc, round.childId, context);
|
|
1906
|
+
const identity = (await harness.snapshot(SubagentsDoc, parent, context))?.items.find((a) => a.childId === round.childId);
|
|
1907
|
+
if (identity?.activeRoundId === round.taskId && live?.run !== undefined)
|
|
1908
|
+
await child?.abort(context);
|
|
1909
|
+
}
|
|
1654
1910
|
return harness.commit((tx) => refreshDelegations(tx, parent), context);
|
|
1655
1911
|
}
|
|
1656
|
-
|
|
1912
|
+
async function installSubagentCoordinator(harness, parents, context) {
|
|
1913
|
+
const known = new Set(parents);
|
|
1914
|
+
let running = false;
|
|
1915
|
+
let dirty = false;
|
|
1916
|
+
const reconcile = async () => {
|
|
1917
|
+
if (running) {
|
|
1918
|
+
dirty = true;
|
|
1919
|
+
return;
|
|
1920
|
+
}
|
|
1921
|
+
running = true;
|
|
1922
|
+
try {
|
|
1923
|
+
do {
|
|
1924
|
+
dirty = false;
|
|
1925
|
+
for (const id of known)
|
|
1926
|
+
await listDelegations(harness, id, context);
|
|
1927
|
+
} while (dirty);
|
|
1928
|
+
} finally {
|
|
1929
|
+
running = false;
|
|
1930
|
+
}
|
|
1931
|
+
};
|
|
1932
|
+
for (const parent of parents) {
|
|
1933
|
+
await harness.commit((tx) => refreshDelegations(tx, parent), context);
|
|
1934
|
+
for (const identity of (await harness.snapshot(SubagentsDoc, parent, context))?.items ?? []) {
|
|
1935
|
+
if (identity.cwd)
|
|
1936
|
+
continue;
|
|
1937
|
+
const child = await harness.conversation(identity.childId, context);
|
|
1938
|
+
const cwd = (await child?.agent(context))?.cwd;
|
|
1939
|
+
if (cwd)
|
|
1940
|
+
await harness.commit(async (tx) => {
|
|
1941
|
+
(await tx.doc(SubagentsDoc, parent)).items.find((a) => a.agentId === identity.agentId).cwd = cwd;
|
|
1942
|
+
}, context);
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
const stop = harness.subscribeCommits(({ changes }) => {
|
|
1946
|
+
for (const c of changes)
|
|
1947
|
+
if (c.type === "document" && c.record.kind === "app.neko.delegations" && c.conversationId !== undefined)
|
|
1948
|
+
known.add(c.conversationId);
|
|
1949
|
+
if (changes.some((c) => c.type === "task" && c.value.state.status === "terminal"))
|
|
1950
|
+
reconcile().catch(() => {});
|
|
1951
|
+
});
|
|
1952
|
+
harness.closed.finally(stop).catch(() => {});
|
|
1953
|
+
}
|
|
1954
|
+
var agentIdFor = (childId) => `agent-${childId}`, DelegationsDoc, SubagentsDoc, SubagentDoc, SlotsDoc, ROLE_PROMPTS, 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.", READ_TOOLS, actionParameters, parameters, structuredOutputSchema;
|
|
1657
1955
|
var init_subagents = __esm(() => {
|
|
1658
1956
|
init_input();
|
|
1957
|
+
init_subagent_usage();
|
|
1659
1958
|
DelegationsDoc = defineDoc({
|
|
1660
1959
|
kind: "app.neko.delegations",
|
|
1661
1960
|
version: 1,
|
|
@@ -1664,6 +1963,14 @@ var init_subagents = __esm(() => {
|
|
|
1664
1963
|
fork: "initial",
|
|
1665
1964
|
initial: () => ({ items: [] })
|
|
1666
1965
|
});
|
|
1966
|
+
SubagentsDoc = defineDoc({
|
|
1967
|
+
kind: "app.neko.subagents",
|
|
1968
|
+
version: 1,
|
|
1969
|
+
scope: "conversation",
|
|
1970
|
+
history: "latest",
|
|
1971
|
+
fork: "initial",
|
|
1972
|
+
initial: () => ({ items: [] })
|
|
1973
|
+
});
|
|
1667
1974
|
SubagentDoc = defineDoc({
|
|
1668
1975
|
kind: "app.neko.subagent",
|
|
1669
1976
|
version: 1,
|
|
@@ -1684,6 +1991,60 @@ var init_subagents = __esm(() => {
|
|
|
1684
1991
|
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."
|
|
1685
1992
|
};
|
|
1686
1993
|
READ_TOOLS = new Set(["read", "find_files", "search_text", "web_search", "web_extract"]);
|
|
1994
|
+
actionParameters = Type2.Union([
|
|
1995
|
+
Type2.Object({
|
|
1996
|
+
action: Type2.Optional(Type2.Literal("create")),
|
|
1997
|
+
type: Type2.Enum(["explore", "plan", "coder"]),
|
|
1998
|
+
title: Type2.String({ minLength: 1, maxLength: 80 }),
|
|
1999
|
+
task: Type2.String({ minLength: 1, maxLength: 32768 })
|
|
2000
|
+
}, { additionalProperties: false }),
|
|
2001
|
+
Type2.Object({
|
|
2002
|
+
action: Type2.Literal("continue"),
|
|
2003
|
+
agentId: Type2.String(),
|
|
2004
|
+
title: Type2.String({ minLength: 1, maxLength: 80 }),
|
|
2005
|
+
task: Type2.String({ minLength: 1, maxLength: 32768 })
|
|
2006
|
+
}, { additionalProperties: false }),
|
|
2007
|
+
Type2.Object({
|
|
2008
|
+
action: Type2.Literal("list"),
|
|
2009
|
+
cursor: Type2.Optional(Type2.Integer({ minimum: 0 })),
|
|
2010
|
+
limit: Type2.Optional(Type2.Integer({ minimum: 1, maximum: 100 }))
|
|
2011
|
+
}, { additionalProperties: false })
|
|
2012
|
+
]);
|
|
2013
|
+
parameters = Type2.Object({
|
|
2014
|
+
action: Type2.Optional(Type2.String({ enum: ["create", "continue", "list"], description: "Defaults to create." })),
|
|
2015
|
+
type: Type2.Optional(Type2.String({ enum: ["explore", "plan", "coder"], description: "Required for create only." })),
|
|
2016
|
+
agentId: Type2.Optional(Type2.String({ description: "Required for continue only; use an ID returned by create/list." })),
|
|
2017
|
+
title: Type2.Optional(Type2.String({ minLength: 1, maxLength: 80, description: "Required for create/continue." })),
|
|
2018
|
+
task: Type2.Optional(Type2.String({ minLength: 1, maxLength: 32768, description: "Required for create/continue." })),
|
|
2019
|
+
cursor: Type2.Optional(Type2.Integer({ minimum: 0, description: "List pagination cursor only." })),
|
|
2020
|
+
limit: Type2.Optional(Type2.Integer({ minimum: 1, maximum: 100, description: "List page size only." }))
|
|
2021
|
+
}, { additionalProperties: false });
|
|
2022
|
+
structuredOutputSchema = Type2.Union([
|
|
2023
|
+
Type2.Object({
|
|
2024
|
+
agentId: Type2.String(),
|
|
2025
|
+
roundId: Type2.Integer(),
|
|
2026
|
+
status: Type2.Enum(["completed", "failed", "cancelled"]),
|
|
2027
|
+
report: Type2.String()
|
|
2028
|
+
}, { additionalProperties: false }),
|
|
2029
|
+
Type2.Object({
|
|
2030
|
+
items: Type2.Array(Type2.Object({
|
|
2031
|
+
agentId: Type2.String(),
|
|
2032
|
+
parentId: Type2.Integer(),
|
|
2033
|
+
childId: Type2.Integer(),
|
|
2034
|
+
role: Type2.Enum(["explore", "plan", "coder"]),
|
|
2035
|
+
title: Type2.String(),
|
|
2036
|
+
status: Type2.Enum(["idle", "queued", "running"]),
|
|
2037
|
+
activeRoundId: Type2.Union([Type2.Integer(), Type2.Null()]),
|
|
2038
|
+
lastRoundId: Type2.Integer(),
|
|
2039
|
+
lastReport: Type2.Optional(Type2.Union([Type2.String(), Type2.Null()])),
|
|
2040
|
+
roundCount: Type2.Integer(),
|
|
2041
|
+
taskTitle: Type2.String(),
|
|
2042
|
+
approvalCount: Type2.Integer(),
|
|
2043
|
+
createdAt: Type2.Number()
|
|
2044
|
+
}, { additionalProperties: false })),
|
|
2045
|
+
next: Type2.Union([Type2.Integer(), Type2.Null()])
|
|
2046
|
+
}, { additionalProperties: false })
|
|
2047
|
+
]);
|
|
1687
2048
|
});
|
|
1688
2049
|
|
|
1689
2050
|
// packages/core/src/prompt/prompt.ts
|
|
@@ -1744,7 +2105,7 @@ var init_prompt = __esm(() => {
|
|
|
1744
2105
|
KEYS = ["preamble", "rules", "project_context", "skills", "cwd"];
|
|
1745
2106
|
TOOL_GUIDELINES = {
|
|
1746
2107
|
subagent: [
|
|
1747
|
-
"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."
|
|
2108
|
+
"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."
|
|
1748
2109
|
],
|
|
1749
2110
|
find_files: [
|
|
1750
2111
|
"Use find_files to discover files; query is a literal substring. Narrow the directory when results are partial."
|
|
@@ -1899,9 +2260,9 @@ import { getSupportedThinkingLevels } from "@earendil-works/pi-ai";
|
|
|
1899
2260
|
import {
|
|
1900
2261
|
defineDoc as defineDoc2,
|
|
1901
2262
|
defineExtension as defineExtension5,
|
|
1902
|
-
defineTask,
|
|
2263
|
+
defineTask as defineTask2,
|
|
1903
2264
|
InboxDoc,
|
|
1904
|
-
LiveDoc
|
|
2265
|
+
LiveDoc as LiveDoc2
|
|
1905
2266
|
} from "@earendil-works/pi-durable";
|
|
1906
2267
|
function shortText(value, limit = 60) {
|
|
1907
2268
|
return [
|
|
@@ -1943,7 +2304,7 @@ function mergeUsage(base, extra) {
|
|
|
1943
2304
|
function namingTask() {
|
|
1944
2305
|
let active = 0;
|
|
1945
2306
|
const waits = new Set;
|
|
1946
|
-
return
|
|
2307
|
+
return defineTask2({
|
|
1947
2308
|
name: "app.neko.name",
|
|
1948
2309
|
version: 1,
|
|
1949
2310
|
initial: () => ({ phase: "wait", until: Date.now() + 100 }),
|
|
@@ -2157,7 +2518,7 @@ class SessionMetadataManager {
|
|
|
2157
2518
|
const answer = record.status === "done" ? (await this.storage.entry(record.answer, this.context))?.entry.model?.[0] : undefined;
|
|
2158
2519
|
return { record, data, answer: answer?.role === "assistant" ? answer : undefined };
|
|
2159
2520
|
}));
|
|
2160
|
-
const live = await this.harness.snapshot(
|
|
2521
|
+
const live = await this.harness.snapshot(LiveDoc2, id, this.context);
|
|
2161
2522
|
await this.harness.commit(async (tx) => {
|
|
2162
2523
|
const d = await tx.doc(SessionMetadataDoc, id);
|
|
2163
2524
|
for (const { record, data, answer } of updates) {
|
|
@@ -2359,7 +2720,8 @@ function createApprovalExtension(service) {
|
|
|
2359
2720
|
const child = await approvals.harness.conversation(api.conversationId, context);
|
|
2360
2721
|
const agent = await child.agent(context);
|
|
2361
2722
|
const cwd = await realpath2(agent.cwd);
|
|
2362
|
-
const
|
|
2723
|
+
const identity = (await api.snapshot(SubagentsDoc, parent, context))?.items.find((a) => a.childId === api.conversationId);
|
|
2724
|
+
const delegation = (await api.snapshot(DelegationsDoc, parent, context))?.items.find((d) => d.childId === api.conversationId && d.historyThrough === undefined && d.taskId === identity?.activeRoundId);
|
|
2363
2725
|
if (!delegation)
|
|
2364
2726
|
return { block: "Missing delegation" };
|
|
2365
2727
|
await approvals.ensure(parent, {
|
|
@@ -2990,7 +3352,7 @@ import {
|
|
|
2990
3352
|
Harness,
|
|
2991
3353
|
LiveDoc as importLiveDoc,
|
|
2992
3354
|
MemoryStorage,
|
|
2993
|
-
UsageDoc,
|
|
3355
|
+
UsageDoc as UsageDoc2,
|
|
2994
3356
|
watchEvents
|
|
2995
3357
|
} from "@earendil-works/pi-durable";
|
|
2996
3358
|
import { createNodePhotonImages } from "@earendil-works/pi-durable/images/node";
|
|
@@ -3073,6 +3435,7 @@ async function createNekoRuntime(options = {}) {
|
|
|
3073
3435
|
approvals = new ToolApprovals(harness, context);
|
|
3074
3436
|
const existing = await scanConversations();
|
|
3075
3437
|
try {
|
|
3438
|
+
await installSubagentCoordinator(harness, existing.map((r) => r.id), context);
|
|
3076
3439
|
await prepareApprovalConversations(harness, existing.map((r) => r.id), approvalExtension, context);
|
|
3077
3440
|
for (const record of existing) {
|
|
3078
3441
|
const target = await harness.conversation(record.id, context);
|
|
@@ -3257,7 +3620,7 @@ async function createNekoRuntime(options = {}) {
|
|
|
3257
3620
|
return watchEvents(harness, target.id, context);
|
|
3258
3621
|
},
|
|
3259
3622
|
async usage() {
|
|
3260
|
-
return mergeUsage(await harness.snapshot(
|
|
3623
|
+
return mergeUsage(await harness.snapshot(UsageDoc2, target.id, context) ?? { models: {}, tools: {} }, (await harness.snapshot(SessionMetadataDoc, target.id, context))?.namingUsage ?? { models: {}, tools: {} });
|
|
3261
3624
|
},
|
|
3262
3625
|
compactionRecord() {
|
|
3263
3626
|
return readCompaction(harness, storage, target.id, context);
|
|
@@ -3306,7 +3669,7 @@ async function createNekoRuntime(options = {}) {
|
|
|
3306
3669
|
async globalUsage() {
|
|
3307
3670
|
let total = { models: {}, tools: {} };
|
|
3308
3671
|
for (const record of await scanConversations()) {
|
|
3309
|
-
total = mergeUsage(total, await harness.snapshot(
|
|
3672
|
+
total = mergeUsage(total, await harness.snapshot(UsageDoc2, record.id, context) ?? { models: {}, tools: {} });
|
|
3310
3673
|
total = mergeUsage(total, (await harness.snapshot(SessionMetadataDoc, record.id, context))?.namingUsage ?? { models: {}, tools: {} });
|
|
3311
3674
|
}
|
|
3312
3675
|
return total;
|
|
@@ -3341,14 +3704,24 @@ async function createNekoRuntime(options = {}) {
|
|
|
3341
3704
|
}
|
|
3342
3705
|
cursor = page.next;
|
|
3343
3706
|
} while (cursor);
|
|
3707
|
+
const frozen = new Map;
|
|
3344
3708
|
for (const reference of references) {
|
|
3345
3709
|
if (!calls.has(reference.callId))
|
|
3346
3710
|
continue;
|
|
3347
|
-
|
|
3348
|
-
|
|
3711
|
+
let snapshot = frozen.get(reference.childId);
|
|
3712
|
+
if (!snapshot) {
|
|
3713
|
+
const latest = await tx.scanEntries({ conversationId: reference.childId }, 1);
|
|
3714
|
+
snapshot = {
|
|
3715
|
+
through: latest.items[0]?.id ?? 0,
|
|
3716
|
+
state: JSON.parse(JSON.stringify(await tx.doc(UsageDoc2, reference.childId)))
|
|
3717
|
+
};
|
|
3718
|
+
frozen.set(reference.childId, snapshot);
|
|
3719
|
+
}
|
|
3720
|
+
const usage = totalUsage(snapshot.state);
|
|
3349
3721
|
inherited.push({
|
|
3350
3722
|
...reference,
|
|
3351
|
-
historyThrough: reference.historyThrough ??
|
|
3723
|
+
historyThrough: reference.historyThrough ?? snapshot.through,
|
|
3724
|
+
roundUsage: reference.roundUsage ?? (reference.usageStart ? subagentUsageTotals(subtractSubagentUsage(snapshot.state, reference.usageStart)) : null),
|
|
3352
3725
|
historyUsage: reference.historyUsage ?? {
|
|
3353
3726
|
input: usage.input,
|
|
3354
3727
|
output: usage.output,
|
|
@@ -3561,6 +3934,7 @@ var init_runtime = __esm(() => {
|
|
|
3561
3934
|
init_prompt();
|
|
3562
3935
|
init_resources();
|
|
3563
3936
|
init_session_metadata();
|
|
3937
|
+
init_subagent_usage();
|
|
3564
3938
|
init_subagents();
|
|
3565
3939
|
init_tool_approvals();
|
|
3566
3940
|
init_web_search();
|
|
@@ -3615,6 +3989,7 @@ var init_src = __esm(() => {
|
|
|
3615
3989
|
init_frontmatter();
|
|
3616
3990
|
init_runtime();
|
|
3617
3991
|
init_session_metadata();
|
|
3992
|
+
init_subagent_usage();
|
|
3618
3993
|
init_subagents();
|
|
3619
3994
|
init_tool_approvals();
|
|
3620
3995
|
init_workspace();
|
|
@@ -3687,7 +4062,7 @@ function contract(name, def) {
|
|
|
3687
4062
|
]));
|
|
3688
4063
|
return remote(name, { ...def, variants });
|
|
3689
4064
|
}
|
|
3690
|
-
var rpc = (args, result) => [args, result], str, num, bool, nil, integer, nullableString, phase, level, thinking, model, change, usage, image, skill, call, DelegationSchema, ChatViewSchema, SessionSummarySchema, DirectoryStateSchema, WorkspaceSummarySchema, WorkspaceStateSchema, credentialStatus, CredentialStateSchema, AttachmentSchema, chatInput, NekoSessionDirectory, NekoSessionManagement, resize, inputLimits, modelInfo, NekoModels, imageChunk, NekoSubagent, NekoChat, browse, page, NekoWorkspaces, provider, NekoCredentials, FileCandidatesSchema, NekoWorkspaceFiles, marketSkill, preview, scope, installResult, NekoSkillMarket, ApplicationInfoSchema, NekoApplicationInfo;
|
|
4065
|
+
var rpc = (args, result) => [args, result], str, num, bool, nil, integer, nullableString, phase, level, thinking, model, change, usage, image, skill, call, DelegationSchema, SubagentSummarySchema, ChatViewSchema, SessionSummarySchema, DirectoryStateSchema, WorkspaceSummarySchema, WorkspaceStateSchema, credentialStatus, CredentialStateSchema, AttachmentSchema, chatInput, NekoSessionDirectory, NekoSessionManagement, resize, inputLimits, modelInfo, NekoModels, imageChunk, NekoSubagent, NekoChat, browse, page, NekoWorkspaces, provider, NekoCredentials, FileCandidatesSchema, NekoWorkspaceFiles, marketSkill, preview, scope, installResult, NekoSkillMarket, ApplicationInfoSchema, NekoApplicationInfo;
|
|
3691
4066
|
var init_contract = __esm(() => {
|
|
3692
4067
|
str = Type4.String();
|
|
3693
4068
|
num = Type4.Number();
|
|
@@ -3721,6 +4096,12 @@ var init_contract = __esm(() => {
|
|
|
3721
4096
|
});
|
|
3722
4097
|
DelegationSchema = Type4.Object({
|
|
3723
4098
|
parentId: integer,
|
|
4099
|
+
agentId: Type4.Optional(str),
|
|
4100
|
+
round: Type4.Optional(integer),
|
|
4101
|
+
runnerId: Type4.Optional(integer),
|
|
4102
|
+
inputEntryId: Type4.Optional(integer),
|
|
4103
|
+
answerEntryId: Type4.Optional(integer),
|
|
4104
|
+
roundUsage: Type4.Optional(Type4.Union([usage, nil])),
|
|
3724
4105
|
taskId: integer,
|
|
3725
4106
|
callId: str,
|
|
3726
4107
|
childId: integer,
|
|
@@ -3735,8 +4116,27 @@ var init_contract = __esm(() => {
|
|
|
3735
4116
|
historyThrough: Type4.Optional(integer),
|
|
3736
4117
|
historyUsage: Type4.Optional(usage)
|
|
3737
4118
|
});
|
|
4119
|
+
SubagentSummarySchema = Type4.Object({
|
|
4120
|
+
agentId: str,
|
|
4121
|
+
parentId: integer,
|
|
4122
|
+
childId: integer,
|
|
4123
|
+
role: Type4.Enum(["explore", "plan", "coder"]),
|
|
4124
|
+
title: str,
|
|
4125
|
+
status: Type4.Enum(["idle", "queued", "running"]),
|
|
4126
|
+
activeRoundId: Type4.Union([integer, nil]),
|
|
4127
|
+
lastRoundId: integer,
|
|
4128
|
+
lastReport: Type4.Optional(nullableString),
|
|
4129
|
+
roundCount: integer,
|
|
4130
|
+
taskTitle: str,
|
|
4131
|
+
approvalCount: integer,
|
|
4132
|
+
createdAt: num,
|
|
4133
|
+
historyThrough: Type4.Optional(integer),
|
|
4134
|
+
historyUsage: Type4.Optional(usage)
|
|
4135
|
+
});
|
|
3738
4136
|
ChatViewSchema = Type4.Object({
|
|
3739
4137
|
historyBefore: Type4.Optional(Type4.Union([integer, nil])),
|
|
4138
|
+
agents: Type4.Optional(Type4.Array(SubagentSummarySchema)),
|
|
4139
|
+
rounds: Type4.Optional(Type4.Array(DelegationSchema)),
|
|
3740
4140
|
subagents: Type4.Optional(Type4.Array(DelegationSchema)),
|
|
3741
4141
|
taskUsage: Type4.Optional(usage),
|
|
3742
4142
|
model: str,
|
|
@@ -3892,7 +4292,12 @@ var init_contract = __esm(() => {
|
|
|
3892
4292
|
imageChunk = Type4.Object({ data: str, total: integer, mimeType: str });
|
|
3893
4293
|
NekoSubagent = contract("neko.subagent", {
|
|
3894
4294
|
version: 1,
|
|
3895
|
-
input: Type4.Object({
|
|
4295
|
+
input: Type4.Object({
|
|
4296
|
+
sessionId: str,
|
|
4297
|
+
attachmentId: str,
|
|
4298
|
+
taskId: Type4.Optional(integer),
|
|
4299
|
+
agentId: Type4.Optional(str)
|
|
4300
|
+
}),
|
|
3896
4301
|
variants: {
|
|
3897
4302
|
ready: {
|
|
3898
4303
|
state: { revision: integer },
|
|
@@ -3905,7 +4310,7 @@ var init_contract = __esm(() => {
|
|
|
3905
4310
|
}), Type4.Object({ token: integer, data: str, next: Type4.Union([integer, nil]) })),
|
|
3906
4311
|
getImage: rpc(Type4.Object({ entryId: integer, contentIndex: integer, offset: integer }), imageChunk),
|
|
3907
4312
|
decideApproval: rpc(Type4.Object({ id: integer, version: str, decision: Type4.Enum(["allowed", "denied"]) }), Type4.Enum(["pending", "allowed", "denied", "cancelled"])),
|
|
3908
|
-
stop: rpc(nil, nil)
|
|
4313
|
+
stop: rpc(Type4.Union([nil, Type4.Object({ taskId: integer })]), nil)
|
|
3909
4314
|
}
|
|
3910
4315
|
}
|
|
3911
4316
|
}
|
|
@@ -4312,17 +4717,18 @@ class ApprovalQueue {
|
|
|
4312
4717
|
update(entries) {
|
|
4313
4718
|
const pending = entries.filter((d) => d.approvalCount && d.historyThrough === undefined);
|
|
4314
4719
|
for (const [id, reader] of this.#readers) {
|
|
4315
|
-
if (!pending.some((d) => d.taskId === id)) {
|
|
4720
|
+
if (!pending.some((d) => (d.agentId ?? d.taskId) === id)) {
|
|
4316
4721
|
this.#readers.delete(id);
|
|
4317
4722
|
reader.stop();
|
|
4318
4723
|
}
|
|
4319
4724
|
}
|
|
4320
4725
|
for (const entry of pending) {
|
|
4321
|
-
|
|
4726
|
+
const key = entry.agentId ?? entry.taskId;
|
|
4727
|
+
if (this.#readers.has(key))
|
|
4322
4728
|
continue;
|
|
4323
|
-
const service = this.#connect(
|
|
4729
|
+
const service = this.#connect(key);
|
|
4324
4730
|
const reader = { service, title: entry.title, stop: () => {} };
|
|
4325
|
-
this.#readers.set(
|
|
4731
|
+
this.#readers.set(key, reader);
|
|
4326
4732
|
reader.stop = service.view.subscribe(() => this.#changed());
|
|
4327
4733
|
}
|
|
4328
4734
|
this.#changed();
|
|
@@ -4392,10 +4798,10 @@ function createGatewayConnection(link, serverId) {
|
|
|
4392
4798
|
let closing;
|
|
4393
4799
|
const connection = {
|
|
4394
4800
|
application,
|
|
4395
|
-
subagent(
|
|
4801
|
+
subagent(target) {
|
|
4396
4802
|
if (!attachment)
|
|
4397
4803
|
throw new Error("No active session");
|
|
4398
|
-
const input = { ...attachment, taskId };
|
|
4804
|
+
const input = { ...attachment, ...typeof target === "string" ? { agentId: target } : { taskId: target } };
|
|
4399
4805
|
return subagentClient(() => host.get(NekoSubagent, input, { at: serverId }));
|
|
4400
4806
|
},
|
|
4401
4807
|
host,
|
|
@@ -15068,7 +15474,14 @@ var init_history_image = __esm(() => {
|
|
|
15068
15474
|
});
|
|
15069
15475
|
|
|
15070
15476
|
// packages/tui/src/components/login.ts
|
|
15071
|
-
import {
|
|
15477
|
+
import {
|
|
15478
|
+
CURSOR_MARKER as CURSOR_MARKER2,
|
|
15479
|
+
Input,
|
|
15480
|
+
isKeyRelease as isKeyRelease2,
|
|
15481
|
+
matchesKey as matchesKey4,
|
|
15482
|
+
renderFakeCursor,
|
|
15483
|
+
truncateToWidth as truncateToWidth3
|
|
15484
|
+
} from "@earendil-works/pi-tui";
|
|
15072
15485
|
|
|
15073
15486
|
class LoginPanel {
|
|
15074
15487
|
height = 10;
|
|
@@ -15079,22 +15492,28 @@ class LoginPanel {
|
|
|
15079
15492
|
#provider = "deepseek";
|
|
15080
15493
|
#message = "";
|
|
15081
15494
|
#busy = false;
|
|
15495
|
+
#confirmingRemove = false;
|
|
15082
15496
|
#generation = 0;
|
|
15497
|
+
#closeTimer;
|
|
15083
15498
|
#unsubscribe;
|
|
15084
15499
|
#changed;
|
|
15085
15500
|
#closed;
|
|
15086
15501
|
#save;
|
|
15087
15502
|
#remove;
|
|
15503
|
+
#autoCloseMs;
|
|
15088
15504
|
constructor(options) {
|
|
15089
15505
|
this.#changed = options.changed;
|
|
15090
15506
|
this.#closed = options.closed;
|
|
15091
15507
|
this.#save = options.save;
|
|
15092
15508
|
this.#remove = options.remove;
|
|
15509
|
+
this.#autoCloseMs = options.autoCloseMs ?? AUTO_CLOSE_MS;
|
|
15093
15510
|
}
|
|
15094
15511
|
show(state, subscribe) {
|
|
15095
15512
|
this.#generation++;
|
|
15513
|
+
this.#clearCloseTimer();
|
|
15096
15514
|
this.open = true;
|
|
15097
15515
|
this.#busy = false;
|
|
15516
|
+
this.#confirmingRemove = false;
|
|
15098
15517
|
this.#message = "";
|
|
15099
15518
|
this.#input = new Input;
|
|
15100
15519
|
this.#provider = "deepseek";
|
|
@@ -15110,7 +15529,9 @@ class LoginPanel {
|
|
|
15110
15529
|
}
|
|
15111
15530
|
close() {
|
|
15112
15531
|
this.#generation++;
|
|
15532
|
+
this.#clearCloseTimer();
|
|
15113
15533
|
this.open = false;
|
|
15534
|
+
this.#confirmingRemove = false;
|
|
15114
15535
|
this.#input = new Input;
|
|
15115
15536
|
this.#unsubscribe?.();
|
|
15116
15537
|
this.#unsubscribe = undefined;
|
|
@@ -15127,11 +15548,20 @@ class LoginPanel {
|
|
|
15127
15548
|
}
|
|
15128
15549
|
if (this.#busy)
|
|
15129
15550
|
return;
|
|
15551
|
+
const confirming = this.#confirmingRemove;
|
|
15552
|
+
this.#confirmingRemove = false;
|
|
15553
|
+
if (confirming && !matchesKey4(data, "ctrl+r"))
|
|
15554
|
+
this.#message = "";
|
|
15130
15555
|
if (matchesKey4(data, "enter"))
|
|
15131
15556
|
this.#apply(false);
|
|
15132
|
-
else if (matchesKey4(data, "ctrl+r") && this.#status?.savedConfigured)
|
|
15133
|
-
|
|
15134
|
-
|
|
15557
|
+
else if (matchesKey4(data, "ctrl+r") && this.#status?.savedConfigured) {
|
|
15558
|
+
if (confirming)
|
|
15559
|
+
this.#apply(true);
|
|
15560
|
+
else {
|
|
15561
|
+
this.#confirmingRemove = true;
|
|
15562
|
+
this.#message = "Press Ctrl+R again to remove the saved key.";
|
|
15563
|
+
}
|
|
15564
|
+
} else if (matchesKey4(data, "tab") || (matchesKey4(data, "left") || matchesKey4(data, "right")) && this.#input.getValue() === "")
|
|
15135
15565
|
this.#switch();
|
|
15136
15566
|
else {
|
|
15137
15567
|
this.#input.handleInput(data);
|
|
@@ -15146,20 +15576,39 @@ class LoginPanel {
|
|
|
15146
15576
|
#switch() {
|
|
15147
15577
|
const index = CREDENTIAL_PROVIDERS.findIndex((spec) => spec.id === this.#provider);
|
|
15148
15578
|
this.#provider = CREDENTIAL_PROVIDERS[(index + 1) % CREDENTIAL_PROVIDERS.length].id;
|
|
15149
|
-
this.#input = new Input;
|
|
15150
15579
|
this.#message = "";
|
|
15151
15580
|
}
|
|
15581
|
+
#clearCloseTimer() {
|
|
15582
|
+
if (this.#closeTimer !== undefined)
|
|
15583
|
+
clearTimeout(this.#closeTimer);
|
|
15584
|
+
this.#closeTimer = undefined;
|
|
15585
|
+
}
|
|
15586
|
+
#scheduleAutoClose(generation) {
|
|
15587
|
+
this.#clearCloseTimer();
|
|
15588
|
+
this.#closeTimer = setTimeout(() => {
|
|
15589
|
+
this.#closeTimer = undefined;
|
|
15590
|
+
if (generation === this.#generation && this.open)
|
|
15591
|
+
this.close();
|
|
15592
|
+
}, this.#autoCloseMs);
|
|
15593
|
+
}
|
|
15152
15594
|
async#apply(remove) {
|
|
15153
|
-
const key = this.#input.getValue();
|
|
15154
|
-
if (!remove
|
|
15155
|
-
|
|
15156
|
-
|
|
15157
|
-
|
|
15595
|
+
const key = this.#input.getValue().trim();
|
|
15596
|
+
if (!remove) {
|
|
15597
|
+
if (!key) {
|
|
15598
|
+
this.#message = "Enter an API key.";
|
|
15599
|
+
this.#changed();
|
|
15600
|
+
return;
|
|
15601
|
+
}
|
|
15602
|
+
if (Array.from(key).some((char) => char.charCodeAt(0) < 32)) {
|
|
15603
|
+
this.#message = "Key contains a newline or control character. Remove it and try again.";
|
|
15604
|
+
this.#changed();
|
|
15605
|
+
return;
|
|
15606
|
+
}
|
|
15158
15607
|
}
|
|
15159
15608
|
const generation = this.#generation;
|
|
15160
15609
|
const provider = this.#provider;
|
|
15161
15610
|
this.#busy = true;
|
|
15162
|
-
this.#message = "Saving...";
|
|
15611
|
+
this.#message = remove ? "Removing..." : "Saving...";
|
|
15163
15612
|
this.#changed();
|
|
15164
15613
|
try {
|
|
15165
15614
|
if (remove)
|
|
@@ -15169,10 +15618,11 @@ class LoginPanel {
|
|
|
15169
15618
|
if (generation === this.#generation) {
|
|
15170
15619
|
this.#input = new Input;
|
|
15171
15620
|
this.#message = remove ? "Saved key removed." : "Saved. Used by the next request.";
|
|
15621
|
+
this.#scheduleAutoClose(generation);
|
|
15172
15622
|
}
|
|
15173
|
-
} catch {
|
|
15623
|
+
} catch (error) {
|
|
15174
15624
|
if (generation === this.#generation)
|
|
15175
|
-
this.#message = "Could not update API key. Try again.";
|
|
15625
|
+
this.#message = error instanceof Error ? error.message : "Could not update API key. Try again.";
|
|
15176
15626
|
} finally {
|
|
15177
15627
|
if (generation === this.#generation) {
|
|
15178
15628
|
this.#busy = false;
|
|
@@ -15185,20 +15635,25 @@ class LoginPanel {
|
|
|
15185
15635
|
const spec = credentialProvider(this.#provider);
|
|
15186
15636
|
const tabs = CREDENTIAL_PROVIDERS.map((item) => item.id === this.#provider ? `[${item.id}]` : ` ${item.id} `).join(" ");
|
|
15187
15637
|
const source = state?.source === "keyless" ? "keyless (rate-limited)" : state?.source ?? "Loading...";
|
|
15638
|
+
const keyLength = this.#input.getValue().length;
|
|
15639
|
+
const marker = this.focused ? CURSOR_MARKER2 : "";
|
|
15640
|
+
const keyRow = keyLength > 0 ? `API Key: ********${marker}${this.focused ? renderFakeCursor(" ") : ""} (${keyLength} characters)` : `API Key: ${marker}${this.focused ? `${renderFakeCursor("(")}paste key)` : "(paste key)"}`;
|
|
15188
15641
|
const rows = [
|
|
15189
15642
|
`Provider: ${tabs}`,
|
|
15190
15643
|
`Environment: ${state ? state.environmentConfigured ? "Configured" : "Not configured" : "Loading..."}`,
|
|
15191
15644
|
`Active source: ${source}`,
|
|
15192
|
-
|
|
15645
|
+
keyRow,
|
|
15193
15646
|
this.#message || (spec.keyless ? "Without a key, Tavily runs in rate-limited keyless mode." : "Saved key overrides environment."),
|
|
15194
15647
|
spec.signupUrl
|
|
15195
15648
|
];
|
|
15196
|
-
const
|
|
15649
|
+
const switchHint = keyLength > 0 ? ["Tab", "Switch"] : ["Left/Right/Tab", "Switch"];
|
|
15650
|
+
const help = this.#busy ? shortcutHints(["Esc", "Close"]) : this.#confirmingRemove ? shortcutHints(["Ctrl+R", "Confirm remove"], ["Esc", "Cancel"]) : state?.savedConfigured ? shortcutHints(["Enter", "Save"], ["Ctrl+R", "Remove"], switchHint, ["Esc", "Cancel"]) : shortcutHints(["Enter", "Save"], switchHint, ["Esc", "Cancel"]);
|
|
15197
15651
|
while (rows.length < this.height - 1)
|
|
15198
15652
|
rows.push("");
|
|
15199
15653
|
return [...rows.slice(0, Math.max(0, this.height - 1)), help].map((line) => theme.panel(` ${truncateToWidth3(line, Math.max(1, width - 2), "...", true)} `));
|
|
15200
15654
|
}
|
|
15201
15655
|
}
|
|
15656
|
+
var AUTO_CLOSE_MS = 800;
|
|
15202
15657
|
var init_login = __esm(() => {
|
|
15203
15658
|
init_src();
|
|
15204
15659
|
init_theme();
|
|
@@ -16311,6 +16766,7 @@ import {
|
|
|
16311
16766
|
opaque,
|
|
16312
16767
|
resource
|
|
16313
16768
|
} from "@earendil-works/chord";
|
|
16769
|
+
import { UsageDoc as UsageDoc3 } from "@earendil-works/pi-durable";
|
|
16314
16770
|
import { Type as Type5 } from "typebox";
|
|
16315
16771
|
async function openReader(runtime, childId, parentId, context, through, historyUsage) {
|
|
16316
16772
|
const child = await runtime.harness.conversation(childId, context);
|
|
@@ -16373,7 +16829,7 @@ async function openReader(runtime, childId, parentId, context, through, historyU
|
|
|
16373
16829
|
return () => {};
|
|
16374
16830
|
const stop = native.subscribe(() => fn(null));
|
|
16375
16831
|
const stopApprovals = runtime.harness.subscribeCommits(({ changes }) => {
|
|
16376
|
-
if (changes.some((c) => c.type === "document" &&
|
|
16832
|
+
if (changes.some((c) => c.type === "document" && ["app.neko.approvals", "app.neko.delegations", "app.neko.subagents"].includes(c.record.kind) && c.conversationId === parentId))
|
|
16377
16833
|
fn(null);
|
|
16378
16834
|
});
|
|
16379
16835
|
return () => {
|
|
@@ -16384,7 +16840,12 @@ async function openReader(runtime, childId, parentId, context, through, historyU
|
|
|
16384
16840
|
async refresh() {
|
|
16385
16841
|
const page = await pageAt();
|
|
16386
16842
|
const approvals = through === undefined ? (await runtime.approvals.list(parentId, context)).filter((a) => a.childId === childId) : [];
|
|
16387
|
-
|
|
16843
|
+
const currentUsage = await runtime.harness.snapshot(UsageDoc3, childId, context);
|
|
16844
|
+
const rounds = (await listDelegations(runtime.harness, parentId, context)).filter((r) => r.childId === childId).map(({ usageStart, usageEnd: _end, ...round }) => ({
|
|
16845
|
+
...round,
|
|
16846
|
+
roundUsage: round.roundUsage ?? (through === undefined && usageStart && currentUsage ? subagentUsageTotals(subtractSubagentUsage(currentUsage, usageStart)) : null)
|
|
16847
|
+
}));
|
|
16848
|
+
revision = remember({ ...project(page.entries), rounds, approvals, historyBefore: page.next });
|
|
16388
16849
|
return revision;
|
|
16389
16850
|
},
|
|
16390
16851
|
async chunk(input) {
|
|
@@ -16436,7 +16897,7 @@ function installSubagentHost(host, runtime, context) {
|
|
|
16436
16897
|
reader: resource({
|
|
16437
16898
|
rev: 1,
|
|
16438
16899
|
async open() {
|
|
16439
|
-
const entry = (await listDelegations(runtime.harness, input.parentId, context)).find((d) => d.taskId === input.
|
|
16900
|
+
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);
|
|
16440
16901
|
if (!entry)
|
|
16441
16902
|
throw new ChordError("gone", "Subagent unavailable");
|
|
16442
16903
|
const r = await openReader(runtime, entry.childId, input.parentId, context, entry.historyThrough, entry.historyUsage);
|
|
@@ -16466,13 +16927,13 @@ function installSubagentHost(host, runtime, context) {
|
|
|
16466
16927
|
throw new ChordError("forbidden", "Attachment expired");
|
|
16467
16928
|
if (!await runtime.getConversation(Number(input.sessionId)))
|
|
16468
16929
|
throw new ChordError("gone", "Session unavailable");
|
|
16469
|
-
const entry = (await listDelegations(runtime.harness, Number(input.sessionId), context)).find((d) => d.taskId === input.taskId);
|
|
16930
|
+
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);
|
|
16470
16931
|
if (!entry)
|
|
16471
16932
|
throw new ChordError("forbidden", "Subagent does not belong to this session");
|
|
16472
16933
|
return entry;
|
|
16473
16934
|
};
|
|
16474
16935
|
const { target } = self.resources({ target: resource({ rev: 1, deps: [owner], open: check }) });
|
|
16475
|
-
const shared = self.get(ChildProjection, { parentId: Number(input.sessionId),
|
|
16936
|
+
const shared = self.get(ChildProjection, { parentId: Number(input.sessionId), agentId: input.agentId ?? `round-${input.taskId}` }, { principal: hostPrincipal });
|
|
16476
16937
|
const { authorizedRevision } = self.state({ authorizedRevision: cell(Type5.Integer(), 0) });
|
|
16477
16938
|
self.listen({
|
|
16478
16939
|
authorizedView: listener(shared.revision, async (value, ctx) => {
|
|
@@ -16501,14 +16962,26 @@ function installSubagentHost(host, runtime, context) {
|
|
|
16501
16962
|
const entry = await check();
|
|
16502
16963
|
if (entry.historyThrough !== undefined)
|
|
16503
16964
|
throw new ChordError("forbidden", "Fork history is read-only");
|
|
16504
|
-
|
|
16965
|
+
const approval = (await runtime.approvals.list(Number(input.sessionId), context)).find((a) => a.id === id && a.childId === entry.childId);
|
|
16966
|
+
if (!approval)
|
|
16967
|
+
throw new ChordError("forbidden", "Approval does not belong to this agent");
|
|
16968
|
+
return runtime.approvals.decide(Number(input.sessionId), approval.delegationTaskId, id, version, decision, context);
|
|
16505
16969
|
},
|
|
16506
|
-
stop: async () => {
|
|
16970
|
+
stop: async (request) => {
|
|
16507
16971
|
const entry = await check();
|
|
16508
16972
|
if (entry.historyThrough !== undefined)
|
|
16509
16973
|
throw new ChordError("forbidden", "Fork history is read-only");
|
|
16510
|
-
|
|
16511
|
-
|
|
16974
|
+
const taskId = request?.taskId ?? input.taskId;
|
|
16975
|
+
if (taskId === undefined)
|
|
16976
|
+
throw new ChordError("invalid", "Select the exact active round to stop");
|
|
16977
|
+
const valid = await runtime.harness.commit(async (tx) => {
|
|
16978
|
+
const identity = (await tx.doc(SubagentsDoc, Number(input.sessionId))).items.find((a) => a.childId === entry.childId);
|
|
16979
|
+
return identity?.activeRoundId === taskId;
|
|
16980
|
+
}, context);
|
|
16981
|
+
if (!valid)
|
|
16982
|
+
return null;
|
|
16983
|
+
await runtime.harness.abortTask(taskId, context);
|
|
16984
|
+
await runtime.harness.waitForTask(taskId, context);
|
|
16512
16985
|
return null;
|
|
16513
16986
|
}
|
|
16514
16987
|
};
|
|
@@ -16522,7 +16995,7 @@ var init_subagent_host = __esm(() => {
|
|
|
16522
16995
|
init_session_host();
|
|
16523
16996
|
ChildProjection = local("neko.subagent-projection", {
|
|
16524
16997
|
version: 1,
|
|
16525
|
-
input: Type5.Object({ parentId: Type5.Integer(),
|
|
16998
|
+
input: Type5.Object({ parentId: Type5.Integer(), agentId: Type5.String() }),
|
|
16526
16999
|
members: {
|
|
16527
17000
|
revision: Type5.Integer(),
|
|
16528
17001
|
reader: opaque("neko.subagent-reader@1")
|
|
@@ -16543,7 +17016,7 @@ import {
|
|
|
16543
17016
|
resource as resource2
|
|
16544
17017
|
} from "@earendil-works/chord";
|
|
16545
17018
|
import { withAbortSignal as withAbortSignal3 } from "@earendil-works/chord/context";
|
|
16546
|
-
import { UsageDoc as
|
|
17019
|
+
import { UsageDoc as UsageDoc4 } from "@earendil-works/pi-durable";
|
|
16547
17020
|
import { Type as Type6 } from "typebox";
|
|
16548
17021
|
async function openProjection(runtime, session, context) {
|
|
16549
17022
|
const view = await session.viewState();
|
|
@@ -16560,7 +17033,7 @@ async function openProjection(runtime, session, context) {
|
|
|
16560
17033
|
const stopMetadata = runtime.onDirectoryChange(() => changed?.(null));
|
|
16561
17034
|
let childIds = new Set;
|
|
16562
17035
|
const stopChildren = runtime.harness.subscribeCommits(({ changes }) => {
|
|
16563
|
-
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))))
|
|
17036
|
+
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))))
|
|
16564
17037
|
changed?.(null);
|
|
16565
17038
|
});
|
|
16566
17039
|
return {
|
|
@@ -16592,15 +17065,19 @@ async function openProjection(runtime, session, context) {
|
|
|
16592
17065
|
entry.approvalCount = entry.historyThrough === undefined ? approvals.filter((a) => a.delegationTaskId === entry.taskId && a.status === "pending").length : 0;
|
|
16593
17066
|
childIds = new Set(subagents.filter((entry) => entry.historyThrough === undefined).map((entry) => entry.childId));
|
|
16594
17067
|
let aggregate = await session.usage();
|
|
16595
|
-
for (const child of subagents.filter((entry) => entry.historyThrough === undefined))
|
|
16596
|
-
aggregate = mergeUsage(aggregate, await runtime.harness.snapshot(
|
|
17068
|
+
for (const child of new Map(subagents.filter((entry) => entry.historyThrough === undefined).map((r) => [r.childId, r])).values())
|
|
17069
|
+
aggregate = mergeUsage(aggregate, await runtime.harness.snapshot(UsageDoc4, child.childId, context) ?? {
|
|
16597
17070
|
models: {},
|
|
16598
17071
|
tools: {}
|
|
16599
17072
|
});
|
|
16600
17073
|
const u = totalUsage(aggregate);
|
|
16601
17074
|
return {
|
|
16602
17075
|
...projected,
|
|
16603
|
-
|
|
17076
|
+
agents: summarizeSubagents(subagents, (await runtime.harness.snapshot(SubagentsDoc, session.id, context))?.items),
|
|
17077
|
+
subagents: subagents.map(({ usageStart: _start, usageEnd: _end, ...entry }) => ({
|
|
17078
|
+
...entry,
|
|
17079
|
+
report: entry.report?.slice(0, 512) ?? null
|
|
17080
|
+
})),
|
|
16604
17081
|
taskUsage: {
|
|
16605
17082
|
input: u.input,
|
|
16606
17083
|
output: u.output,
|
|
@@ -18229,8 +18706,8 @@ function helpLines(platform = process.platform) {
|
|
|
18229
18706
|
["Enter", "Confirm selected approval option"],
|
|
18230
18707
|
["PgUp / PgDn", "Scroll full approval command"],
|
|
18231
18708
|
["[/]", "Select another pending approval"],
|
|
18232
|
-
["S", "Stop
|
|
18233
|
-
["Click", "Open
|
|
18709
|
+
["S", "Stop current round; keep agent context (Enter confirms)"],
|
|
18710
|
+
["Click", "Open round card or status-bar Agents entry"]
|
|
18234
18711
|
]);
|
|
18235
18712
|
section("Skills", [
|
|
18236
18713
|
["U", "Install for user"],
|
|
@@ -18707,7 +19184,7 @@ import { matchesKey as matchesKey9 } from "@earendil-works/pi-tui";
|
|
|
18707
19184
|
|
|
18708
19185
|
class SubagentDetail {
|
|
18709
19186
|
get help() {
|
|
18710
|
-
const canStop = this.entry?.historyThrough === undefined && (this.entry?.status === "running" || this.entry?.status === "queued");
|
|
19187
|
+
const canStop = this.identity ? this.identity.activeRoundId !== null && this.identity.historyThrough === undefined : this.entry?.historyThrough === undefined && (this.entry?.status === "running" || this.entry?.status === "queued");
|
|
18711
19188
|
return shortcutHints(["\u2190", "Main"], ["\u2191\u2193/Wheel", "Scroll"], ["E", "Expand"], ...canStop ? [["S", "Stop"]] : []);
|
|
18712
19189
|
}
|
|
18713
19190
|
#view;
|
|
@@ -18729,6 +19206,9 @@ class SubagentDetail {
|
|
|
18729
19206
|
#panel;
|
|
18730
19207
|
#active = false;
|
|
18731
19208
|
entry;
|
|
19209
|
+
identity;
|
|
19210
|
+
#targetRound;
|
|
19211
|
+
#locating = false;
|
|
18732
19212
|
#epoch = 0;
|
|
18733
19213
|
constructor(client, taskId, panel, changed) {
|
|
18734
19214
|
this.#service = client.subagent(taskId);
|
|
@@ -18755,10 +19235,35 @@ class SubagentDetail {
|
|
|
18755
19235
|
if (this.#before === undefined)
|
|
18756
19236
|
this.#before = view.historyBefore;
|
|
18757
19237
|
this.#entries = new Map(mergeTranscriptItems([...this.#entries.values()], view.items).map((item) => [item.key, item]));
|
|
19238
|
+
this.#loadTarget();
|
|
18758
19239
|
this.#changed();
|
|
18759
19240
|
});
|
|
18760
19241
|
return this;
|
|
18761
19242
|
}
|
|
19243
|
+
locate(taskId) {
|
|
19244
|
+
this.#targetRound = taskId;
|
|
19245
|
+
this.#loadTarget();
|
|
19246
|
+
this.#changed();
|
|
19247
|
+
}
|
|
19248
|
+
async#loadTarget() {
|
|
19249
|
+
if (this.#locating || !this.#active || this.#targetRound === undefined)
|
|
19250
|
+
return;
|
|
19251
|
+
const entryId = this.#view?.rounds?.find((r) => r.taskId === this.#targetRound)?.inputEntryId;
|
|
19252
|
+
if (entryId === undefined)
|
|
19253
|
+
return;
|
|
19254
|
+
this.#locating = true;
|
|
19255
|
+
try {
|
|
19256
|
+
while (this.#active && !this.#entries.has(`e${entryId}`) && this.#before != null && !this.#loading) {
|
|
19257
|
+
const before = this.#before;
|
|
19258
|
+
await this.history();
|
|
19259
|
+
if (before === this.#before)
|
|
19260
|
+
break;
|
|
19261
|
+
}
|
|
19262
|
+
} finally {
|
|
19263
|
+
this.#locating = false;
|
|
19264
|
+
this.#changed();
|
|
19265
|
+
}
|
|
19266
|
+
}
|
|
18762
19267
|
close() {
|
|
18763
19268
|
this.#epoch++;
|
|
18764
19269
|
this.#active = false;
|
|
@@ -18811,14 +19316,15 @@ class SubagentDetail {
|
|
|
18811
19316
|
if (component instanceof AssistantMessageComponent)
|
|
18812
19317
|
component.setThinkingExpanded(this.#expanded);
|
|
18813
19318
|
}
|
|
18814
|
-
} else if (data.toLowerCase() === "s" && this.entry?.historyThrough === undefined && (this.entry?.status === "running" || this.entry?.status === "queued"))
|
|
19319
|
+
} else if (data.toLowerCase() === "s" && this.entry?.historyThrough === undefined && (this.identity?.activeRoundId != null || !this.identity && (this.entry?.status === "running" || this.entry?.status === "queued"))) {
|
|
19320
|
+
const stopTaskId = this.identity?.activeRoundId ?? this.entry?.taskId;
|
|
18815
19321
|
this.#panel.showDetail({
|
|
18816
19322
|
help: shortcutHints(["Enter", "Stop"], ["Esc", "Back"]),
|
|
18817
19323
|
lines: () => [theme.warning("Stop this task? Existing changes will be kept.")],
|
|
18818
19324
|
handleKey: (key) => {
|
|
18819
19325
|
if (!matchesKey9(key, "enter"))
|
|
18820
19326
|
return false;
|
|
18821
|
-
this.#service.stop(null).catch((e) => {
|
|
19327
|
+
this.#service.stop(stopTaskId === undefined ? null : { taskId: stopTaskId }).catch((e) => {
|
|
18822
19328
|
this.#error = String(e);
|
|
18823
19329
|
}).finally(() => {
|
|
18824
19330
|
this.#panel.popDetail();
|
|
@@ -18827,7 +19333,7 @@ class SubagentDetail {
|
|
|
18827
19333
|
return true;
|
|
18828
19334
|
}
|
|
18829
19335
|
});
|
|
18830
|
-
else
|
|
19336
|
+
} else
|
|
18831
19337
|
return false;
|
|
18832
19338
|
return true;
|
|
18833
19339
|
}
|
|
@@ -18851,14 +19357,25 @@ class SubagentDetail {
|
|
|
18851
19357
|
this.#pageSize = Math.max(1, height - 1);
|
|
18852
19358
|
const entry = this.entry;
|
|
18853
19359
|
const rows = entry ? [
|
|
18854
|
-
theme.accent(`${entry.role} \xB7 ${entry.title}`),
|
|
18855
|
-
theme.dim(`${entry.historyThrough !== undefined ? "Snapshot \xB7 " : ""}${labels2[entry.status]} \xB7 ${Math.max(0, Math.round(((entry.endedAt ?? Date.now()) - entry.createdAt) / 1000))}s`),
|
|
19360
|
+
theme.accent(`${entry.role} \xB7 ${this.identity?.title ?? entry.title}`),
|
|
19361
|
+
theme.dim(`${entry.historyThrough !== undefined ? "Snapshot \xB7 " : ""}${this.identity ? this.identity.status === "idle" ? "Idle" : this.identity.status === "queued" ? "Queued" : "Running" : labels2[entry.status]} \xB7 ${Math.max(0, Math.round(((entry.endedAt ?? Date.now()) - entry.createdAt) / 1000))}s`),
|
|
18856
19362
|
""
|
|
18857
19363
|
] : [];
|
|
18858
19364
|
if (this.#view)
|
|
18859
|
-
rows.push(theme.dim(`In ${this.#view.usage.input} \xB7 Out ${this.#view.usage.output} \xB7 $${this.#view.usage.cost.toFixed(4)}`), "");
|
|
19365
|
+
rows.push(theme.dim(`Total In ${this.#view.usage.input} \xB7 Out ${this.#view.usage.output} \xB7 $${this.#view.usage.cost.toFixed(4)}`), "");
|
|
18860
19366
|
const imageRows = [];
|
|
19367
|
+
let targetOffset;
|
|
18861
19368
|
for (const item of [...this.#entries.values()].sort((a, b) => (a.order ?? 0) - (b.order ?? 0))) {
|
|
19369
|
+
if (item.type === "user") {
|
|
19370
|
+
const round = this.#view?.rounds?.find((r) => r.inputEntryId === item.id || r.taskId === item.subagentRoundId);
|
|
19371
|
+
if (round?.taskId === this.#targetRound)
|
|
19372
|
+
targetOffset = rows.length;
|
|
19373
|
+
if (round) {
|
|
19374
|
+
rows.push(theme.accent(`Task ${round.round ?? ""} \xB7 ${round.title} \xB7 ${labels2[round.status]}`));
|
|
19375
|
+
const u = round.roundUsage;
|
|
19376
|
+
rows.push(theme.dim(u ? `Round In ${u.input} \xB7 Out ${u.output} \xB7 Cache ${u.cacheRead}/${u.cacheWrite} \xB7 $${u.cost.toFixed(4)}` : "Round usage not recorded"));
|
|
19377
|
+
}
|
|
19378
|
+
}
|
|
18862
19379
|
rows.push(...this.#component(item).render(width));
|
|
18863
19380
|
if (item.type === "tool") {
|
|
18864
19381
|
for (const approval of this.#view?.approvals ?? [])
|
|
@@ -18904,6 +19421,11 @@ class SubagentDetail {
|
|
|
18904
19421
|
this.#offset += Math.max(0, this.#maxOffset - this.#prependOffset);
|
|
18905
19422
|
this.#prependOffset = undefined;
|
|
18906
19423
|
}
|
|
19424
|
+
if (targetOffset !== undefined) {
|
|
19425
|
+
this.#offset = targetOffset;
|
|
19426
|
+
this.#follow = false;
|
|
19427
|
+
this.#targetRound = undefined;
|
|
19428
|
+
}
|
|
18907
19429
|
this.#offset = this.#follow ? this.#maxOffset : Math.min(this.#offset, this.#maxOffset);
|
|
18908
19430
|
for (const image of imageRows) {
|
|
18909
19431
|
image.image.setVisible(this.#active && image.start < this.#offset + height && image.end > this.#offset);
|
|
@@ -18912,11 +19434,11 @@ class SubagentDetail {
|
|
|
18912
19434
|
return rows.slice(this.#offset, this.#offset + height);
|
|
18913
19435
|
}
|
|
18914
19436
|
}
|
|
18915
|
-
var labels2, subagentItems = (entries) =>
|
|
18916
|
-
value:
|
|
19437
|
+
var labels2, subagentItems = (entries, summaries = summarizeSubagents(entries)) => summaries.map((d) => ({
|
|
19438
|
+
value: d.agentId,
|
|
18917
19439
|
label: `${d.role} \xB7 ${d.title}`,
|
|
18918
|
-
description: `${d.historyThrough !== undefined ? "Snapshot \xB7 " : ""}${
|
|
18919
|
-
rightLabel: d.approvalCount ? `Approval ${d.approvalCount}` :
|
|
19440
|
+
description: `${d.historyThrough !== undefined ? "Snapshot \xB7 " : ""}${d.taskTitle} \xB7 ${d.roundCount} rounds`,
|
|
19441
|
+
rightLabel: d.approvalCount ? `Approval ${d.approvalCount}` : d.status === "idle" ? "Idle" : d.status === "queued" ? "Queued" : "Running"
|
|
18920
19442
|
}));
|
|
18921
19443
|
var init_subagents_ui = __esm(() => {
|
|
18922
19444
|
init_src();
|
|
@@ -19753,19 +20275,25 @@ class NekoTuiApp {
|
|
|
19753
20275
|
this.#agentsPanel = true;
|
|
19754
20276
|
this.#panel.pointerSelection = true;
|
|
19755
20277
|
this.#panel.agentNavigation = true;
|
|
19756
|
-
this.#panel.show("Agents", async () => subagentItems(this.#lastView?.subagents ?? []), async (value) => this.#agentDetail(
|
|
20278
|
+
this.#panel.show("Agents", async () => subagentItems(this.#lastView?.subagents ?? [], this.#lastView?.agents), async (value) => this.#agentDetail(value));
|
|
19757
20279
|
this.#ui.setFocus(this.#panel);
|
|
19758
20280
|
if (taskId !== undefined)
|
|
19759
20281
|
this.#panel.showDetail(this.#agentDetail(taskId));
|
|
19760
20282
|
}
|
|
19761
|
-
#agentDetail(
|
|
19762
|
-
|
|
20283
|
+
#agentDetail(target) {
|
|
20284
|
+
const round = typeof target === "number" ? this.#lastView?.subagents?.find((r) => r.taskId === target) : undefined;
|
|
20285
|
+
const id = typeof target === "string" ? target : round?.agentId ?? `agent-${round?.childId}`;
|
|
20286
|
+
let detail = this.#agentDetails.get(id);
|
|
19763
20287
|
if (!detail) {
|
|
19764
|
-
detail = new SubagentDetail(this.#gateway.client,
|
|
19765
|
-
this.#agentDetails.set(
|
|
20288
|
+
detail = new SubagentDetail(this.#gateway.client, id, this.#panel, () => this.#ui.requestRender());
|
|
20289
|
+
this.#agentDetails.set(id, detail);
|
|
19766
20290
|
}
|
|
19767
|
-
detail.
|
|
19768
|
-
|
|
20291
|
+
detail.identity = this.#lastView?.agents?.find((a) => a.agentId === id);
|
|
20292
|
+
detail.entry = this.#lastView?.subagents?.find((r) => r.taskId === (detail.identity?.activeRoundId ?? detail.identity?.lastRoundId));
|
|
20293
|
+
detail.open();
|
|
20294
|
+
if (typeof target === "number")
|
|
20295
|
+
detail.locate(target);
|
|
20296
|
+
return detail;
|
|
19769
20297
|
}
|
|
19770
20298
|
#closeOverlay() {
|
|
19771
20299
|
if (this.#panel.open)
|
|
@@ -19810,11 +20338,13 @@ class NekoTuiApp {
|
|
|
19810
20338
|
else
|
|
19811
20339
|
this.#approval.close();
|
|
19812
20340
|
if (this.#agentsPanel)
|
|
19813
|
-
this.#panel.update(subagentItems(view.subagents ?? []));
|
|
19814
|
-
for (const
|
|
19815
|
-
const detail = this.#agentDetails.get(
|
|
19816
|
-
if (detail)
|
|
19817
|
-
detail.
|
|
20341
|
+
this.#panel.update(subagentItems(view.subagents ?? [], view.agents));
|
|
20342
|
+
for (const identity of view.agents ?? []) {
|
|
20343
|
+
const detail = this.#agentDetails.get(identity.agentId);
|
|
20344
|
+
if (detail) {
|
|
20345
|
+
detail.identity = identity;
|
|
20346
|
+
detail.entry = view.subagents?.find((r) => r.taskId === (identity.activeRoundId ?? identity.lastRoundId));
|
|
20347
|
+
}
|
|
19818
20348
|
}
|
|
19819
20349
|
const imageModel = `${this.#sessionId}:${view.agent.model?.provider}/${view.agent.model?.modelId}`;
|
|
19820
20350
|
if (this.#imageWarningModel !== imageModel && view.items.some((i) => (i.type === "user" || i.type === "tool") && i.images?.length)) {
|
|
@@ -20014,7 +20544,7 @@ class NekoTuiApp {
|
|
|
20014
20544
|
#syncFooter(view) {
|
|
20015
20545
|
const usage = view.taskUsage ?? view.usage;
|
|
20016
20546
|
this.#footer.setStats({
|
|
20017
|
-
subagentCount: view.
|
|
20547
|
+
subagentCount: view.agents?.length ?? 0,
|
|
20018
20548
|
approvalCount: view.subagents?.reduce((n, d) => n + (d.approvalCount ?? 0), 0) ?? 0,
|
|
20019
20549
|
model: view.model,
|
|
20020
20550
|
thinkingLevel: view.agent.thinkingLevel,
|