pi-subagents 0.13.3 → 0.14.0

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.
@@ -175,8 +175,8 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
175
175
 
176
176
  const taskAgentConfig = input.agents.find((agent) => agent.name === task.agent);
177
177
  const effectiveModel =
178
- (task.model ? resolveModelCandidate(task.model, input.availableModels) : null)
179
- ?? resolveModelCandidate(taskAgentConfig?.model, input.availableModels);
178
+ (task.model ? resolveModelCandidate(task.model, input.availableModels, input.ctx.model?.provider) : null)
179
+ ?? resolveModelCandidate(taskAgentConfig?.model, input.availableModels, input.ctx.model?.provider);
180
180
  const maxSubagentDepth = resolveChildMaxSubagentDepth(input.maxSubagentDepth, taskAgentConfig?.maxSubagentDepth);
181
181
 
182
182
  const taskCwd = input.worktreeSetup
@@ -201,6 +201,7 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
201
201
  maxSubagentDepth,
202
202
  modelOverride: effectiveModel,
203
203
  availableModels: input.availableModels,
204
+ preferredModelProvider: input.ctx.model?.provider,
204
205
  skills: behavior.skills === false ? [] : behavior.skills,
205
206
  onUpdate: input.onUpdate
206
207
  ? (progressUpdate) => {
@@ -293,7 +294,6 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
293
294
  const allProgress: AgentProgress[] = [];
294
295
  const allArtifactPaths: ArtifactPaths[] = [];
295
296
 
296
- // Compute chain metadata for observability
297
297
  const chainAgents: string[] = chainSteps.map((step) =>
298
298
  isParallelStep(step)
299
299
  ? `[${step.parallel.map((t) => t.agent).join("+")}]`
@@ -301,39 +301,24 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
301
301
  );
302
302
  const totalSteps = chainSteps.length;
303
303
 
304
- // Get original task from params or first step
305
304
  const firstStep = chainSteps[0]!;
306
305
  const originalTask = params.task
307
306
  ?? (isParallelStep(firstStep) ? firstStep.parallel[0]!.task! : (firstStep as SequentialStep).task!);
308
307
 
309
- // Create chain directory
310
308
  const chainDir = createChainDir(runId, chainDirBase);
311
-
312
- // Check if chain has any parallel steps
313
309
  const hasParallelSteps = chainSteps.some(isParallelStep);
314
-
315
- // Resolve templates (parallel-aware)
316
310
  let templates: ResolvedTemplates = resolveChainTemplates(chainSteps);
317
-
318
- // For TUI: only show if no parallel steps (TUI v1 doesn't support parallel display)
319
311
  const shouldClarify = clarify !== false && ctx.hasUI && !hasParallelSteps;
320
-
321
- // Behavior overrides from TUI (set if TUI is shown, undefined otherwise)
322
312
  let tuiBehaviorOverrides: (BehaviorOverride | undefined)[] | undefined;
323
-
324
- // Get available models for model resolution (used in TUI and execution)
325
313
  const availableModels: ModelInfo[] = ctx.modelRegistry.getAvailable().map((m) => ({
326
314
  provider: m.provider,
327
315
  id: m.id,
328
316
  fullId: `${m.provider}/${m.id}`,
329
317
  }));
330
- const availableSkills = discoverAvailableSkills(ctx.cwd);
318
+ const availableSkills = discoverAvailableSkills(cwd ?? ctx.cwd);
331
319
 
332
320
  if (shouldClarify) {
333
- // Sequential-only chain: use existing TUI
334
321
  const seqSteps = chainSteps as SequentialStep[];
335
-
336
- // Load agent configs for sequential steps
337
322
  const agentConfigs: AgentConfig[] = [];
338
323
  for (const step of seqSteps) {
339
324
  const config = agents.find((a) => a.name === step.agent);
@@ -348,7 +333,6 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
348
333
  agentConfigs.push(config);
349
334
  }
350
335
 
351
- // Build step overrides
352
336
  const stepOverrides: StepOverrides[] = seqSteps.map((step) => ({
353
337
  output: step.output,
354
338
  reads: step.reads,
@@ -357,12 +341,9 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
357
341
  model: step.model,
358
342
  }));
359
343
 
360
- // Pre-resolve behaviors for TUI display
361
344
  const resolvedBehaviors = agentConfigs.map((config, i) =>
362
345
  resolveStepBehavior(config, stepOverrides[i]!, chainSkills),
363
346
  );
364
-
365
- // Flatten templates for TUI (all strings for sequential)
366
347
  const flatTemplates = templates as string[];
367
348
 
368
349
  const result = await ctx.ui.custom<ChainClarifyResult>(
@@ -376,6 +357,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
376
357
  chainDir,
377
358
  resolvedBehaviors,
378
359
  availableModels,
360
+ ctx.model?.provider,
379
361
  availableSkills,
380
362
  done,
381
363
  ),
@@ -393,16 +375,14 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
393
375
  };
394
376
  }
395
377
 
396
- // User requested background execution - return early so caller can dispatch to async
397
378
  if (result.runInBackground) {
398
- removeChainDir(chainDir); // Will be recreated by async runner
399
- // Apply TUI edits (templates + behavior overrides) to chain steps
400
- const updatedChain = chainSteps.map((step, i) => {
401
- if (isParallelStep(step)) return step; // Parallel steps unchanged (TUI skipped for parallel chains)
379
+ removeChainDir(chainDir);
380
+ const updatedChain: ChainStep[] = chainSteps.map((step, i) => {
381
+ if (isParallelStep(step)) return step;
402
382
  const override = result.behaviorOverrides[i];
403
383
  return {
404
384
  ...step,
405
- task: result.templates[i] as string, // Always use edited template
385
+ task: result.templates[i]!,
406
386
  ...(override?.model ? { model: override.model } : {}),
407
387
  ...(override?.output !== undefined ? { output: override.output } : {}),
408
388
  ...(override?.reads !== undefined ? { reads: override.reads } : {}),
@@ -413,21 +393,18 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
413
393
  return {
414
394
  content: [{ type: "text", text: "Launching in background..." }],
415
395
  details: { mode: "chain", results: [] },
416
- requestedAsync: { chain: updatedChain as ChainStep[], chainSkills },
396
+ requestedAsync: { chain: updatedChain, chainSkills },
417
397
  };
418
398
  }
419
399
 
420
- // Update templates from TUI result
421
400
  templates = result.templates;
422
- // Store behavior overrides from TUI (used below in sequential step execution)
423
401
  tuiBehaviorOverrides = result.behaviorOverrides;
424
402
  }
425
403
 
426
- // Execute chain (handles both sequential and parallel steps)
427
404
  const results: SingleResult[] = [];
428
405
  let prev = "";
429
- let globalTaskIndex = 0; // For unique artifact naming
430
- let progressCreated = false; // Track if progress.md has been created
406
+ let globalTaskIndex = 0;
407
+ let progressCreated = false;
431
408
 
432
409
  for (let stepIndex = 0; stepIndex < chainSteps.length; stepIndex++) {
433
410
  const step = chainSteps[stepIndex]!;
@@ -572,11 +549,9 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
572
549
  if (worktreeSetup) cleanupWorktrees(worktreeSetup);
573
550
  }
574
551
  } else {
575
- // === SEQUENTIAL STEP EXECUTION ===
576
552
  const seqStep = step as SequentialStep;
577
553
  const stepTemplate = stepTemplates as string;
578
554
 
579
- // Get agent config
580
555
  const agentConfig = agents.find((a) => a.name === seqStep.agent);
581
556
  if (!agentConfig) {
582
557
  removeChainDir(chainDir);
@@ -587,7 +562,6 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
587
562
  };
588
563
  }
589
564
 
590
- // Resolve behavior first (TUI overrides take precedence over step config)
591
565
  const tuiOverride = tuiBehaviorOverrides?.[stepIndex];
592
566
  const stepOverride: StepOverrides = {
593
567
  output: tuiOverride?.output !== undefined ? tuiOverride.output : seqStep.output,
@@ -600,38 +574,31 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
600
574
  };
601
575
  const behavior = resolveStepBehavior(agentConfig, stepOverride, chainSkills);
602
576
 
603
- // Determine if this is the first agent to create progress.md
604
577
  const isFirstProgress = behavior.progress && !progressCreated;
605
578
  if (isFirstProgress) {
606
579
  progressCreated = true;
607
580
  }
608
581
 
609
- // Build chain instructions (prefix goes BEFORE task, suffix goes AFTER)
610
582
  const templateHasPrevious = stepTemplate.includes("{previous}");
611
583
  const { prefix, suffix } = buildChainInstructions(
612
- behavior,
613
- chainDir,
614
- isFirstProgress,
615
- templateHasPrevious ? undefined : prev
584
+ behavior,
585
+ chainDir,
586
+ isFirstProgress,
587
+ templateHasPrevious ? undefined : prev,
616
588
  );
617
589
 
618
- // Build task string with variable substitution
619
590
  let stepTask = stepTemplate;
620
591
  stepTask = stepTask.replace(/\{task\}/g, originalTask);
621
592
  stepTask = stepTask.replace(/\{previous\}/g, prev);
622
593
  stepTask = stepTask.replace(/\{chain_dir\}/g, chainDir);
623
594
  const cleanTask = stepTask;
624
-
625
- // Assemble final task: prefix (READ/WRITE instructions) + task + suffix (progress, previous summary)
626
595
  stepTask = prefix + stepTask + suffix;
627
596
 
628
- // Resolve model: TUI override (already full format) or agent's model resolved to full format
629
597
  const effectiveModel =
630
598
  tuiOverride?.model
631
- ?? (seqStep.model ? resolveModelCandidate(seqStep.model, availableModels) : null)
632
- ?? resolveModelCandidate(agentConfig.model, availableModels);
599
+ ?? (seqStep.model ? resolveModelCandidate(seqStep.model, availableModels, ctx.model?.provider) : null)
600
+ ?? resolveModelCandidate(agentConfig.model, availableModels, ctx.model?.provider);
633
601
 
634
- // Run step
635
602
  const outputPath = typeof behavior.output === "string"
636
603
  ? (path.isAbsolute(behavior.output) ? behavior.output : path.join(chainDir, behavior.output))
637
604
  : undefined;
@@ -651,10 +618,10 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
651
618
  maxSubagentDepth,
652
619
  modelOverride: effectiveModel,
653
620
  availableModels,
621
+ preferredModelProvider: ctx.model?.provider,
654
622
  skills: behavior.skills === false ? [] : behavior.skills,
655
623
  onUpdate: onUpdate
656
624
  ? (p) => {
657
- // Use concat instead of spread for better performance
658
625
  const stepResults = p.details?.results || [];
659
626
  const stepProgress = p.details?.progress || [];
660
627
  onUpdate({
@@ -678,28 +645,24 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
678
645
  if (r.progress) allProgress.push(r.progress);
679
646
  if (r.artifactPaths) allArtifactPaths.push(r.artifactPaths);
680
647
 
681
- // Validate expected output file was created
682
648
  if (behavior.output && r.exitCode === 0) {
683
649
  try {
684
650
  const expectedPath = path.isAbsolute(behavior.output)
685
- ? behavior.output
651
+ ? behavior.output
686
652
  : path.join(chainDir, behavior.output);
687
653
  if (!fs.existsSync(expectedPath)) {
688
- // Look for similar files that might have been created instead
689
654
  const dirFiles = fs.readdirSync(chainDir);
690
- const mdFiles = dirFiles.filter(f => f.endsWith(".md") && f !== "progress.md");
691
- const warning = mdFiles.length > 0
655
+ const mdFiles = dirFiles.filter((file) => file.endsWith(".md") && file !== "progress.md");
656
+ const warning = mdFiles.length > 0
692
657
  ? `Agent wrote to different file(s): ${mdFiles.join(", ")} instead of ${behavior.output}`
693
658
  : `Agent did not create expected output file: ${behavior.output}`;
694
- // Add warning to result but don't fail
695
- r.error = r.error ? `${r.error}\n⚠️ ${warning}` : `⚠️ ${warning}`;
659
+ r.error = r.error ? `${r.error}\n${warning}` : warning;
696
660
  }
697
661
  } catch {
698
662
  // Ignore validation errors - this is just a diagnostic
699
663
  }
700
664
  }
701
665
 
702
- // On failure, leave chain_dir for debugging
703
666
  if (r.exitCode !== 0) {
704
667
  const summary = buildChainSummary(chainSteps, results, chainDir, "failed", {
705
668
  index: stepIndex,
@@ -724,8 +687,6 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
724
687
  }
725
688
  }
726
689
 
727
- // Chain complete - return summary with paths
728
- // Chain dir left for inspection (cleaned up after 24h)
729
690
  const summary = buildChainSummary(chainSteps, results, chainDir, "completed");
730
691
 
731
692
  return {
@@ -737,7 +698,6 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
737
698
  artifacts: allArtifactPaths.length ? { dir: artifactsDir, files: allArtifactPaths } : undefined,
738
699
  chainAgents,
739
700
  totalSteps,
740
- // currentStepIndex omitted for completed chains
741
701
  },
742
702
  };
743
703
  }
package/execution.ts CHANGED
@@ -31,7 +31,7 @@ import {
31
31
  extractToolArgsPreview,
32
32
  extractTextFromContent,
33
33
  } from "./utils.ts";
34
- import { buildSkillInjection, resolveSkills } from "./skills.ts";
34
+ import { buildSkillInjection, resolveSkillsWithFallback } from "./skills.ts";
35
35
  import { getPiSpawnCommand } from "./pi-spawn.ts";
36
36
  import { createJsonlWriter } from "./jsonl-writer.ts";
37
37
  import { applyThinkingSuffix, buildPiArgs, cleanupTempDir } from "./pi-args.ts";
@@ -155,7 +155,7 @@ async function runSingleAttempt(
155
155
  finish(-2);
156
156
  };
157
157
 
158
- const unsubscribeIntercomDetach = options.intercomEvents?.on(INTERCOM_DETACH_REQUEST_EVENT, (payload) => {
158
+ const unsubscribeIntercomDetach = options.intercomEvents?.on?.(INTERCOM_DETACH_REQUEST_EVENT, (payload) => {
159
159
  if (!options.allowIntercomDetach || detached || processClosed) return;
160
160
  if (!payload || typeof payload !== "object") return;
161
161
  const requestId = (payload as { requestId?: unknown }).requestId;
@@ -186,61 +186,64 @@ async function runSingleAttempt(
186
186
  const processLine = (line: string) => {
187
187
  if (!line.trim()) return;
188
188
  jsonlWriter.writeLine(line);
189
+ let evt: { type?: string; message?: Message; toolName?: string; args?: unknown };
189
190
  try {
190
- const evt = JSON.parse(line) as { type?: string; message?: Message; toolName?: string; args?: unknown };
191
- const now = Date.now();
192
- progress.durationMs = now - startTime;
191
+ evt = JSON.parse(line) as { type?: string; message?: Message; toolName?: string; args?: unknown };
192
+ } catch {
193
+ // Non-JSON stdout lines are expected; only structured events are parsed.
194
+ return;
195
+ }
193
196
 
194
- if (evt.type === "tool_execution_start") {
195
- if (options.allowIntercomDetach && evt.toolName === "intercom") {
196
- intercomStarted = true;
197
- }
198
- progress.toolCount++;
199
- progress.currentTool = evt.toolName;
200
- progress.currentToolArgs = extractToolArgsPreview((evt.args || {}) as Record<string, unknown>);
201
- fireUpdate();
202
- }
197
+ const now = Date.now();
198
+ progress.durationMs = now - startTime;
203
199
 
204
- if (evt.type === "tool_execution_end") {
205
- if (progress.currentTool) {
206
- progress.recentTools.push({
207
- tool: progress.currentTool,
208
- args: progress.currentToolArgs || "",
209
- endMs: now,
210
- });
211
- }
212
- progress.currentTool = undefined;
213
- progress.currentToolArgs = undefined;
214
- fireUpdate();
200
+ if (evt.type === "tool_execution_start") {
201
+ if (options.allowIntercomDetach && evt.toolName === "intercom") {
202
+ intercomStarted = true;
215
203
  }
204
+ progress.toolCount++;
205
+ progress.currentTool = evt.toolName;
206
+ progress.currentToolArgs = extractToolArgsPreview((evt.args || {}) as Record<string, unknown>);
207
+ fireUpdate();
208
+ }
216
209
 
217
- if (evt.type === "message_end" && evt.message) {
218
- result.messages.push(evt.message);
219
- if (evt.message.role === "assistant") {
220
- result.usage.turns++;
221
- const u = evt.message.usage;
222
- if (u) {
223
- result.usage.input += u.input || 0;
224
- result.usage.output += u.output || 0;
225
- result.usage.cacheRead += u.cacheRead || 0;
226
- result.usage.cacheWrite += u.cacheWrite || 0;
227
- result.usage.cost += u.cost?.total || 0;
228
- progress.tokens = result.usage.input + result.usage.output;
229
- }
230
- if (!result.model && evt.message.model) result.model = evt.message.model;
231
- if (evt.message.errorMessage) result.error = evt.message.errorMessage;
232
- appendRecentOutput(progress, extractTextFromContent(evt.message.content).split("\n").slice(-10));
233
- }
234
- fireUpdate();
210
+ if (evt.type === "tool_execution_end") {
211
+ if (progress.currentTool) {
212
+ progress.recentTools.push({
213
+ tool: progress.currentTool,
214
+ args: progress.currentToolArgs || "",
215
+ endMs: now,
216
+ });
235
217
  }
218
+ progress.currentTool = undefined;
219
+ progress.currentToolArgs = undefined;
220
+ fireUpdate();
221
+ }
236
222
 
237
- if (evt.type === "tool_result_end" && evt.message) {
238
- result.messages.push(evt.message);
223
+ if (evt.type === "message_end" && evt.message) {
224
+ result.messages.push(evt.message);
225
+ if (evt.message.role === "assistant") {
226
+ result.usage.turns++;
227
+ const u = evt.message.usage;
228
+ if (u) {
229
+ result.usage.input += u.input || 0;
230
+ result.usage.output += u.output || 0;
231
+ result.usage.cacheRead += u.cacheRead || 0;
232
+ result.usage.cacheWrite += u.cacheWrite || 0;
233
+ result.usage.cost += u.cost?.total || 0;
234
+ progress.tokens = result.usage.input + result.usage.output;
235
+ }
236
+ if (!result.model && evt.message.model) result.model = evt.message.model;
237
+ if (evt.message.errorMessage) result.error = evt.message.errorMessage;
239
238
  appendRecentOutput(progress, extractTextFromContent(evt.message.content).split("\n").slice(-10));
240
- fireUpdate();
241
239
  }
242
- } catch {
243
- // Non-JSON stdout lines are expected; only structured events are parsed.
240
+ fireUpdate();
241
+ }
242
+
243
+ if (evt.type === "tool_result_end" && evt.message) {
244
+ result.messages.push(evt.message);
245
+ appendRecentOutput(progress, extractTextFromContent(evt.message.content).split("\n").slice(-10));
246
+ fireUpdate();
244
247
  }
245
248
  };
246
249
 
@@ -368,7 +371,8 @@ export async function runSync(
368
371
  const sessionEnabled = Boolean(options.sessionFile || options.sessionDir) || shareEnabled;
369
372
  const outputSnapshot = captureSingleOutputSnapshot(options.outputPath);
370
373
  const skillNames = options.skills ?? agent.skills ?? [];
371
- const { resolved: resolvedSkills, missing: missingSkills } = resolveSkills(skillNames, runtimeCwd);
374
+ const skillCwd = options.cwd ?? runtimeCwd;
375
+ const { resolved: resolvedSkills, missing: missingSkills } = resolveSkillsWithFallback(skillNames, skillCwd, runtimeCwd);
372
376
  let systemPrompt = agent.systemPrompt?.trim() || "";
373
377
  if (resolvedSkills.length > 0) {
374
378
  const skillInjection = buildSkillInjection(resolvedSkills);
@@ -379,6 +383,7 @@ export async function runSync(
379
383
  options.modelOverride ?? agent.model,
380
384
  agent.fallbackModels,
381
385
  options.availableModels,
386
+ options.preferredModelProvider,
382
387
  );
383
388
  const attemptedModels: string[] = [];
384
389
  const modelAttempts: ModelAttempt[] = [];
package/index.ts CHANGED
@@ -259,7 +259,7 @@ EXECUTION (use exactly ONE mode):
259
259
  CHAIN TEMPLATE VARIABLES (use in task strings):
260
260
  • {task} - The original task/request from the user
261
261
  • {previous} - Text response from the previous step (empty for first step)
262
- • {chain_dir} - Shared directory for chain files (e.g., <tmpdir>/pi-chain-runs/abc123/)
262
+ • {chain_dir} - Shared directory for chain files (e.g., <tmpdir>/pi-subagents-<scope>/chain-runs/abc123/)
263
263
 
264
264
  Example: { chain: [{agent:"scout", task:"Analyze {task}"}, {agent:"planner", task:"Plan based on {previous}"}] }
265
265
 
@@ -7,6 +7,7 @@ import type { ExtensionConfig, IntercomBridgeConfig, IntercomBridgeMode } from "
7
7
  const DEFAULT_INTERCOM_EXTENSION_DIR = path.join(os.homedir(), ".pi", "agent", "extensions", "pi-intercom");
8
8
  const DEFAULT_INTERCOM_CONFIG_PATH = path.join(os.homedir(), ".pi", "agent", "intercom", "config.json");
9
9
  const DEFAULT_SUBAGENT_CONFIG_DIR = path.join(os.homedir(), ".pi", "agent", "extensions", "subagent");
10
+ const DEFAULT_INTERCOM_TARGET_PREFIX = "subagent-chat";
10
11
  const INTERCOM_BRIDGE_MARKER = "Intercom orchestration channel:";
11
12
  const DEFAULT_INTERCOM_BRIDGE_TEMPLATE = `Use intercom only for coordination with the orchestrator session:
12
13
  - Need a decision or blocked: intercom({ action: "ask", to: "{orchestratorTarget}", message: "<question>" })
@@ -30,6 +31,13 @@ interface ResolveIntercomBridgeInput {
30
31
  settingsDir?: string;
31
32
  }
32
33
 
34
+ export function resolveIntercomSessionTarget(sessionName: string | undefined, sessionId: string): string {
35
+ const trimmedName = sessionName?.trim();
36
+ if (trimmedName) return trimmedName;
37
+ const normalizedSessionId = sessionId.startsWith("session-") ? sessionId.slice("session-".length) : sessionId;
38
+ return `${DEFAULT_INTERCOM_TARGET_PREFIX}-${normalizedSessionId.slice(0, 8)}`;
39
+ }
40
+
33
41
  export function resolveIntercomBridgeMode(value: unknown): IntercomBridgeMode {
34
42
  if (value === "off" || value === "always" || value === "fork-only") return value;
35
43
  return "always";
package/model-fallback.ts CHANGED
@@ -14,7 +14,7 @@ export interface ModelAttemptSummary {
14
14
  usage?: Usage;
15
15
  }
16
16
 
17
- function splitThinkingSuffix(model: string): { baseModel: string; thinkingSuffix: string } {
17
+ export function splitThinkingSuffix(model: string): { baseModel: string; thinkingSuffix: string } {
18
18
  const colonIdx = model.lastIndexOf(":");
19
19
  if (colonIdx === -1) return { baseModel: model, thinkingSuffix: "" };
20
20
  return {
@@ -26,6 +26,7 @@ function splitThinkingSuffix(model: string): { baseModel: string; thinkingSuffix
26
26
  export function resolveModelCandidate(
27
27
  model: string | undefined,
28
28
  availableModels: AvailableModelInfo[] | undefined,
29
+ preferredProvider?: string,
29
30
  ): string | undefined {
30
31
  if (!model) return undefined;
31
32
  if (model.includes("/")) return model;
@@ -33,6 +34,10 @@ export function resolveModelCandidate(
33
34
 
34
35
  const { baseModel, thinkingSuffix } = splitThinkingSuffix(model);
35
36
  const matches = availableModels.filter((entry) => entry.id === baseModel);
37
+ if (preferredProvider) {
38
+ const preferredMatch = matches.find((entry) => entry.provider === preferredProvider);
39
+ if (preferredMatch) return `${preferredMatch.fullId}${thinkingSuffix}`;
40
+ }
36
41
  if (matches.length !== 1) return model;
37
42
  return `${matches[0]!.fullId}${thinkingSuffix}`;
38
43
  }
@@ -41,12 +46,13 @@ export function buildModelCandidates(
41
46
  primaryModel: string | undefined,
42
47
  fallbackModels: string[] | undefined,
43
48
  availableModels: AvailableModelInfo[] | undefined,
49
+ preferredProvider?: string,
44
50
  ): string[] {
45
51
  const seen = new Set<string>();
46
52
  const candidates: string[] = [];
47
53
  for (const raw of [primaryModel, ...(fallbackModels ?? [])]) {
48
54
  if (!raw) continue;
49
- const normalized = resolveModelCandidate(raw.trim(), availableModels);
55
+ const normalized = resolveModelCandidate(raw.trim(), availableModels, preferredProvider);
50
56
  if (!normalized || seen.has(normalized)) continue;
51
57
  seen.add(normalized);
52
58
  candidates.push(normalized);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-subagents",
3
- "version": "0.13.3",
3
+ "version": "0.14.0",
4
4
  "description": "Pi extension for delegating tasks to subagents with chains, parallel execution, and TUI clarification",
5
5
  "author": "Nico Bailon",
6
6
  "license": "MIT",
package/schemas.ts CHANGED
@@ -83,7 +83,7 @@ export const SubagentParams = Type.Object({
83
83
  enum: ["fresh", "fork"],
84
84
  description: "'fresh' (default) or 'fork' to branch from parent session",
85
85
  })),
86
- chainDir: Type.Optional(Type.String({ description: "Persistent directory for chain artifacts. Default: <tmpdir>/pi-chain-runs/ (auto-cleaned after 24h)" })),
86
+ chainDir: Type.Optional(Type.String({ description: "Persistent directory for chain artifacts. Default: a user-scoped temp directory under <tmpdir>/ (auto-cleaned after 24h)" })),
87
87
  async: Type.Optional(Type.Boolean({ description: "Run in background (default: false, or per config)" })),
88
88
  agentScope: Type.Optional(Type.String({ description: "Agent discovery scope: 'user', 'project', or 'both' (default: 'both'; project wins on name collisions)" })),
89
89
  cwd: Type.Optional(Type.String()),
package/settings.ts CHANGED
@@ -3,12 +3,10 @@
3
3
  */
4
4
 
5
5
  import * as fs from "node:fs";
6
- import * as os from "node:os";
7
6
  import * as path from "node:path";
8
7
  import type { AgentConfig } from "./agents.ts";
9
8
  import { normalizeSkillInput } from "./skills.ts";
10
-
11
- const CHAIN_RUNS_DIR = path.join(os.tmpdir(), "pi-chain-runs");
9
+ import { CHAIN_RUNS_DIR } from "./types.ts";
12
10
  const CHAIN_DIR_MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours
13
11
 
14
12
  // =============================================================================
@@ -100,7 +98,9 @@ export function createChainDir(runId: string, baseDir?: string): string {
100
98
  export function removeChainDir(chainDir: string): void {
101
99
  try {
102
100
  fs.rmSync(chainDir, { recursive: true });
103
- } catch {}
101
+ } catch {
102
+ // Chain cleanup is best-effort. Runs can already have cleaned their temp dir.
103
+ }
104
104
  }
105
105
 
106
106
  export function cleanupOldChainDirs(): void {
@@ -110,6 +110,8 @@ export function cleanupOldChainDirs(): void {
110
110
  try {
111
111
  dirs = fs.readdirSync(CHAIN_RUNS_DIR);
112
112
  } catch {
113
+ // Startup cleanup is best-effort. If the scoped temp root is unreadable,
114
+ // skip cleanup instead of failing extension startup.
113
115
  return;
114
116
  }
115
117