@fitchmultz/pi-subagents 0.50.0 → 0.51.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 (169) hide show
  1. package/CHANGELOG.md +51 -2
  2. package/README.md +36 -5
  3. package/dist/agent/contracts.js +6 -24
  4. package/dist/agent/host.js +3 -16
  5. package/dist/agent/index.js +207 -173
  6. package/dist/agent/journal-tail.js +115 -156
  7. package/dist/agent/launch.js +20 -8
  8. package/dist/agent/legacy.js +65 -23
  9. package/dist/agent/notifications.js +138 -47
  10. package/dist/agent/observation-state.js +246 -0
  11. package/dist/agent/observation-view.js +258 -0
  12. package/dist/agent/observation.js +346 -0
  13. package/dist/agent/observe-card.js +243 -0
  14. package/dist/agent/observe-chrome.js +85 -0
  15. package/dist/agent/observe-client.js +285 -0
  16. package/dist/agent/observe-layout.js +65 -0
  17. package/dist/agent/observe-reading.js +84 -0
  18. package/dist/agent/observe-screen.js +400 -96
  19. package/dist/agent/observe-transcript.js +382 -0
  20. package/dist/agent/observe.js +250 -43
  21. package/dist/agent/process.js +198 -25
  22. package/dist/agent/receipts.js +92 -20
  23. package/dist/agent/runner.js +125 -20
  24. package/dist/agent/shell.js +2 -1
  25. package/dist/agent/storage.js +29 -12
  26. package/dist/agent/tool-schemas.js +20 -0
  27. package/dist/agent/typebox.js +10 -5
  28. package/dist/agent/usage.js +0 -115
  29. package/dist/agent/view.js +46 -16
  30. package/dist/agent/wake.js +99 -0
  31. package/dist/build-info.js +1 -1
  32. package/dist/intercom-client.d.ts +26 -0
  33. package/dist/intercom-client.js +45 -0
  34. package/dist/pi-intercom/agent-dir.d.ts +1 -0
  35. package/dist/pi-intercom/bridge-session.js +2 -1
  36. package/dist/pi-intercom/broker/broker.js +27 -21
  37. package/dist/pi-intercom/broker/client-last.d.ts +13 -0
  38. package/dist/pi-intercom/broker/client-last.js +17 -5
  39. package/dist/pi-intercom/broker/client.d.ts +60 -0
  40. package/dist/pi-intercom/broker/client.js +47 -16
  41. package/dist/pi-intercom/broker/connection.d.ts +29 -0
  42. package/dist/pi-intercom/broker/framing.d.ts +17 -0
  43. package/dist/pi-intercom/broker/framing.js +10 -7
  44. package/dist/pi-intercom/broker/guard.js +23 -0
  45. package/dist/pi-intercom/broker/last.d.ts +26 -0
  46. package/dist/pi-intercom/broker/last.js +57 -32
  47. package/dist/pi-intercom/broker/paths.d.ts +13 -0
  48. package/dist/pi-intercom/broker/paths.js +35 -12
  49. package/dist/pi-intercom/broker/pid.js +5 -1
  50. package/dist/pi-intercom/broker/server-connection.js +19 -0
  51. package/dist/pi-intercom/broker/snapshot-reader.d.ts +9 -0
  52. package/dist/pi-intercom/broker/snapshots.d.ts +29 -0
  53. package/dist/pi-intercom/broker/targets.d.ts +9 -0
  54. package/dist/pi-intercom/connection.js +10 -2
  55. package/dist/pi-intercom/identity.js +6 -5
  56. package/dist/pi-intercom/inbound-delivery.js +16 -60
  57. package/dist/pi-intercom/inbound-journal.js +83 -6
  58. package/dist/pi-intercom/inbound-record.js +18 -1
  59. package/dist/pi-intercom/inbound.js +12 -38
  60. package/dist/pi-intercom/index.js +3 -0
  61. package/dist/pi-intercom/last-message.js +4 -16
  62. package/dist/pi-intercom/lifecycle.js +88 -3
  63. package/dist/pi-intercom/outbound.js +5 -15
  64. package/dist/pi-intercom/reply-tracker.js +7 -6
  65. package/dist/pi-intercom/reply-wait.js +2 -2
  66. package/dist/pi-intercom/session-targets.d.ts +24 -0
  67. package/dist/pi-intercom/tool-registration.js +4 -1
  68. package/dist/pi-intercom/tool-results.js +5 -7
  69. package/dist/pi-intercom/tool-schemas.js +12 -26
  70. package/dist/pi-intercom/types.d.ts +212 -0
  71. package/dist/pi-intercom/types.js +54 -3
  72. package/dist/pi-intercom/validation.d.ts +12 -0
  73. package/dist/pi-intercom/validation.js +3 -0
  74. package/dist/shared/bounded-json.js +151 -0
  75. package/dist/shared/compact-cards.js +66 -0
  76. package/dist/shared/journal-frames.js +3 -6
  77. package/dist/shared/json-projection.js +267 -129
  78. package/dist/shared/native-journal.js +123 -56
  79. package/dist/shared/post-exit-stdio-guard.js +120 -7
  80. package/dist/shared/tui-action.js +29 -0
  81. package/dist/shared/unknown.d.ts +9 -0
  82. package/docs/code-quality.md +2 -0
  83. package/docs/development.md +40 -1
  84. package/docs/intercom-bridge.md +9 -4
  85. package/docs/intercom.md +226 -6
  86. package/docs/performance.md +87 -11
  87. package/docs/reference.md +157 -20
  88. package/docs/retired-history-maintenance.md +252 -0
  89. package/package.json +28 -15
  90. package/scripts/archive-retired-history.mjs +464 -0
  91. package/scripts/build.mjs +48 -28
  92. package/scripts/measure-size.mjs +1 -1
  93. package/scripts/native-package-smoke.mjs +153 -12
  94. package/scripts/package-smoke.mjs +80 -11
  95. package/scripts/prepare.mjs +2 -2
  96. package/scripts/real-pi-live.mjs +377 -0
  97. package/scripts/real-pi-processes.mjs +6 -1
  98. package/scripts/real-pi-smoke.mjs +6 -282
  99. package/scripts/run-tests.mjs +4 -2
  100. package/scripts/typescript.mjs +35 -0
  101. package/skills/pi-intercom/SKILL.md +2 -1
  102. package/skills/pi-subagents/SKILL.md +23 -5
  103. package/src/agent/contracts.ts +8 -34
  104. package/src/agent/host.ts +19 -20
  105. package/src/agent/index.ts +224 -190
  106. package/src/agent/journal-tail.ts +141 -153
  107. package/src/agent/launch.ts +33 -9
  108. package/src/agent/legacy.ts +85 -31
  109. package/src/agent/notifications.ts +192 -67
  110. package/src/agent/observation-state.ts +277 -0
  111. package/src/agent/observation-view.ts +267 -0
  112. package/src/agent/observation.ts +405 -0
  113. package/src/agent/observe-card.ts +304 -0
  114. package/src/agent/observe-chrome.ts +170 -0
  115. package/src/agent/observe-client.ts +318 -0
  116. package/src/agent/observe-layout.ts +79 -0
  117. package/src/agent/observe-reading.ts +97 -0
  118. package/src/agent/observe-screen.ts +443 -129
  119. package/src/agent/observe-transcript.ts +434 -0
  120. package/src/agent/observe.ts +304 -71
  121. package/src/agent/process.ts +240 -34
  122. package/src/agent/receipts.ts +114 -24
  123. package/src/agent/runner.ts +125 -27
  124. package/src/agent/shell.ts +2 -1
  125. package/src/agent/storage.ts +27 -11
  126. package/src/agent/tool-schemas.ts +33 -0
  127. package/src/agent/typebox.ts +12 -7
  128. package/src/agent/usage.ts +0 -130
  129. package/src/agent/view.ts +70 -20
  130. package/src/agent/wake.ts +107 -0
  131. package/src/intercom-client.ts +108 -0
  132. package/src/pi-intercom/bridge-session.ts +2 -2
  133. package/src/pi-intercom/broker/broker.ts +27 -23
  134. package/src/pi-intercom/broker/client-last.ts +25 -12
  135. package/src/pi-intercom/broker/client.ts +55 -33
  136. package/src/pi-intercom/broker/framing.ts +11 -8
  137. package/src/pi-intercom/broker/guard.ts +33 -0
  138. package/src/pi-intercom/broker/last.ts +86 -33
  139. package/src/pi-intercom/broker/paths.ts +39 -12
  140. package/src/pi-intercom/broker/pid.ts +6 -1
  141. package/src/pi-intercom/broker/server-connection.ts +25 -0
  142. package/src/pi-intercom/connection.ts +21 -10
  143. package/src/pi-intercom/identity.ts +8 -5
  144. package/src/pi-intercom/inbound-delivery.ts +20 -66
  145. package/src/pi-intercom/inbound-journal.ts +103 -7
  146. package/src/pi-intercom/inbound-record.ts +21 -4
  147. package/src/pi-intercom/inbound.ts +18 -49
  148. package/src/pi-intercom/index.ts +3 -0
  149. package/src/pi-intercom/last-message.ts +10 -19
  150. package/src/pi-intercom/lifecycle.ts +117 -14
  151. package/src/pi-intercom/outbound.ts +7 -16
  152. package/src/pi-intercom/reply-tracker.ts +8 -6
  153. package/src/pi-intercom/reply-wait.ts +3 -2
  154. package/src/pi-intercom/runtime-types.ts +0 -1
  155. package/src/pi-intercom/tool-registration.ts +4 -1
  156. package/src/pi-intercom/tool-results.ts +10 -9
  157. package/src/pi-intercom/tool-schemas.ts +12 -44
  158. package/src/pi-intercom/types.ts +115 -4
  159. package/src/pi-intercom/validation.ts +4 -0
  160. package/src/shared/bounded-json.ts +162 -0
  161. package/src/shared/compact-cards.ts +118 -0
  162. package/src/shared/journal-frames.ts +3 -19
  163. package/src/shared/json-projection.ts +267 -147
  164. package/src/shared/native-journal.ts +157 -75
  165. package/src/shared/post-exit-stdio-guard.ts +165 -7
  166. package/src/shared/tui-action.ts +39 -0
  167. package/tsconfig.client.json +8 -0
  168. package/tsconfig.json +1 -0
  169. package/src/shared/stream-json.d.ts +0 -10
@@ -2,6 +2,7 @@ import {
2
2
  finiteNumber,
3
3
  isRecord,
4
4
  isUnknownArray,
5
+ nonNegativeInteger,
5
6
  optionalBoolean,
6
7
  optionalFiniteNumber,
7
8
  optionalLabel,
@@ -18,6 +19,9 @@ export interface SessionInfo {
18
19
  readonly projectId?: string;
19
20
  readonly rootSessionId?: string;
20
21
  readonly parentSessionId?: string;
22
+ readonly incarnation?: string;
23
+ readonly guardedDelivery?: 1;
24
+ readonly applicationGuard?: string;
21
25
  readonly status?: string;
22
26
  readonly lastSeen?: number;
23
27
  readonly lastIntercomActivity?: number;
@@ -83,6 +87,42 @@ export function isTopicUpdate(value: unknown): value is TopicUpdate {
83
87
 
84
88
  export type MessageDelivery = "queue" | "steer" | "passive";
85
89
  export type QueueMode = "stack" | "replace";
90
+ export interface ContextTarget {
91
+ readonly rootSessionId: string;
92
+ readonly peerId: string;
93
+ readonly incarnation: string;
94
+ }
95
+ export interface DeliveryGuard extends ContextTarget {
96
+ readonly applicationGuard: string;
97
+ }
98
+ export interface PublicationReplyHint {
99
+ readonly tool: string;
100
+ readonly commandId: string;
101
+ readonly commandFingerprint?: string;
102
+ }
103
+ export type SendOptions = Omit<Message, "id" | "timestamp" | "content"> &
104
+ Message["content"] & { readonly messageId?: string };
105
+ export function isContextTarget(value: unknown): value is ContextTarget {
106
+ return (
107
+ isRecord(value) &&
108
+ ["rootSessionId", "peerId", "incarnation"].every((key) => safeLabel(value[key]))
109
+ );
110
+ }
111
+ export function isDeliveryGuard(value: unknown): value is DeliveryGuard {
112
+ return isContextTarget(value) && "applicationGuard" in value && safeLabel(value.applicationGuard);
113
+ }
114
+ export function isPublicationReplyHint(value: unknown): value is PublicationReplyHint {
115
+ return (
116
+ isRecord(value) &&
117
+ typeof value.tool === "string" &&
118
+ /^[a-zA-Z_][a-zA-Z0-9_]{0,79}$/.test(value.tool) &&
119
+ typeof value.commandId === "string" &&
120
+ /^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/.test(value.commandId) &&
121
+ (value.commandFingerprint === undefined ||
122
+ (typeof value.commandFingerprint === "string" &&
123
+ /^[a-f0-9]{64}$/.test(value.commandFingerprint)))
124
+ );
125
+ }
86
126
  export interface HumanMessageOrigin {
87
127
  readonly ownerSessionId: string;
88
128
  readonly runId: string;
@@ -115,6 +155,8 @@ export interface Message {
115
155
  readonly queueMode?: QueueMode;
116
156
  readonly threadId?: string;
117
157
  readonly passive?: boolean;
158
+ readonly guard?: DeliveryGuard;
159
+ readonly replyHint?: PublicationReplyHint;
118
160
  readonly content: {
119
161
  readonly text: string;
120
162
  readonly attachments?: readonly Attachment[];
@@ -137,6 +179,9 @@ export function isAttachment(value: unknown): value is Attachment {
137
179
  }
138
180
 
139
181
  function validMessageOrigin(message: Readonly<Record<string, unknown>>): boolean {
182
+ if (!validMessageGuard(message)) {
183
+ return false;
184
+ }
140
185
  if (message.human !== undefined && !isHumanMessageOrigin(message.human)) {
141
186
  return false;
142
187
  }
@@ -151,6 +196,20 @@ function validMessageOrigin(message: Readonly<Record<string, unknown>>): boolean
151
196
  );
152
197
  }
153
198
 
199
+ function validMessageGuard(message: Readonly<Record<string, unknown>>): boolean {
200
+ if (message.guard === undefined) {
201
+ return message.replyHint === undefined;
202
+ }
203
+ return (
204
+ isDeliveryGuard(message.guard) &&
205
+ message.expectsReply === false &&
206
+ message.replyTo === undefined &&
207
+ message.human === undefined &&
208
+ message.topic === undefined &&
209
+ (message.replyHint === undefined || isPublicationReplyHint(message.replyHint))
210
+ );
211
+ }
212
+
154
213
  function validDelivery(message: Readonly<Record<string, unknown>>): boolean {
155
214
  if (
156
215
  message.delivery !== undefined &&
@@ -254,8 +313,13 @@ function validSessionTopics(session: Readonly<Record<string, unknown>>): boolean
254
313
 
255
314
  function validSessionLineage(value: Readonly<Record<string, unknown>>): boolean {
256
315
  return (
257
- optionalLabel(value.rootSessionId) &&
258
- optionalLabel(value.parentSessionId) &&
316
+ ["rootSessionId", "parentSessionId", "incarnation", "applicationGuard"].every((key) =>
317
+ optionalLabel(value[key]),
318
+ ) &&
319
+ (value.guardedDelivery === undefined ||
320
+ (value.guardedDelivery === 1 &&
321
+ safeLabel(value.incarnation) &&
322
+ safeLabel(value.rootSessionId))) &&
259
323
  (value.parentSessionId === undefined || value.rootSessionId === value.parentSessionId)
260
324
  );
261
325
  }
@@ -361,6 +425,23 @@ export interface LastMessageQuery {
361
425
  readonly limit?: number;
362
426
  }
363
427
 
428
+ export const LAST_MESSAGE_PAGE_LIMIT = 16384;
429
+ function optionalEntryId(value: unknown): boolean {
430
+ return value === undefined || (typeof value === "string" && value.length > 0);
431
+ }
432
+ export function isLastMessageQuery(value: unknown): value is LastMessageQuery {
433
+ return (
434
+ isRecord(value) &&
435
+ value.messageId === undefined &&
436
+ optionalEntryId(value.entryId) &&
437
+ (value.offset === undefined || nonNegativeInteger(value.offset)) &&
438
+ (value.limit === undefined ||
439
+ (nonNegativeInteger(value.limit) &&
440
+ value.limit > 0 &&
441
+ value.limit <= LAST_MESSAGE_PAGE_LIMIT))
442
+ );
443
+ }
444
+
364
445
  export type LastMessageResult =
365
446
  | {
366
447
  readonly session: SessionInfo;
@@ -372,6 +453,34 @@ export type LastMessageResult =
372
453
  }
373
454
  | { readonly error: string };
374
455
 
456
+ export interface ReceiptQuery {
457
+ readonly rootSessionId: string;
458
+ readonly messageId: string;
459
+ }
460
+ export function isReceiptQuery(value: unknown): value is ReceiptQuery {
461
+ return isRecord(value) && [value.rootSessionId, value.messageId].every(safeLabel);
462
+ }
463
+ export interface NativeReceipt extends ReceiptQuery {
464
+ readonly version: 1;
465
+ readonly sender: {
466
+ readonly peerId: string;
467
+ readonly rootSessionId?: string;
468
+ readonly incarnation?: string;
469
+ };
470
+ readonly textSha256: string;
471
+ readonly delivery: MessageDelivery;
472
+ readonly guard?: DeliveryGuard;
473
+ readonly replyHint?: PublicationReplyHint;
474
+ readonly checkpointEntryId: string;
475
+ readonly receivedAt: number;
476
+ /** Historical observations, not a promise that an input is still queued. */
477
+ readonly queued: boolean;
478
+ readonly nativeHandoff: boolean;
479
+ readonly placementEntryId: string | null;
480
+ readonly discarded: boolean;
481
+ }
482
+ export type ReceiptResult = { readonly receipt: NativeReceipt } | { readonly error: string };
483
+
375
484
  export type BrokerMessage =
376
485
  | {
377
486
  readonly type: "registered";
@@ -379,6 +488,8 @@ export type BrokerMessage =
379
488
  readonly topicsSupported?: true;
380
489
  readonly topicFrames?: true;
381
490
  readonly lastMessages?: true;
491
+ readonly receiptReads?: 1;
492
+ readonly guardedDelivery?: 1;
382
493
  }
383
494
  | {
384
495
  readonly type: "sessions";
@@ -397,12 +508,12 @@ export type BrokerMessage =
397
508
  readonly type: "last_request";
398
509
  readonly requestId: string;
399
510
  readonly from: SessionInfo;
400
- readonly query: LastMessageQuery;
511
+ readonly query: LastMessageQuery | ReceiptQuery;
401
512
  }
402
513
  | {
403
514
  readonly type: "last_response";
404
515
  readonly requestId: string;
405
- readonly result: LastMessageResult;
516
+ readonly result: LastMessageResult | ReceiptResult;
406
517
  }
407
518
  | { readonly type: "session_left"; readonly sessionId: string }
408
519
  | { readonly type: "delivered"; readonly messageId: string }
@@ -64,6 +64,10 @@ export function finiteNumber(value: unknown): value is number {
64
64
  return typeof value === "number" && Number.isFinite(value);
65
65
  }
66
66
 
67
+ export function nonNegativeInteger(value: unknown): value is number {
68
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
69
+ }
70
+
67
71
  export function optionalFiniteNumber(value: unknown): boolean {
68
72
  return value === undefined || finiteNumber(value);
69
73
  }
@@ -0,0 +1,162 @@
1
+ import { types } from "node:util";
2
+
3
+ /** Live previews of arbitrary native values; durable journal records remain authoritative. */
4
+ export type Unavailable = "oversize" | "cyclic" | "unreadable" | "unsupported";
5
+ export type Bounded = { readonly json: string } | { readonly unavailable: Unavailable };
6
+ export interface JsonBudget {
7
+ readonly bytes: number;
8
+ readonly nodes: number;
9
+ readonly depth: number;
10
+ }
11
+ class Limit extends Error {
12
+ readonly reason: Unavailable;
13
+ constructor(reason: Unavailable) {
14
+ super(reason);
15
+ this.reason = reason;
16
+ }
17
+ }
18
+
19
+ /**
20
+ * Proxies, accessors and custom serializers would run arbitrary code inside the actor; binary
21
+ * values allocate their full representation. None of them is read.
22
+ */
23
+ function opaque(value: object): boolean {
24
+ return (
25
+ types.isProxy(value) ||
26
+ ArrayBuffer.isView(value) ||
27
+ types.isAnyArrayBuffer(value) ||
28
+ "toJSON" in value
29
+ );
30
+ }
31
+ /** An own data property's value, without invoking a getter. */
32
+ function data(value: object, key: string): unknown {
33
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
34
+ if (descriptor !== undefined && !("value" in descriptor)) {
35
+ throw new Limit("unsupported");
36
+ }
37
+ return descriptor?.value;
38
+ }
39
+
40
+ /** Serialize like JSON.stringify within byte, node and depth limits, without first cloning. */
41
+ class Writer {
42
+ private readonly parts: string[] = [];
43
+ private bytes = 0;
44
+ private nodes = 0;
45
+ private readonly ancestors = new Set<object>();
46
+ private readonly budget: JsonBudget;
47
+ constructor(budget: JsonBudget) {
48
+ this.budget = budget;
49
+ }
50
+ text(): string {
51
+ return this.parts.join("");
52
+ }
53
+ private add(text: string, bytes = text.length): void {
54
+ this.bytes += bytes;
55
+ if (this.bytes > this.budget.bytes) {
56
+ throw new Limit("oversize");
57
+ }
58
+ this.parts.push(text);
59
+ }
60
+ private string(value: string): void {
61
+ // A string never encodes to fewer bytes than code units; refuse before copying it.
62
+ if (value.length > this.budget.bytes - this.bytes) {
63
+ throw new Limit("oversize");
64
+ }
65
+ const json = JSON.stringify(value);
66
+ this.add(json, Buffer.byteLength(json));
67
+ }
68
+ /** Returns false for values JSON omits from objects and nulls in arrays. */
69
+ value(value: unknown, depth: number): boolean {
70
+ if (++this.nodes > this.budget.nodes) {
71
+ throw new Limit("oversize");
72
+ }
73
+ switch (typeof value) {
74
+ case "string":
75
+ this.string(value);
76
+ return true;
77
+ case "number":
78
+ this.add(Number.isFinite(value) ? String(value) : "null");
79
+ return true;
80
+ case "boolean":
81
+ this.add(String(value));
82
+ return true;
83
+ case "bigint":
84
+ throw new Limit("unsupported");
85
+ case "object":
86
+ this.object(value, depth);
87
+ return true;
88
+ case "undefined":
89
+ case "function":
90
+ case "symbol":
91
+ return false;
92
+ }
93
+ }
94
+ private object(value: object | null, depth: number): void {
95
+ if (value === null) {
96
+ this.add("null");
97
+ return;
98
+ }
99
+ if (depth >= this.budget.depth) {
100
+ throw new Limit("oversize");
101
+ }
102
+ if (this.ancestors.has(value)) {
103
+ throw new Limit("cyclic");
104
+ }
105
+ if (types.isDate(value) && !types.isProxy(value)) {
106
+ const time = Date.prototype.getTime.call(value);
107
+ this.add(Number.isNaN(time) ? "null" : JSON.stringify(new Date(time).toISOString()));
108
+ return;
109
+ }
110
+ if (opaque(value)) {
111
+ throw new Limit("unsupported");
112
+ }
113
+ this.ancestors.add(value);
114
+ if (Array.isArray(value)) {
115
+ this.array(value, depth + 1);
116
+ } else {
117
+ this.record(value, depth + 1);
118
+ }
119
+ this.ancestors.delete(value);
120
+ }
121
+ private array(value: readonly unknown[], depth: number): void {
122
+ this.add("[");
123
+ for (let index = 0; index < value.length; index++) {
124
+ if (index > 0) {
125
+ this.add(",");
126
+ }
127
+ if (!this.value(data(value, String(index)), depth)) {
128
+ this.add("null");
129
+ }
130
+ }
131
+ this.add("]");
132
+ }
133
+ private record(value: object, depth: number): void {
134
+ this.add("{");
135
+ let first = true;
136
+ for (const key of Object.keys(value)) {
137
+ const mark = { parts: this.parts.length, bytes: this.bytes };
138
+ if (!first) {
139
+ this.add(",");
140
+ }
141
+ this.string(key);
142
+ this.add(":");
143
+ if (this.value(data(value, key), depth)) {
144
+ first = false;
145
+ } else {
146
+ this.parts.length = mark.parts;
147
+ this.bytes = mark.bytes;
148
+ }
149
+ }
150
+ this.add("}");
151
+ }
152
+ }
153
+
154
+ export function boundedJson(value: unknown, budget: JsonBudget): Bounded {
155
+ const writer = new Writer(budget);
156
+ try {
157
+ return writer.value(value, 0) ? { json: writer.text() } : { unavailable: "unsupported" };
158
+ } catch (error) {
159
+ // Getters, proxies and revoked proxies are untrusted tool data, not observer failures.
160
+ return { unavailable: error instanceof Limit ? error.reason : "unreadable" };
161
+ }
162
+ }
@@ -0,0 +1,118 @@
1
+ import {
2
+ getMarkdownTheme,
3
+ keyHint,
4
+ truncateToVisualLines,
5
+ type ExtensionAPI,
6
+ type ExtensionContext,
7
+ type Theme,
8
+ type ToolDefinition,
9
+ } from "@earendil-works/pi-coding-agent";
10
+ import {
11
+ Box,
12
+ Container,
13
+ Markdown,
14
+ Spacer,
15
+ Text,
16
+ truncateToWidth,
17
+ type Component,
18
+ } from "@earendil-works/pi-tui";
19
+ import { actionText } from "./tui-action.ts";
20
+
21
+ function preview(content: Component, expanded: boolean, theme: Theme, showHint = true): Component {
22
+ if (expanded) {
23
+ return content;
24
+ }
25
+ let cache: { width: number; lines: string[] } | undefined;
26
+ return {
27
+ render(width) {
28
+ if (cache?.width !== width) {
29
+ const { visualLines, skippedCount } = truncateToVisualLines(
30
+ content.render(width).join("\n"),
31
+ 4,
32
+ width,
33
+ 0,
34
+ "start",
35
+ );
36
+ const hint = theme.fg(
37
+ "dim",
38
+ `… ${skippedCount} more lines${showHint ? ` · ${keyHint("app.tools.expand", "to expand")}` : ""}`,
39
+ );
40
+ cache = {
41
+ width,
42
+ lines: skippedCount > 0 ? [...visualLines, truncateToWidth(hint, width)] : visualLines,
43
+ };
44
+ }
45
+ return cache.lines;
46
+ },
47
+ invalidate() {
48
+ cache = undefined;
49
+ content.invalidate();
50
+ },
51
+ };
52
+ }
53
+
54
+ export const renderCompactResult: NonNullable<ToolDefinition["renderResult"]> = (
55
+ result,
56
+ { expanded },
57
+ theme,
58
+ ) =>
59
+ preview(
60
+ new Text(
61
+ theme.fg(
62
+ "toolOutput",
63
+ result.content
64
+ .filter((block) => block.type === "text")
65
+ .map((block) => block.text)
66
+ .join("\n"),
67
+ ),
68
+ 0,
69
+ 0,
70
+ ),
71
+ expanded,
72
+ theme,
73
+ );
74
+
75
+ export function registerCompactMessages(pi: ExtensionAPI, types: readonly string[]): void {
76
+ let ui: ExtensionContext["ui"] | undefined;
77
+ pi.on("session_start", (_event, ctx) => {
78
+ ui = ctx.mode === "tui" ? ctx.ui : undefined;
79
+ });
80
+ pi.on("session_shutdown", () => {
81
+ ui = undefined;
82
+ });
83
+ for (const type of types) {
84
+ pi.registerMessageRenderer(type, (message, { expanded, outputPad }, theme) => {
85
+ const text =
86
+ typeof message.content === "string"
87
+ ? message.content
88
+ : message.content
89
+ .filter((block) => block.type === "text")
90
+ .map((block) => block.text)
91
+ .join("\n");
92
+ const box = new Box(outputPad, 1, (line) => theme.bg("customMessageBg", line));
93
+ const markdown = new Markdown(text, 0, 0, getMarkdownTheme(), {
94
+ color: (line) => theme.fg("customMessageText", line),
95
+ });
96
+ box.addChild(
97
+ new (class extends Container {
98
+ override render(width: number): string[] {
99
+ this.clear();
100
+ const label = theme.fg("customMessageLabel", theme.bold(`[${type}]`));
101
+ this.addChild(
102
+ markdown.render(width).length > 4
103
+ ? actionText(
104
+ `${label} · ${keyHint("app.tools.expand", expanded ? "to collapse" : "to expand")}`,
105
+ () => ui?.setToolsExpanded(!expanded),
106
+ )
107
+ : new Text(label, 0, 0),
108
+ );
109
+ return super.render(width);
110
+ }
111
+ })(),
112
+ );
113
+ box.addChild(new Spacer(1));
114
+ box.addChild(preview(markdown, expanded, theme, false));
115
+ return box;
116
+ });
117
+ }
118
+ }
@@ -1,11 +1,5 @@
1
1
  import * as fs from "node:fs";
2
- import {
3
- JsonProjection,
4
- type Projection,
5
- type ProjectionLimits,
6
- type StringChunk,
7
- type KeyLimit,
8
- } from "./json-projection.ts";
2
+ import { JsonProjection, type Projection, type StringChunk } from "./json-projection.ts";
9
3
  import { errorDescription, isRecord, type UnknownRecord } from "./unknown.ts";
10
4
 
11
5
  export interface JournalRecord {
@@ -19,10 +13,7 @@ export interface JournalOptions {
19
13
  readonly requireNewline?: boolean;
20
14
  readonly start?: number;
21
15
  readonly end?: number;
22
- readonly malformed?: (start: number, end: number, error: unknown) => void;
23
16
  readonly stringChunk?: StringChunk;
24
- readonly keys?: KeyLimit;
25
- readonly limits?: ProjectionLimits;
26
17
  }
27
18
 
28
19
  /** Byte-framed JSONL. Publication framing is independent of malformed-record tolerance. */
@@ -54,12 +45,7 @@ export class JournalFrames {
54
45
  this.projection = this.newProjection();
55
46
  }
56
47
  private newProjection(): JsonProjection {
57
- return new JsonProjection(
58
- this.select,
59
- this.options.stringChunk,
60
- this.options.keys,
61
- this.options.limits,
62
- );
48
+ return new JsonProjection(this.select, this.options.stringChunk);
63
49
  }
64
50
  private part(bytes: Buffer): void {
65
51
  if (!this.nonblank && bytes.some((byte) => ![9, 10, 13, 32].includes(byte))) {
@@ -107,9 +93,7 @@ export class JournalFrames {
107
93
  this.error = undefined;
108
94
  this.failed = false;
109
95
  this.nonblank = false;
110
- if (failed) {
111
- this.options.malformed?.(start, end, error);
112
- } else if (record) {
96
+ if (!failed && record) {
113
97
  this.record(record);
114
98
  }
115
99
  }