@arhen/pi-core-subagent 1.3.56 → 1.3.58

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.56",
3
+ "version": "1.3.58",
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/format.ts CHANGED
@@ -111,10 +111,17 @@ export function isTalking(task: TaskSnapshot): boolean {
111
111
  return TALK_TOOLS.some((t) => a.startsWith(t));
112
112
  }
113
113
  let pulsePhase = 0;
114
+ const NOTE_CAP = 240;
115
+ function noteSnippet(note: string): string {
116
+ return note.length > NOTE_CAP ? `${note.slice(0, NOTE_CAP)}…` : note;
117
+ }
114
118
  export function compactLines(run: RunSnapshot): string[] {
115
119
  const lines: string[] = [];
116
120
  for (const task of run.tasks.slice(0, MAX_TASKS)) {
117
121
  lines.push(taskLine(task));
122
+ // a swapped model or toolset changes what the task did, so it cannot stay out of the status view
123
+ if (task.modelNote) lines.push(` ↳ Model: ${noteSnippet(task.modelNote)}`);
124
+ if (task.toolsNote) lines.push(` ↳ Tools: ${noteSnippet(task.toolsNote)}`);
118
125
  }
119
126
  if (run.tasks.length > MAX_TASKS) lines.push(`… +${run.tasks.length - MAX_TASKS} more`);
120
127
  return lines;
package/src/index.ts CHANGED
@@ -299,7 +299,8 @@ export default function (pi: ExtensionAPI) {
299
299
  ? `\nApplied IN PLACE (no branch) — ${t.isolationReason ?? "worktree unavailable"}. The changes are already in your working tree.`
300
300
  : "";
301
301
  const wtErr = t.worktreeError ? `\nWorktree: ${t.worktreeError}` : "";
302
- return `\n## ${t.agent} ${statusIcon(t.status)}\nGoal: ${truncateText(t.task, 300)}\n${t.error ? `Error: ${t.error}` : t.finalText || "(no output yet)"}${wt}${wtErr}\n${formatUsage(t.usage)}`;
302
+ const modelNote = t.modelNote ? `\nModel: ${t.modelNote}` : "";
303
+ return `\n## ${t.agent} ${statusIcon(t.status)}\nGoal: ${truncateText(t.task, 300)}\n${t.error ? `Error: ${t.error}` : t.finalText || "(no output yet)"}${wt}${wtErr}${modelNote}\n${formatUsage(t.usage)}`;
303
304
  }),
304
305
  ].join("\n");
305
306
  return { content: [{ type: "text", text: truncateText(text) }], details: { run: cloneRun(run) } };
package/src/manager.ts CHANGED
@@ -2,7 +2,13 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, rmSync
2
2
  import { rename, rm, writeFile } from "node:fs/promises";
3
3
  import { basename, dirname, join, relative, sep } from "node:path";
4
4
  import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
5
- import type { Api, AssistantMessage, Model } from "@earendil-works/pi-ai";
5
+ import {
6
+ type Api,
7
+ type AssistantMessage,
8
+ clampThinkingLevel,
9
+ getSupportedThinkingLevels,
10
+ type Model,
11
+ } from "@earendil-works/pi-ai";
6
12
  import {
7
13
  type AgentSessionEvent,
8
14
  createAgentSession,
@@ -157,16 +163,36 @@ export function resolveChildModel(ctx: ExtensionContext, explicit: string | unde
157
163
  throw new Error(`Model not found: ${ref}`);
158
164
  }
159
165
 
166
+ const PROBE_THINKING_LEVELS: ThinkingLevel[] = ["low", "minimal", "medium", "high", "xhigh", "max"];
167
+
168
+ /**
169
+ * Thinking level for the usability probe: exactly what the child session will send — the clamped
170
+ * requested level, or the cheapest the model accepts when none was requested. Probing without a
171
+ * level makes adaptive-thinking providers reject the request (9router claude models answer
172
+ * "thinking.type.disabled is not supported"), which used to make every preflight fail and fall
173
+ * back to the session model.
174
+ */
175
+ function probeThinking(model: Model<Api>, thinking?: string): ThinkingLevel | undefined {
176
+ if (!model.reasoning) return undefined;
177
+ const supported = getSupportedThinkingLevels(model);
178
+ if (thinking && thinking !== "off") return clampThinkingLevel(model, thinking as ThinkingLevel);
179
+ // the child clamps an unsupported "off" up to its cheapest level, so probe that instead
180
+ if (thinking === "off") return supported.includes("off") ? undefined : supported[0];
181
+ return PROBE_THINKING_LEVELS.find((level) => supported.includes(level));
182
+ }
183
+
160
184
  async function probeModel(
161
185
  ctx: ExtensionContext,
162
186
  model: Model<Api>,
163
187
  signal: AbortSignal | undefined,
188
+ thinking?: string,
164
189
  ): Promise<string | undefined> {
165
190
  try {
191
+ const reasoningEffort = probeThinking(model, thinking);
166
192
  const reply = await ctx.modelRegistry.complete(
167
193
  model,
168
194
  { messages: [{ role: "user", content: "ping", timestamp: Date.now() }] },
169
- { maxTokens: 16, signal },
195
+ { maxTokens: 16, signal, ...(reasoningEffort ? { reasoningEffort } : {}) },
170
196
  );
171
197
  return reply.stopReason === "error" ? (reply.errorMessage ?? "provider returned an error") : undefined;
172
198
  } catch (err) {
@@ -178,11 +204,12 @@ export async function ensureUsableModel(
178
204
  ctx: ExtensionContext,
179
205
  model: Model<Api> | undefined,
180
206
  signal: AbortSignal | undefined,
207
+ thinking?: string,
181
208
  ): Promise<{ model: Model<Api> | undefined; note?: string }> {
182
209
  const session = ctx.model;
183
210
  if (!model || !ctx.modelRegistry) return { model };
184
211
  if (session && model.provider === session.provider && model.id === session.id) return { model };
185
- const error = await probeModel(ctx, model, signal);
212
+ const error = await probeModel(ctx, model, signal, thinking);
186
213
  if (!error) return { model };
187
214
  if (model.provider === "opencode-go" && /MissingSessionID|x-opencode-session/i.test(error)) {
188
215
  // ponytail: opencode-go rejects stateless probes but accepts AgentSession requests, which add the session header.
@@ -302,10 +329,10 @@ export class SubagentManager {
302
329
  return false;
303
330
  }
304
331
 
305
- clearWidget(ctx: ExtensionContext): void {
332
+ clearWidget(ctx?: ExtensionContext): void {
306
333
  this.widgetRuns = [];
307
334
  this.widgetTui = null;
308
- if (ctx.hasUI) {
335
+ if (ctx?.hasUI) {
309
336
  try {
310
337
  ctx.ui.setWidget("subagents", undefined);
311
338
  } catch {}
@@ -483,8 +510,14 @@ export class SubagentManager {
483
510
  }
484
511
 
485
512
  private widgetTui: TUI | null = null;
513
+
514
+ /** Settled runs are history (subagent_status/result still reach them) — the widget shows live work only. */
515
+ private pruneSettledWidget(): void {
516
+ this.widgetRuns = this.widgetRuns.filter((r) => !TERMINAL.includes(r.status));
517
+ }
486
518
  private upsertWidgetRun(run: RunSnapshot | undefined): void {
487
- if (!run) return;
519
+ this.pruneSettledWidget();
520
+ if (!run || TERMINAL.includes(run.status)) return;
488
521
  const idx = this.widgetRuns.findIndex((r) => r.id === run.id);
489
522
  if (idx >= 0) this.widgetRuns[idx] = run;
490
523
  else this.widgetRuns.push(run);
@@ -524,12 +557,17 @@ export class SubagentManager {
524
557
  this.widgetTimers.delete(run.id);
525
558
  }
526
559
  }
527
- if (!run || this.widgetRuns.length === 0) return;
528
- if (ctx?.hasUI) {
529
- this.ensureWidget(ctx);
530
- this.widgetTui?.requestRender();
560
+ this.pruneSettledWidget();
561
+ if (this.widgetRuns.length === 0) {
562
+ if (this.widgetTui) this.clearWidget(ctx);
563
+ } else {
564
+ if (ctx?.hasUI) {
565
+ this.ensureWidget(ctx);
566
+ this.widgetTui?.requestRender();
567
+ }
568
+ this.maybePulse(ctx);
531
569
  }
532
- this.maybePulse(ctx);
570
+ if (!run) return;
533
571
 
534
572
  onUpdate?.({
535
573
  content: [
@@ -747,7 +785,7 @@ export class SubagentManager {
747
785
  model = resolveChildModel(ctx, file?.model ?? input.model);
748
786
  validateThinking(model, thinking);
749
787
 
750
- const checked = await ensureUsableModel(ctx, model, signal);
788
+ const checked = await ensureUsableModel(ctx, model, signal, thinking);
751
789
  model = checked.model;
752
790
  if (checked.note) {
753
791
  task.modelNote = checked.note;