@henryqw/pi-subagent 25.1.0 → 25.1.1

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.
@@ -243,11 +243,6 @@ export function createDirectHerdr(pi, cwd, idleMs) {
243
243
  const waited = await herdr.exec(args, options(signal));
244
244
  if (waited.code !== 0 || waited.killed) {
245
245
  if (!waited.killed && hasHerdrErrorCode(waited, "timeout")) {
246
- if (current === "idle") {
247
- const answer = await maybeAnswer();
248
- if (answer !== undefined)
249
- return answer;
250
- }
251
246
  continue;
252
247
  }
253
248
  throw new Error(herdrCommandFailure(args, waited));
package/dist/ephemeral.js CHANGED
@@ -429,9 +429,8 @@ async function runPi(prepared, input, budget, invocation) {
429
429
  let spawnError;
430
430
  let protocolError;
431
431
  let aborted = false;
432
- let turnLimited = false;
432
+ let limit;
433
433
  let tokenBudget = "within";
434
- const tokenLimited = () => tokenBudget === "limited";
435
434
  let startedTurns = 0;
436
435
  let lastEventAt = startedAt;
437
436
  let deadline = Math.min(startedAt + timeoutPolicy.idleMs, maxDeadline ?? startedAt + timeoutPolicy.idleMs);
@@ -514,15 +513,12 @@ async function runPi(prepared, input, budget, invocation) {
514
513
  : `Subagent timed out after ${formatDuration(timeoutPolicy.idleMs)} without a recognized Pi event.`;
515
514
  reject(new EphemeralSubagentError("timeout", message, new Error(message), accumulatedUsage()));
516
515
  }
517
- else if (turnLimited) {
518
- const summary = `Subagent reached its maximum turn limit of ${budget.maxTurns}.`;
516
+ else if (limit) {
517
+ const summary = limit === "turn_limit"
518
+ ? `Subagent reached its maximum turn limit of ${budget.maxTurns}.`
519
+ : `Subagent reached its maximum token limit of ${budget.maxTokens}.`;
519
520
  const message = output ? capEphemeralSubagentOutput(`${summary}\n\nLast assistant output:\n${output}`) : summary;
520
- reject(new EphemeralSubagentError("turn_limit", message, new Error(message), accumulatedUsage(), output));
521
- }
522
- else if (tokenLimited()) {
523
- const summary = `Subagent reached its maximum token limit of ${budget.maxTokens}.`;
524
- const message = output ? capEphemeralSubagentOutput(`${summary}\n\nLast assistant output:\n${output}`) : summary;
525
- reject(new EphemeralSubagentError("token_limit", message, new Error(message), accumulatedUsage(), output));
521
+ reject(new EphemeralSubagentError(limit, message, new Error(message), accumulatedUsage(), output));
526
522
  }
527
523
  else if (protocolError) {
528
524
  reject(new EphemeralSubagentError("protocol", protocolError.message, protocolError, accumulatedUsage()));
@@ -585,7 +581,7 @@ async function runPi(prepared, input, budget, invocation) {
585
581
  deadlineTimer.unref();
586
582
  };
587
583
  const observeEvent = () => {
588
- if (callbackFailure || aborted || timedOutAfterMs !== undefined || turnLimited || tokenLimited() || childExited)
584
+ if (callbackFailure || aborted || timedOutAfterMs !== undefined || limit || childExited)
589
585
  return;
590
586
  const now = Date.now();
591
587
  if (now >= deadline) {
@@ -609,17 +605,15 @@ async function runPi(prepared, input, budget, invocation) {
609
605
  case "final_turn":
610
606
  if (event === "turn_start") {
611
607
  if (!callbackFailure && !aborted && timedOutAfterMs === undefined)
612
- tokenBudget = "limited";
608
+ limit = "token_limit";
613
609
  stop(true);
614
610
  return true;
615
611
  }
616
612
  return false;
617
- case "limited":
618
- return true;
619
613
  }
620
614
  };
621
615
  const processLine = (line) => {
622
- if (turnLimited || tokenLimited() || !line.trim())
616
+ if (limit || !line.trim())
623
617
  return;
624
618
  let event;
625
619
  try {
@@ -637,7 +631,7 @@ async function runPi(prepared, input, budget, invocation) {
637
631
  if (record.type === "turn_start") {
638
632
  if (++startedTurns > budget.maxTurns) {
639
633
  if (!callbackFailure && !aborted && timedOutAfterMs === undefined)
640
- turnLimited = true;
634
+ limit = "turn_limit";
641
635
  stop(true);
642
636
  return;
643
637
  }
@@ -775,7 +769,7 @@ async function runPi(prepared, input, budget, invocation) {
775
769
  };
776
770
  onStdoutData = (data) => {
777
771
  armPostExitIdleDeadline();
778
- if (callbackFailure || protocolError || turnLimited || tokenLimited())
772
+ if (callbackFailure || protocolError || limit)
779
773
  return;
780
774
  let offset = 0;
781
775
  while (offset < data.length) {
@@ -816,7 +810,7 @@ async function runPi(prepared, input, budget, invocation) {
816
810
  invokeCallback("onActivity", input.onActivity, activity);
817
811
  }
818
812
  }
819
- if (callbackFailure || turnLimited || tokenLimited())
813
+ if (callbackFailure || limit)
820
814
  return;
821
815
  lineParts = [];
822
816
  lineBytes = 0;
@@ -856,13 +850,13 @@ async function runPi(prepared, input, budget, invocation) {
856
850
  killTimer.unref();
857
851
  }
858
852
  const abort = () => {
859
- if (timedOutAfterMs !== undefined || turnLimited || tokenLimited() || childExited)
853
+ if (timedOutAfterMs !== undefined || limit || childExited)
860
854
  return;
861
855
  aborted = true;
862
856
  stop();
863
857
  };
864
858
  function timeout(afterMs, reason) {
865
- if (timedOutAfterMs !== undefined || turnLimited || tokenLimited() || childExited)
859
+ if (timedOutAfterMs !== undefined || limit || childExited)
866
860
  return;
867
861
  if (reason === "maximum") {
868
862
  if (!aborted) {
@@ -1,17 +1,10 @@
1
- import { spawnBounded } from "@henryqw/pi-process";
1
+ import { runProcess } from "./process.js";
2
2
  const GIT_TIMEOUT_MS = 30_000;
3
- const GIT_OUTPUT_LIMIT_BYTES = 1 * 1024 * 1024;
4
3
  const GIT_DIAGNOSTIC_LIMIT = 200;
5
4
  /** Run one ordinary Git command with bounded output; transport failures never throw. */
6
5
  export const runGit = async (args, cwd, signal) => {
7
6
  try {
8
- const result = await spawnBounded("git", ["--no-pager", ...args], {
9
- cwd,
10
- signal,
11
- timeoutMs: GIT_TIMEOUT_MS,
12
- stdoutLimitBytes: GIT_OUTPUT_LIMIT_BYTES,
13
- stderrLimitBytes: GIT_OUTPUT_LIMIT_BYTES,
14
- });
7
+ const result = await runProcess("git", ["--no-pager", ...args], { cwd, signal, timeoutMs: GIT_TIMEOUT_MS });
15
8
  if (result.killed) {
16
9
  return {
17
10
  code: -1,
package/dist/index.js CHANGED
@@ -374,10 +374,9 @@ export async function resolveConfiguredRoleLaunch(pi, ctx, input) {
374
374
  const roleName = parseRoleName(input.role);
375
375
  if (input.modelClass === undefined)
376
376
  throw new Error("Configured Role launch requires an explicit modelClass.");
377
- const matches = loadRoles().filter((role) => role.name === roleName);
378
- if (matches.length !== 1)
377
+ const role = loadRoles().find((role) => role.name === roleName);
378
+ if (!role)
379
379
  throw new Error(`Required configured Role ${roleName} is missing or ambiguous.`);
380
- const role = matches[0];
381
380
  if (role.mcps?.length)
382
381
  loadRoleMcpConfig(getAgentDir(), role.mcps, { codemode: roleActivatesCodemode(role.tools) });
383
382
  const resources = await resolveRolePackageResources(role, ctx);
@@ -125,11 +125,7 @@ export class RoleLaunchRuntime {
125
125
  }
126
126
  async prepareLaunch(role, modelClass, context) {
127
127
  abortIfNeeded(context.signal);
128
- const input = { role, modelClass, task: ISOLATED_MODEL_TASK };
129
- const prepared = await resolveConfiguredRoleLaunch(this.options.pi, this.options.context(), input);
130
- if (prepared.missingSkills.length) {
131
- throw new Error(`Role ${role} requires missing Skills: ${prepared.missingSkills.join(", ")}.`);
132
- }
128
+ const prepared = await resolveConfiguredRoleLaunch(this.options.pi, this.options.context(), { role, modelClass });
133
129
  return Object.freeze({
134
130
  launch: Object.freeze({
135
131
  role: prepared.role,
package/dist/process.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  type ProcessOptions = {
2
2
  cwd: string;
3
- signal: AbortSignal;
3
+ signal?: AbortSignal;
4
4
  timeoutMs?: number;
5
5
  stdin?: string;
6
6
  };
package/dist/runner.js CHANGED
@@ -131,12 +131,12 @@ class FollowupControl {
131
131
  if (this.queue.length >= MAX_QUEUED_FOLLOWUPS)
132
132
  throw new Error(`A task may queue at most ${MAX_QUEUED_FOLLOWUPS} follow-ups.`);
133
133
  this.validateFollowup(instruction);
134
- this.queue.push({ kind: "followup", instruction });
134
+ this.queue.push(instruction);
135
135
  }
136
136
  drainPending() {
137
137
  if (this.sealed)
138
138
  throw new Error("This task candidate is already sealed for integration.");
139
- return this.queue.splice(0).map(({ instruction }) => instruction);
139
+ return this.queue.splice(0);
140
140
  }
141
141
  takeQueuedFollowup() {
142
142
  if (this.sealed)
@@ -578,7 +578,7 @@ export class IsolatedRunner {
578
578
  }
579
579
  drainFollowups(root, requestId, taskId) {
580
580
  const active = this.activeControl(realpathSync.native(root), requestId, taskId);
581
- if (!active || !this.canFollowup(root, requestId, taskId)) {
581
+ if (!active || active.task.status !== "working" || active.control.isSealed) {
582
582
  throw new Error(`Task ${taskId} is not an active unsealed changeset.`);
583
583
  }
584
584
  return active.control.drainPending();
@@ -589,7 +589,7 @@ export class IsolatedRunner {
589
589
  throw new Error("Follow-up instruction must be non-empty exact text of at most 32000 characters.");
590
590
  }
591
591
  const active = this.activeControl(realpathSync.native(root), requestId, taskId);
592
- if (!active || !this.canFollowup(realpathSync.native(root), requestId, taskId)) {
592
+ if (!active || active.task.status !== "working" || active.control.isSealed) {
593
593
  throw new Error(`Task ${taskId} is not an active unsealed changeset.`);
594
594
  }
595
595
  if (latestAttempt(active.task).prompts.length + active.control.pendingFollowups >= MAX_WORKER_PROMPTS) {
@@ -1093,7 +1093,8 @@ export class IsolatedRunner {
1093
1093
  return await this.withProductiveRun(root, async (lifecycle) => {
1094
1094
  const handle = await this.store.withLock(root, async () => await this.store.load(root, action.id), { productiveRunLease: lifecycle.lease });
1095
1095
  const state = handle.state;
1096
- const request = parseExecuteRequest(state.request);
1096
+ // Loading state has already normalized optional request fields, including finalChecks.
1097
+ const request = state.request;
1097
1098
  const generation = state.integration.generations.at(-1);
1098
1099
  if (action.action === "release") {
1099
1100
  const scope = new ProductiveScope(outerSignal);
@@ -2300,7 +2301,7 @@ export class IsolatedRunner {
2300
2301
  const queuedFollowup = control.takeQueuedFollowupOrSeal();
2301
2302
  if (queuedFollowup) {
2302
2303
  kind = "followup";
2303
- instruction = queuedFollowup.instruction;
2304
+ instruction = queuedFollowup;
2304
2305
  task.status = "working";
2305
2306
  continue;
2306
2307
  }
@@ -2336,7 +2337,7 @@ export class IsolatedRunner {
2336
2337
  const queuedFollowup = control.takeQueuedFollowup();
2337
2338
  if (queuedFollowup) {
2338
2339
  kind = "followup";
2339
- instruction = queuedFollowup.instruction;
2340
+ instruction = queuedFollowup;
2340
2341
  task.status = "working";
2341
2342
  continue;
2342
2343
  }
package/dist/store.js CHANGED
@@ -61,7 +61,7 @@ async function canonicalPlannedPath(path) {
61
61
  }
62
62
  }
63
63
  function serialize(state, maxBytes = STATE_MAX_BYTES) {
64
- const validated = parseRunState(structuredClone(state));
64
+ const validated = parseRunState(state);
65
65
  const contents = `${JSON.stringify(validated, null, 2)}\n`;
66
66
  if (Buffer.byteLength(contents, "utf8") > maxBytes) {
67
67
  throw new Error(`pi-subagent state exceeds ${maxBytes} bytes.`);
@@ -23,12 +23,12 @@ import {
23
23
  IntegrationActionParameters,
24
24
  parseIntegrationAction,
25
25
  parseStageRequest,
26
- parseExecuteRequest,
27
26
  parseIdOnly,
28
27
  parseResumeRequest,
29
28
  sameIdentity,
30
29
  StatusOutputSchema,
31
30
  type CheckBatchEvidence,
31
+ type ExecuteRequest,
32
32
  type ModelClass,
33
33
  type ReviewEvidence,
34
34
  type RunState,
@@ -412,7 +412,7 @@ function toolResult(response: RunResponse, ctx: ExtensionContext, rowsByRequest:
412
412
  }
413
413
 
414
414
  export interface IsolatedSurface {
415
- execute(params: unknown, signal: AbortSignal | undefined, ctx: ExtensionContext): Promise<ReturnType<typeof toolResult>>;
415
+ execute(request: ExecuteRequest, signal: AbortSignal | undefined, ctx: ExtensionContext): Promise<ReturnType<typeof toolResult>>;
416
416
  inventory(cwd: string, current?: () => boolean): Promise<IsolatedInventory>;
417
417
  recover(cwd: string): Promise<string>;
418
418
  inspect(root: string, requestId: string): Promise<readonly string[]>;
@@ -767,10 +767,9 @@ export function registerIsolatedExtension(pi: ExtensionAPI, options: RegisterIso
767
767
  requireCurrent(current);
768
768
  return getComponents().runner.drainFollowups(root, requestId, taskId);
769
769
  },
770
- async execute(params, signal, ctx) {
770
+ async execute(request, signal, ctx) {
771
771
  latestCtx = ctx;
772
772
  latestContext();
773
- const request = parseExecuteRequest(params);
774
773
  const root = await lookupRoot(ctx.cwd, signal);
775
774
  return await startInSession(request.id, root, signal, ctx,
776
775
  (state) => state.status === "pending" && state.createdAt === state.updatedAt
@@ -42,8 +42,7 @@ export const DirectWorkflowSchema = Type.Object({
42
42
  chain: Type.Optional(Type.Array(DelegationSchema, { minItems: 1, maxItems: MAX_WORKFLOW_ENTRIES })),
43
43
  }, { additionalProperties: false, description: "Direct mode: exactly one compact single, tasks, or chain workflow" });
44
44
 
45
- export const DelegateTaskSchema = Type.Union([DirectWorkflowSchema, ExecuteRequestSchema]);
46
- // Keep the strict mode union for parsing; expose a plain object root to tool providers.
45
+ // Parse each mode with its strict schema; expose a plain object root to tool providers.
47
46
  export const DelegateTaskParameters = Type.Object({
48
47
  ...DirectWorkflowSchema.properties,
49
48
  mode: Type.Union([DirectWorkflowSchema.properties.mode, ExecuteRequestSchema.properties.mode]),
@@ -56,7 +55,6 @@ export const DelegateTaskParameters = Type.Object({
56
55
  finalChecks: ExecuteRequestSchema.properties.finalChecks,
57
56
  finalJudgment: ExecuteRequestSchema.properties.finalJudgment,
58
57
  }, { additionalProperties: false });
59
- export const WorkflowSchema = DirectWorkflowSchema;
60
58
 
61
59
  export type Delegation = Static<typeof DelegationSchema> & { kind: "text" };
62
60
  export type WorkflowMode = "single" | "parallel" | "chain";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@henryqw/pi-subagent",
3
- "version": "25.1.0",
3
+ "version": "25.1.1",
4
4
  "description": "Delegate bounded direct work or durable checked isolated task graphs to Pi roles.",
5
5
  "keywords": [
6
6
  "pi-package",