@arhen/pi-core-subagent 1.3.35 → 1.3.37
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/package.json +1 -1
- package/src/agentfile.ts +47 -3
- package/src/format.ts +5 -1
- package/src/manager.ts +25 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arhen/pi-core-subagent",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.37",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "pi extension: fast in-process subagents with a dependency-graph scheduler (needs edges gate tasks and carry upstream output into dependent prompts), plus background runs, intercom and agent-to-agent mailbox. Leader defines agents inline.",
|
|
6
6
|
"license": "MIT",
|
package/src/agentfile.ts
CHANGED
|
@@ -44,11 +44,40 @@ const STOP = new Set([
|
|
|
44
44
|
"how",
|
|
45
45
|
"what",
|
|
46
46
|
"who",
|
|
47
|
+
// Generic connectors/verbs that carry no routing signal. Two of these shared
|
|
48
|
+
// used to be enough to bind a task to an unrelated agent file (a "release
|
|
49
|
+
// audit" landing on a PRD writer via "from" + "user").
|
|
50
|
+
"from",
|
|
51
|
+
"into",
|
|
52
|
+
"user",
|
|
53
|
+
"note",
|
|
54
|
+
"creat",
|
|
55
|
+
"create",
|
|
56
|
+
"make",
|
|
57
|
+
"use",
|
|
58
|
+
"work",
|
|
59
|
+
"run",
|
|
60
|
+
"new",
|
|
61
|
+
"other",
|
|
62
|
+
"single",
|
|
63
|
+
"formal",
|
|
64
|
+
"description",
|
|
65
|
+
"this",
|
|
66
|
+
"that",
|
|
67
|
+
"it",
|
|
68
|
+
"all",
|
|
69
|
+
"any",
|
|
70
|
+
"per",
|
|
71
|
+
"via",
|
|
47
72
|
]);
|
|
48
73
|
|
|
49
74
|
/** A body becomes the child's ENTIRE system prompt — an oversized reference file
|
|
50
75
|
* would blow the context window and kill the session with a cryptic error. */
|
|
51
76
|
const MAX_BODY_CHARS = 64_000;
|
|
77
|
+
/** A file takes over the prompt AND the model, so a weak match is expensive.
|
|
78
|
+
* Both gates must pass: distinct shared terms, and share of the description. */
|
|
79
|
+
const MIN_SHARED_TERMS = 2;
|
|
80
|
+
const MIN_DESC_COVERAGE = 0.2;
|
|
52
81
|
|
|
53
82
|
/** Per-cwd memo of the ancestor walk (project dirs then home), since the files
|
|
54
83
|
* can't meaningfully change within one run and the walk costs 3 sync stats per
|
|
@@ -72,10 +101,25 @@ function tokens(text: string): string[] {
|
|
|
72
101
|
});
|
|
73
102
|
}
|
|
74
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Overlap between the spawn goal and a file's description.
|
|
106
|
+
*
|
|
107
|
+
* Absolute count alone is a bad signal: two shared filler words bound unrelated
|
|
108
|
+
* tasks to whichever agent file happened to share them, and the file then
|
|
109
|
+
* overrode the model too (403s on a plan without that model). Require BOTH a
|
|
110
|
+
* floor of distinct shared terms AND meaningful coverage of the description,
|
|
111
|
+
* so a match means "this file is about that", not "these strings brushed past
|
|
112
|
+
* each other".
|
|
113
|
+
*/
|
|
75
114
|
function score(query: string[], desc: string[]): number {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
115
|
+
if (desc.length === 0) return 0;
|
|
116
|
+
const q = new Set(query);
|
|
117
|
+
const shared = new Set<string>();
|
|
118
|
+
for (const t of desc) if (q.has(t)) shared.add(t);
|
|
119
|
+
if (shared.size < MIN_SHARED_TERMS) return 0;
|
|
120
|
+
const uniqueDesc = new Set(desc);
|
|
121
|
+
if (shared.size / uniqueDesc.size < MIN_DESC_COVERAGE) return 0;
|
|
122
|
+
return shared.size;
|
|
79
123
|
}
|
|
80
124
|
|
|
81
125
|
function readAgentFile(dir: string): AgentFileInfo[] {
|
package/src/format.ts
CHANGED
|
@@ -241,9 +241,13 @@ export function makeTaskNotice(run: RunSnapshot, task: TaskSnapshot, kind: strin
|
|
|
241
241
|
const wt = task.branch
|
|
242
242
|
? ` · branch ${task.branch}${task.changedFiles?.length ? `, ${task.changedFiles.length} file(s)` : ""}`
|
|
243
243
|
: "";
|
|
244
|
+
// A matched agent file overrides prompt AND model — on a failure (bad model,
|
|
245
|
+
// 403, wrong persona) that override is the likeliest cause, so it has to be
|
|
246
|
+
// in the notice, not only in the run summary the leader may never read.
|
|
247
|
+
const src = task.agentFile ? `\nAgent file: ${task.agentFile}${task.model ? ` (model ${task.model})` : ""}` : "";
|
|
244
248
|
return [
|
|
245
249
|
`Task ${task.agent} (${task.id}) ${kind} in run ${run.id}: ${detail}${wt}`,
|
|
246
|
-
`Goal: ${goal}`,
|
|
250
|
+
`Goal: ${goal}${src}`,
|
|
247
251
|
`Use subagent_result(runId: "${run.id}", taskId: "${task.id}") for full output.`,
|
|
248
252
|
].join("\n");
|
|
249
253
|
}
|
package/src/manager.ts
CHANGED
|
@@ -154,6 +154,7 @@ export function cloneRun(run: RunSnapshot): RunSnapshot {
|
|
|
154
154
|
export function resolveChildModel(ctx: ExtensionContext, explicit: string | undefined) {
|
|
155
155
|
if (!explicit?.trim()) return ctx.model; // inherit the parent's active model
|
|
156
156
|
const ref = explicit.trim();
|
|
157
|
+
if (!ctx.modelRegistry) return ctx.model; // no registry to check against (tests, headless)
|
|
157
158
|
const available = ctx.modelRegistry.getAvailable();
|
|
158
159
|
// Model ids can contain slashes (e.g. 9router/cc/claude-opus-5), so a bare id
|
|
159
160
|
// match and every provider/id split point must be tried, not just the first.
|
|
@@ -755,6 +756,10 @@ export class SubagentManager {
|
|
|
755
756
|
try {
|
|
756
757
|
model = resolveChildModel(ctx, file?.model ?? input.model);
|
|
757
758
|
validateThinking(model, thinking);
|
|
759
|
+
// Record the model ACTUALLY used — an agent file's `model:` overrides the
|
|
760
|
+
// requested one, and showing the request in the widget hides that entirely
|
|
761
|
+
// (a 403 then names a model the leader never asked for).
|
|
762
|
+
if (model) this.updateTask(run, task, { model: model.id }, ctx, onUpdate);
|
|
758
763
|
} catch (err) {
|
|
759
764
|
this.updateTask(
|
|
760
765
|
run,
|
|
@@ -1115,6 +1120,26 @@ export class SubagentManager {
|
|
|
1115
1120
|
}
|
|
1116
1121
|
}
|
|
1117
1122
|
const edges = resolveNeeds(inputs, mode);
|
|
1123
|
+
// Pre-flight every task's model BEFORE the run exists. A matched agent file
|
|
1124
|
+
// overrides the requested model, so an unresolvable one is the leader's
|
|
1125
|
+
// mistake to see NOW — not N children dying one by one on their first turn
|
|
1126
|
+
// with an error naming a model the leader never asked for.
|
|
1127
|
+
for (let i = 0; i < inputs.length; i++) {
|
|
1128
|
+
const input = inputs[i] as TaskInput;
|
|
1129
|
+
const cwd = input.cwd ?? ctx.cwd;
|
|
1130
|
+
const file = resolveAgentFile(input.agent, input.task, cwd, getAgentDir());
|
|
1131
|
+
const requested = file?.model ?? input.model;
|
|
1132
|
+
try {
|
|
1133
|
+
validateThinking(resolveChildModel(ctx, requested), input.thinking);
|
|
1134
|
+
} catch (err) {
|
|
1135
|
+
const where = file?.model
|
|
1136
|
+
? ` (from agent file ${file.path}, which overrides the requested model${input.model ? ` "${input.model}"` : ""})`
|
|
1137
|
+
: "";
|
|
1138
|
+
throw new Error(
|
|
1139
|
+
`Task ${input.id ?? `task_${i + 1}`} (${input.agent}): ${err instanceof Error ? err.message : String(err)}${where}`,
|
|
1140
|
+
);
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1118
1143
|
|
|
1119
1144
|
const run: RunSnapshot = {
|
|
1120
1145
|
id: newId("run"),
|