oira666_pi-subagent 0.2.5 → 0.2.7

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.
Files changed (3) hide show
  1. package/index.ts +43 -3
  2. package/package.json +1 -1
  3. package/runner.ts +20 -0
package/index.ts CHANGED
@@ -376,18 +376,20 @@ const RESUME_STATE_KEY = "__piSubagentResumeState";
376
376
  type SyntheticResumeState = {
377
377
  plan: ResumableSubagentCall | null;
378
378
  phase: "tool" | "final";
379
+ trigger: "resumePrompt" | "nextRequest";
379
380
  };
380
381
 
381
382
  function clearSyntheticResumeState(): void {
382
383
  const state = getSyntheticResumeState();
383
384
  state.plan = null;
384
385
  state.phase = "tool";
386
+ state.trigger = "resumePrompt";
385
387
  }
386
388
 
387
389
  function getSyntheticResumeState(): SyntheticResumeState {
388
390
  const g = globalThis as any;
389
391
  if (!g[RESUME_STATE_KEY]) {
390
- g[RESUME_STATE_KEY] = { plan: null, phase: "tool" } satisfies SyntheticResumeState;
392
+ g[RESUME_STATE_KEY] = { plan: null, phase: "tool", trigger: "resumePrompt" } satisfies SyntheticResumeState;
391
393
  }
392
394
  return g[RESUME_STATE_KEY] as SyntheticResumeState;
393
395
  }
@@ -423,6 +425,35 @@ function formatModelFlag(model: any): string | undefined {
423
425
  return `${model.provider}/${model.id}`;
424
426
  }
425
427
 
428
+ function findLastNonResumeModel(ctx: any): any | undefined {
429
+ const entries = (() => {
430
+ const leafId = ctx.sessionManager?.getLeafId?.();
431
+ if (leafId) {
432
+ const branch = ctx.sessionManager?.getBranch?.(leafId);
433
+ if (Array.isArray(branch)) return branch;
434
+ }
435
+ const all = ctx.sessionManager?.getEntries?.();
436
+ return Array.isArray(all) ? all : [];
437
+ })();
438
+
439
+ for (let i = entries.length - 1; i >= 0; i--) {
440
+ const entry = entries[i];
441
+ if (entry?.type !== "model_change") continue;
442
+ const provider = entry.provider;
443
+ const modelId = entry.modelId;
444
+ if (provider === RESUME_PROVIDER) continue;
445
+ if (typeof provider !== "string" || typeof modelId !== "string") continue;
446
+ const model = ctx.modelRegistry?.find?.(provider, modelId);
447
+ if (model) return model;
448
+ }
449
+ return undefined;
450
+ }
451
+
452
+ function getRestorableModel(ctx: any): any | undefined {
453
+ if (ctx.model?.provider && ctx.model.provider !== RESUME_PROVIDER) return ctx.model;
454
+ return findLastNonResumeModel(ctx);
455
+ }
456
+
426
457
  // ---------------------------------------------------------------------------
427
458
  // Extension entry point
428
459
  // ---------------------------------------------------------------------------
@@ -449,7 +480,10 @@ export default function (pi: ExtensionAPI) {
449
480
  const state = getSyntheticResumeState();
450
481
  const plan = state.plan;
451
482
  const phase = state.phase;
452
- if (plan && phase === "tool" && isSyntheticResumePrompt(context, plan.tasks.length)) {
483
+ const triggerMatches =
484
+ state.trigger === "nextRequest" ||
485
+ (plan ? isSyntheticResumePrompt(context, plan.tasks.length) : false);
486
+ if (plan && phase === "tool" && triggerMatches) {
453
487
  state.phase = "final";
454
488
  const toolCall = {
455
489
  type: "toolCall" as const,
@@ -579,6 +613,11 @@ export default function (pi: ExtensionAPI) {
579
613
  );
580
614
  }
581
615
 
616
+ const restorableModel = getRestorableModel(ctx);
617
+ if (ctx.model?.provider === RESUME_PROVIDER && restorableModel) {
618
+ await pi.setModel(restorableModel);
619
+ }
620
+
582
621
  const resumeDisabled = parseBooleanEnv(process.env[SUBAGENT_RESUME_DISABLE_ENV]) === true;
583
622
  if (resumeDisabled || (event.reason !== "resume" && event.reason !== "startup")) return;
584
623
 
@@ -599,7 +638,8 @@ export default function (pi: ExtensionAPI) {
599
638
  const resumeState = getSyntheticResumeState();
600
639
  resumeState.plan = plan;
601
640
  resumeState.phase = "tool";
602
- modelToRestoreAfterResume = ctx.model;
641
+ resumeState.trigger = hasCliInitialPrompt(process.argv) ? "nextRequest" : "resumePrompt";
642
+ modelToRestoreAfterResume = restorableModel ?? ctx.model;
603
643
  resumeModelRegistry = ctx.modelRegistry;
604
644
  ensureSubagentToolActive(pi);
605
645
  const resumeModel = ctx.modelRegistry.find(RESUME_PROVIDER, RESUME_MODEL_ID);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oira666_pi-subagent",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "Subagent extension for Pi coding agent. Delegate tasks to specialized agents.",
5
5
  "type": "module",
6
6
  "main": "index.ts",
package/runner.ts CHANGED
@@ -48,11 +48,24 @@ const TERMINAL_STOP_REASONS = new Set(["end_turn", "stop", "max_tokens", "error"
48
48
  function isTerminalStopReason(reason: string | undefined): boolean {
49
49
  return reason !== undefined && TERMINAL_STOP_REASONS.has(reason);
50
50
  }
51
+
52
+ function endedWithEmptySyntheticResume(messages: Message[]): boolean {
53
+ const lastAssistant = [...messages].reverse().find((message: any) => message?.role === "assistant") as any;
54
+ return (
55
+ lastAssistant?.provider === RESUME_PROVIDER &&
56
+ lastAssistant?.model === RESUME_MODEL_ID &&
57
+ lastAssistant?.stopReason === "stop" &&
58
+ Array.isArray(lastAssistant.content) &&
59
+ lastAssistant.content.length === 0
60
+ );
61
+ }
51
62
  const SUBAGENT_DEPTH_ENV = "PI_SUBAGENT_DEPTH";
52
63
  const SUBAGENT_MAX_DEPTH_ENV = "PI_SUBAGENT_MAX_DEPTH";
53
64
  const SUBAGENT_STACK_ENV = "PI_SUBAGENT_STACK";
54
65
  const SUBAGENT_PREVENT_CYCLES_ENV = "PI_SUBAGENT_PREVENT_CYCLES";
55
66
  const SUBAGENT_FALLBACK_MODEL_ENV = "PI_SUBAGENT_FALLBACK_MODEL";
67
+ const RESUME_PROVIDER = "pi-subagent-resume";
68
+ const RESUME_MODEL_ID = "synthetic-tool-call";
56
69
  // PI_OFFLINE intentionally removed: setting it on child processes blocks all API
57
70
  // calls and renders subagents unable to do any LLM work. Children inherit the
58
71
  // parent's PI_OFFLINE value via process.env spread if needed.
@@ -730,6 +743,13 @@ export async function runAgentSubprocess(opts: RunAgentOptions): Promise<SingleR
730
743
  if (!result.stderr.trim()) result.stderr = "Subagent was aborted.";
731
744
  }
732
745
 
746
+ if (result.exitCode === 0 && endedWithEmptySyntheticResume(result.messages)) {
747
+ result.exitCode = 1;
748
+ result.stopReason = "error";
749
+ result.errorMessage = "Subagent resume failed before the real model continued.";
750
+ if (!result.stderr.trim()) result.stderr = result.errorMessage;
751
+ }
752
+
733
753
  if (result.exitCode === 0) {
734
754
  const nestedErrorSummary = getNestedSubagentErrorSummary(result.messages);
735
755
  if (nestedErrorSummary) {