@fitchmultz/pi-subagents 0.50.0 → 0.52.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.
Files changed (180) hide show
  1. package/CHANGELOG.md +63 -2
  2. package/README.md +36 -5
  3. package/dist/agent/child-runtime.js +80 -0
  4. package/dist/agent/contracts.js +15 -24
  5. package/dist/agent/host.js +3 -16
  6. package/dist/agent/index.js +212 -192
  7. package/dist/agent/journal-tail.js +115 -156
  8. package/dist/agent/launch.js +20 -8
  9. package/dist/agent/legacy.js +65 -23
  10. package/dist/agent/notifications.js +138 -47
  11. package/dist/agent/observation-state.js +246 -0
  12. package/dist/agent/observation-view.js +258 -0
  13. package/dist/agent/observation.js +350 -0
  14. package/dist/agent/observe-card.js +243 -0
  15. package/dist/agent/observe-chrome.js +85 -0
  16. package/dist/agent/observe-client.js +285 -0
  17. package/dist/agent/observe-layout.js +65 -0
  18. package/dist/agent/observe-reading.js +84 -0
  19. package/dist/agent/observe-screen.js +400 -96
  20. package/dist/agent/observe-transcript.js +382 -0
  21. package/dist/agent/observe.js +250 -43
  22. package/dist/agent/parking.js +253 -0
  23. package/dist/agent/process.js +228 -28
  24. package/dist/agent/receipts.js +92 -20
  25. package/dist/agent/runner.js +229 -126
  26. package/dist/agent/shell.js +2 -1
  27. package/dist/agent/storage.js +47 -12
  28. package/dist/agent/tool-schemas.js +20 -0
  29. package/dist/agent/typebox.js +10 -5
  30. package/dist/agent/usage.js +0 -115
  31. package/dist/agent/view.js +46 -16
  32. package/dist/agent/wake.js +99 -0
  33. package/dist/build-info.js +1 -1
  34. package/dist/intercom-client.d.ts +26 -0
  35. package/dist/intercom-client.js +45 -0
  36. package/dist/pi-intercom/agent-dir.d.ts +1 -0
  37. package/dist/pi-intercom/bridge-session.js +2 -1
  38. package/dist/pi-intercom/broker/broker.js +62 -43
  39. package/dist/pi-intercom/broker/client-last.d.ts +24 -0
  40. package/dist/pi-intercom/broker/client-last.js +42 -5
  41. package/dist/pi-intercom/broker/client-park.d.ts +9 -0
  42. package/dist/pi-intercom/broker/client-park.js +21 -0
  43. package/dist/pi-intercom/broker/client.d.ts +59 -0
  44. package/dist/pi-intercom/broker/client.js +55 -29
  45. package/dist/pi-intercom/broker/connection.d.ts +29 -0
  46. package/dist/pi-intercom/broker/framing.d.ts +17 -0
  47. package/dist/pi-intercom/broker/framing.js +10 -7
  48. package/dist/pi-intercom/broker/guard.js +23 -0
  49. package/dist/pi-intercom/broker/last.d.ts +30 -0
  50. package/dist/pi-intercom/broker/last.js +87 -40
  51. package/dist/pi-intercom/broker/parked.js +107 -0
  52. package/dist/pi-intercom/broker/paths.d.ts +13 -0
  53. package/dist/pi-intercom/broker/paths.js +35 -12
  54. package/dist/pi-intercom/broker/pid.js +5 -1
  55. package/dist/pi-intercom/broker/server-connection.js +20 -1
  56. package/dist/pi-intercom/broker/snapshot-reader.d.ts +9 -0
  57. package/dist/pi-intercom/broker/snapshots.d.ts +29 -0
  58. package/dist/pi-intercom/broker/targets.d.ts +9 -0
  59. package/dist/pi-intercom/connection.js +71 -20
  60. package/dist/pi-intercom/identity.js +6 -5
  61. package/dist/pi-intercom/inbound-delivery.js +16 -60
  62. package/dist/pi-intercom/inbound-journal.js +83 -6
  63. package/dist/pi-intercom/inbound-record.js +18 -1
  64. package/dist/pi-intercom/inbound.js +22 -37
  65. package/dist/pi-intercom/index.js +41 -0
  66. package/dist/pi-intercom/last-message.js +4 -16
  67. package/dist/pi-intercom/lifecycle.js +88 -3
  68. package/dist/pi-intercom/outbound.js +5 -15
  69. package/dist/pi-intercom/reply-tracker.js +7 -6
  70. package/dist/pi-intercom/reply-wait.js +2 -2
  71. package/dist/pi-intercom/session-targets.d.ts +24 -0
  72. package/dist/pi-intercom/tool-registration.js +4 -1
  73. package/dist/pi-intercom/tool-results.js +5 -7
  74. package/dist/pi-intercom/tool-schemas.js +12 -26
  75. package/dist/pi-intercom/types.d.ts +212 -0
  76. package/dist/pi-intercom/types.js +54 -3
  77. package/dist/pi-intercom/validation.d.ts +12 -0
  78. package/dist/pi-intercom/validation.js +3 -0
  79. package/dist/shared/bounded-json.js +151 -0
  80. package/dist/shared/compact-cards.js +66 -0
  81. package/dist/shared/journal-frames.js +3 -6
  82. package/dist/shared/json-projection.js +267 -129
  83. package/dist/shared/native-journal.js +123 -56
  84. package/dist/shared/parking.js +12 -0
  85. package/dist/shared/post-exit-stdio-guard.js +120 -7
  86. package/dist/shared/tui-action.js +29 -0
  87. package/dist/shared/unknown.d.ts +9 -0
  88. package/docs/code-quality.md +5 -3
  89. package/docs/development.md +42 -3
  90. package/docs/intercom-bridge.md +9 -4
  91. package/docs/intercom.md +228 -7
  92. package/docs/performance.md +118 -14
  93. package/docs/reference.md +168 -20
  94. package/docs/retired-history-maintenance.md +252 -0
  95. package/package.json +28 -15
  96. package/scripts/archive-retired-history.mjs +464 -0
  97. package/scripts/build.mjs +48 -28
  98. package/scripts/measure-size.mjs +1 -1
  99. package/scripts/native-package-smoke.mjs +153 -12
  100. package/scripts/package-smoke.mjs +80 -11
  101. package/scripts/prepare.mjs +2 -2
  102. package/scripts/real-pi-live.mjs +377 -0
  103. package/scripts/real-pi-processes.mjs +6 -1
  104. package/scripts/real-pi-smoke.mjs +6 -282
  105. package/scripts/run-tests.mjs +4 -2
  106. package/scripts/typescript.mjs +35 -0
  107. package/skills/pi-intercom/SKILL.md +2 -1
  108. package/skills/pi-subagents/SKILL.md +23 -5
  109. package/src/agent/child-runtime.ts +105 -0
  110. package/src/agent/contracts.ts +18 -34
  111. package/src/agent/host.ts +39 -20
  112. package/src/agent/index.ts +234 -207
  113. package/src/agent/journal-tail.ts +141 -153
  114. package/src/agent/launch.ts +33 -9
  115. package/src/agent/legacy.ts +85 -31
  116. package/src/agent/notifications.ts +192 -67
  117. package/src/agent/observation-state.ts +277 -0
  118. package/src/agent/observation-view.ts +267 -0
  119. package/src/agent/observation.ts +409 -0
  120. package/src/agent/observe-card.ts +304 -0
  121. package/src/agent/observe-chrome.ts +170 -0
  122. package/src/agent/observe-client.ts +318 -0
  123. package/src/agent/observe-layout.ts +79 -0
  124. package/src/agent/observe-reading.ts +97 -0
  125. package/src/agent/observe-screen.ts +443 -129
  126. package/src/agent/observe-transcript.ts +434 -0
  127. package/src/agent/observe.ts +304 -71
  128. package/src/agent/parking.ts +341 -0
  129. package/src/agent/process.ts +280 -37
  130. package/src/agent/receipts.ts +114 -24
  131. package/src/agent/runner.ts +227 -149
  132. package/src/agent/shell.ts +2 -1
  133. package/src/agent/storage.ts +44 -11
  134. package/src/agent/tool-schemas.ts +33 -0
  135. package/src/agent/typebox.ts +12 -7
  136. package/src/agent/usage.ts +0 -130
  137. package/src/agent/view.ts +70 -20
  138. package/src/agent/wake.ts +107 -0
  139. package/src/intercom-client.ts +108 -0
  140. package/src/pi-intercom/bridge-session.ts +2 -2
  141. package/src/pi-intercom/broker/broker.ts +64 -53
  142. package/src/pi-intercom/broker/client-last.ts +65 -12
  143. package/src/pi-intercom/broker/client-park.ts +23 -0
  144. package/src/pi-intercom/broker/client.ts +63 -45
  145. package/src/pi-intercom/broker/framing.ts +11 -8
  146. package/src/pi-intercom/broker/guard.ts +33 -0
  147. package/src/pi-intercom/broker/last.ts +131 -43
  148. package/src/pi-intercom/broker/parked.ts +130 -0
  149. package/src/pi-intercom/broker/paths.ts +39 -12
  150. package/src/pi-intercom/broker/pid.ts +6 -1
  151. package/src/pi-intercom/broker/server-connection.ts +27 -2
  152. package/src/pi-intercom/connection.ts +103 -39
  153. package/src/pi-intercom/identity.ts +8 -5
  154. package/src/pi-intercom/inbound-delivery.ts +20 -66
  155. package/src/pi-intercom/inbound-journal.ts +103 -7
  156. package/src/pi-intercom/inbound-record.ts +21 -4
  157. package/src/pi-intercom/inbound.ts +32 -49
  158. package/src/pi-intercom/index.ts +44 -0
  159. package/src/pi-intercom/last-message.ts +11 -20
  160. package/src/pi-intercom/lifecycle.ts +117 -14
  161. package/src/pi-intercom/outbound.ts +7 -16
  162. package/src/pi-intercom/reply-tracker.ts +8 -6
  163. package/src/pi-intercom/reply-wait.ts +3 -2
  164. package/src/pi-intercom/runtime-types.ts +0 -1
  165. package/src/pi-intercom/tool-registration.ts +4 -1
  166. package/src/pi-intercom/tool-results.ts +10 -9
  167. package/src/pi-intercom/tool-schemas.ts +12 -44
  168. package/src/pi-intercom/types.ts +115 -4
  169. package/src/pi-intercom/validation.ts +4 -0
  170. package/src/shared/bounded-json.ts +162 -0
  171. package/src/shared/compact-cards.ts +118 -0
  172. package/src/shared/journal-frames.ts +3 -19
  173. package/src/shared/json-projection.ts +267 -147
  174. package/src/shared/native-journal.ts +157 -75
  175. package/src/shared/parking.ts +22 -0
  176. package/src/shared/post-exit-stdio-guard.ts +165 -7
  177. package/src/shared/tui-action.ts +39 -0
  178. package/tsconfig.client.json +8 -0
  179. package/tsconfig.json +1 -0
  180. package/src/shared/stream-json.d.ts +0 -10
@@ -1,10 +1,10 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import { receiptId } from "./receipts.js";
3
+ import { completionReceipts } from "./receipts.js";
4
4
  import { readOutputPage } from "../shared/output-page.js";
5
- import { isRecord } from "../shared/unknown.js";
5
+ import { journalStamp } from "../shared/native-journal.js";
6
6
  import { launchAt, list, runDirectory } from "./storage.js";
7
- import { readResult } from "./usage.js";
7
+ import { readCanonicalResult, readResult } from "./usage.js";
8
8
  function queuedAdmissions() {
9
9
  const store = globalThis;
10
10
  const existing = store.__pi_single_agent_admissions__;
@@ -22,7 +22,7 @@ function queuedAdmissions() {
22
22
  return queued;
23
23
  }
24
24
  const admissions = queuedAdmissions();
25
- export function hold(owner, id) {
25
+ function hold(owner, id) {
26
26
  const key = `${owner}/${id}`;
27
27
  if (admissions.has(key)) {
28
28
  return false;
@@ -36,52 +36,51 @@ export function release(owner, id) {
36
36
  export function isHeld(owner, id) {
37
37
  return admissions.has(`${owner}/${id}`);
38
38
  }
39
+ /** Returns whether any admission was released for reconciliation. */
39
40
  export function settle(owner, pending) {
40
41
  if (pending) {
41
- return;
42
+ return false;
42
43
  }
44
+ let released = false;
43
45
  for (const key of admissions) {
44
46
  if (key.startsWith(`${owner}/`)) {
45
47
  admissions.delete(key);
48
+ released = true;
46
49
  }
47
50
  }
51
+ return released;
48
52
  }
49
- function matches(value, launch) {
50
- return (isRecord(value) &&
51
- value.owner === launch.owner &&
52
- value.id === launch.id &&
53
- value.completed === true);
54
- }
55
- function toolReceipt(message, launch) {
56
- if (!isRecord(message) || message.role !== "toolResult") {
57
- return false;
58
- }
59
- return ((message.toolName === "delegate" || message.toolName === "agent_runs") &&
60
- isRecord(message.details) &&
61
- message.details.receivedResult === true &&
62
- matches(message.details, launch));
53
+ export function published(ctx, launch) {
54
+ return completionReceipts(ctx.sessionManager.getSessionFile(), launch.owner).get(launch.id);
63
55
  }
64
- function receipt(body, launch) {
65
- if (!isRecord(body)) {
66
- return false;
67
- }
68
- if (body.type === "custom_message" && body.customType === "agent-completion") {
69
- return matches(body.details, launch);
56
+ export function deliver(pi, ctx, notices) {
57
+ const idle = ctx.isIdle();
58
+ for (const [index, notice] of notices.entries()) {
59
+ hold(notice.owner, notice.id);
60
+ try {
61
+ // Pi defers settlement-triggered runs separately. Publish the idle batch first,
62
+ // then trigger once so no unstarted wakeup can outlive its admission hold.
63
+ pi.sendMessage(notice.message, {
64
+ triggerTurn: !idle || index === notices.length - 1,
65
+ deliverAs: "steer",
66
+ });
67
+ }
68
+ catch (error) {
69
+ release(notice.owner, notice.id);
70
+ throw error;
71
+ }
70
72
  }
71
- return body.type === "message" && toolReceipt(body.message, launch);
72
- }
73
- export function published(ctx, launch) {
74
- return receiptId(ctx.sessionManager.getSessionFile(), launch.owner, (body) => receipt(body, launch));
75
73
  }
76
- function send(pi, base, launch, result) {
74
+ function notification(base, launch, result) {
77
75
  const file = path.join(runDirectory(base, launch.id), "output.txt");
78
76
  const page = fs.existsSync(file) ? readOutputPage(file, { offset: 0, length: 32768 }) : undefined;
79
77
  const more = page?.nextOffset !== undefined
80
78
  ? `\n[More output: agent_runs inspect id=${launch.id} offset=${page.nextOffset}; ${page.size} bytes total.]`
81
79
  : "";
82
- hold(launch.owner, launch.id);
83
- try {
84
- pi.sendMessage({
80
+ return {
81
+ owner: launch.owner,
82
+ id: launch.id,
83
+ message: {
85
84
  customType: "agent-completion",
86
85
  display: true,
87
86
  content: `Agent run ${launch.id}: ${result.status}. ${result.error ?? ""}\n${page?.text ?? ""}${more}\nNative journal: ${launch.sessionFile}`,
@@ -93,27 +92,119 @@ function send(pi, base, launch, result) {
93
92
  outputFile: file,
94
93
  page,
95
94
  },
96
- }, { triggerTurn: true, deliverAs: "followUp" });
95
+ },
96
+ };
97
+ }
98
+ // Validated settled runs by file stamp, so polling does not reparse unchanged history.
99
+ const settledRuns = new Map();
100
+ function stamp(file) {
101
+ const stat = fs.statSync(file, { bigint: true, throwIfNoEntry: false });
102
+ return stat === undefined ? undefined : journalStamp(stat);
103
+ }
104
+ function pendingJournal(file) {
105
+ try {
106
+ return { file, stamp: stamp(file) };
97
107
  }
98
- catch (error) {
99
- admissions.delete(`${launch.owner}/${launch.id}`);
100
- throw error;
108
+ catch {
109
+ // Unavailable metadata cannot validate a cached attempt or hide execution.
110
+ return undefined;
101
111
  }
102
112
  }
103
- export function notify(pi, ctx, base, owner) {
104
- for (const id of list(base)) {
105
- const launch = launchAt(base, id, owner);
106
- const result = readResult(base, launch);
107
- if (!result) {
108
- continue;
113
+ function unchangedPending(pending) {
114
+ if (pending === undefined) {
115
+ return true;
116
+ }
117
+ const journal = pending.journal;
118
+ if (journal === undefined) {
119
+ return false;
120
+ }
121
+ const current = pendingJournal(journal.file);
122
+ return current !== undefined && current.stamp === journal.stamp;
123
+ }
124
+ function load(base, id, owner) {
125
+ const launch = launchAt(base, id, owner);
126
+ const result = readCanonicalResult(base, launch);
127
+ if (result?.accountingPending === undefined) {
128
+ return { launch, result };
129
+ }
130
+ // Only pending runs need this pre-read stamp to observe concurrent publication.
131
+ // readResult revalidates canonical state afterward: a miss intentionally reads result.json twice.
132
+ const journal = pendingJournal(launch.sessionFile);
133
+ return { launch, result: readResult(base, launch), journal };
134
+ }
135
+ function cacheableResult(loaded) {
136
+ return loaded.result?.accountingPending === undefined || loaded.journal !== undefined;
137
+ }
138
+ function completedRun({ base, owner, known, settled }, id) {
139
+ const directory = runDirectory(base, id);
140
+ const launch = stamp(path.join(directory, "launch.json"));
141
+ const result = stamp(path.join(directory, "result.json"));
142
+ const prior = known.get(id);
143
+ if (prior !== undefined &&
144
+ prior.launch === launch &&
145
+ prior.result === result &&
146
+ unchangedPending(prior.pending)) {
147
+ settled.set(id, prior);
148
+ return { id, ...prior, loaded: prior.pending };
149
+ }
150
+ const loaded = load(base, id, owner);
151
+ if (loaded.result === undefined) {
152
+ return;
153
+ }
154
+ const order = { endedAt: loaded.result.endedAt, createdAt: loaded.launch.createdAt };
155
+ // Pending accounting retries on child changes, even after the completion receipt.
156
+ // Ordinary settled history retains its launch/result-only idle probes.
157
+ const pending = loaded.result.accountingPending === undefined ? undefined : loaded;
158
+ if (launch !== undefined && result !== undefined && cacheableResult(loaded)) {
159
+ settled.set(id, { launch, result, pending, ...order });
160
+ }
161
+ return { id, ...order, loaded };
162
+ }
163
+ function completed(base, owner, awaited) {
164
+ const key = JSON.stringify([base, owner]);
165
+ const scan = {
166
+ base,
167
+ owner,
168
+ known: settledRuns.get(key) ?? new Map(),
169
+ settled: new Map(),
170
+ };
171
+ const runs = list(base).flatMap((id) => {
172
+ const run = completedRun(scan, id);
173
+ if (run === undefined) {
174
+ awaited.add(path.join(runDirectory(base, id), "result.json"));
109
175
  }
110
- const key = `${owner}/${id}`;
111
- if (published(ctx, launch) !== undefined) {
176
+ else if (run.loaded?.result?.accountingPending !== undefined) {
177
+ awaited.add(run.loaded.launch.sessionFile);
178
+ }
179
+ return run ?? [];
180
+ });
181
+ settledRuns.delete(key);
182
+ settledRuns.set(key, scan.settled);
183
+ if (settledRuns.size > 8) {
184
+ settledRuns.delete(settledRuns.keys().next().value ?? key);
185
+ }
186
+ return runs.sort((a, b) => {
187
+ if (a.endedAt !== b.endedAt) {
188
+ return a.endedAt - b.endedAt;
189
+ }
190
+ return a.createdAt !== b.createdAt ? a.createdAt - b.createdAt : a.id.localeCompare(b.id);
191
+ });
192
+ }
193
+ export function notifications(ctx, base, owner) {
194
+ const notices = [];
195
+ const awaited = new Set();
196
+ const runs = completed(base, owner, awaited);
197
+ const receipts = runs.length === 0 ? undefined : completionReceipts(ctx.sessionManager.getSessionFile(), owner);
198
+ for (const run of runs) {
199
+ const key = `${owner}/${run.id}`;
200
+ if (receipts?.get(run.id) !== undefined) {
112
201
  admissions.delete(key);
113
202
  continue;
114
203
  }
115
- if (!admissions.has(key)) {
116
- send(pi, base, launch, result);
204
+ const loaded = admissions.has(key) ? undefined : (run.loaded ?? load(base, run.id, owner));
205
+ if (loaded?.result !== undefined) {
206
+ notices.push(notification(base, loaded.launch, loaded.result));
117
207
  }
118
208
  }
209
+ return { notices, awaited };
119
210
  }
@@ -0,0 +1,246 @@
1
+ import { boundedJson } from "../shared/bounded-json.js";
2
+ const VALUE_BUDGET = { bytes: 64 * 1024, nodes: 4096, depth: 32 };
3
+ /** Longer native identifiers are left to the journal rather than keyed live. */
4
+ export const NAME_CHARS = 1024;
5
+ const RETAINED = { settlements: 128, tools: 256 };
6
+ const WRITING = { text: "writing", thinking: "thinking", toolCall: "calling" };
7
+ export class LiveTool {
8
+ partial;
9
+ rev = 0;
10
+ ended;
11
+ id;
12
+ name;
13
+ args;
14
+ argsJson;
15
+ partialJson;
16
+ resultJson;
17
+ constructor(id, name, args) {
18
+ this.id = id;
19
+ this.name = name;
20
+ this.args = args;
21
+ }
22
+ /** Replaceable progress: only the latest reference is projected at the next flush. */
23
+ progress(value) {
24
+ this.partial = value;
25
+ this.rev++;
26
+ }
27
+ projectedArgs(project) {
28
+ this.argsJson ??= project(this.args);
29
+ return this.argsJson;
30
+ }
31
+ projectedPartial(project) {
32
+ if (this.partialJson?.rev !== this.rev) {
33
+ this.partialJson = { rev: this.rev, json: project(this.partial) };
34
+ }
35
+ return this.partialJson.json;
36
+ }
37
+ projectedResult(project) {
38
+ this.resultJson ??= project(this.ended?.result);
39
+ return this.resultJson;
40
+ }
41
+ }
42
+ /**
43
+ * Single-writer actor state. Sparse tool hooks always keep the tool table, because native state
44
+ * retains neither progress, unpublished parallel results nor nested calls. Token-rate message
45
+ * events arrive only while a detail viewer observes; attaching seeds from public native state.
46
+ */
47
+ export class LiveState {
48
+ version = 0;
49
+ activityVersion = 0;
50
+ message;
51
+ tools = new Map();
52
+ settlements = [];
53
+ settled = 0;
54
+ /** Live items dropped by a retention bound or a branch change; viewers resynchronize. */
55
+ evicted = 0;
56
+ events = 0;
57
+ toolEvents = 0;
58
+ progressUpdates = 0;
59
+ projections = 0;
60
+ unavailable = 0;
61
+ observing = false;
62
+ nextKey = 0;
63
+ queued = false;
64
+ closed = false;
65
+ watermark;
66
+ session;
67
+ changed;
68
+ failed;
69
+ constructor(session, changed, failed) {
70
+ this.session = session;
71
+ this.changed = changed;
72
+ this.failed = failed;
73
+ this.watermark = session.sessionManager.getLeafId();
74
+ }
75
+ /** Start or stop the transient message view; tools persist across viewers. */
76
+ observe(on) {
77
+ this.resolve();
78
+ this.observing = on;
79
+ this.message = undefined;
80
+ this.settlements.length = 0;
81
+ if (on) {
82
+ const streaming = this.session.state.streamingMessage;
83
+ if (streaming?.role === "assistant") {
84
+ this.assistant("message_start", streaming);
85
+ }
86
+ }
87
+ this.version++;
88
+ this.activityVersion++;
89
+ }
90
+ apply(event) {
91
+ this.events++;
92
+ if (event.type === "message_start" ||
93
+ event.type === "message_update" ||
94
+ event.type === "message_end") {
95
+ this.messageEvent(event);
96
+ }
97
+ }
98
+ messageEvent(event) {
99
+ if (event.message.role === "assistant") {
100
+ this.assistant(event.type, event.message);
101
+ }
102
+ if (event.type === "message_end" && !this.queued) {
103
+ this.queued = true;
104
+ // Native message_end listeners run just before the same job appends the entry.
105
+ queueMicrotask(this.settle);
106
+ }
107
+ }
108
+ assistant(type, message) {
109
+ let live = this.message;
110
+ if (type === "message_start" || live === undefined) {
111
+ // A previous message still here was never found in the session tree.
112
+ this.evicted += live === undefined ? 0 : 1;
113
+ live = { key: ++this.nextKey, message, ended: false };
114
+ this.message = live;
115
+ }
116
+ live.message = message;
117
+ live.ended = type === "message_end";
118
+ this.version++;
119
+ }
120
+ tool(event) {
121
+ this.toolEvents++;
122
+ this.version++;
123
+ if (event.type === "tool_execution_start") {
124
+ this.resolve();
125
+ this.startTool(event.toolCallId, event.toolName, event.args);
126
+ return;
127
+ }
128
+ const tool = this.tools.get(event.toolCallId);
129
+ if (event.type === "tool_execution_update") {
130
+ tool?.progress(event.partialResult);
131
+ this.progressUpdates++;
132
+ }
133
+ else if (tool) {
134
+ tool.ended = { isError: event.isError, result: event.result };
135
+ this.activityVersion++;
136
+ }
137
+ }
138
+ startTool(id, name, args) {
139
+ if (id.length > NAME_CHARS) {
140
+ this.unavailable++;
141
+ return;
142
+ }
143
+ const oldest = this.tools.size >= RETAINED.tools ? this.tools.keys().next() : undefined;
144
+ if (oldest?.done === false) {
145
+ this.tools.delete(oldest.value);
146
+ this.evicted++;
147
+ }
148
+ this.tools.set(id, new LiveTool(id, name, args));
149
+ this.activityVersion++;
150
+ }
151
+ settle = () => {
152
+ this.queued = false;
153
+ try {
154
+ if (!this.closed && this.resolve()) {
155
+ this.changed();
156
+ }
157
+ }
158
+ catch (error) {
159
+ this.failed(error);
160
+ }
161
+ };
162
+ /**
163
+ * Hand native messages appended since the last walk over to their session entries. A live
164
+ * message without an entry is unresolved publication and stays live. Returns whether anything
165
+ * changed.
166
+ */
167
+ resolve() {
168
+ const { appended, departed } = this.appended();
169
+ this.evicted += departed && this.observing ? 1 : 0;
170
+ for (const entry of appended) {
171
+ this.publish(entry, !departed && this.observing);
172
+ }
173
+ this.settlements.splice(0, Math.max(0, this.settlements.length - RETAINED.settlements));
174
+ const changed = appended.length > 0;
175
+ this.version += changed ? 1 : 0;
176
+ this.activityVersion += changed ? 1 : 0;
177
+ return changed;
178
+ }
179
+ /** Message entries since the previous walk, in journal order; each append is visited once. */
180
+ appended() {
181
+ const manager = this.session.sessionManager;
182
+ const appended = [];
183
+ let entry = manager.getLeafEntry();
184
+ const leaf = entry?.id ?? null;
185
+ while (entry !== undefined && entry.id !== this.watermark) {
186
+ if (entry.type === "message") {
187
+ appended.push(entry);
188
+ }
189
+ entry = entry.parentId === null ? undefined : manager.getEntry(entry.parentId);
190
+ }
191
+ // A leaf off the previous branch walked to the root once; its entries are not new.
192
+ const departed = entry === undefined && this.watermark !== null;
193
+ this.watermark = leaf;
194
+ return { appended: appended.reverse(), departed };
195
+ }
196
+ publish({ id, message }, settle) {
197
+ const call = message.role === "toolResult" ? message.toolCallId : "";
198
+ const tool = call.length > 0 && call.length <= NAME_CHARS ? call : null;
199
+ // A published result also retires the calls it made through ctx.executeTool().
200
+ const retired = (key) => tool !== null && (key === tool || key.startsWith(`${tool}/`));
201
+ for (const live of [...this.tools.keys()].filter(retired)) {
202
+ this.tools.delete(live);
203
+ }
204
+ const key = this.message?.message === message ? this.message.key : null;
205
+ this.message = key === null ? this.message : undefined;
206
+ if (settle) {
207
+ const seq = ++this.settled;
208
+ this.settlements.push({ seq, json: JSON.stringify([seq, id, key, tool]) });
209
+ }
210
+ }
211
+ dispose() {
212
+ this.closed = true;
213
+ }
214
+ project = (value) => {
215
+ this.projections++;
216
+ const result = boundedJson(value, VALUE_BUDGET);
217
+ if ("json" in result) {
218
+ return `{"json":${result.json}}`;
219
+ }
220
+ this.unavailable++;
221
+ return `{"unavailable":"${result.unavailable}"}`;
222
+ };
223
+ /** Sampled from public native state, so compact peers never need token-rate events. */
224
+ activity(busy) {
225
+ const { session } = this;
226
+ const running = [...this.tools.values()].filter((tool) => tool.ended === undefined);
227
+ const streaming = session.state.streamingMessage;
228
+ const content = streaming?.role === "assistant" ? streaming.content : undefined;
229
+ const last = content?.at(-1)?.type;
230
+ const phases = [
231
+ [running.length > 0, "tool"],
232
+ [content !== undefined, last === undefined ? "responding" : WRITING[last]],
233
+ [session.isCompacting, "compacting"],
234
+ [session.isRetrying, "retrying"],
235
+ [busy || session.isStreaming, "working"],
236
+ ];
237
+ const phase = phases.find(([active]) => active)?.[1] ?? "idle";
238
+ return JSON.stringify({
239
+ phase,
240
+ tools: running.slice(0, 3).map((tool) => tool.name.slice(0, 64)),
241
+ running: running.length,
242
+ model: this.session.model?.id,
243
+ thinking: this.session.thinkingLevel,
244
+ });
245
+ }
246
+ }