@nredd/pi-subagents 0.22.0-nredd.6 → 0.22.0-nredd.8
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 +160 -43
- package/package.json +8 -8
- package/src/agent-file-toggle.ts +6 -4
- package/src/agent-manager.ts +418 -118
- package/src/agent-runner.ts +155 -62
- package/src/cross-extension-rpc.ts +11 -36
- package/src/custom-agents.ts +9 -5
- package/src/default-agents.ts +2 -4
- package/src/enabled-models.ts +8 -7
- package/src/enterprise-local.ts +5 -5
- package/src/home-paths.ts +27 -0
- package/src/index.ts +231 -107
- package/src/invocation-config.ts +8 -35
- package/src/memory.ts +9 -9
- package/src/mention-clone.ts +9 -5
- package/src/model-resolver.ts +41 -11
- package/src/model-scope.ts +2 -1
- package/src/model-selection.ts +159 -0
- package/src/model-tiers.ts +171 -0
- package/src/nested-tools.ts +31 -26
- package/src/output-file.ts +23 -1
- package/src/package-resources.ts +25 -10
- package/src/process-budget.ts +346 -0
- package/src/quota-admission.ts +21 -9
- package/src/resource-loader.ts +51 -0
- package/src/schedule.ts +41 -19
- package/src/settings.ts +111 -8
- package/src/skill-loader.ts +8 -7
- package/src/stale-ctx.ts +16 -0
- package/src/types.ts +33 -0
- package/src/ui/agent-row.ts +13 -3
- package/src/ui/agent-widget.ts +15 -4
- package/src/ui/conversation-viewer.ts +6 -3
- package/src/ui/fallback-flag.ts +90 -0
- package/src/ui/fleet-list.ts +363 -53
- package/src/ui/fleet-state.ts +154 -0
- package/src/ui/notification-view.ts +18 -2
- package/src/ui/workflow-card.ts +5 -1
- package/src/ui/workflow-dialog.ts +10 -0
- package/src/workflow/host.ts +43 -45
- package/src/workflow/journal.ts +13 -1
- package/src/workflow/progress.ts +4 -0
- package/src/workflow/runtime.ts +25 -8
- package/src/workflow/saved.ts +19 -17
- package/src/workflow/task.ts +10 -0
- package/src/workflow/worker-source.ts +156 -38
package/src/workflow/saved.ts
CHANGED
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
|
|
40
40
|
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
41
41
|
import { isAbsolute, join } from "node:path";
|
|
42
|
-
import {
|
|
42
|
+
import { resolveAgentDir } from "../home-paths.js";
|
|
43
43
|
import { isSymlink, isUnsafeName, safeReadFile } from "../memory.js";
|
|
44
44
|
import { isPackageResourceExcluded, packageWorkflowDirs, packageWorkflowFiles } from "../package-resources.js";
|
|
45
45
|
import { hasMetaDeclaration } from "./meta.js";
|
|
@@ -61,12 +61,12 @@ const WORKFLOW_EXTENSION = ".js";
|
|
|
61
61
|
* reaches this without an options bag travelling through `resolveWorkflowScript`,
|
|
62
62
|
* `resolveWorkflowSource` and `readSavedWorkflow` purely to be consulted once.
|
|
63
63
|
*/
|
|
64
|
-
export function savedWorkflowRoots(cwd: string): string[] {
|
|
64
|
+
export function savedWorkflowRoots(cwd: string, homeDir?: string): string[] {
|
|
65
65
|
return [
|
|
66
66
|
join(cwd, ".pi", "workflows"),
|
|
67
67
|
join(cwd, ".agents", "workflows"),
|
|
68
|
-
join(
|
|
69
|
-
...packageWorkflowDirs(cwd),
|
|
68
|
+
join(resolveAgentDir(homeDir), "workflows"),
|
|
69
|
+
...packageWorkflowDirs(cwd, { homeDir }),
|
|
70
70
|
];
|
|
71
71
|
}
|
|
72
72
|
|
|
@@ -81,7 +81,7 @@ export type SavedWorkflow =
|
|
|
81
81
|
* names that do exist — a model that guessed the name can correct itself from
|
|
82
82
|
* the error instead of spending a turn asking.
|
|
83
83
|
*/
|
|
84
|
-
export function readSavedWorkflow(name: string, cwd: string): SavedWorkflow {
|
|
84
|
+
export function readSavedWorkflow(name: string, cwd: string, homeDir?: string): SavedWorkflow {
|
|
85
85
|
const trimmed = name.trim();
|
|
86
86
|
if (isUnsafeName(trimmed)) {
|
|
87
87
|
return {
|
|
@@ -92,13 +92,13 @@ export function readSavedWorkflow(name: string, cwd: string): SavedWorkflow {
|
|
|
92
92
|
};
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
const roots = savedWorkflowRoots(cwd);
|
|
95
|
+
const roots = savedWorkflowRoots(cwd, homeDir);
|
|
96
96
|
for (const root of roots) {
|
|
97
97
|
if (isSymlink(root)) continue; // reject a symlinked root entirely, as skill-loader does
|
|
98
98
|
const path = join(root, `${trimmed}${WORKFLOW_EXTENSION}`);
|
|
99
99
|
// A `!` entry in the declaring package's manifest. Only ever true for a path
|
|
100
100
|
// under a package root, so the local roots pay one set lookup for it.
|
|
101
|
-
if (isPackageResourceExcluded(path, cwd, "workflows")) continue;
|
|
101
|
+
if (isPackageResourceExcluded(path, cwd, "workflows", { homeDir })) continue;
|
|
102
102
|
const script = safeReadFile(path);
|
|
103
103
|
if (script === undefined) continue;
|
|
104
104
|
// Found the file, so stop looking — a shadowing name that turns out not to
|
|
@@ -117,7 +117,7 @@ export function readSavedWorkflow(name: string, cwd: string): SavedWorkflow {
|
|
|
117
117
|
// Last: a package that declared an individual file rather than a directory.
|
|
118
118
|
// It carries no root to look a name up in, so it is matched by name here
|
|
119
119
|
// instead — after every root, which keeps package scripts lowest-precedence.
|
|
120
|
-
const declared = packageWorkflowFiles(cwd).get(trimmed);
|
|
120
|
+
const declared = packageWorkflowFiles(cwd, { homeDir }).get(trimmed);
|
|
121
121
|
if (declared !== undefined) {
|
|
122
122
|
const script = safeReadFile(declared);
|
|
123
123
|
if (script !== undefined) {
|
|
@@ -133,7 +133,7 @@ export function readSavedWorkflow(name: string, cwd: string): SavedWorkflow {
|
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
const known = listSavedWorkflows(cwd);
|
|
136
|
+
const known = listSavedWorkflows(cwd, homeDir);
|
|
137
137
|
return {
|
|
138
138
|
ok: false,
|
|
139
139
|
message:
|
|
@@ -154,6 +154,7 @@ export function readSavedWorkflow(name: string, cwd: string): SavedWorkflow {
|
|
|
154
154
|
export function resolveWorkflowSource(
|
|
155
155
|
ref: { name?: string; scriptPath?: string },
|
|
156
156
|
cwd: string,
|
|
157
|
+
homeDir?: string,
|
|
157
158
|
): SavedWorkflow {
|
|
158
159
|
const path = ref.scriptPath?.trim();
|
|
159
160
|
if (path !== undefined && path !== "") {
|
|
@@ -168,14 +169,14 @@ export function resolveWorkflowSource(
|
|
|
168
169
|
}
|
|
169
170
|
}
|
|
170
171
|
const name = ref.name?.trim();
|
|
171
|
-
if (name !== undefined && name !== "") return readSavedWorkflow(name, cwd);
|
|
172
|
+
if (name !== undefined && name !== "") return readSavedWorkflow(name, cwd, homeDir);
|
|
172
173
|
return { ok: false, message: "A workflow reference needs a `name` or a `scriptPath`." };
|
|
173
174
|
}
|
|
174
175
|
|
|
175
176
|
/** Every saved workflow name, de-duplicated across roots and sorted. */
|
|
176
|
-
export function listSavedWorkflows(cwd: string): string[] {
|
|
177
|
+
export function listSavedWorkflows(cwd: string, homeDir?: string): string[] {
|
|
177
178
|
const names = new Set<string>();
|
|
178
|
-
for (const root of savedWorkflowRoots(cwd)) {
|
|
179
|
+
for (const root of savedWorkflowRoots(cwd, homeDir)) {
|
|
179
180
|
if (!existsSync(root) || isSymlink(root)) continue;
|
|
180
181
|
let entries: string[];
|
|
181
182
|
try {
|
|
@@ -188,11 +189,11 @@ export function listSavedWorkflows(cwd: string): string[] {
|
|
|
188
189
|
const name = entry.slice(0, -WORKFLOW_EXTENSION.length);
|
|
189
190
|
if (isUnsafeName(name)) continue;
|
|
190
191
|
const path = join(root, entry);
|
|
191
|
-
if (isPackageResourceExcluded(path, cwd, "workflows")) continue;
|
|
192
|
+
if (isPackageResourceExcluded(path, cwd, "workflows", { homeDir })) continue;
|
|
192
193
|
if (isWorkflowFile(path)) names.add(name);
|
|
193
194
|
}
|
|
194
195
|
}
|
|
195
|
-
for (const [name, path] of packageWorkflowFiles(cwd)) {
|
|
196
|
+
for (const [name, path] of packageWorkflowFiles(cwd, { homeDir })) {
|
|
196
197
|
if (isUnsafeName(name)) continue;
|
|
197
198
|
if (isWorkflowFile(path)) names.add(name);
|
|
198
199
|
}
|
|
@@ -228,10 +229,11 @@ function isWorkflowFile(path: string): boolean {
|
|
|
228
229
|
export function resolveWorkflowScript(
|
|
229
230
|
params: { script?: string; scriptPath?: string; name?: string },
|
|
230
231
|
cwd: string,
|
|
232
|
+
homeDir?: string,
|
|
231
233
|
): { ok: true; script: string; scriptPath?: string } | { ok: false; message: string } {
|
|
232
234
|
const path = params.scriptPath?.trim();
|
|
233
235
|
if (path !== undefined && path !== "") {
|
|
234
|
-
const resolved = resolveWorkflowSource({ scriptPath: path }, cwd);
|
|
236
|
+
const resolved = resolveWorkflowSource({ scriptPath: path }, cwd, homeDir);
|
|
235
237
|
return resolved.ok ? { ok: true, script: resolved.script, scriptPath: resolved.path } : resolved;
|
|
236
238
|
}
|
|
237
239
|
const script = params.script;
|
|
@@ -244,11 +246,11 @@ export function resolveWorkflowScript(
|
|
|
244
246
|
// reference means.
|
|
245
247
|
const name = params.name?.trim();
|
|
246
248
|
if (name !== undefined && name !== "") {
|
|
247
|
-
const saved = resolveWorkflowSource({ name }, cwd);
|
|
249
|
+
const saved = resolveWorkflowSource({ name }, cwd, homeDir);
|
|
248
250
|
return saved.ok ? { ok: true, script: saved.script, scriptPath: saved.path } : saved;
|
|
249
251
|
}
|
|
250
252
|
|
|
251
|
-
const known = listSavedWorkflows(cwd);
|
|
253
|
+
const known = listSavedWorkflows(cwd, homeDir);
|
|
252
254
|
return {
|
|
253
255
|
ok: false,
|
|
254
256
|
message:
|
package/src/workflow/task.ts
CHANGED
|
@@ -86,6 +86,8 @@ export interface WorkflowTask {
|
|
|
86
86
|
/** The script's return value, once the run produced one. */
|
|
87
87
|
value?: unknown;
|
|
88
88
|
error?: string;
|
|
89
|
+
/** Full authored stack for the result; the compact status keeps only error. */
|
|
90
|
+
errorStack?: string;
|
|
89
91
|
}
|
|
90
92
|
|
|
91
93
|
export function createWorkflowTask(init: {
|
|
@@ -205,6 +207,7 @@ export function completeWorkflowTask(task: WorkflowTask, result: WorkflowRunResu
|
|
|
205
207
|
task.replayedCount = result.replayedCount;
|
|
206
208
|
task.value = result.value;
|
|
207
209
|
task.error = result.error;
|
|
210
|
+
task.errorStack = result.errorStack;
|
|
208
211
|
task.endTime = Date.now();
|
|
209
212
|
}
|
|
210
213
|
|
|
@@ -222,6 +225,13 @@ export function failWorkflowTask(task: WorkflowTask, error: string): void {
|
|
|
222
225
|
|
|
223
226
|
/** The run's outcome as text, for the notification and the LLM-facing result. */
|
|
224
227
|
export function workflowResultText(task: WorkflowTask): string {
|
|
228
|
+
if (task.errorStack !== undefined) {
|
|
229
|
+
const stack = task.errorStack.slice(0, 16384);
|
|
230
|
+
const firstLine = stack.split("\n", 1)[0];
|
|
231
|
+
const message = task.error;
|
|
232
|
+
if (message === undefined || message === "" || firstLine === message || firstLine.endsWith(`: ${message}`)) return stack;
|
|
233
|
+
return `${message}\n${stack}`;
|
|
234
|
+
}
|
|
225
235
|
if (task.error !== undefined) return task.error;
|
|
226
236
|
if (task.value === undefined) return "No output.";
|
|
227
237
|
if (typeof task.value === "string") return task.value;
|
|
@@ -26,10 +26,11 @@
|
|
|
26
26
|
*
|
|
27
27
|
* That said: our injected globals are themselves host closures, so
|
|
28
28
|
* `agent.constructor` is still the host `Function`. The hygiene shrinks the
|
|
29
|
-
* surface; it does not close the hole.
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* as a determinism boundary, not a
|
|
29
|
+
* surface; it does not close the hole. `codeGeneration: { strings: false }`
|
|
30
|
+
* makes realm-local `Function` and `eval` throw `EvalError`, but does not stop a
|
|
31
|
+
* captured host `Function`. The stack formatter stays realm-native rather than
|
|
32
|
+
* adding another host callback. Treat this as a determinism boundary, not a
|
|
33
|
+
* security boundary against a hostile script.
|
|
33
34
|
*
|
|
34
35
|
* ## Why determinism is a prelude and not a stub
|
|
35
36
|
*
|
|
@@ -48,9 +49,8 @@
|
|
|
48
49
|
/**
|
|
49
50
|
* Runs inside the realm, ahead of the script body, on a single line.
|
|
50
51
|
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* newline in here shifts every stack frame in every workflow script.
|
|
52
|
+
* The shared compiler puts this and the wrapper on the body's first line.
|
|
53
|
+
* No lineOffset is needed (Bun ignores it); only first-line columns need mapping.
|
|
54
54
|
*/
|
|
55
55
|
const DETERMINISM_PRELUDE =
|
|
56
56
|
"const Date = (function () {" +
|
|
@@ -96,13 +96,13 @@ const pendingCalls = new Map();
|
|
|
96
96
|
*/
|
|
97
97
|
let spentOutput = 0;
|
|
98
98
|
|
|
99
|
-
function callHost(method, payload) {
|
|
99
|
+
function callHost(method, payload, onModelNote) {
|
|
100
100
|
// Drain first, so the phase() that named this agent reaches the host ahead of
|
|
101
101
|
// the agent entry rather than a tick behind it.
|
|
102
102
|
flushProgress();
|
|
103
103
|
return new Promise(function (resolve, reject) {
|
|
104
104
|
const callId = nextCallId++;
|
|
105
|
-
pendingCalls.set(callId, { resolve: resolve, reject: reject });
|
|
105
|
+
pendingCalls.set(callId, { resolve: resolve, reject: reject, onModelNote: onModelNote });
|
|
106
106
|
port.postMessage({ type: "call", callId: callId, method: method, payload: payload });
|
|
107
107
|
});
|
|
108
108
|
}
|
|
@@ -114,6 +114,7 @@ port.on("message", function (message) {
|
|
|
114
114
|
if (!waiter) return;
|
|
115
115
|
pendingCalls.delete(message.callId);
|
|
116
116
|
if (message.ok) {
|
|
117
|
+
if (message.modelNote !== undefined && waiter.onModelNote) waiter.onModelNote(message.modelNote);
|
|
117
118
|
waiter.resolve(message.value);
|
|
118
119
|
return;
|
|
119
120
|
}
|
|
@@ -523,6 +524,8 @@ async function agentIn(scope, prompt, opts) {
|
|
|
523
524
|
const phaseIndex = phaseName !== undefined ? definePhaseIn(scope, phaseName) : scope.ambientPhaseIndex;
|
|
524
525
|
const phaseTitle = phaseName !== undefined ? scopedTitle(scope, phaseName) : scope.ambientPhaseTitle;
|
|
525
526
|
|
|
527
|
+
// Set when the host ran the child on another model than configured.
|
|
528
|
+
let modelNote;
|
|
526
529
|
const result = await callHost("agent", {
|
|
527
530
|
prompt: text,
|
|
528
531
|
label: label,
|
|
@@ -535,20 +538,60 @@ async function agentIn(scope, prompt, opts) {
|
|
|
535
538
|
resume: resume,
|
|
536
539
|
effort: effort,
|
|
537
540
|
schema: schema,
|
|
538
|
-
});
|
|
539
|
-
if (result === undefined || result === null)
|
|
541
|
+
}, function (note) { modelNote = note; });
|
|
542
|
+
if (result === undefined || result === null) {
|
|
543
|
+
// A failed child is a null; its note would otherwise be lost with it.
|
|
544
|
+
if (modelNote !== undefined) logIn(scope, modelNote);
|
|
545
|
+
return null;
|
|
546
|
+
}
|
|
540
547
|
if (schema === undefined) return result;
|
|
541
548
|
// Parsed with the realm's own JSON.parse so the script gets an object whose
|
|
542
549
|
// prototype is its own — \`x instanceof Object\` and \`x.list instanceof Array\`
|
|
543
550
|
// both hold, and it survives assertBoundary if the script returns it.
|
|
544
551
|
try {
|
|
545
|
-
|
|
552
|
+
const parsed = realmParse(result);
|
|
553
|
+
// A schema'd payload must stay exactly what the schema describes, so the
|
|
554
|
+
// note cannot lead it as text. It goes to the run log, and onto a plain
|
|
555
|
+
// object payload as a sibling \`_modelNote\` the script can read.
|
|
556
|
+
if (modelNote !== undefined) {
|
|
557
|
+
logIn(scope, modelNote);
|
|
558
|
+
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) && canCarryModelNote(schema) && !Object.prototype.hasOwnProperty.call(parsed, "_modelNote")) parsed._modelNote = modelNote;
|
|
559
|
+
}
|
|
560
|
+
return parsed;
|
|
546
561
|
} catch (error) {
|
|
547
562
|
logIn(scope, "agent(): the host returned a structured result that is not JSON");
|
|
548
563
|
return null;
|
|
549
564
|
}
|
|
550
565
|
}
|
|
551
566
|
|
|
567
|
+
/**
|
|
568
|
+
* Whether the host may add a sibling \`_modelNote\` to a schema'd payload.
|
|
569
|
+
*
|
|
570
|
+
* Deliberately conservative: only a PLAIN object-with-properties schema, using
|
|
571
|
+
* nothing but the keywords below, qualifies. Anything that could declare the
|
|
572
|
+
* field somewhere we do not look (allOf/anyOf/oneOf/$ref/patternProperties/
|
|
573
|
+
* if-then-else/dependent schemas/a schema-valued additionalProperties) or forbid
|
|
574
|
+
* it (additionalProperties: false) never gets an injected field, and the note
|
|
575
|
+
* goes to the run log instead.
|
|
576
|
+
*/
|
|
577
|
+
var PLAIN_SCHEMA_KEYS = ["$schema", "type", "properties", "required", "description", "title"];
|
|
578
|
+
function canCarryModelNote(schema) {
|
|
579
|
+
if (!schema || typeof schema !== "object" || Array.isArray(schema)) return false;
|
|
580
|
+
if (schema.type !== "object") return false;
|
|
581
|
+
const props = schema.properties;
|
|
582
|
+
if (!props || typeof props !== "object" || Array.isArray(props)) return false;
|
|
583
|
+
if (Object.prototype.hasOwnProperty.call(props, "_modelNote")) return false;
|
|
584
|
+
const keys = Object.keys(schema);
|
|
585
|
+
for (let i = 0; i < keys.length; i++) {
|
|
586
|
+
if (PLAIN_SCHEMA_KEYS.indexOf(keys[i]) === -1) {
|
|
587
|
+
// \`additionalProperties: true\` changes nothing, so it stays plain.
|
|
588
|
+
if (keys[i] === "additionalProperties" && schema.additionalProperties === true) continue;
|
|
589
|
+
return false;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
return true;
|
|
593
|
+
}
|
|
594
|
+
|
|
552
595
|
/**
|
|
553
596
|
* A barrier: every thunk starts now, and nothing past the await runs until all
|
|
554
597
|
* of them have settled. A thunk that throws resolves to null rather than
|
|
@@ -675,19 +718,7 @@ async function workflowIn(scope, nameOrRef, args) {
|
|
|
675
718
|
child.ambientPhaseIndex = definePhaseIn(child, undefined);
|
|
676
719
|
child.ambientPhaseTitle = scopedTitle(child, undefined);
|
|
677
720
|
|
|
678
|
-
|
|
679
|
-
try {
|
|
680
|
-
const compiled = new vm.Script(
|
|
681
|
-
// \`meta\` is deliberately not a parameter: the body still opens with its
|
|
682
|
-
// own \`const meta = { ... }\` (extractMeta strips only the \`export\`), so a
|
|
683
|
-
// parameter of that name would collide with it.
|
|
684
|
-
"(async (agent, phase, log, workflow, console, args) => {" + PRELUDE + "\\n" + loaded.body + "\\n})",
|
|
685
|
-
{ filename: "workflow:" + loaded.name + ".js", lineOffset: -1 }
|
|
686
|
-
);
|
|
687
|
-
run = compiled.runInContext(realmContext);
|
|
688
|
-
} catch (error) {
|
|
689
|
-
throw new Error('workflow("' + label + '"): ' + describe(error));
|
|
690
|
-
}
|
|
721
|
+
const run = compileWorkflow(loaded.body, "workflow:" + loaded.name + ".js");
|
|
691
722
|
|
|
692
723
|
const value = await run(child.agent, child.phase, child.log, child.workflow, child.console, args);
|
|
693
724
|
checkBoundary(value, 'the result of workflow("' + label + '")');
|
|
@@ -721,6 +752,73 @@ function makeBudget() {
|
|
|
721
752
|
};
|
|
722
753
|
}
|
|
723
754
|
|
|
755
|
+
/* ------------------------------------------------------------------ *
|
|
756
|
+
* Authored source locations
|
|
757
|
+
* ------------------------------------------------------------------ */
|
|
758
|
+
|
|
759
|
+
// Neither lineOffset nor columnOffset is implemented by Bun 1.3/1.4. Keep
|
|
760
|
+
// authored lines intact and map only the prefix on line 1. Both entry points
|
|
761
|
+
// compile the same function; calling it outside the VM adds no synthetic frame.
|
|
762
|
+
const WORKFLOW_PREFIX = "(async (agent, phase, log, workflow, console, args) => {" + PRELUDE;
|
|
763
|
+
let registerWorkflowFile = null;
|
|
764
|
+
|
|
765
|
+
// Compiled from source in the realm, not installed as a host callback. Only
|
|
766
|
+
// primitive prefix length and filenames cross into its private realm-native Set.
|
|
767
|
+
function installAuthoredStacks(prefixLength) {
|
|
768
|
+
const workflowFiles = new Set();
|
|
769
|
+
function authoredFrame(frame) {
|
|
770
|
+
const text = String(frame);
|
|
771
|
+
const file = frame.getFileName();
|
|
772
|
+
if (!workflowFiles.has(file) || frame.getLineNumber() !== 1) return text;
|
|
773
|
+
const location = file + ":1:";
|
|
774
|
+
const at = text.lastIndexOf(location);
|
|
775
|
+
if (at === -1) return text;
|
|
776
|
+
const start = at + location.length;
|
|
777
|
+
// Bun 1.3's getColumnNumber() is zero-based, but CallSite.toString()
|
|
778
|
+
// is one-based on all supported engines.
|
|
779
|
+
const column = Number.parseInt(text.slice(start), 10);
|
|
780
|
+
if (column <= prefixLength) return text;
|
|
781
|
+
const digits = String(column).length;
|
|
782
|
+
return text.slice(0, start) + (column - prefixLength) + text.slice(start + digits);
|
|
783
|
+
}
|
|
784
|
+
Error.prepareStackTrace = function (error, frames) {
|
|
785
|
+
return Error.prototype.toString.call(error) + frames.map(function (frame) {
|
|
786
|
+
return "\\n at " + authoredFrame(frame);
|
|
787
|
+
}).join("");
|
|
788
|
+
};
|
|
789
|
+
return function (filename) { workflowFiles.add(filename); };
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function compileWorkflow(body, filename) {
|
|
793
|
+
registerWorkflowFile(filename);
|
|
794
|
+
try {
|
|
795
|
+
return new vm.Script(WORKFLOW_PREFIX + body + "\\n})", { filename: filename }).runInContext(realmContext);
|
|
796
|
+
} catch (error) {
|
|
797
|
+
// Parser errors are created outside the realm. Retain the actual error,
|
|
798
|
+
// but remove the generated prefix from a first-line source/caret excerpt.
|
|
799
|
+
if (error && typeof error.stack === "string") {
|
|
800
|
+
if (error.stack.startsWith(filename + ":1\\n")) {
|
|
801
|
+
const lines = error.stack.split("\\n");
|
|
802
|
+
if (lines[1]?.startsWith(WORKFLOW_PREFIX)) {
|
|
803
|
+
lines[1] = lines[1].slice(WORKFLOW_PREFIX.length);
|
|
804
|
+
if (lines[2] !== undefined) lines[2] = lines[2].slice(WORKFLOW_PREFIX.length);
|
|
805
|
+
error.stack = lines.join("\\n");
|
|
806
|
+
}
|
|
807
|
+
} else if (error.sourceURL === filename && error.stack.startsWith("[native code]:0\\n")) {
|
|
808
|
+
// Bun 1.3 defers parsing until runInContext and loses the source
|
|
809
|
+
// excerpt/column. It still supplies the authored line; don't invent
|
|
810
|
+
// a caret for a column the engine did not report.
|
|
811
|
+
const line = error.line;
|
|
812
|
+
const sourceLines = body.split(/\\r\\n|[\\n\\r\\u2028\\u2029]/);
|
|
813
|
+
if (Number.isInteger(line) && line > 0 && line <= sourceLines.length) {
|
|
814
|
+
error.stack = filename + ":" + line + "\\n" + sourceLines[line - 1] + "\\n" + error.stack.slice(error.stack.indexOf("\\n") + 1);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
throw error;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
724
822
|
/* ------------------------------------------------------------------ *
|
|
725
823
|
* Run
|
|
726
824
|
* ------------------------------------------------------------------ */
|
|
@@ -754,14 +852,15 @@ async function main() {
|
|
|
754
852
|
sandbox.meta = realmParse(workerData.metaJson);
|
|
755
853
|
sandbox.args = workerData.argsJson === undefined ? undefined : realmParse(workerData.argsJson);
|
|
756
854
|
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
const
|
|
855
|
+
// Structured CallSites retain constructor-expression columns on both V8 and
|
|
856
|
+
// JSC. Formatting changes no Error constructor, prototype, identity or cause.
|
|
857
|
+
registerWorkflowFile = vm.runInContext(
|
|
858
|
+
"(" + installAuthoredStacks.toString() + ")(" + WORKFLOW_PREFIX.length + ")",
|
|
859
|
+
context,
|
|
860
|
+
{ filename: "workflow-stack.js" }
|
|
861
|
+
);
|
|
862
|
+
const run = compileWorkflow(workerData.body, "workflow.js");
|
|
863
|
+
const value = await run(rootScope.agent, rootScope.phase, rootScope.log, rootScope.workflow, rootScope.console, sandbox.args);
|
|
765
864
|
checkBoundary(value, "the workflow result");
|
|
766
865
|
flushProgress();
|
|
767
866
|
port.postMessage({
|
|
@@ -770,12 +869,31 @@ async function main() {
|
|
|
770
869
|
});
|
|
771
870
|
}
|
|
772
871
|
|
|
872
|
+
/** Serialize diagnostics only: keep realm errors untouched and bound cause traversal/output. */
|
|
873
|
+
function failureDetails(error) {
|
|
874
|
+
const limit = 16384;
|
|
875
|
+
let message = "Workflow threw an unreadable error.";
|
|
876
|
+
let stack = "";
|
|
877
|
+
const seen = new Set();
|
|
878
|
+
try {
|
|
879
|
+
message = String(error && error.message ? error.message : error).slice(0, 4096);
|
|
880
|
+
let current = error;
|
|
881
|
+
for (let depth = 0; depth < 8 && stack.length < limit; depth++) {
|
|
882
|
+
if (seen.has(current)) break;
|
|
883
|
+
seen.add(current);
|
|
884
|
+
const text = current && typeof current.stack === "string" ? current.stack : String(current);
|
|
885
|
+
stack += (depth === 0 ? "" : "\\nCaused by: ") + text.slice(0, limit - stack.length);
|
|
886
|
+
if (!current || current.cause === undefined) break;
|
|
887
|
+
current = current.cause;
|
|
888
|
+
}
|
|
889
|
+
} catch {
|
|
890
|
+
// A diagnostic getter must not prevent the failed run from settling.
|
|
891
|
+
}
|
|
892
|
+
return { type: "error", message: message, stack: stack.slice(0, limit) || undefined };
|
|
893
|
+
}
|
|
894
|
+
|
|
773
895
|
main().catch(function (error) {
|
|
774
896
|
flushProgress();
|
|
775
|
-
port.postMessage(
|
|
776
|
-
type: "error",
|
|
777
|
-
message: error && error.message ? String(error.message) : String(error),
|
|
778
|
-
stack: error && error.stack ? String(error.stack) : undefined,
|
|
779
|
-
});
|
|
897
|
+
port.postMessage(failureDetails(error));
|
|
780
898
|
});
|
|
781
899
|
`;
|