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/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.0", APPLICATION_PROTOCOL = 3, ApplicationProtocolError;
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 { args: `${record.type}: ${record.title}`, detail: String(record.task ?? "") };
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
- defineTool as defineTool2
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.status === "completed" || item.status === "failed" || item.status === "cancelled")
1503
- continue;
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
- if (task?.state.status !== "terminal")
1506
- continue;
1507
- item.status = task.state.outcome.status === "aborted" ? "cancelled" : "failed";
1508
- item.endedAt = Date.now();
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
- async function admit(api, cwd, role, context) {
1513
- for (;; ) {
1514
- context.abortSignal?.throwIfAborted();
1515
- const acquired = await api.commit(async (tx) => {
1516
- const slots = await tx.doc(SlotsDoc);
1517
- const live = [];
1518
- for (const slot of slots.items) {
1519
- const task = await tx.task(slot.taskId);
1520
- if (task && task.state.status !== "terminal")
1521
- live.push(slot);
1522
- }
1523
- slots.items = live;
1524
- if (live.some((slot) => slot.taskId === api.taskId))
1525
- return true;
1526
- if (live.filter((slot) => slot.parentId === api.conversationId).length >= 3 || role === "coder" && live.some((slot) => slot.coder && slot.cwd === cwd))
1527
- return false;
1528
- slots.items.push({ taskId: api.taskId, parentId: api.conversationId, cwd, coder: role === "coder" });
1529
- return true;
1530
- }, context);
1531
- if (acquired)
1532
- return;
1533
- await new Promise((resolve, reject) => {
1534
- const cleanup = () => {
1535
- clearTimeout(timer);
1536
- context.abortSignal?.removeEventListener("abort", abort);
1537
- };
1538
- const abort = () => {
1539
- cleanup();
1540
- reject(context.abortSignal?.reason ?? new Error("Cancelled"));
1541
- };
1542
- const timer = setTimeout(() => {
1543
- cleanup();
1544
- resolve();
1545
- }, 100);
1546
- context.abortSignal?.addEventListener("abort", abort, { once: true });
1547
- if (context.abortSignal?.aborted)
1548
- abort();
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: "Delegate a self-contained task to an independent foreground subagent. explore: investigation, Bash requires user approval; plan: read-only implementation planning; coder: scoped changes and verification. Include all needed context, constraints and expected delivery. Use only when isolation or parallel independent work helps; handle simple tasks directly. Returns the final report; the main agent waits and must verify/integrate it.",
1559
- parameters: Type2.Object({
1560
- type: Type2.Union([Type2.Literal("explore"), Type2.Literal("plan"), Type2.Literal("coder")]),
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 (args, api, context) => {
1566
- if (!args.task.trim() || !args.title.trim() || Buffer.byteLength(args.task) > 32768)
1567
- throw new Error("Task/title must be nonempty; task limit is 32 KiB.");
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 childId = await api.commit(async (tx) => {
1572
- const existing = (await tx.scanConversations({ ownerTaskId: api.taskId }, 1)).items[0];
1573
- if (existing)
1574
- return existing.id;
1575
- const child = await tx.createConversation({ ownership: { kind: "task", taskId: api.taskId } });
1576
- await configure(tx, child.id, {
1577
- model: agent.model,
1578
- thinkingLevel: agent.thinkingLevel,
1579
- cwd: agent.cwd,
1580
- extensions: agent.extensions.filter((e) => e.name !== extension.name),
1581
- tools: agent.tools.filter((tool) => tool.name !== "subagent" && (args.type === "coder" || READ_TOOLS.has(tool.name) || args.type === "explore" && tool.name === "bash"))
1582
- });
1583
- Object.assign(await tx.doc(SubagentDoc, child.id), { role: args.type, parentId: api.conversationId });
1584
- (await tx.doc(DelegationsDoc, api.conversationId)).items.push({
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: child.id,
1589
- role: args.type,
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
- return child.id;
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({ conversationId: childId, role: args.type, title: args.title }, context);
1600
- const cwd = api.env ? await api.env.canonicalPath(agent.cwd ?? api.env.cwd, context) : undefined;
1601
- await admit(api, cwd?.ok ? cwd.value : agent.cwd ?? "", args.type, context);
1602
- await api.commit(async (tx) => {
1603
- const item = (await tx.doc(DelegationsDoc, api.conversationId)).items.find((d) => d.taskId === api.taskId);
1604
- item.status = "running";
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: [{ type: "text", text: report || "Subagent completed without a text report." }],
1644
- isError: status !== "completed",
1645
- details: { conversationId: childId, role: args.type, title: args.title, status }
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
- var DelegationsDoc, SubagentDoc, SlotsDoc, ROLE_PROMPTS, SUBAGENT_DELIVERY = " Work only from the assigned task and your project resources; you cannot see the main conversation. Do not delegate or ask the user directly. Your final message must be a self-contained delivery to the main agent, including evidence, results and limitations. Skills never expand your allowed tools.", READ_TOOLS;
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 defineTask({
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(LiveDoc, id, this.context);
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 delegation = (await api.snapshot(DelegationsDoc, parent, context))?.items.find((d) => d.childId === api.conversationId && d.historyThrough === undefined);
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(UsageDoc, target.id, context) ?? { models: {}, tools: {} }, (await harness.snapshot(SessionMetadataDoc, target.id, context))?.namingUsage ?? { models: {}, tools: {} });
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(UsageDoc, record.id, context) ?? { models: {}, tools: {} });
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
- const latest = await tx.scanEntries({ conversationId: reference.childId }, 1);
3348
- const usage = totalUsage(await tx.doc(UsageDoc, reference.childId));
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 ?? latest.items[0]?.id ?? 0,
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({ sessionId: str, attachmentId: str, taskId: integer }),
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
- if (this.#readers.has(entry.taskId))
4726
+ const key = entry.agentId ?? entry.taskId;
4727
+ if (this.#readers.has(key))
4322
4728
  continue;
4323
- const service = this.#connect(entry.taskId);
4729
+ const service = this.#connect(key);
4324
4730
  const reader = { service, title: entry.title, stop: () => {} };
4325
- this.#readers.set(entry.taskId, reader);
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(taskId) {
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 { Input, isKeyRelease as isKeyRelease2, matchesKey as matchesKey4, truncateToWidth as truncateToWidth3 } from "@earendil-works/pi-tui";
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
- this.#apply(true);
15134
- else if (matchesKey4(data, "left") || matchesKey4(data, "right") || matchesKey4(data, "tab"))
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 && !key.trim()) {
15155
- this.#message = "Enter an API key.";
15156
- this.#changed();
15157
- return;
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
- `API Key: ${"*".repeat(Math.min(this.#input.getValue().length, Math.max(1, width - 15))) || "(paste key)"}`,
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 help = this.#busy ? shortcutHints(["Esc", "Close"]) : state?.savedConfigured ? shortcutHints(["Enter", "Save"], ["Ctrl+R", "Remove"], ["Left/Right", "Switch"], ["Esc", "Cancel"]) : shortcutHints(["Enter", "Save"], ["Left/Right", "Switch"], ["Esc", "Cancel"]);
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" && c.record.kind === "app.neko.approvals" && c.conversationId === parentId))
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
- revision = remember({ ...project(page.entries), approvals, historyBefore: page.next });
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.taskId);
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), taskId: input.taskId }, { principal: hostPrincipal });
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
- return runtime.approvals.decide(Number(input.sessionId), entry.taskId, id, version, decision, context);
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
- await runtime.harness.abortTask(entry.taskId, context);
16511
- await runtime.harness.waitForTask(entry.taskId, context);
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(), taskId: 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 UsageDoc2 } from "@earendil-works/pi-durable";
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(UsageDoc2, child.childId, context) ?? {
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
- subagents: subagents.map((entry) => ({ ...entry, report: entry.report?.slice(0, 512) ?? null })),
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 active task (Enter confirms)"],
18233
- ["Click", "Open task card or status-bar Agents entry"]
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) => entries.map((d) => ({
18916
- value: String(d.taskId),
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 " : ""}${labels2[d.status]}`,
18919
- rightLabel: d.approvalCount ? `Approval ${d.approvalCount}` : labels2[d.status]
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(Number(value)));
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(taskId) {
19762
- let detail = this.#agentDetails.get(taskId);
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, taskId, this.#panel, () => this.#ui.requestRender());
19765
- this.#agentDetails.set(taskId, detail);
20288
+ detail = new SubagentDetail(this.#gateway.client, id, this.#panel, () => this.#ui.requestRender());
20289
+ this.#agentDetails.set(id, detail);
19766
20290
  }
19767
- detail.entry = this.#lastView?.subagents?.find((entry) => entry.taskId === taskId);
19768
- return detail.open();
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 entry of view.subagents ?? []) {
19815
- const detail = this.#agentDetails.get(entry.taskId);
19816
- if (detail)
19817
- detail.entry = entry;
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.subagents?.length ?? 0,
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,