@arhen/pi-core-subagent 1.3.34 → 1.3.36

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.34",
3
+ "version": "1.3.36",
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
@@ -755,6 +755,10 @@ export class SubagentManager {
755
755
  try {
756
756
  model = resolveChildModel(ctx, file?.model ?? input.model);
757
757
  validateThinking(model, thinking);
758
+ // Record the model ACTUALLY used — an agent file's `model:` overrides the
759
+ // requested one, and showing the request in the widget hides that entirely
760
+ // (a 403 then names a model the leader never asked for).
761
+ if (model) this.updateTask(run, task, { model: model.id }, ctx, onUpdate);
758
762
  } catch (err) {
759
763
  this.updateTask(
760
764
  run,
@@ -1062,9 +1066,18 @@ export class SubagentManager {
1062
1066
  createRun(params: SubagentParamsShape, ctx: ExtensionContext): { run: RunSnapshot; inputs: TaskInput[] } {
1063
1067
  const hasChain = (params.chain?.length ?? 0) > 0;
1064
1068
  const hasTasks = (params.tasks?.length ?? 0) > 0;
1065
- const hasSingle = Boolean(params.agent && params.task);
1066
- if (Number(hasChain) + Number(hasTasks) + Number(hasSingle) !== 1) {
1067
- throw new Error(`Provide exactly one subagent mode (single, tasks, or chain).`);
1069
+ // An array mode wins over stray top-level agent/task: models routinely leave
1070
+ // those in place when switching to tasks:[...], and the intent is not
1071
+ // ambiguous — rejecting a well-formed 3-task call over leftovers is worse
1072
+ // than ignoring them. Genuine ambiguity (tasks AND chain) is still refused.
1073
+ const hasSingle = !hasChain && !hasTasks && Boolean(params.agent && params.task);
1074
+ if (hasChain && hasTasks) {
1075
+ throw new Error(`Provide either tasks (parallel) or chain (sequential), not both.`);
1076
+ }
1077
+ if (!hasChain && !hasTasks && !hasSingle) {
1078
+ throw new Error(
1079
+ `Provide one subagent mode: agent+task (single), tasks: [...] (parallel), or chain: [...] (sequential).`,
1080
+ );
1068
1081
  }
1069
1082
 
1070
1083
  const mode: RunMode = hasChain ? "chain" : hasTasks ? "parallel" : "single";