@nredd/pi-subagents 0.22.0-nredd.7 → 0.22.0-nredd.9
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 +245 -77
- package/package.json +16 -13
- package/src/abortable.ts +1 -1
- package/src/agent-file-toggle.ts +6 -5
- package/src/agent-manager.ts +489 -129
- package/src/agent-runner.ts +244 -93
- package/src/child-context.ts +19 -1
- package/src/completion-outbox.ts +22 -3
- package/src/cross-extension-rpc.ts +21 -42
- package/src/custom-agents.ts +11 -6
- package/src/default-agents.ts +21 -21
- package/src/detached-results.ts +18 -2
- package/src/enabled-models.ts +8 -7
- package/src/enterprise-local.ts +7 -6
- package/src/error-message.ts +11 -0
- package/src/git-path.ts +66 -0
- package/src/group-join.ts +4 -10
- package/src/home-paths.ts +27 -0
- package/src/index.ts +898 -297
- package/src/invocation-config.ts +4 -21
- package/src/memory.ts +9 -9
- package/src/mention-clone.ts +13 -8
- package/src/model-resolver.ts +92 -25
- package/src/model-scope.ts +2 -1
- package/src/model-selection.ts +170 -0
- package/src/model-tiers.ts +176 -0
- package/src/nested-tools.ts +99 -44
- package/src/notification-result.ts +118 -0
- package/src/output-file.ts +23 -1
- package/src/package-resources.ts +25 -10
- package/src/plan-mode-children.ts +176 -0
- package/src/process-budget.ts +6 -3
- package/src/prompts.ts +7 -2
- package/src/quota-admission.ts +35 -13
- package/src/resource-loader.ts +65 -0
- package/src/schedule.ts +94 -30
- package/src/settings.ts +63 -13
- package/src/skill-loader.ts +244 -93
- package/src/stale-ctx.ts +16 -0
- package/src/status-note.ts +28 -5
- package/src/types.ts +79 -2
- package/src/ui/agent-mention.ts +5 -2
- package/src/ui/agent-row.ts +13 -3
- package/src/ui/agent-widget.ts +55 -25
- package/src/ui/conversation-viewer.ts +10 -5
- package/src/ui/fallback-flag.ts +114 -0
- package/src/ui/fleet-list.ts +431 -86
- package/src/ui/fleet-state.ts +154 -0
- package/src/ui/notification-view.ts +22 -3
- package/src/ui/workflow-card.ts +6 -9
- package/src/ui/workflow-dialog.ts +36 -13
- package/src/workflow/host.ts +47 -50
- package/src/workflow/journal.ts +13 -1
- package/src/workflow/meta.ts +7 -4
- package/src/workflow/progress.ts +8 -3
- package/src/workflow/runtime.ts +48 -15
- package/src/workflow/saved.ts +21 -21
- package/src/workflow/task.ts +22 -7
- package/src/workflow/tool-description.ts +2 -2
- package/src/workflow/worker-source.ts +162 -40
- package/src/worktree.ts +309 -19
|
@@ -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,28 +852,52 @@ 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
|
+
const result = {
|
|
768
867
|
type: "complete",
|
|
769
868
|
resultJson: value === undefined ? undefined : JSON.stringify(value),
|
|
770
|
-
}
|
|
869
|
+
};
|
|
870
|
+
if (workerData.collectCoverage) {
|
|
871
|
+
result.coverage = vm.runInContext("globalThis.__coverage__", context);
|
|
872
|
+
}
|
|
873
|
+
port.postMessage(result);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
/** Serialize diagnostics only: keep realm errors untouched and bound cause traversal/output. */
|
|
877
|
+
function failureDetails(error) {
|
|
878
|
+
const limit = 16384;
|
|
879
|
+
let message = "Workflow threw an unreadable error.";
|
|
880
|
+
let stack = "";
|
|
881
|
+
const seen = new Set();
|
|
882
|
+
try {
|
|
883
|
+
message = String(error && error.message ? error.message : error).slice(0, 4096);
|
|
884
|
+
let current = error;
|
|
885
|
+
for (let depth = 0; depth < 8 && stack.length < limit; depth++) {
|
|
886
|
+
if (seen.has(current)) break;
|
|
887
|
+
seen.add(current);
|
|
888
|
+
const text = current && typeof current.stack === "string" ? current.stack : String(current);
|
|
889
|
+
stack += (depth === 0 ? "" : "\\nCaused by: ") + text.slice(0, limit - stack.length);
|
|
890
|
+
if (!current || current.cause === undefined) break;
|
|
891
|
+
current = current.cause;
|
|
892
|
+
}
|
|
893
|
+
} catch {
|
|
894
|
+
// A diagnostic getter must not prevent the failed run from settling.
|
|
895
|
+
}
|
|
896
|
+
return { type: "error", message: message, stack: stack.slice(0, limit) || undefined };
|
|
771
897
|
}
|
|
772
898
|
|
|
773
899
|
main().catch(function (error) {
|
|
774
900
|
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
|
-
});
|
|
901
|
+
port.postMessage(failureDetails(error));
|
|
780
902
|
});
|
|
781
903
|
`;
|