@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.
- package/README.md +184 -59
- package/SETUP.md +23 -7
- package/assets/openpi-launch-card-v1.webp +0 -0
- package/bin/openpi.js +145 -0
- package/extensions/ask-user/index.ts +30 -14
- package/extensions/background-terminals/index.ts +30 -2
- package/extensions/background-terminals/src/domain.ts +2 -0
- package/extensions/background-terminals/src/manager.ts +486 -106
- package/extensions/background-terminals/src/output.ts +33 -0
- package/extensions/background-terminals/src/prompt.ts +14 -6
- package/extensions/background-terminals/src/result-delivery.ts +4 -1
- package/extensions/background-terminals/src/ui/ps.ts +132 -129
- package/extensions/capabilities/index.ts +30 -42
- package/extensions/capabilities/src/ui.ts +93 -0
- package/extensions/clear-context/index.ts +83 -0
- package/extensions/context-pivot/index.ts +16 -6
- package/extensions/cron/schedule.ts +7 -1
- package/extensions/file-mutation-display/index.ts +34 -76
- package/extensions/file-mutation-display/render.ts +146 -87
- package/extensions/file-search/index.ts +8 -7
- package/extensions/file-search/src/binaries.ts +75 -59
- package/extensions/git-info/src/changed-files-view.ts +47 -14
- package/extensions/git-read/index.ts +328 -0
- package/extensions/git-read/src/args.ts +171 -0
- package/extensions/git-read/src/process.ts +81 -0
- package/extensions/git-read/src/prompt.ts +56 -0
- package/extensions/model-info/index.ts +21 -33
- package/extensions/model-info/session-metrics.ts +96 -0
- package/extensions/plan-mode/bash-policy.ts +54 -9
- package/extensions/plan-mode/index.ts +7 -2
- package/extensions/post-edit/index.ts +16 -6
- package/extensions/sessions/git-stats.ts +258 -72
- package/extensions/sessions/index.ts +222 -140
- package/extensions/sessions/preview-cache.ts +104 -0
- package/extensions/sessions/preview-loader.ts +856 -0
- package/extensions/sessions/sessions.ts +43 -4
- package/extensions/setup/index.ts +127 -131
- package/extensions/shared/activity-status.ts +36 -5
- package/extensions/shared/agent-session-page.ts +319 -0
- package/extensions/shared/agent-tool-renderer.ts +218 -0
- package/extensions/shared/agent-transcript.ts +524 -0
- package/extensions/shared/below-editor-navigation.ts +26 -0
- package/extensions/shared/capability-intent.ts +53 -0
- package/extensions/shared/child-session.ts +444 -22
- package/extensions/shared/result-budget.ts +134 -0
- package/extensions/shared/result-delivery.ts +34 -0
- package/extensions/shared/screen-chrome.ts +133 -0
- package/extensions/shared/setup-config.ts +97 -38
- package/extensions/shared/setup-episode-state.ts +1 -1
- package/extensions/shared/spinner.ts +28 -0
- package/extensions/shared/terminal-text.ts +110 -23
- package/extensions/shared/text-projection.ts +113 -0
- package/extensions/shared/tool-activity.ts +382 -0
- package/extensions/shared/tool-surface.ts +42 -8
- package/extensions/shared/transcript-viewport.ts +46 -0
- package/extensions/shared/web-observer-registry.ts +390 -0
- package/extensions/shared/worktree.ts +11 -0
- package/extensions/subagents/index.ts +461 -186
- package/extensions/subagents/navigation.ts +86 -28
- package/extensions/subagents/src/agent-types.ts +37 -15
- package/extensions/subagents/src/backend.ts +12 -1
- package/extensions/subagents/src/backends/pi.ts +375 -66
- package/extensions/subagents/src/domain.ts +5 -0
- package/extensions/subagents/src/id-sequence.ts +84 -0
- package/extensions/subagents/src/manager.ts +651 -536
- package/extensions/subagents/src/prompt.ts +185 -42
- package/extensions/subagents/src/result-artifact.ts +146 -0
- package/extensions/subagents/src/result-delivery.ts +7 -1
- package/extensions/subagents/src/runtime.ts +23 -6
- package/extensions/subagents/src/ui/takeover.ts +128 -337
- package/extensions/subagents/src/ui/transcript.ts +38 -501
- package/extensions/subagents/src/ui/wait-result.ts +103 -15
- package/extensions/suggestions/src/ui.ts +10 -4
- package/extensions/tasks/index.ts +0 -3
- package/extensions/tasks/ui.ts +79 -62
- package/extensions/ui-customization/footer.ts +7 -44
- package/extensions/ui-customization/index.ts +0 -4
- package/extensions/user-input-fold/index.ts +185 -0
- package/extensions/web/index.ts +234 -0
- package/extensions/workflows/artifacts.ts +147 -22
- package/extensions/workflows/completion-projection.ts +457 -0
- package/extensions/workflows/controller.ts +14 -2
- package/extensions/workflows/coordinator.ts +62 -0
- package/extensions/workflows/dashboard.ts +458 -339
- package/extensions/workflows/handoff.ts +121 -25
- package/extensions/workflows/index.ts +1042 -492
- package/extensions/workflows/journal.ts +148 -13
- package/extensions/workflows/model.ts +131 -19
- package/extensions/workflows/navigation.ts +61 -18
- package/extensions/workflows/progress-projection.ts +306 -0
- package/extensions/workflows/prompt.ts +166 -10
- package/extensions/workflows/replay-safety.ts +58 -27
- package/extensions/workflows/result-delivery.ts +253 -0
- package/extensions/workflows/retention.ts +593 -0
- package/extensions/workflows/runner.ts +388 -279
- package/extensions/workflows/sandbox-child.cjs +36 -3
- package/extensions/workflows/sandbox.ts +62 -8
- package/extensions/workflows/serialization.ts +325 -17
- package/extensions/workflows/tool-renderer.ts +22 -0
- package/extensions/workflows/transcript.ts +149 -0
- package/extensions/workspace-cleanup-guard/index.ts +54 -0
- package/extensions/workspace-cleanup-guard/workspace-provenance.ts +563 -0
- package/package.json +28 -8
- package/skills/subagents/REFERENCE.md +189 -0
- package/skills/subagents/SKILL.md +2 -2
- package/skills/workflows/REFERENCE.md +10 -5
- package/skills/workflows/SKILL.md +53 -10
- package/web/adapter/pi-adapter.ts +661 -0
- package/web/host/browser-launcher.ts +20 -0
- package/web/host/static-assets.ts +4 -0
- package/web/host/terminal-status.ts +38 -0
- package/web/host/web-host.ts +789 -0
- package/web/http-dispatcher.ts +125 -0
- package/web/protocol/types.ts +462 -0
- package/web/runtime/pi-runtime.ts +991 -0
- package/web/runtime/types.ts +71 -0
- package/web/runtime/web-host-lease.ts +497 -0
- package/web/trace.ts +18 -0
- package/web/ui/app.js +1398 -0
- package/web/ui/index.html +139 -0
- package/web/ui/styles.css +598 -0
- package/web/vite.config.mjs +34 -0
- package/extensions/execution-convergence/active-evidence.ts +0 -129
- package/extensions/execution-convergence/index.ts +0 -442
- package/extensions/execution-convergence/workspace-provenance.ts +0 -338
- package/extensions/setup/intercom-fs-helper.cjs +0 -130
- package/extensions/setup/intercom.ts +0 -603
- package/extensions/subagents/src/backends/stub.ts +0 -296
- package/extensions/subagents/src/format.ts +0 -48
|
@@ -164,7 +164,17 @@ const BOOTSTRAP = String.raw`
|
|
|
164
164
|
}
|
|
165
165
|
return value;
|
|
166
166
|
} catch (error) {
|
|
167
|
-
|
|
167
|
+
callHost(
|
|
168
|
+
"log",
|
|
169
|
+
JSON.stringify({
|
|
170
|
+
text:
|
|
171
|
+
"pipeline: item " +
|
|
172
|
+
index +
|
|
173
|
+
" dropped — " +
|
|
174
|
+
((error && error.message) || String(error)),
|
|
175
|
+
kind: "pipeline-drop",
|
|
176
|
+
}),
|
|
177
|
+
);
|
|
168
178
|
return null;
|
|
169
179
|
}
|
|
170
180
|
});
|
|
@@ -207,6 +217,11 @@ const BOOTSTRAP = String.raw`
|
|
|
207
217
|
const value = raw && typeof raw === "object" ? raw[key] : undefined;
|
|
208
218
|
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
209
219
|
};
|
|
220
|
+
const readLimit = (key) => {
|
|
221
|
+
const limits = raw && typeof raw === "object" ? raw.limits : undefined;
|
|
222
|
+
const value = limits && typeof limits === "object" ? limits[key] : undefined;
|
|
223
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
224
|
+
};
|
|
210
225
|
// Every field is coerced, so a missing or malformed snapshot reads as zero
|
|
211
226
|
// rather than undefined: a script doing arithmetic on it gets 0, not NaN.
|
|
212
227
|
return deepFreeze({
|
|
@@ -217,6 +232,12 @@ const BOOTSTRAP = String.raw`
|
|
|
217
232
|
total: read("total"),
|
|
218
233
|
cost: read("cost"),
|
|
219
234
|
agents: read("agents"),
|
|
235
|
+
limits: {
|
|
236
|
+
concurrency: readLimit("concurrency"),
|
|
237
|
+
maxAgentCalls: readLimit("maxAgentCalls"),
|
|
238
|
+
callsUsed: readLimit("callsUsed"),
|
|
239
|
+
callsRemaining: readLimit("callsRemaining"),
|
|
240
|
+
},
|
|
220
241
|
});
|
|
221
242
|
}
|
|
222
243
|
|
|
@@ -298,8 +319,9 @@ process.on("message", (message) => {
|
|
|
298
319
|
}
|
|
299
320
|
initialized = true;
|
|
300
321
|
token = message.token;
|
|
322
|
+
const seq = typeof message.seq === "number" ? message.seq : undefined;
|
|
301
323
|
if (typeof message.usageJson === "string") usageJson = message.usageJson;
|
|
302
|
-
run(message.source, message.argsJson, message.maxConcurrency);
|
|
324
|
+
run(message.source, message.argsJson, message.maxConcurrency, seq);
|
|
303
325
|
return;
|
|
304
326
|
}
|
|
305
327
|
if (message.token !== token || message.kind !== "agentResult") return;
|
|
@@ -307,6 +329,7 @@ process.on("message", (message) => {
|
|
|
307
329
|
const pending = pendingAgents.get(message.id);
|
|
308
330
|
if (!pending) return;
|
|
309
331
|
pendingAgents.delete(message.id);
|
|
332
|
+
const seq = typeof message.seq === "number" ? message.seq : undefined;
|
|
310
333
|
if (typeof message.resultJson === "string")
|
|
311
334
|
pending.resolve(message.resultJson);
|
|
312
335
|
else
|
|
@@ -315,9 +338,14 @@ process.on("message", (message) => {
|
|
|
315
338
|
typeof message.error === "string" ? message.error : "Agent IPC failed",
|
|
316
339
|
),
|
|
317
340
|
);
|
|
341
|
+
if (seq !== undefined) {
|
|
342
|
+
setImmediate(() => {
|
|
343
|
+
send({ kind: "idle", seq });
|
|
344
|
+
});
|
|
345
|
+
}
|
|
318
346
|
});
|
|
319
347
|
|
|
320
|
-
function run(source, argsJson, maxConcurrency) {
|
|
348
|
+
function run(source, argsJson, maxConcurrency, initSeq) {
|
|
321
349
|
try {
|
|
322
350
|
const sandbox = Object.create(null);
|
|
323
351
|
sandbox.__argsJson = argsJson;
|
|
@@ -389,6 +417,11 @@ function run(source, argsJson, maxConcurrency) {
|
|
|
389
417
|
context,
|
|
390
418
|
{ timeout: 1000 },
|
|
391
419
|
);
|
|
420
|
+
if (initSeq !== undefined) {
|
|
421
|
+
setImmediate(() => {
|
|
422
|
+
send({ kind: "idle", seq: initSeq });
|
|
423
|
+
});
|
|
424
|
+
}
|
|
392
425
|
Promise.resolve(context.__workflowPromise)
|
|
393
426
|
.then((resultJson) => {
|
|
394
427
|
if (typeof resultJson !== "string")
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
+
import { type ChildProcess, spawn } from "node:child_process";
|
|
1
2
|
import { randomBytes } from "node:crypto";
|
|
2
|
-
import { spawn, type ChildProcess } from "node:child_process";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { MAX_WORKFLOW_AGENT_CALLS } from "../shared/setup-config.ts";
|
|
6
|
-
import {
|
|
6
|
+
import { encodeCompleteJson, toSerializable } from "./serialization.ts";
|
|
7
7
|
|
|
8
8
|
const MAX_SOURCE_BYTES = 512 * 1024;
|
|
9
9
|
const MAX_ARGS_BYTES = 256 * 1024;
|
|
@@ -19,6 +19,11 @@ const MAX_LOG_MESSAGE_BYTES = 8 * 1024;
|
|
|
19
19
|
* bypasses the controller entirely.
|
|
20
20
|
*/
|
|
21
21
|
export const AGENT_CALL_BACKSTOP_MARGIN = 8;
|
|
22
|
+
/**
|
|
23
|
+
* Maximum synchronous execution time allowed between async agent boundaries.
|
|
24
|
+
* Non-yielding code (e.g. while(true){}) after await is terminated by this timeout.
|
|
25
|
+
*/
|
|
26
|
+
const SANDBOX_SYNC_TIMEOUT_MS = 1_000;
|
|
22
27
|
|
|
23
28
|
export interface SandboxAgentOptions {
|
|
24
29
|
agent_type?: unknown;
|
|
@@ -53,7 +58,7 @@ export interface RunWorkflowSandboxOptions {
|
|
|
53
58
|
signal: AbortSignal,
|
|
54
59
|
) => Promise<SandboxAgentResult>;
|
|
55
60
|
onPhase: (title: string) => void;
|
|
56
|
-
onLog: (text: string) => void;
|
|
61
|
+
onLog: (text: string, kind?: "pipeline-drop") => void;
|
|
57
62
|
/**
|
|
58
63
|
* Cumulative run usage, read at send time so the child's `usage()` reflects
|
|
59
64
|
* the agent that just settled rather than a value captured at launch.
|
|
@@ -147,13 +152,23 @@ export function runWorkflowSandbox(options: RunWorkflowSandboxOptions) {
|
|
|
147
152
|
),
|
|
148
153
|
);
|
|
149
154
|
|
|
150
|
-
const
|
|
155
|
+
const encodedArgs = encodeCompleteJson(
|
|
151
156
|
{ defined: options.args !== undefined, value: options.args },
|
|
152
|
-
{
|
|
157
|
+
{
|
|
158
|
+
maxBytes: MAX_ARGS_BYTES,
|
|
159
|
+
maxDepth: 16,
|
|
160
|
+
maxNodes: 10_000,
|
|
161
|
+
maxStringBytes: MAX_ARGS_BYTES,
|
|
162
|
+
},
|
|
153
163
|
);
|
|
154
|
-
if (
|
|
155
|
-
return Promise.reject(
|
|
164
|
+
if (!encodedArgs.ok) {
|
|
165
|
+
return Promise.reject(
|
|
166
|
+
new Error(
|
|
167
|
+
`Workflow args exceed the ${MAX_ARGS_BYTES}-byte IPC limit (${encodedArgs.limit} limit at ${encodedArgs.path})`,
|
|
168
|
+
),
|
|
169
|
+
);
|
|
156
170
|
}
|
|
171
|
+
const argsJson = encodedArgs.json;
|
|
157
172
|
|
|
158
173
|
return new Promise<unknown>((resolve, reject) => {
|
|
159
174
|
const workerPath = fileURLToPath(
|
|
@@ -182,6 +197,28 @@ export function runWorkflowSandbox(options: RunWorkflowSandboxOptions) {
|
|
|
182
197
|
const activeAgentRequests = new Map<number, AbortController>();
|
|
183
198
|
let requestCount = 0;
|
|
184
199
|
let finished = false;
|
|
200
|
+
let executionSeq = 0;
|
|
201
|
+
let executionWatchdog: NodeJS.Timeout | undefined;
|
|
202
|
+
|
|
203
|
+
const armWatchdog = (timeoutMs = SANDBOX_SYNC_TIMEOUT_MS) => {
|
|
204
|
+
if (finished) return 0;
|
|
205
|
+
if (executionWatchdog) clearTimeout(executionWatchdog);
|
|
206
|
+
const currentSeq = ++executionSeq;
|
|
207
|
+
executionWatchdog = setTimeout(() => {
|
|
208
|
+
if (finished || currentSeq !== executionSeq) return;
|
|
209
|
+
finish(new Error("Script execution timed out"));
|
|
210
|
+
}, timeoutMs);
|
|
211
|
+
executionWatchdog.unref?.();
|
|
212
|
+
return currentSeq;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const disarmWatchdog = (seq?: number) => {
|
|
216
|
+
if (seq !== undefined && seq !== executionSeq) return;
|
|
217
|
+
if (executionWatchdog) {
|
|
218
|
+
clearTimeout(executionWatchdog);
|
|
219
|
+
executionWatchdog = undefined;
|
|
220
|
+
}
|
|
221
|
+
};
|
|
185
222
|
|
|
186
223
|
// The child parses this and falls back to zeros if it is ever unusable, so
|
|
187
224
|
// a broken snapshot degrades `usage()` to a zero reading instead of
|
|
@@ -195,6 +232,7 @@ export function runWorkflowSandbox(options: RunWorkflowSandboxOptions) {
|
|
|
195
232
|
};
|
|
196
233
|
|
|
197
234
|
const cleanup = () => {
|
|
235
|
+
disarmWatchdog();
|
|
198
236
|
for (const abortController of activeAgentRequests.values()) {
|
|
199
237
|
abortController.abort(new Error("Workflow stopped"));
|
|
200
238
|
}
|
|
@@ -277,12 +315,21 @@ export function runWorkflowSandbox(options: RunWorkflowSandboxOptions) {
|
|
|
277
315
|
if (!isRecord(payload) || typeof payload.text !== "string") {
|
|
278
316
|
throw new Error("invalid text");
|
|
279
317
|
}
|
|
280
|
-
|
|
318
|
+
if (payload.kind !== undefined && payload.kind !== "pipeline-drop") {
|
|
319
|
+
throw new Error("invalid log kind");
|
|
320
|
+
}
|
|
321
|
+
options.onLog(payload.text, payload.kind);
|
|
281
322
|
} catch {
|
|
282
323
|
finish(new Error("Workflow sandbox sent an invalid log line"));
|
|
283
324
|
}
|
|
284
325
|
return;
|
|
285
326
|
}
|
|
327
|
+
if (raw.kind === "idle") {
|
|
328
|
+
if (typeof raw.seq === "number") {
|
|
329
|
+
disarmWatchdog(raw.seq);
|
|
330
|
+
}
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
286
333
|
if (raw.kind === "agent") {
|
|
287
334
|
if (
|
|
288
335
|
typeof raw.payloadJson !== "string" ||
|
|
@@ -335,10 +382,12 @@ export function runWorkflowSandbox(options: RunWorkflowSandboxOptions) {
|
|
|
335
382
|
error: "Agent result exceeded the workflow IPC output limit",
|
|
336
383
|
});
|
|
337
384
|
}
|
|
385
|
+
const seq = armWatchdog();
|
|
338
386
|
child.send({
|
|
339
387
|
token,
|
|
340
388
|
kind: "agentResult",
|
|
341
389
|
id,
|
|
390
|
+
seq,
|
|
342
391
|
resultJson,
|
|
343
392
|
usageJson: usageJson(),
|
|
344
393
|
});
|
|
@@ -385,6 +434,10 @@ export function runWorkflowSandbox(options: RunWorkflowSandboxOptions) {
|
|
|
385
434
|
finish(new Error("Workflow sandbox sent an unknown IPC message"));
|
|
386
435
|
});
|
|
387
436
|
|
|
437
|
+
// Arm the synchronous execution watchdog for the initial script invocation
|
|
438
|
+
// (covers non-yielding code before and after initial microtask yields).
|
|
439
|
+
const initSeq = armWatchdog();
|
|
440
|
+
|
|
388
441
|
child.send(
|
|
389
442
|
{
|
|
390
443
|
kind: "init",
|
|
@@ -393,6 +446,7 @@ export function runWorkflowSandbox(options: RunWorkflowSandboxOptions) {
|
|
|
393
446
|
argsJson,
|
|
394
447
|
maxConcurrency: options.maxConcurrency,
|
|
395
448
|
usageJson: usageJson(),
|
|
449
|
+
seq: initSeq,
|
|
396
450
|
},
|
|
397
451
|
(error) => {
|
|
398
452
|
if (error) finish(error);
|
|
@@ -8,6 +8,24 @@ export interface SerializationOptions {
|
|
|
8
8
|
maxStringBytes?: number;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
export type CompleteJsonLimit = "bytes" | "depth" | "nodes" | "string";
|
|
12
|
+
|
|
13
|
+
export type CompleteJsonEncoding =
|
|
14
|
+
| {
|
|
15
|
+
readonly ok: true;
|
|
16
|
+
readonly json: string;
|
|
17
|
+
readonly bytes: number;
|
|
18
|
+
readonly nodes: number;
|
|
19
|
+
}
|
|
20
|
+
| {
|
|
21
|
+
readonly ok: false;
|
|
22
|
+
readonly limit: CompleteJsonLimit;
|
|
23
|
+
readonly path: string;
|
|
24
|
+
readonly maximum: number;
|
|
25
|
+
readonly observedAtLeast: number;
|
|
26
|
+
readonly nodes: number;
|
|
27
|
+
};
|
|
28
|
+
|
|
11
29
|
const DEFAULT_MAX_BYTES = 1024 * 1024;
|
|
12
30
|
const DEFAULT_MAX_DEPTH = 16;
|
|
13
31
|
const DEFAULT_MAX_NODES = 20_000;
|
|
@@ -17,6 +35,269 @@ function byteLength(value: string) {
|
|
|
17
35
|
return Buffer.byteLength(value, "utf8");
|
|
18
36
|
}
|
|
19
37
|
|
|
38
|
+
function boundedPath(parent: string, child: string) {
|
|
39
|
+
const childPreview =
|
|
40
|
+
child.length <= 128 ? child : `${child.slice(0, 128)}...[key truncated]`;
|
|
41
|
+
const path = `${parent}.${childPreview}`;
|
|
42
|
+
return byteLength(path) <= 256
|
|
43
|
+
? path
|
|
44
|
+
: `${truncateUtf8(path, 240)}...[path truncated]`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function configuredBudget(name: string, value: number, minimum: number) {
|
|
48
|
+
if (!Number.isSafeInteger(value) || !Number.isFinite(value)) {
|
|
49
|
+
throw new RangeError(`${name} must be a finite integer`);
|
|
50
|
+
}
|
|
51
|
+
if (value < minimum) {
|
|
52
|
+
throw new RangeError(`${name} must be at least ${minimum}`);
|
|
53
|
+
}
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Encode a complete inert JSON value while bounding work and output during
|
|
59
|
+
* traversal. Unlike `safeStringify`, this interface never returns a truncated
|
|
60
|
+
* value: callers that persist authoritative data can fail closed instead.
|
|
61
|
+
*/
|
|
62
|
+
export function encodeCompleteJson(
|
|
63
|
+
value: unknown,
|
|
64
|
+
options: Required<SerializationOptions>,
|
|
65
|
+
): CompleteJsonEncoding {
|
|
66
|
+
const maxBytes = configuredBudget("maxBytes", options.maxBytes, 1);
|
|
67
|
+
const maxDepth = configuredBudget("maxDepth", options.maxDepth, 0);
|
|
68
|
+
const maxNodes = configuredBudget("maxNodes", options.maxNodes, 1);
|
|
69
|
+
const maxStringBytes = configuredBudget(
|
|
70
|
+
"maxStringBytes",
|
|
71
|
+
options.maxStringBytes,
|
|
72
|
+
0,
|
|
73
|
+
);
|
|
74
|
+
const chunks: string[] = [];
|
|
75
|
+
const seen = new WeakMap<object, string>();
|
|
76
|
+
let bytes = 0;
|
|
77
|
+
let nodes = 0;
|
|
78
|
+
let failure: Exclude<CompleteJsonEncoding, { ok: true }> | undefined;
|
|
79
|
+
|
|
80
|
+
const stop = (
|
|
81
|
+
limit: CompleteJsonLimit,
|
|
82
|
+
path: string,
|
|
83
|
+
maximum: number,
|
|
84
|
+
observedAtLeast: number,
|
|
85
|
+
) => {
|
|
86
|
+
failure ??= {
|
|
87
|
+
ok: false,
|
|
88
|
+
limit,
|
|
89
|
+
path: truncateUtf8(path, 256),
|
|
90
|
+
maximum,
|
|
91
|
+
observedAtLeast,
|
|
92
|
+
nodes,
|
|
93
|
+
};
|
|
94
|
+
return false;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const append = (text: string, path: string) => {
|
|
98
|
+
const addition = byteLength(text);
|
|
99
|
+
if (bytes + addition > maxBytes) {
|
|
100
|
+
return stop("bytes", path, maxBytes, bytes + addition);
|
|
101
|
+
}
|
|
102
|
+
chunks.push(text);
|
|
103
|
+
bytes += addition;
|
|
104
|
+
return true;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const encodedString = (text: string, path: string) => {
|
|
108
|
+
const sourceBytes = byteLength(text);
|
|
109
|
+
if (sourceBytes > maxStringBytes) {
|
|
110
|
+
stop("string", path, maxStringBytes, sourceBytes);
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
return JSON.stringify(text);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const safeErrorText = (error: unknown) => {
|
|
117
|
+
try {
|
|
118
|
+
return error instanceof Error ? error.message : String(error);
|
|
119
|
+
} catch {
|
|
120
|
+
return "unknown error";
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const visit = (current: unknown, depth: number, path: string): boolean => {
|
|
125
|
+
if (failure) return false;
|
|
126
|
+
if (nodes >= maxNodes) {
|
|
127
|
+
return stop("nodes", path, maxNodes, nodes + 1);
|
|
128
|
+
}
|
|
129
|
+
nodes++;
|
|
130
|
+
if (depth > maxDepth) {
|
|
131
|
+
return stop("depth", path, maxDepth, depth);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (current === null) return append("null", path);
|
|
135
|
+
if (typeof current === "boolean")
|
|
136
|
+
return append(current ? "true" : "false", path);
|
|
137
|
+
if (typeof current === "string") {
|
|
138
|
+
const encoded = encodedString(current, path);
|
|
139
|
+
return encoded === undefined ? false : append(encoded, path);
|
|
140
|
+
}
|
|
141
|
+
if (typeof current === "number") {
|
|
142
|
+
return append(
|
|
143
|
+
Number.isFinite(current)
|
|
144
|
+
? JSON.stringify(current)
|
|
145
|
+
: JSON.stringify(`[number: ${String(current)}]`),
|
|
146
|
+
path,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
if (typeof current === "bigint")
|
|
150
|
+
return append(JSON.stringify(`${current.toString()}n`), path);
|
|
151
|
+
if (typeof current === "undefined")
|
|
152
|
+
return append(JSON.stringify("[undefined]"), path);
|
|
153
|
+
if (typeof current === "symbol")
|
|
154
|
+
return append(
|
|
155
|
+
JSON.stringify(`[symbol: ${current.description ?? ""}]`),
|
|
156
|
+
path,
|
|
157
|
+
);
|
|
158
|
+
if (typeof current === "function")
|
|
159
|
+
return append(
|
|
160
|
+
JSON.stringify(`[function: ${current.name || "anonymous"}]`),
|
|
161
|
+
path,
|
|
162
|
+
);
|
|
163
|
+
if (typeof current !== "object")
|
|
164
|
+
return append(JSON.stringify(String(current)), path);
|
|
165
|
+
|
|
166
|
+
const prior = seen.get(current);
|
|
167
|
+
if (prior) return append(JSON.stringify(`[circular: ${prior}]`), path);
|
|
168
|
+
seen.set(current, path);
|
|
169
|
+
|
|
170
|
+
if (current instanceof Date) {
|
|
171
|
+
try {
|
|
172
|
+
return append(
|
|
173
|
+
JSON.stringify(
|
|
174
|
+
Number.isNaN(current.getTime())
|
|
175
|
+
? "[date: invalid]"
|
|
176
|
+
: current.toISOString(),
|
|
177
|
+
),
|
|
178
|
+
path,
|
|
179
|
+
);
|
|
180
|
+
} catch (error) {
|
|
181
|
+
return append(
|
|
182
|
+
JSON.stringify(`[unreadable date: ${safeErrorText(error)}]`),
|
|
183
|
+
path,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (current instanceof Error) {
|
|
189
|
+
let name = "Error";
|
|
190
|
+
let message = "";
|
|
191
|
+
let stack: string | undefined;
|
|
192
|
+
try {
|
|
193
|
+
name = current.name;
|
|
194
|
+
} catch {}
|
|
195
|
+
try {
|
|
196
|
+
message = current.message;
|
|
197
|
+
} catch {}
|
|
198
|
+
try {
|
|
199
|
+
stack = current.stack;
|
|
200
|
+
} catch {}
|
|
201
|
+
return visit(
|
|
202
|
+
{
|
|
203
|
+
name,
|
|
204
|
+
message,
|
|
205
|
+
...(stack ? { stack: truncateUtf8(stack, 16 * 1024) } : {}),
|
|
206
|
+
},
|
|
207
|
+
depth,
|
|
208
|
+
path,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (Array.isArray(current)) {
|
|
213
|
+
let length: number;
|
|
214
|
+
try {
|
|
215
|
+
length = current.length;
|
|
216
|
+
} catch (error) {
|
|
217
|
+
return visit(
|
|
218
|
+
`[unreadable array: ${safeErrorText(error)}]`,
|
|
219
|
+
depth + 1,
|
|
220
|
+
path,
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
if (!append("[", path)) return false;
|
|
224
|
+
for (let index = 0; index < length; index++) {
|
|
225
|
+
if (nodes >= maxNodes) {
|
|
226
|
+
return stop(
|
|
227
|
+
"nodes",
|
|
228
|
+
boundedPath(path, String(index)),
|
|
229
|
+
maxNodes,
|
|
230
|
+
nodes + 1,
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
if (index > 0 && !append(",", path)) return false;
|
|
234
|
+
if (!(index in current)) {
|
|
235
|
+
nodes++;
|
|
236
|
+
if (!append("null", boundedPath(path, String(index)))) return false;
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
let item: unknown;
|
|
240
|
+
try {
|
|
241
|
+
item = current[index];
|
|
242
|
+
} catch (error) {
|
|
243
|
+
item = `[unreadable property: ${safeErrorText(error)}]`;
|
|
244
|
+
}
|
|
245
|
+
if (!visit(item, depth + 1, boundedPath(path, String(index)))) {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return append("]", path);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const objectChunkStart = chunks.length;
|
|
253
|
+
const objectByteStart = bytes;
|
|
254
|
+
if (!append("{", path)) return false;
|
|
255
|
+
let count = 0;
|
|
256
|
+
try {
|
|
257
|
+
// JavaScript has no interruptible own-key iterator: a Proxy's ownKeys
|
|
258
|
+
// trap may allocate before iteration begins. Once keys are available,
|
|
259
|
+
// the budgets below stop before reading any further property values.
|
|
260
|
+
for (const key in current) {
|
|
261
|
+
if (!Object.prototype.hasOwnProperty.call(current, key)) continue;
|
|
262
|
+
if (nodes >= maxNodes) {
|
|
263
|
+
return stop("nodes", boundedPath(path, key), maxNodes, nodes + 1);
|
|
264
|
+
}
|
|
265
|
+
const keyBytes = byteLength(key);
|
|
266
|
+
const childPath = boundedPath(path, key);
|
|
267
|
+
if (keyBytes > maxStringBytes) {
|
|
268
|
+
return stop("string", childPath, maxStringBytes, keyBytes);
|
|
269
|
+
}
|
|
270
|
+
const keyJson = JSON.stringify(key);
|
|
271
|
+
if (count > 0 && !append(",", path)) return false;
|
|
272
|
+
if (!append(keyJson, childPath) || !append(":", childPath)) {
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
let child: unknown;
|
|
276
|
+
try {
|
|
277
|
+
child = (current as Record<string, unknown>)[key];
|
|
278
|
+
} catch (error) {
|
|
279
|
+
child = `[unreadable property: ${safeErrorText(error)}]`;
|
|
280
|
+
}
|
|
281
|
+
if (!visit(child, depth + 1, childPath)) return false;
|
|
282
|
+
count++;
|
|
283
|
+
}
|
|
284
|
+
} catch (error) {
|
|
285
|
+
chunks.length = objectChunkStart;
|
|
286
|
+
bytes = objectByteStart;
|
|
287
|
+
return visit(
|
|
288
|
+
`[unreadable object: ${safeErrorText(error)}]`,
|
|
289
|
+
depth + 1,
|
|
290
|
+
path,
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
return append("}", path);
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
visit(value, 0, "$root");
|
|
297
|
+
if (failure) return failure;
|
|
298
|
+
return { ok: true, json: chunks.join(""), bytes, nodes };
|
|
299
|
+
}
|
|
300
|
+
|
|
20
301
|
export function truncateUtf8(value: string, maxBytes: number) {
|
|
21
302
|
if (maxBytes <= 0) return "";
|
|
22
303
|
if (byteLength(value) <= maxBytes) return value;
|
|
@@ -72,9 +353,29 @@ export function toSerializable(
|
|
|
72
353
|
seen.set(current, location);
|
|
73
354
|
|
|
74
355
|
if (Array.isArray(current)) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
)
|
|
356
|
+
const result: unknown[] = [];
|
|
357
|
+
const length = current.length;
|
|
358
|
+
for (let index = 0; index < length; index++) {
|
|
359
|
+
if (nodes >= maxNodes) {
|
|
360
|
+
result.push("[truncated: node limit]");
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
363
|
+
if (!(index in current)) {
|
|
364
|
+
nodes++;
|
|
365
|
+
result.length++;
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
try {
|
|
369
|
+
result.push(
|
|
370
|
+
visit(current[index], depth + 1, `${location}[${index}]`),
|
|
371
|
+
);
|
|
372
|
+
} catch (error) {
|
|
373
|
+
result.push(
|
|
374
|
+
`[unreadable property: ${error instanceof Error ? error.message : String(error)}]`,
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return result;
|
|
78
379
|
}
|
|
79
380
|
|
|
80
381
|
if (current instanceof Date) {
|
|
@@ -93,24 +394,31 @@ export function toSerializable(
|
|
|
93
394
|
}
|
|
94
395
|
|
|
95
396
|
const result: Record<string, unknown> = Object.create(null);
|
|
96
|
-
let keys: string[];
|
|
97
397
|
try {
|
|
98
|
-
|
|
398
|
+
for (const key in current) {
|
|
399
|
+
if (!Object.prototype.hasOwnProperty.call(current, key)) continue;
|
|
400
|
+
if (nodes >= maxNodes) {
|
|
401
|
+
let marker = "[truncated: node limit]";
|
|
402
|
+
while (Object.prototype.hasOwnProperty.call(result, marker)) {
|
|
403
|
+
marker += ".";
|
|
404
|
+
}
|
|
405
|
+
result[marker] = true;
|
|
406
|
+
break;
|
|
407
|
+
}
|
|
408
|
+
try {
|
|
409
|
+
result[key] = visit(
|
|
410
|
+
(current as Record<string, unknown>)[key],
|
|
411
|
+
depth + 1,
|
|
412
|
+
`${location}.${key}`,
|
|
413
|
+
);
|
|
414
|
+
} catch (error) {
|
|
415
|
+
result[key] =
|
|
416
|
+
`[unreadable property: ${error instanceof Error ? error.message : String(error)}]`;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
99
419
|
} catch (error) {
|
|
100
420
|
return `[unreadable object: ${error instanceof Error ? error.message : String(error)}]`;
|
|
101
421
|
}
|
|
102
|
-
for (const key of keys) {
|
|
103
|
-
try {
|
|
104
|
-
result[key] = visit(
|
|
105
|
-
(current as Record<string, unknown>)[key],
|
|
106
|
-
depth + 1,
|
|
107
|
-
`${location}.${key}`,
|
|
108
|
-
);
|
|
109
|
-
} catch (error) {
|
|
110
|
-
result[key] =
|
|
111
|
-
`[unreadable property: ${error instanceof Error ? error.message : String(error)}]`;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
422
|
return result;
|
|
115
423
|
};
|
|
116
424
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** In-memory association between a workflow transcript projection and its child renderer. */
|
|
2
|
+
|
|
3
|
+
import type { AgentToolRenderer } from "../shared/agent-tool-renderer.ts";
|
|
4
|
+
import type { TranscriptEntry } from "./model.ts";
|
|
5
|
+
|
|
6
|
+
const renderers = new WeakMap<
|
|
7
|
+
ReadonlyArray<TranscriptEntry>,
|
|
8
|
+
AgentToolRenderer
|
|
9
|
+
>();
|
|
10
|
+
|
|
11
|
+
export function bindWorkflowToolRenderer<
|
|
12
|
+
T extends ReadonlyArray<TranscriptEntry>,
|
|
13
|
+
>(transcript: T, renderer: AgentToolRenderer): T {
|
|
14
|
+
renderers.set(transcript, renderer);
|
|
15
|
+
return transcript;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function workflowToolRenderer(
|
|
19
|
+
transcript: ReadonlyArray<TranscriptEntry>,
|
|
20
|
+
) {
|
|
21
|
+
return renderers.get(transcript);
|
|
22
|
+
}
|