@ferris1225/pi-subagents 0.24.0 → 0.25.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.
package/src/spawn.ts CHANGED
@@ -74,6 +74,11 @@ export interface SingleResult {
74
74
  * exit (a concurrent pi startup race) before it produced a result. Set only when
75
75
  * the run actually recovered after retrying, so callers can surface it. */
76
76
  startupRetries?: number;
77
+ /** Tool calls that failed inside the run (from tool_execution_end events). A
78
+ * clean process exit can still hide a failed build/test/tool — the completion
79
+ * message must surface these so the main agent is never misled by a rosy final
80
+ * text (e.g. a worker that ended with "keep waiting" while its build failed). */
81
+ failedTools?: Array<{ toolName: string; error: string }>;
77
82
  }
78
83
 
79
84
  export interface SubagentDetails {
@@ -123,6 +128,29 @@ export function reviewVerdict(output: string): "pass" | "fail" | undefined {
123
128
  return undefined;
124
129
  }
125
130
 
131
+ /** Tool errors are usually the trailing lines of a long output (build logs);
132
+ * keep the last non-empty lines, clipped to RESULT_LINE_MAX each. */
133
+ export function extractToolErrorText(content: unknown): string {
134
+ const parts = Array.isArray(content) ? content : [];
135
+ const text = parts
136
+ .filter(
137
+ (part): part is { type: "text"; text: string } =>
138
+ typeof part === "object" &&
139
+ part !== null &&
140
+ (part as { type?: unknown }).type === "text" &&
141
+ typeof (part as { text?: unknown }).text === "string",
142
+ )
143
+ .map((part) => part.text)
144
+ .join("\n");
145
+ return text
146
+ .split("\n")
147
+ .map((line) => line.trim())
148
+ .filter(Boolean)
149
+ .slice(-3)
150
+ .map((line) => (line.length > RESULT_LINE_MAX ? `${line.slice(0, RESULT_LINE_MAX)}…` : line))
151
+ .join("\n");
152
+ }
153
+
126
154
  /** Hard cap for a single line inside a truncated result (minified blobs must not blow up). */
127
155
  export const RESULT_LINE_MAX = 200;
128
156
 
@@ -493,6 +521,12 @@ export async function runSingleAgent(options: RunSingleOptions): Promise<SingleR
493
521
  onLive({ kind: "tool_end", toolName: event.toolName ?? "unknown", isError: Boolean(event.isError) });
494
522
  } catch { /* never throw from event handling */ }
495
523
  }
524
+ if (event.isError) {
525
+ (currentResult.failedTools ??= []).push({
526
+ toolName: event.toolName ?? "unknown",
527
+ error: extractToolErrorText(event.result?.content),
528
+ });
529
+ }
496
530
  }
497
531
 
498
532
  if (event.type === "message_end" && event.message) {
@@ -695,5 +729,11 @@ export async function runSingleAgentWithModelFallback(
695
729
  if (!agent || !launchedRef || !fallbackModelRef || launchedRef === fallbackModelRef) return result;
696
730
  if (!isModelLevelFailure(result)) return result;
697
731
  const retried = await runWithStartupRetry({ ...options, agent: { ...agent, model: fallbackModelRef } });
732
+ // The fallback replaces the result wholesale: `retried.failedTools` reflect
733
+ // ONLY the fallback (final) attempt. The original attempt's failedTools are
734
+ // intentionally not merged — a fallback relaunch redoes the work, so attaching
735
+ // the first attempt's stale build errors to a clean final attempt would
736
+ // misattribute failures the worker already fixed. This makes the README's
737
+ // "failed tool calls from the run's final attempt" claim accurate.
698
738
  return { ...retried, modelFallbackFrom: launchedRef };
699
739
  }