@tt-a1i/openpi 0.3.1 → 0.5.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 (129) hide show
  1. package/README.md +184 -59
  2. package/SETUP.md +23 -7
  3. package/assets/openpi-launch-card-v1.webp +0 -0
  4. package/bin/openpi.js +145 -0
  5. package/extensions/ask-user/index.ts +30 -14
  6. package/extensions/background-terminals/index.ts +30 -2
  7. package/extensions/background-terminals/src/domain.ts +2 -0
  8. package/extensions/background-terminals/src/manager.ts +486 -106
  9. package/extensions/background-terminals/src/output.ts +33 -0
  10. package/extensions/background-terminals/src/prompt.ts +14 -6
  11. package/extensions/background-terminals/src/result-delivery.ts +4 -1
  12. package/extensions/background-terminals/src/ui/ps.ts +132 -129
  13. package/extensions/capabilities/index.ts +30 -42
  14. package/extensions/capabilities/src/ui.ts +93 -0
  15. package/extensions/clear-context/index.ts +83 -0
  16. package/extensions/context-pivot/index.ts +16 -6
  17. package/extensions/cron/schedule.ts +7 -1
  18. package/extensions/file-mutation-display/index.ts +34 -76
  19. package/extensions/file-mutation-display/render.ts +146 -87
  20. package/extensions/file-search/index.ts +8 -7
  21. package/extensions/file-search/src/binaries.ts +75 -59
  22. package/extensions/git-info/src/changed-files-view.ts +47 -14
  23. package/extensions/git-read/index.ts +328 -0
  24. package/extensions/git-read/src/args.ts +171 -0
  25. package/extensions/git-read/src/process.ts +81 -0
  26. package/extensions/git-read/src/prompt.ts +56 -0
  27. package/extensions/model-info/index.ts +21 -33
  28. package/extensions/model-info/session-metrics.ts +96 -0
  29. package/extensions/plan-mode/bash-policy.ts +54 -9
  30. package/extensions/plan-mode/index.ts +7 -2
  31. package/extensions/post-edit/index.ts +16 -6
  32. package/extensions/sessions/git-stats.ts +258 -72
  33. package/extensions/sessions/index.ts +222 -140
  34. package/extensions/sessions/preview-cache.ts +104 -0
  35. package/extensions/sessions/preview-loader.ts +856 -0
  36. package/extensions/sessions/sessions.ts +43 -4
  37. package/extensions/setup/index.ts +127 -131
  38. package/extensions/shared/activity-status.ts +36 -5
  39. package/extensions/shared/agent-session-page.ts +319 -0
  40. package/extensions/shared/agent-tool-renderer.ts +218 -0
  41. package/extensions/shared/agent-transcript.ts +524 -0
  42. package/extensions/shared/below-editor-navigation.ts +26 -0
  43. package/extensions/shared/capability-intent.ts +53 -0
  44. package/extensions/shared/child-session.ts +444 -22
  45. package/extensions/shared/result-budget.ts +134 -0
  46. package/extensions/shared/result-delivery.ts +34 -0
  47. package/extensions/shared/screen-chrome.ts +133 -0
  48. package/extensions/shared/setup-config.ts +97 -38
  49. package/extensions/shared/setup-episode-state.ts +1 -1
  50. package/extensions/shared/spinner.ts +28 -0
  51. package/extensions/shared/terminal-text.ts +110 -23
  52. package/extensions/shared/text-projection.ts +113 -0
  53. package/extensions/shared/tool-activity.ts +382 -0
  54. package/extensions/shared/tool-surface.ts +42 -8
  55. package/extensions/shared/transcript-viewport.ts +46 -0
  56. package/extensions/shared/web-observer-registry.ts +390 -0
  57. package/extensions/shared/worktree.ts +11 -0
  58. package/extensions/subagents/index.ts +461 -186
  59. package/extensions/subagents/navigation.ts +86 -28
  60. package/extensions/subagents/src/agent-types.ts +37 -15
  61. package/extensions/subagents/src/backend.ts +12 -1
  62. package/extensions/subagents/src/backends/pi.ts +375 -66
  63. package/extensions/subagents/src/domain.ts +5 -0
  64. package/extensions/subagents/src/id-sequence.ts +84 -0
  65. package/extensions/subagents/src/manager.ts +651 -536
  66. package/extensions/subagents/src/prompt.ts +185 -42
  67. package/extensions/subagents/src/result-artifact.ts +146 -0
  68. package/extensions/subagents/src/result-delivery.ts +7 -1
  69. package/extensions/subagents/src/runtime.ts +23 -6
  70. package/extensions/subagents/src/ui/takeover.ts +128 -337
  71. package/extensions/subagents/src/ui/transcript.ts +38 -501
  72. package/extensions/subagents/src/ui/wait-result.ts +103 -15
  73. package/extensions/suggestions/src/ui.ts +10 -4
  74. package/extensions/tasks/index.ts +0 -3
  75. package/extensions/tasks/ui.ts +79 -62
  76. package/extensions/ui-customization/footer.ts +7 -44
  77. package/extensions/ui-customization/index.ts +0 -4
  78. package/extensions/user-input-fold/index.ts +185 -0
  79. package/extensions/web/index.ts +234 -0
  80. package/extensions/workflows/artifacts.ts +147 -22
  81. package/extensions/workflows/completion-projection.ts +457 -0
  82. package/extensions/workflows/controller.ts +14 -2
  83. package/extensions/workflows/coordinator.ts +62 -0
  84. package/extensions/workflows/dashboard.ts +458 -339
  85. package/extensions/workflows/handoff.ts +121 -25
  86. package/extensions/workflows/index.ts +1042 -492
  87. package/extensions/workflows/journal.ts +148 -13
  88. package/extensions/workflows/model.ts +131 -19
  89. package/extensions/workflows/navigation.ts +61 -18
  90. package/extensions/workflows/progress-projection.ts +306 -0
  91. package/extensions/workflows/prompt.ts +166 -10
  92. package/extensions/workflows/replay-safety.ts +58 -27
  93. package/extensions/workflows/result-delivery.ts +253 -0
  94. package/extensions/workflows/retention.ts +593 -0
  95. package/extensions/workflows/runner.ts +388 -279
  96. package/extensions/workflows/sandbox-child.cjs +36 -3
  97. package/extensions/workflows/sandbox.ts +62 -8
  98. package/extensions/workflows/serialization.ts +325 -17
  99. package/extensions/workflows/tool-renderer.ts +22 -0
  100. package/extensions/workflows/transcript.ts +149 -0
  101. package/extensions/workspace-cleanup-guard/index.ts +54 -0
  102. package/extensions/workspace-cleanup-guard/workspace-provenance.ts +563 -0
  103. package/package.json +28 -8
  104. package/skills/subagents/REFERENCE.md +189 -0
  105. package/skills/subagents/SKILL.md +2 -2
  106. package/skills/workflows/REFERENCE.md +10 -5
  107. package/skills/workflows/SKILL.md +53 -10
  108. package/web/adapter/pi-adapter.ts +661 -0
  109. package/web/host/browser-launcher.ts +20 -0
  110. package/web/host/static-assets.ts +4 -0
  111. package/web/host/terminal-status.ts +38 -0
  112. package/web/host/web-host.ts +789 -0
  113. package/web/http-dispatcher.ts +125 -0
  114. package/web/protocol/types.ts +462 -0
  115. package/web/runtime/pi-runtime.ts +991 -0
  116. package/web/runtime/types.ts +71 -0
  117. package/web/runtime/web-host-lease.ts +497 -0
  118. package/web/trace.ts +18 -0
  119. package/web/ui/app.js +1398 -0
  120. package/web/ui/index.html +139 -0
  121. package/web/ui/styles.css +598 -0
  122. package/web/vite.config.mjs +34 -0
  123. package/extensions/execution-convergence/active-evidence.ts +0 -129
  124. package/extensions/execution-convergence/index.ts +0 -442
  125. package/extensions/execution-convergence/workspace-provenance.ts +0 -338
  126. package/extensions/setup/intercom-fs-helper.cjs +0 -130
  127. package/extensions/setup/intercom.ts +0 -603
  128. package/extensions/subagents/src/backends/stub.ts +0 -296
  129. package/extensions/subagents/src/format.ts +0 -48
@@ -0,0 +1,125 @@
1
+ import { EventEmitter } from "node:events";
2
+ import * as undici from "undici";
3
+
4
+ export const DEFAULT_HTTP_IDLE_TIMEOUT_MS = 300_000;
5
+ const DEFAULT_AUTO_SELECT_FAMILY_ATTEMPT_TIMEOUT_MS = 2_000;
6
+ const originalGlobalFetch = globalThis.fetch;
7
+ let installedGlobalFetch: typeof globalThis.fetch | undefined;
8
+
9
+ type ManagedDispatcher = {
10
+ dispatcher: undici.Dispatcher;
11
+ previous: undici.Dispatcher;
12
+ released: boolean;
13
+ };
14
+
15
+ const managedDispatchers = new WeakMap<undici.Dispatcher, ManagedDispatcher>();
16
+
17
+ export interface HttpDispatcherLease {
18
+ timeoutMs: number;
19
+ release(): Promise<void>;
20
+ }
21
+
22
+ function ignoreDispatcherError(_error: unknown) {}
23
+
24
+ function withErrorListener<T extends undici.Dispatcher>(dispatcher: T) {
25
+ if (dispatcher instanceof EventEmitter) {
26
+ EventEmitter.prototype.on.call(
27
+ dispatcher,
28
+ "error",
29
+ ignoreDispatcherError,
30
+ );
31
+ }
32
+ return dispatcher;
33
+ }
34
+
35
+ function createClient(origin: string | URL, options: object) {
36
+ return withErrorListener(
37
+ new undici.Client(origin, options as undici.Client.Options),
38
+ );
39
+ }
40
+
41
+ function createOriginDispatcher(origin: string | URL, options: object) {
42
+ const dispatcherOptions = options as undici.Pool.Options;
43
+ if (dispatcherOptions.connections === 1) {
44
+ return createClient(origin, dispatcherOptions);
45
+ }
46
+ return withErrorListener(
47
+ new undici.Pool(origin, {
48
+ ...dispatcherOptions,
49
+ factory: createClient,
50
+ }),
51
+ );
52
+ }
53
+
54
+ export function applyHttpProxySettings(httpProxy: string | undefined) {
55
+ const proxy = httpProxy?.trim();
56
+ if (!proxy) return false;
57
+ process.env.HTTP_PROXY ??= proxy;
58
+ process.env.HTTPS_PROXY ??= proxy;
59
+ return true;
60
+ }
61
+
62
+ function parseHttpIdleTimeoutMs(value: unknown) {
63
+ if (typeof value === "string") {
64
+ const trimmed = value.trim();
65
+ if (trimmed.toLowerCase() === "disabled") return 0;
66
+ if (trimmed.length === 0) return undefined;
67
+ return parseHttpIdleTimeoutMs(Number(trimmed));
68
+ }
69
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
70
+ return undefined;
71
+ }
72
+ return Math.floor(value);
73
+ }
74
+
75
+ export function configureHttpDispatcher(
76
+ timeoutMs = DEFAULT_HTTP_IDLE_TIMEOUT_MS,
77
+ ): HttpDispatcherLease {
78
+ const normalizedTimeoutMs = parseHttpIdleTimeoutMs(timeoutMs);
79
+ if (normalizedTimeoutMs === undefined) {
80
+ throw new Error(`Invalid HTTP idle timeout: ${String(timeoutMs)}`);
81
+ }
82
+ const previous = undici.getGlobalDispatcher();
83
+ const dispatcher = withErrorListener(
84
+ new undici.EnvHttpProxyAgent({
85
+ allowH2: false,
86
+ bodyTimeout: normalizedTimeoutMs,
87
+ connect: {
88
+ autoSelectFamilyAttemptTimeout: DEFAULT_AUTO_SELECT_FAMILY_ATTEMPT_TIMEOUT_MS,
89
+ },
90
+ headersTimeout: normalizedTimeoutMs,
91
+ clientFactory: createClient,
92
+ factory: createOriginDispatcher,
93
+ }),
94
+ );
95
+ const managed = { dispatcher, previous, released: false };
96
+ managedDispatchers.set(dispatcher, managed);
97
+ undici.setGlobalDispatcher(dispatcher);
98
+ const shouldInstallGlobals =
99
+ installedGlobalFetch === undefined
100
+ ? globalThis.fetch === originalGlobalFetch
101
+ : globalThis.fetch === installedGlobalFetch;
102
+ if (shouldInstallGlobals) {
103
+ undici.install?.();
104
+ installedGlobalFetch = globalThis.fetch;
105
+ }
106
+ let released = false;
107
+ return {
108
+ timeoutMs: normalizedTimeoutMs,
109
+ async release() {
110
+ if (released) return;
111
+ released = true;
112
+ managed.released = true;
113
+ if (undici.getGlobalDispatcher() === dispatcher) {
114
+ let replacement = managed.previous;
115
+ let replaced = managedDispatchers.get(replacement);
116
+ while (replaced?.released) {
117
+ replacement = replaced.previous;
118
+ replaced = managedDispatchers.get(replacement);
119
+ }
120
+ undici.setGlobalDispatcher(replacement);
121
+ }
122
+ await dispatcher.close();
123
+ },
124
+ };
125
+ }
@@ -0,0 +1,462 @@
1
+ import type { SessionEntry } from "@earendil-works/pi-coding-agent";
2
+ import type { WebCapabilitySnapshot } from "../../extensions/shared/web-observer-registry.ts";
3
+
4
+ export const WEB_PROTOCOL_VERSION = 1;
5
+ export const WEB_MAX_EVENTS = 200;
6
+ export const WEB_MAX_EVENT_BYTES = 64 * 1024;
7
+ export const WEB_MAX_TEXT = 12_000;
8
+ export const WEB_MAX_SESSION_PREVIEW = 500;
9
+ const WEB_MAX_METADATA_TEXT = 500;
10
+ export const WEB_MAX_ENTRIES = 250;
11
+ export const WEB_MAX_MESSAGE_PARTS = 64;
12
+ export const WEB_MAX_SESSIONS = 500;
13
+ export const WEB_MAX_WORKSPACES = 250;
14
+ export const WEB_MAX_MODELS = 250;
15
+ export const WEB_MAX_SELECTED_TRANSCRIPT_BYTES = 2 * 1024 * 1024;
16
+ export const WEB_MAX_SNAPSHOT_BYTES = 4 * 1024 * 1024;
17
+
18
+ export interface WebEvent {
19
+ protocolVersion: typeof WEB_PROTOCOL_VERSION;
20
+ sequence: number;
21
+ type: string;
22
+ timestamp: string;
23
+ detail?: Record<string, unknown>;
24
+ }
25
+
26
+ export interface WebSessionSummary {
27
+ id: string;
28
+ path: string;
29
+ cwd: string;
30
+ name?: string;
31
+ modified: string;
32
+ created: string;
33
+ messageCount: number;
34
+ firstMessage: string;
35
+ archived?: boolean;
36
+ ungrouped?: boolean;
37
+ }
38
+
39
+ export interface WebWorkspaceSummary {
40
+ path: string;
41
+ name: string;
42
+ current: boolean;
43
+ }
44
+
45
+ export interface WebModelSummary {
46
+ provider: string;
47
+ id: string;
48
+ name: string;
49
+ label: string;
50
+ current: boolean;
51
+ }
52
+
53
+ export interface WebProjectionTruncation {
54
+ readonly truncated: boolean;
55
+ readonly entriesOmitted: number;
56
+ readonly messagePartsOmitted: number;
57
+ readonly messagesTruncated: number;
58
+ readonly maxBytes: number;
59
+ }
60
+
61
+ export interface WebSessionProjection {
62
+ id: string;
63
+ path: string;
64
+ cwd: string;
65
+ entries: ReturnType<typeof projectEntry>[];
66
+ bytes: number;
67
+ truncation: WebProjectionTruncation;
68
+ }
69
+
70
+ export interface WebMessageTruncation {
71
+ readonly truncated: true;
72
+ readonly text?: true;
73
+ readonly partsOmitted?: number;
74
+ readonly details?: true;
75
+ }
76
+
77
+ export interface WebLiveMessage {
78
+ role?: string;
79
+ toolName?: string;
80
+ content: string;
81
+ parts?: WebMessagePart[];
82
+ toolCallId?: string;
83
+ isError?: boolean;
84
+ customType?: string;
85
+ display?: boolean;
86
+ details?: unknown;
87
+ truncation?: WebMessageTruncation;
88
+ }
89
+
90
+ export type WebMessagePart =
91
+ | { type: "text"; text: string }
92
+ | { type: "thinking"; text: string }
93
+ | { type: "toolCall"; id?: string; name: string; arguments: string };
94
+
95
+ export interface WebSnapshotTruncation {
96
+ truncated: boolean;
97
+ sessionsOmitted: number;
98
+ workspacesOmitted: number;
99
+ modelsOmitted: number;
100
+ maxBytes: number;
101
+ bytes: number;
102
+ }
103
+
104
+ export interface WebSnapshot {
105
+ protocolVersion: typeof WEB_PROTOCOL_VERSION;
106
+ generatedAt: string;
107
+ cursor: number;
108
+ /** Absent until the browser selects or creates a real Web Session. */
109
+ currentSessionId?: string;
110
+ workspaces: WebWorkspaceSummary[];
111
+ sessions: WebSessionSummary[];
112
+ selectedSession?: WebSessionProjection;
113
+ models: WebModelSummary[];
114
+ runtime: {
115
+ status: "idle" | "running" | "unknown";
116
+ capabilities: WebCapabilitySnapshot;
117
+ };
118
+ truncation: WebSnapshotTruncation;
119
+ }
120
+
121
+ interface BoundedText {
122
+ readonly value: string;
123
+ readonly truncated: boolean;
124
+ }
125
+
126
+ function boundedTextProjection(value: string, maxLength: number): BoundedText {
127
+ return value.length > maxLength
128
+ ? {
129
+ value: `${value.slice(0, maxLength)}\n[truncated]`,
130
+ truncated: true,
131
+ }
132
+ : { value, truncated: false };
133
+ }
134
+
135
+ export function boundedText(value: string, maxLength = WEB_MAX_TEXT): string {
136
+ return boundedTextProjection(value, maxLength).value;
137
+ }
138
+
139
+ const WEB_MAX_DETAILS_BYTES = 24 * 1024;
140
+ const WEB_MAX_STRUCTURED_DEPTH = 6;
141
+ const WEB_MAX_STRUCTURED_NODES = 512;
142
+ const WEB_MAX_STRUCTURED_PROPERTIES = 128;
143
+ const WEB_MAX_STRUCTURED_ARRAY_ITEMS = 128;
144
+
145
+ interface StructuredBudget {
146
+ bytes: number;
147
+ nodes: number;
148
+ truncated: boolean;
149
+ readonly seen: WeakSet<object>;
150
+ }
151
+
152
+ function consumeStructuredText(value: string, budget: StructuredBudget) {
153
+ const candidate = value.slice(0, Math.min(value.length, budget.bytes));
154
+ const encoded = textEncoder.encode(candidate);
155
+ if (encoded.byteLength <= budget.bytes) {
156
+ budget.bytes -= encoded.byteLength;
157
+ if (candidate.length < value.length) budget.truncated = true;
158
+ return candidate;
159
+ }
160
+ budget.truncated = true;
161
+ if (budget.bytes <= 0) return "";
162
+ const bounded = new TextDecoder().decode(encoded.slice(0, budget.bytes));
163
+ budget.bytes = 0;
164
+ return bounded;
165
+ }
166
+
167
+ function boundedStructuredValue(
168
+ value: unknown,
169
+ budget: StructuredBudget,
170
+ depth = 0,
171
+ ): unknown {
172
+ if (budget.nodes-- <= 0 || depth > WEB_MAX_STRUCTURED_DEPTH) {
173
+ budget.truncated = true;
174
+ return undefined;
175
+ }
176
+ if (value === null || typeof value === "boolean") return value;
177
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
178
+ if (typeof value === "string") return consumeStructuredText(value, budget);
179
+ if (typeof value === "bigint") return consumeStructuredText(String(value), budget);
180
+ if (typeof value !== "object") {
181
+ budget.truncated = true;
182
+ return undefined;
183
+ }
184
+ if (budget.seen.has(value)) {
185
+ budget.truncated = true;
186
+ return undefined;
187
+ }
188
+ budget.seen.add(value);
189
+ if (Array.isArray(value)) {
190
+ const length = Math.min(value.length, WEB_MAX_STRUCTURED_ARRAY_ITEMS);
191
+ if (value.length > length) budget.truncated = true;
192
+ const projected: unknown[] = [];
193
+ for (let index = 0; index < length; index++) {
194
+ const item = boundedStructuredValue(value[index], budget, depth + 1);
195
+ if (item !== undefined) projected.push(item);
196
+ if (budget.nodes <= 0 || budget.bytes <= 0) break;
197
+ }
198
+ return projected;
199
+ }
200
+ const projected: Record<string, unknown> = {};
201
+ let scannedProperties = 0;
202
+ for (const key in value) {
203
+ if (scannedProperties++ >= WEB_MAX_STRUCTURED_PROPERTIES) {
204
+ budget.truncated = true;
205
+ break;
206
+ }
207
+ if (!Object.prototype.hasOwnProperty.call(value, key)) continue;
208
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
209
+ if (!descriptor || !("value" in descriptor)) {
210
+ budget.truncated = true;
211
+ continue;
212
+ }
213
+ const boundedKey = consumeStructuredText(key, budget);
214
+ const item = boundedStructuredValue(descriptor.value, budget, depth + 1);
215
+ if (item !== undefined) projected[boundedKey] = item;
216
+ if (budget.nodes <= 0 || budget.bytes <= 0) break;
217
+ }
218
+ return projected;
219
+ }
220
+
221
+ function detailsProjection(value: unknown) {
222
+ if (value === undefined || value === null) {
223
+ return { value: undefined, truncated: false };
224
+ }
225
+ const budget: StructuredBudget = {
226
+ bytes: WEB_MAX_DETAILS_BYTES,
227
+ nodes: WEB_MAX_STRUCTURED_NODES,
228
+ truncated: false,
229
+ seen: new WeakSet(),
230
+ };
231
+ const projected = boundedStructuredValue(value, budget);
232
+ if (!budget.truncated && jsonByteLength(projected) > WEB_MAX_DETAILS_BYTES) {
233
+ budget.truncated = true;
234
+ }
235
+ return {
236
+ value: budget.truncated ? undefined : projected,
237
+ truncated: budget.truncated,
238
+ };
239
+ }
240
+
241
+ /** Keep structured tool details only when they serialize small and clean. */
242
+ export function boundedDetails(value: unknown): unknown {
243
+ return detailsProjection(value).value;
244
+ }
245
+
246
+ function projectContent(message: Record<string, unknown>) {
247
+ const content = message.content;
248
+ if (typeof content === "string") {
249
+ const text = boundedTextProjection(content, WEB_MAX_TEXT);
250
+ return {
251
+ content: text.value,
252
+ parts: [] as WebMessagePart[],
253
+ partsOmitted: 0,
254
+ textTruncated: text.truncated,
255
+ };
256
+ }
257
+ if (!Array.isArray(content)) {
258
+ const fallback =
259
+ typeof message.output === "string"
260
+ ? message.output
261
+ : typeof message.summary === "string"
262
+ ? message.summary
263
+ : "";
264
+ const text = boundedTextProjection(
265
+ fallback,
266
+ WEB_MAX_TEXT,
267
+ );
268
+ return {
269
+ content: text.value,
270
+ parts: [] as WebMessagePart[],
271
+ partsOmitted: 0,
272
+ textTruncated: text.truncated,
273
+ };
274
+ }
275
+
276
+ const parts: WebMessagePart[] = [];
277
+ let visibleText = "";
278
+ let textTruncated = false;
279
+ const retainedParts = Math.min(content.length, WEB_MAX_MESSAGE_PARTS);
280
+ for (let index = 0; index < retainedParts; index++) {
281
+ const part = content[index];
282
+ if (typeof part !== "object" || part === null) continue;
283
+ const typed = part as Record<string, unknown>;
284
+ let projected: WebMessagePart | undefined;
285
+ if (typed.type === "text" && typeof typed.text === "string") {
286
+ const text = boundedTextProjection(typed.text, WEB_MAX_TEXT);
287
+ projected = { type: "text", text: text.value };
288
+ textTruncated ||= text.truncated;
289
+ if (visibleText.length < WEB_MAX_TEXT) {
290
+ const separator = visibleText.length > 0 ? "\n" : "";
291
+ const remaining = WEB_MAX_TEXT - visibleText.length - separator.length;
292
+ if (remaining > 0) visibleText += `${separator}${text.value.slice(0, remaining)}`;
293
+ if (text.value.length > remaining) textTruncated = true;
294
+ } else {
295
+ textTruncated = true;
296
+ }
297
+ } else if (
298
+ typed.type === "thinking" &&
299
+ typeof typed.thinking === "string"
300
+ ) {
301
+ const text = boundedTextProjection(typed.thinking, WEB_MAX_TEXT);
302
+ projected = { type: "thinking", text: text.value };
303
+ textTruncated ||= text.truncated;
304
+ } else if (typed.type === "toolCall") {
305
+ const argumentsBudget: StructuredBudget = {
306
+ bytes: WEB_MAX_TEXT,
307
+ nodes: WEB_MAX_STRUCTURED_NODES,
308
+ truncated: false,
309
+ seen: new WeakSet(),
310
+ };
311
+ const boundedArguments = boundedStructuredValue(
312
+ typed.arguments ?? {},
313
+ argumentsBudget,
314
+ );
315
+ const argumentsText = JSON.stringify(boundedArguments ?? {});
316
+ const argumentsProjection = boundedTextProjection(
317
+ argumentsText,
318
+ WEB_MAX_TEXT,
319
+ );
320
+ const id =
321
+ typeof typed.id === "string"
322
+ ? boundedTextProjection(typed.id, WEB_MAX_METADATA_TEXT)
323
+ : undefined;
324
+ const name = boundedTextProjection(
325
+ typeof typed.name === "string" ? typed.name : "tool",
326
+ WEB_MAX_METADATA_TEXT,
327
+ );
328
+ projected = {
329
+ type: "toolCall",
330
+ ...(id ? { id: id.value } : {}),
331
+ name: name.value,
332
+ arguments: argumentsProjection.value,
333
+ };
334
+ textTruncated ||=
335
+ argumentsProjection.truncated ||
336
+ argumentsBudget.truncated ||
337
+ id?.truncated === true ||
338
+ name.truncated;
339
+ }
340
+ if (!projected) continue;
341
+ parts.push(projected);
342
+ }
343
+ const contentText = boundedTextProjection(visibleText, WEB_MAX_TEXT);
344
+ return {
345
+ content: contentText.value,
346
+ parts,
347
+ partsOmitted: Math.max(0, content.length - retainedParts),
348
+ textTruncated: textTruncated || contentText.truncated,
349
+ };
350
+ }
351
+
352
+ export function projectMessage(message: unknown): WebLiveMessage {
353
+ const value =
354
+ message && typeof message === "object"
355
+ ? (message as Record<string, unknown>)
356
+ : {};
357
+ const content = projectContent(value);
358
+ const details = detailsProjection(value.details);
359
+ const role =
360
+ typeof value.role === "string"
361
+ ? boundedTextProjection(value.role, WEB_MAX_METADATA_TEXT)
362
+ : undefined;
363
+ const toolName =
364
+ typeof value.toolName === "string"
365
+ ? boundedTextProjection(value.toolName, WEB_MAX_METADATA_TEXT)
366
+ : undefined;
367
+ const toolCallId =
368
+ typeof value.toolCallId === "string"
369
+ ? boundedTextProjection(value.toolCallId, WEB_MAX_METADATA_TEXT)
370
+ : undefined;
371
+ const customType =
372
+ typeof value.customType === "string"
373
+ ? boundedTextProjection(value.customType, WEB_MAX_METADATA_TEXT)
374
+ : undefined;
375
+ const metadataTruncated =
376
+ role?.truncated === true ||
377
+ toolName?.truncated === true ||
378
+ toolCallId?.truncated === true ||
379
+ customType?.truncated === true;
380
+ const truncated =
381
+ content.textTruncated ||
382
+ content.partsOmitted > 0 ||
383
+ details.truncated ||
384
+ metadataTruncated;
385
+ return {
386
+ role: role?.value,
387
+ toolName: toolName?.value,
388
+ content: content.content,
389
+ ...(content.parts.length > 0 ? { parts: content.parts } : {}),
390
+ ...(toolCallId ? { toolCallId: toolCallId.value } : {}),
391
+ ...(typeof value.isError === "boolean" ? { isError: value.isError } : {}),
392
+ ...(customType ? { customType: customType.value } : {}),
393
+ ...(typeof value.display === "boolean" ? { display: value.display } : {}),
394
+ ...(details.value !== undefined ? { details: details.value } : {}),
395
+ ...(truncated
396
+ ? {
397
+ truncation: {
398
+ truncated: true as const,
399
+ ...(content.textTruncated || metadataTruncated
400
+ ? { text: true as const }
401
+ : {}),
402
+ ...(content.partsOmitted > 0
403
+ ? { partsOmitted: content.partsOmitted }
404
+ : {}),
405
+ ...(details.truncated ? { details: true as const } : {}),
406
+ },
407
+ }
408
+ : {}),
409
+ };
410
+ }
411
+
412
+ export function projectEntry(entry: SessionEntry) {
413
+ if (entry.type !== "message") {
414
+ return { type: entry.type, id: entry.id, timestamp: entry.timestamp };
415
+ }
416
+ return {
417
+ type: entry.type,
418
+ id: entry.id,
419
+ timestamp: entry.timestamp,
420
+ message: projectMessage(entry.message),
421
+ };
422
+ }
423
+
424
+ const textEncoder = new TextEncoder();
425
+
426
+ export function jsonByteLength(value: unknown) {
427
+ return textEncoder.encode(JSON.stringify(value)).byteLength;
428
+ }
429
+
430
+ export function projectEntries(entries: readonly SessionEntry[]) {
431
+ const retained = entries.slice(-WEB_MAX_ENTRIES);
432
+ const projected: ReturnType<typeof projectEntry>[] = [];
433
+ let bytes = 2;
434
+ let messagePartsOmitted = 0;
435
+ let messagesTruncated = 0;
436
+ for (let index = retained.length - 1; index >= 0; index--) {
437
+ const entry = projectEntry(retained[index]!);
438
+ const entryBytes = jsonByteLength(entry) + (projected.length > 0 ? 1 : 0);
439
+ if (bytes + entryBytes > WEB_MAX_SELECTED_TRANSCRIPT_BYTES) break;
440
+ projected.unshift(entry);
441
+ bytes += entryBytes;
442
+ if (entry.type === "message" && entry.message) {
443
+ messagePartsOmitted += entry.message.truncation?.partsOmitted ?? 0;
444
+ if (entry.message.truncation) messagesTruncated++;
445
+ }
446
+ }
447
+ const entriesOmitted = entries.length - projected.length;
448
+ return {
449
+ entries: projected,
450
+ bytes,
451
+ truncation: {
452
+ truncated:
453
+ entriesOmitted > 0 ||
454
+ messagePartsOmitted > 0 ||
455
+ messagesTruncated > 0,
456
+ entriesOmitted,
457
+ messagePartsOmitted,
458
+ messagesTruncated,
459
+ maxBytes: WEB_MAX_SELECTED_TRANSCRIPT_BYTES,
460
+ } satisfies WebProjectionTruncation,
461
+ };
462
+ }