@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.
- package/CHANGELOG.md +63 -2
- package/README.md +36 -5
- package/dist/agent/child-runtime.js +80 -0
- package/dist/agent/contracts.js +15 -24
- package/dist/agent/host.js +3 -16
- package/dist/agent/index.js +212 -192
- package/dist/agent/journal-tail.js +115 -156
- package/dist/agent/launch.js +20 -8
- package/dist/agent/legacy.js +65 -23
- package/dist/agent/notifications.js +138 -47
- package/dist/agent/observation-state.js +246 -0
- package/dist/agent/observation-view.js +258 -0
- package/dist/agent/observation.js +350 -0
- package/dist/agent/observe-card.js +243 -0
- package/dist/agent/observe-chrome.js +85 -0
- package/dist/agent/observe-client.js +285 -0
- package/dist/agent/observe-layout.js +65 -0
- package/dist/agent/observe-reading.js +84 -0
- package/dist/agent/observe-screen.js +400 -96
- package/dist/agent/observe-transcript.js +382 -0
- package/dist/agent/observe.js +250 -43
- package/dist/agent/parking.js +253 -0
- package/dist/agent/process.js +228 -28
- package/dist/agent/receipts.js +92 -20
- package/dist/agent/runner.js +229 -126
- package/dist/agent/shell.js +2 -1
- package/dist/agent/storage.js +47 -12
- package/dist/agent/tool-schemas.js +20 -0
- package/dist/agent/typebox.js +10 -5
- package/dist/agent/usage.js +0 -115
- package/dist/agent/view.js +46 -16
- package/dist/agent/wake.js +99 -0
- package/dist/build-info.js +1 -1
- package/dist/intercom-client.d.ts +26 -0
- package/dist/intercom-client.js +45 -0
- package/dist/pi-intercom/agent-dir.d.ts +1 -0
- package/dist/pi-intercom/bridge-session.js +2 -1
- package/dist/pi-intercom/broker/broker.js +62 -43
- package/dist/pi-intercom/broker/client-last.d.ts +24 -0
- package/dist/pi-intercom/broker/client-last.js +42 -5
- package/dist/pi-intercom/broker/client-park.d.ts +9 -0
- package/dist/pi-intercom/broker/client-park.js +21 -0
- package/dist/pi-intercom/broker/client.d.ts +59 -0
- package/dist/pi-intercom/broker/client.js +55 -29
- package/dist/pi-intercom/broker/connection.d.ts +29 -0
- package/dist/pi-intercom/broker/framing.d.ts +17 -0
- package/dist/pi-intercom/broker/framing.js +10 -7
- package/dist/pi-intercom/broker/guard.js +23 -0
- package/dist/pi-intercom/broker/last.d.ts +30 -0
- package/dist/pi-intercom/broker/last.js +87 -40
- package/dist/pi-intercom/broker/parked.js +107 -0
- package/dist/pi-intercom/broker/paths.d.ts +13 -0
- package/dist/pi-intercom/broker/paths.js +35 -12
- package/dist/pi-intercom/broker/pid.js +5 -1
- package/dist/pi-intercom/broker/server-connection.js +20 -1
- package/dist/pi-intercom/broker/snapshot-reader.d.ts +9 -0
- package/dist/pi-intercom/broker/snapshots.d.ts +29 -0
- package/dist/pi-intercom/broker/targets.d.ts +9 -0
- package/dist/pi-intercom/connection.js +71 -20
- package/dist/pi-intercom/identity.js +6 -5
- package/dist/pi-intercom/inbound-delivery.js +16 -60
- package/dist/pi-intercom/inbound-journal.js +83 -6
- package/dist/pi-intercom/inbound-record.js +18 -1
- package/dist/pi-intercom/inbound.js +22 -37
- package/dist/pi-intercom/index.js +41 -0
- package/dist/pi-intercom/last-message.js +4 -16
- package/dist/pi-intercom/lifecycle.js +88 -3
- package/dist/pi-intercom/outbound.js +5 -15
- package/dist/pi-intercom/reply-tracker.js +7 -6
- package/dist/pi-intercom/reply-wait.js +2 -2
- package/dist/pi-intercom/session-targets.d.ts +24 -0
- package/dist/pi-intercom/tool-registration.js +4 -1
- package/dist/pi-intercom/tool-results.js +5 -7
- package/dist/pi-intercom/tool-schemas.js +12 -26
- package/dist/pi-intercom/types.d.ts +212 -0
- package/dist/pi-intercom/types.js +54 -3
- package/dist/pi-intercom/validation.d.ts +12 -0
- package/dist/pi-intercom/validation.js +3 -0
- package/dist/shared/bounded-json.js +151 -0
- package/dist/shared/compact-cards.js +66 -0
- package/dist/shared/journal-frames.js +3 -6
- package/dist/shared/json-projection.js +267 -129
- package/dist/shared/native-journal.js +123 -56
- package/dist/shared/parking.js +12 -0
- package/dist/shared/post-exit-stdio-guard.js +120 -7
- package/dist/shared/tui-action.js +29 -0
- package/dist/shared/unknown.d.ts +9 -0
- package/docs/code-quality.md +5 -3
- package/docs/development.md +42 -3
- package/docs/intercom-bridge.md +9 -4
- package/docs/intercom.md +228 -7
- package/docs/performance.md +118 -14
- package/docs/reference.md +168 -20
- package/docs/retired-history-maintenance.md +252 -0
- package/package.json +28 -15
- package/scripts/archive-retired-history.mjs +464 -0
- package/scripts/build.mjs +48 -28
- package/scripts/measure-size.mjs +1 -1
- package/scripts/native-package-smoke.mjs +153 -12
- package/scripts/package-smoke.mjs +80 -11
- package/scripts/prepare.mjs +2 -2
- package/scripts/real-pi-live.mjs +377 -0
- package/scripts/real-pi-processes.mjs +6 -1
- package/scripts/real-pi-smoke.mjs +6 -282
- package/scripts/run-tests.mjs +4 -2
- package/scripts/typescript.mjs +35 -0
- package/skills/pi-intercom/SKILL.md +2 -1
- package/skills/pi-subagents/SKILL.md +23 -5
- package/src/agent/child-runtime.ts +105 -0
- package/src/agent/contracts.ts +18 -34
- package/src/agent/host.ts +39 -20
- package/src/agent/index.ts +234 -207
- package/src/agent/journal-tail.ts +141 -153
- package/src/agent/launch.ts +33 -9
- package/src/agent/legacy.ts +85 -31
- package/src/agent/notifications.ts +192 -67
- package/src/agent/observation-state.ts +277 -0
- package/src/agent/observation-view.ts +267 -0
- package/src/agent/observation.ts +409 -0
- package/src/agent/observe-card.ts +304 -0
- package/src/agent/observe-chrome.ts +170 -0
- package/src/agent/observe-client.ts +318 -0
- package/src/agent/observe-layout.ts +79 -0
- package/src/agent/observe-reading.ts +97 -0
- package/src/agent/observe-screen.ts +443 -129
- package/src/agent/observe-transcript.ts +434 -0
- package/src/agent/observe.ts +304 -71
- package/src/agent/parking.ts +341 -0
- package/src/agent/process.ts +280 -37
- package/src/agent/receipts.ts +114 -24
- package/src/agent/runner.ts +227 -149
- package/src/agent/shell.ts +2 -1
- package/src/agent/storage.ts +44 -11
- package/src/agent/tool-schemas.ts +33 -0
- package/src/agent/typebox.ts +12 -7
- package/src/agent/usage.ts +0 -130
- package/src/agent/view.ts +70 -20
- package/src/agent/wake.ts +107 -0
- package/src/intercom-client.ts +108 -0
- package/src/pi-intercom/bridge-session.ts +2 -2
- package/src/pi-intercom/broker/broker.ts +64 -53
- package/src/pi-intercom/broker/client-last.ts +65 -12
- package/src/pi-intercom/broker/client-park.ts +23 -0
- package/src/pi-intercom/broker/client.ts +63 -45
- package/src/pi-intercom/broker/framing.ts +11 -8
- package/src/pi-intercom/broker/guard.ts +33 -0
- package/src/pi-intercom/broker/last.ts +131 -43
- package/src/pi-intercom/broker/parked.ts +130 -0
- package/src/pi-intercom/broker/paths.ts +39 -12
- package/src/pi-intercom/broker/pid.ts +6 -1
- package/src/pi-intercom/broker/server-connection.ts +27 -2
- package/src/pi-intercom/connection.ts +103 -39
- package/src/pi-intercom/identity.ts +8 -5
- package/src/pi-intercom/inbound-delivery.ts +20 -66
- package/src/pi-intercom/inbound-journal.ts +103 -7
- package/src/pi-intercom/inbound-record.ts +21 -4
- package/src/pi-intercom/inbound.ts +32 -49
- package/src/pi-intercom/index.ts +44 -0
- package/src/pi-intercom/last-message.ts +11 -20
- package/src/pi-intercom/lifecycle.ts +117 -14
- package/src/pi-intercom/outbound.ts +7 -16
- package/src/pi-intercom/reply-tracker.ts +8 -6
- package/src/pi-intercom/reply-wait.ts +3 -2
- package/src/pi-intercom/runtime-types.ts +0 -1
- package/src/pi-intercom/tool-registration.ts +4 -1
- package/src/pi-intercom/tool-results.ts +10 -9
- package/src/pi-intercom/tool-schemas.ts +12 -44
- package/src/pi-intercom/types.ts +115 -4
- package/src/pi-intercom/validation.ts +4 -0
- package/src/shared/bounded-json.ts +162 -0
- package/src/shared/compact-cards.ts +118 -0
- package/src/shared/journal-frames.ts +3 -19
- package/src/shared/json-projection.ts +267 -147
- package/src/shared/native-journal.ts +157 -75
- package/src/shared/parking.ts +22 -0
- package/src/shared/post-exit-stdio-guard.ts +165 -7
- package/src/shared/tui-action.ts +39 -0
- package/tsconfig.client.json +8 -0
- package/tsconfig.json +1 -0
- package/src/shared/stream-json.d.ts +0 -10
|
@@ -1,26 +1,14 @@
|
|
|
1
1
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
2
2
|
import { Type } from "../agent/typebox.ts";
|
|
3
3
|
export const intercomSchema = Type.Object({
|
|
4
|
-
action: StringEnum(["list", "send", "ask", "reply", "pending", "status", "last"] as const,
|
|
5
|
-
description:
|
|
6
|
-
"List, send, ask, reply, pending asks, connection status, or last completed assistant response.",
|
|
7
|
-
}),
|
|
4
|
+
action: StringEnum(["list", "send", "ask", "reply", "pending", "status", "last"] as const),
|
|
8
5
|
scope: Type.Optional(
|
|
9
6
|
StringEnum(["project", "all"] as const, {
|
|
10
|
-
description:
|
|
11
|
-
"For list/status: 'all' (default) includes every reachable session; 'project' further filters this repository and its worktrees.",
|
|
12
|
-
}),
|
|
13
|
-
),
|
|
14
|
-
to: Type.Optional(
|
|
15
|
-
Type.String({
|
|
16
|
-
description: "Target session name or ID (for 'send', 'ask', or disambiguating 'reply')",
|
|
17
|
-
}),
|
|
18
|
-
),
|
|
19
|
-
message: Type.Optional(
|
|
20
|
-
Type.String({
|
|
21
|
-
description: "Message for send/ask/reply",
|
|
7
|
+
description: "all (default) or project (repo and worktrees)",
|
|
22
8
|
}),
|
|
23
9
|
),
|
|
10
|
+
to: Type.Optional(Type.String({ description: "Session name or ID" })),
|
|
11
|
+
message: Type.Optional(Type.String()),
|
|
24
12
|
attachments: Type.Optional(
|
|
25
13
|
Type.Array(
|
|
26
14
|
Type.Object({
|
|
@@ -31,41 +19,21 @@ export const intercomSchema = Type.Object({
|
|
|
31
19
|
}),
|
|
32
20
|
),
|
|
33
21
|
),
|
|
34
|
-
replyTo: Type.Optional(
|
|
35
|
-
Type.String({
|
|
36
|
-
description: "Message ID to reply to (for threading or responding to an 'ask')",
|
|
37
|
-
}),
|
|
38
|
-
),
|
|
22
|
+
replyTo: Type.Optional(Type.String({ description: "Message ID to answer or thread" })),
|
|
39
23
|
delivery: Type.Optional(
|
|
40
24
|
StringEnum(["queue", "steer", "passive"] as const, {
|
|
41
25
|
description:
|
|
42
|
-
"
|
|
26
|
+
"steer (default) at next tool boundary; queue after active work; passive never wakes the model",
|
|
43
27
|
}),
|
|
44
28
|
),
|
|
45
29
|
queueMode: Type.Optional(
|
|
46
30
|
StringEnum(["stack", "replace"] as const, {
|
|
47
|
-
description:
|
|
48
|
-
"For delivery='queue': 'stack' keeps all messages; 'replace' keeps only the latest undelivered message for the same threadId.",
|
|
49
|
-
}),
|
|
50
|
-
),
|
|
51
|
-
threadId: Type.Optional(
|
|
52
|
-
Type.String({ description: "Stable topic key for queueMode='replace'." }),
|
|
53
|
-
),
|
|
54
|
-
entryId: Type.Optional(Type.String({ description: "Last response entry ID for stable paging." })),
|
|
55
|
-
offset: Type.Optional(
|
|
56
|
-
Type.Integer({ minimum: 0, description: "Last response character offset (UTF-16)." }),
|
|
57
|
-
),
|
|
58
|
-
limit: Type.Optional(
|
|
59
|
-
Type.Integer({
|
|
60
|
-
minimum: 1,
|
|
61
|
-
maximum: 16384,
|
|
62
|
-
description: "Last response page size; default/max 16384 characters.",
|
|
63
|
-
}),
|
|
64
|
-
),
|
|
65
|
-
passive: Type.Optional(
|
|
66
|
-
Type.Boolean({
|
|
67
|
-
description:
|
|
68
|
-
"For action='send' only: legacy alias for delivery='passive'. Discouraged for agent-to-agent messages.",
|
|
31
|
+
description: "stack keeps all; replace keeps only the latest per threadId",
|
|
69
32
|
}),
|
|
70
33
|
),
|
|
34
|
+
threadId: Type.Optional(Type.String()),
|
|
35
|
+
entryId: Type.Optional(Type.String()),
|
|
36
|
+
offset: Type.Optional(Type.Integer({ minimum: 0, description: "UTF-16 characters" })),
|
|
37
|
+
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 16384 })),
|
|
38
|
+
passive: Type.Optional(Type.Boolean({ description: "Legacy alias for delivery=passive" })),
|
|
71
39
|
});
|
package/src/pi-intercom/types.ts
CHANGED
|
@@ -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
|
-
|
|
258
|
-
|
|
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
|
}
|