@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arhen/pi-core-subagent",
3
- "version": "1.3.35",
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
- let shared = 0;
77
- for (const t of query) if (desc.includes(t)) shared += 1;
78
- return shared >= 2 ? shared : 0;
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"),