@yaag/tui 0.10.0 → 0.11.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yaag/tui",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -19,6 +19,6 @@
19
19
  },
20
20
  "dependencies": {
21
21
  "@earendil-works/pi-tui": "^0.84.0",
22
- "@yaag/runtime": "0.10.0"
22
+ "@yaag/runtime": "0.11.0"
23
23
  }
24
24
  }
@@ -0,0 +1,66 @@
1
+ import type { AgentInfo } from "@yaag/runtime";
2
+ import {
3
+ costText,
4
+ durationText,
5
+ modelText,
6
+ sanitizeTerminalLine,
7
+ tokensText,
8
+ } from "../text/index.ts";
9
+ import type { AgentTimers } from "./agent-timers.ts";
10
+
11
+ /** Everything one Agent row's facts are built from; `now` is already resolved. */
12
+ export interface AgentFactsOptions {
13
+ readonly agent: AgentInfo;
14
+ readonly settled: number;
15
+ readonly timers: AgentTimers;
16
+ /** The Agent's Parent Link resolved to a row in this tree (`lineageOf`). */
17
+ readonly parentShown: boolean;
18
+ }
19
+
20
+ /**
21
+ * The Agent row's facts. The headline fact keeps the Agent's state and both
22
+ * timers in one string, because the compact and inline frames render only the
23
+ * first fact. Fork and compaction facts stay short so the row still fits a
24
+ * narrow pane.
25
+ */
26
+ export function agentFacts(options: AgentFactsOptions): readonly string[] {
27
+ const { agent, settled, timers } = options;
28
+ const clocks = `active ${durationText(timers.activeMs)} · idle ${durationText(timers.idleMs)}`;
29
+ const facts: string[] = [];
30
+ if (agent.state === "asking") facts.push(`ask #${agent.askIndex + 1} · ${clocks}`);
31
+ else if (agent.state === "exited")
32
+ facts.push(`exited · ${settled} ask${settled === 1 ? "" : "s"} · ${clocks}`);
33
+ else facts.push(clocks);
34
+ facts.push(
35
+ `${costText(agent.cost, agent.incomplete)} · ${tokensText(agent.tokens?.total ?? null)}`,
36
+ );
37
+ if (agent.model !== null) facts.push(modelText(agent.model));
38
+ const last = agent.modelFallbacks.at(-1);
39
+ if (last !== undefined) {
40
+ facts.push(sanitizeTerminalLine(`fallback · ${last.failedModel} → ${last.resolvedModel}`));
41
+ }
42
+ const fork = forkFact(agent, options.parentShown);
43
+ if (fork !== null) facts.push(fork);
44
+ const compaction = compactionFact(agent);
45
+ if (compaction !== null) facts.push(compaction);
46
+ return facts;
47
+ }
48
+
49
+ /** Names the fork source only when its row is absent, so it is not repeated. */
50
+ function forkFact(agent: AgentInfo, parentShown: boolean): string | null {
51
+ if (agent.origin !== "fork") return null;
52
+ if (parentShown || agent.parent === null) return "fork";
53
+ return sanitizeTerminalLine(`fork of ${agent.parent}`);
54
+ }
55
+
56
+ /**
57
+ * Compaction spend is never summed here: `agent_exit` already includes it
58
+ * (ADR-0043), so the row states the count and the context sizes only.
59
+ */
60
+ function compactionFact(agent: AgentInfo): string | null {
61
+ if (agent.compactions <= 0) return null;
62
+ const count = `compacted ×${agent.compactions}`;
63
+ const last = agent.lastCompaction;
64
+ if (last === null || last.tokensBefore === null || last.tokensAfter === null) return count;
65
+ return `${count} · ${tokensText(last.tokensBefore)} → ${tokensText(last.tokensAfter)}`;
66
+ }
@@ -1,14 +1,8 @@
1
1
  import type { AgentInfo } from "@yaag/runtime";
2
- import {
3
- activityText,
4
- costText,
5
- durationText,
6
- modelText,
7
- sanitizeTerminalLine,
8
- tokensText,
9
- } from "../text/index.ts";
10
- import { type AgentTimers, agentTimers } from "./agent-timers.ts";
2
+ import { activityText, durationText, modelText, sanitizeTerminalLine } from "../text/index.ts";
3
+ import { agentTimers } from "./agent-timers.ts";
11
4
  import { type AskRow, askDwellMs } from "./ask-ledger.ts";
5
+ import { agentFacts } from "./tree-agent-facts.ts";
12
6
  import { type Lineage, lineageOf } from "./tree-lineage.ts";
13
7
  import { graftNestedNodes } from "./tree-nested.ts";
14
8
  import type { TreeNode } from "./tree-node.ts";
@@ -36,33 +30,46 @@ export function buildTree(state: TreeState, options: TreeModelOptions): readonly
36
30
  if (agent !== undefined) known.set(name, agent);
37
31
  }
38
32
  const lineage = lineageOf([...known.keys()], (name) => state.parentLinkOf(name));
39
- return subtrees(state, lineage.roots, known, lineage, options.now);
33
+ return subtrees({ state, known, lineage, now: options.now }, lineage.roots, false);
34
+ }
35
+
36
+ /** What every level of the recursion shares; only the names and depth change. */
37
+ interface BuildContext {
38
+ readonly state: TreeState;
39
+ readonly known: ReadonlyMap<string, AgentInfo>;
40
+ readonly lineage: Lineage;
41
+ readonly now: number;
40
42
  }
41
43
 
42
44
  function subtrees(
43
- state: TreeState,
45
+ context: BuildContext,
44
46
  names: readonly string[],
45
- known: ReadonlyMap<string, AgentInfo>,
46
- lineage: Lineage,
47
- now: number,
47
+ parentShown: boolean,
48
48
  ): readonly TreeNode[] {
49
49
  const nodes: TreeNode[] = [];
50
50
  for (const name of names) {
51
- const agent = known.get(name);
51
+ const agent = context.known.get(name);
52
52
  if (agent === undefined) continue;
53
- const children = subtrees(state, lineage.children(name), known, lineage, now);
54
- nodes.push(agentNode(state, name, agent, now, children));
53
+ const children = subtrees(context, context.lineage.children(name), true);
54
+ nodes.push(agentNode(context, { name, agent, parentShown }, children));
55
55
  }
56
56
  return nodes;
57
57
  }
58
58
 
59
+ /** The Agent this node is for, plus whether `lineageOf` gave its parent a row. */
60
+ interface AgentSlot {
61
+ readonly name: string;
62
+ readonly agent: AgentInfo;
63
+ readonly parentShown: boolean;
64
+ }
65
+
59
66
  function agentNode(
60
- state: TreeState,
61
- name: string,
62
- agent: AgentInfo,
63
- now: number,
67
+ context: BuildContext,
68
+ slot: AgentSlot,
64
69
  childAgents: readonly TreeNode[],
65
70
  ): TreeNode {
71
+ const { state, now } = context;
72
+ const { name, agent } = slot;
66
73
  const ledger = state.ledger(name);
67
74
  const nested = graftNestedNodes(name, agent.nodes, now);
68
75
  const gist = agent.activity === null ? null : activityText(agent.activity);
@@ -82,11 +89,12 @@ function agentNode(
82
89
  kind: "agent",
83
90
  label: sanitizeTerminalLine(name),
84
91
  state: agentState(agent),
85
- facts: agentFacts(
92
+ facts: agentFacts({
86
93
  agent,
87
- state.settledAsks(name),
88
- agentTimers({ agent, ledger, spawnedAt: state.spawnedAt(name), now }),
89
- ),
94
+ settled: state.settledAsks(name),
95
+ timers: agentTimers({ agent, ledger, spawnedAt: state.spawnedAt(name), now }),
96
+ parentShown: slot.parentShown,
97
+ }),
90
98
  children,
91
99
  activityGist: gist,
92
100
  startedAt: agent.stateChangedAt,
@@ -138,26 +146,3 @@ function agentState(agent: AgentInfo): TreeNode["state"] {
138
146
  if (agent.state === "exited") return "exited";
139
147
  return agent.state === "asking" ? "running" : "idle";
140
148
  }
141
-
142
- /**
143
- * The Agent row's facts. The headline fact keeps the Agent's state and both
144
- * timers in one string, because the compact and inline frames render only the
145
- * first fact.
146
- */
147
- function agentFacts(agent: AgentInfo, settled: number, timers: AgentTimers): readonly string[] {
148
- const clocks = `active ${durationText(timers.activeMs)} · idle ${durationText(timers.idleMs)}`;
149
- const facts: string[] = [];
150
- if (agent.state === "asking") facts.push(`ask #${agent.askIndex + 1} · ${clocks}`);
151
- else if (agent.state === "exited")
152
- facts.push(`exited · ${settled} ask${settled === 1 ? "" : "s"} · ${clocks}`);
153
- else facts.push(clocks);
154
- facts.push(
155
- `${costText(agent.cost, agent.incomplete)} · ${tokensText(agent.tokens?.total ?? null)}`,
156
- );
157
- if (agent.model !== null) facts.push(modelText(agent.model));
158
- const last = agent.modelFallbacks.at(-1);
159
- if (last !== undefined) {
160
- facts.push(sanitizeTerminalLine(`fallback · ${last.failedModel} → ${last.resolvedModel}`));
161
- }
162
- return facts;
163
- }