pi-subagents 0.42.0 → 0.42.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.
- package/CHANGELOG.md +7 -0
- package/package.json +1 -1
- package/src/extension/schemas.ts +3 -3
- package/src/extension/tool-description.ts +4 -4
- package/src/runs/foreground/chain-clarify.ts +13 -16
- package/src/runs/foreground/subagent-executor.ts +4 -5
- package/src/shared/types.ts +1 -1
- package/src/slash/slash-commands.ts +3 -3
- package/src/tui/render.ts +3 -2
- package/src/workflows/chat-progress.ts +2 -2
- package/src/workflows/scripted-workflow.ts +6 -4
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.42.1] - 2026-08-06
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- Prevent Pi from crashing when subagent status widgets and overlays are shown in narrow or resized terminal layouts. Thanks to @alanvardy for the report in #858 and @meatcar for the fix.
|
|
9
|
+
- Keep async scripted workflows running without an implicit 30-minute timeout, while preserving the foreground default and explicit timeout controls.
|
|
10
|
+
- Limit `workflowScript` chat progress to the supported `auto`, `off`, and `live-card` projections.
|
|
11
|
+
|
|
5
12
|
## [0.42.0] - 2026-08-06
|
|
6
13
|
|
|
7
14
|
### Added
|
package/package.json
CHANGED
package/src/extension/schemas.ts
CHANGED
|
@@ -314,7 +314,7 @@ const SubagentParamsSchema = Type.Object({
|
|
|
314
314
|
description: "Agent/chain config for create/update. Object or JSON string; presence of steps creates a chain."
|
|
315
315
|
})),
|
|
316
316
|
workflowScript: Type.Optional(Type.String({ minLength: 1, description: "Trusted inline JavaScript orchestration. Starts asynchronously by default; pass async:false for a small foreground run. Use await runs.run(key, {agent, task, worktree?}), runs.all([...]), runs.status(id), runs.ref(s), emit(value), console, and return. Use ordinary JavaScript loops, branches, awaits, and arrays to mix sequential and parallel phases dynamically. Set worktree:true at workflow or child level for a separate managed worktree per child; child fields override workflow defaults. runs.run accepts one child only. No filesystem, shell, Pi tools, or host globals." })),
|
|
317
|
-
chatProgress: Type.Optional(Type.String({ enum: ["auto", "off", "
|
|
317
|
+
chatProgress: Type.Optional(Type.String({ enum: ["auto", "off", "live-card"], description: "WorkflowScript chat progress projection. auto shows a live in-chat card only for watched foreground workflows in the same Git repository; it is off otherwise." })),
|
|
318
318
|
worktree: Type.Optional(Type.Boolean({ description: "Managed child isolation. true gives a direct single child or each workflow child a separate git worktree; an individual runs.run/runs.all item can override a workflow default with worktree:false." })),
|
|
319
319
|
step: Type.Optional(Type.Unsafe({ ...ChainItem, description: "One chain step for action='append-step' only. Not an execution mode." })),
|
|
320
320
|
context: Type.Optional(Type.String({
|
|
@@ -322,8 +322,8 @@ const SubagentParamsSchema = Type.Object({
|
|
|
322
322
|
description: "'fresh' or 'fork' to branch from parent session. Explicit context overrides every child in the invocation. If omitted, each requested agent uses its own defaultContext; agents without defaultContext: 'fork' run fresh.",
|
|
323
323
|
})),
|
|
324
324
|
async: Type.Optional(Type.Boolean({ description: "Run in background (default: false, or per config)" })),
|
|
325
|
-
timeoutMs: Type.Optional(Type.Integer({ minimum: 1, description: "
|
|
326
|
-
maxRuntimeMs: Type.Optional(Type.Integer({ minimum: 1, description: "Alias timeoutMs for foreground and async/background runs
|
|
325
|
+
timeoutMs: Type.Optional(Type.Integer({ minimum: 1, description: "Optional timeout for foreground and async/background runs. Foreground workflows default to 30m; async workflows have no default timeout. Alias maxRuntimeMs." })),
|
|
326
|
+
maxRuntimeMs: Type.Optional(Type.Integer({ minimum: 1, description: "Alias timeoutMs for foreground and async/background runs. Foreground workflows default to 30m; async workflows have no default timeout." })),
|
|
327
327
|
turnBudget: Type.Optional(TurnBudgetOverride),
|
|
328
328
|
toolBudget: Type.Optional(ToolBudgetOverride),
|
|
329
329
|
usageBudget: Type.Optional(UsageBudgetOverride),
|
|
@@ -19,10 +19,10 @@ export const FULL_SUBAGENT_TOOL_DESCRIPTION = `Delegate one child with { agent,
|
|
|
19
19
|
EXECUTION (use exactly one mode):
|
|
20
20
|
• Before executing, use { action: "list" } and run only executable/non-disabled configured agents.
|
|
21
21
|
• SINGLE: { agent, task? } launches one child. Omit task for a self-contained agent.
|
|
22
|
-
• SCRIPTED WORKFLOW: { workflowScript: "const scan = await runs.run('scan', {agent:'agent-a', task:'...'}); return scan.output" }. Use stable-key runs.run for one child and runs.all for parallel children; ordinary JavaScript provides sequence, branching, filtering, retries, and aggregation. Scripts start asynchronously by default; pass async:false only for a small foreground run. Same-repo foreground workflows default to a live in-chat card; set chatProgress to auto, off,
|
|
22
|
+
• SCRIPTED WORKFLOW: { workflowScript: "const scan = await runs.run('scan', {agent:'agent-a', task:'...'}); return scan.output" }. Use stable-key runs.run for one child and runs.all for parallel children; ordinary JavaScript provides sequence, branching, filtering, retries, and aggregation. Scripts start asynchronously by default; pass async:false only for a small foreground run. Same-repo foreground workflows default to a live in-chat card; set chatProgress to auto, off, or live-card to control that projection. Workflow-level child controls default onto each runs.run launch, and explicit child fields override them. Direct single-child calls also support worktree:true; use workflowScript only when coordination is needed. For repository mutation lanes, set worktree:true on a direct single child, workflow, or individual runs.run/runs.all item for managed isolation instead of manual Git worktrees; each parallel child gets a separate worktree and handoff artifact. A workflow usageBudget is enforced once across the workflow. Available globals are runs.run, runs.all, runs.status, runs.ref/refs, emit, console, and standard JavaScript only. Scripts cannot access filesystem, shell, arbitrary Pi tools, or host globals.
|
|
23
23
|
• Sequential replacement: { workflowScript: "const a = await runs.run('analyze', {agent:'agent-a', task:'Analyze the request'}); return (await runs.run('plan', {agent:'agent-b', task:'Plan from: '+a.output})).output" }
|
|
24
24
|
• Parallel replacement: { workflowScript: "const [a,b] = await runs.all([{key:'correctness',agent:'agent-a',task:'Review correctness'},{key:'tests',agent:'agent-b',task:'Review tests'}]); return {correctness:a.output,tests:b.output}" }
|
|
25
|
-
• Optional context is "fresh" or "fork". timeoutMs/maxRuntimeMs apply to foreground and async runs. Omit acceptance for reviewer/read-only calls; evidence levels end at verified, and acceptance.review.required requests independent writer review.
|
|
25
|
+
• Optional context is "fresh" or "fork". timeoutMs/maxRuntimeMs apply to foreground and async runs; foreground workflows default to 30 minutes and async workflows have no default timeout. Omit acceptance for reviewer/read-only calls; evidence levels end at verified, and acceptance.review.required requests independent writer review.
|
|
26
26
|
• Durable mission attachment is automatic by default. Use missionId to attach an existing mission, mission:{...} to override auto-create, or mission:false for ephemeral work.
|
|
27
27
|
|
|
28
28
|
MANAGEMENT / CONTROL (use action; omit execution fields):
|
|
@@ -38,9 +38,9 @@ export const COMPACT_SUBAGENT_TOOL_DESCRIPTION = `Delegate one child with { agen
|
|
|
38
38
|
|
|
39
39
|
EXECUTE:
|
|
40
40
|
• Call { action:"list" } first and use only executable/non-disabled agents.
|
|
41
|
-
• SINGLE {agent, task?}; SCRIPT {workflowScript:"..."} with stable-key runs.run for one child and runs.all for parallel work. Use JavaScript for sequence, branching, retries, and aggregation. For repository mutation lanes, use worktree:true on a direct single child or runs.run/runs.all item for managed isolation instead of manual Git worktrees. Scripts start async by default; async:false is the foreground escape hatch and auto-enables a same-repo live chat card unless chatProgress is off
|
|
41
|
+
• SINGLE {agent, task?}; SCRIPT {workflowScript:"..."} with stable-key runs.run for one child and runs.all for parallel work. Use JavaScript for sequence, branching, retries, and aggregation. For repository mutation lanes, use worktree:true on a direct single child or runs.run/runs.all item for managed isolation instead of manual Git worktrees. Scripts start async by default; async:false is the foreground escape hatch and auto-enables a same-repo live chat card unless chatProgress is off.
|
|
42
42
|
• Example: {workflowScript:"const [a,b]=await runs.all([{key:'a',agent:'agent-a',task:'Implement A',worktree:true},{key:'b',agent:'agent-b',task:'Implement B',worktree:true}]); return [a.output,b.output]"}
|
|
43
|
-
• context can be fresh or fork. timeoutMs/maxRuntimeMs apply to foreground and async runs. Omit acceptance for reviewer/read-only calls.
|
|
43
|
+
• context can be fresh or fork. timeoutMs/maxRuntimeMs apply to foreground and async runs; foreground workflows default to 30 minutes and async workflows have no default timeout. Omit acceptance for reviewer/read-only calls.
|
|
44
44
|
|
|
45
45
|
MANAGE / CONTROL:
|
|
46
46
|
• Use action without execution fields for list/get/models/authoring, mission, watchdog, status, interrupt, stop, resume, steer, scheduling, diagnostics, and other management actions.
|
|
@@ -882,25 +882,22 @@ export class ChainClarifyComponent implements Component {
|
|
|
882
882
|
}
|
|
883
883
|
}
|
|
884
884
|
|
|
885
|
-
render(
|
|
885
|
+
render(width: number): string[] {
|
|
886
|
+
let lines: string[] = [];
|
|
886
887
|
if (this.editingStep !== null) {
|
|
887
|
-
if (this.editMode === "model")
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
888
|
+
if (this.editMode === "model") lines = this.renderModelSelector();
|
|
889
|
+
else if (this.editMode === "thinking") lines = this.renderThinkingSelector();
|
|
890
|
+
else if (this.editMode === "skills") lines = this.renderSkillSelector();
|
|
891
|
+
else lines = this.renderFullEditMode();
|
|
892
|
+
} else {
|
|
893
|
+
switch (this.mode) {
|
|
894
|
+
case 'single': lines = this.renderSingleMode(); break;
|
|
895
|
+
case 'parallel': lines = this.renderParallelMode(); break;
|
|
896
|
+
case 'chain': lines = this.renderChainMode(); break;
|
|
895
897
|
}
|
|
896
|
-
return this.renderFullEditMode();
|
|
897
|
-
}
|
|
898
|
-
// Mode-based navigation rendering
|
|
899
|
-
switch (this.mode) {
|
|
900
|
-
case 'single': return this.renderSingleMode();
|
|
901
|
-
case 'parallel': return this.renderParallelMode();
|
|
902
|
-
case 'chain': return this.renderChainMode();
|
|
903
898
|
}
|
|
899
|
+
const renderWidth = Math.max(0, Math.min(this.width, Math.floor(width)));
|
|
900
|
+
return lines.map((line) => truncateToWidth(line, renderWidth));
|
|
904
901
|
}
|
|
905
902
|
|
|
906
903
|
/** Render the model selector view */
|
|
@@ -212,7 +212,7 @@ export interface SubagentParamsLike {
|
|
|
212
212
|
message?: string;
|
|
213
213
|
steeringRecovery?: boolean;
|
|
214
214
|
workflowScript?: string;
|
|
215
|
-
chatProgress?: "auto" | "off" | "
|
|
215
|
+
chatProgress?: "auto" | "off" | "live-card";
|
|
216
216
|
step?: ChainStep;
|
|
217
217
|
/** Internal workflow ownership metadata; not part of the public schema. */
|
|
218
218
|
workflowParentRunId?: string;
|
|
@@ -4049,7 +4049,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
4049
4049
|
if (requestParams.clarify === true) {
|
|
4050
4050
|
return { content: [{ type: "text", text: "workflowScript does not support clarify UI." }], isError: true, details: { mode: "workflow", results: [] } };
|
|
4051
4051
|
}
|
|
4052
|
-
const timeout = requestParams.timeoutMs ?? requestParams.maxRuntimeMs ?? DEFAULT_FOREGROUND_TIMEOUT_MS;
|
|
4052
|
+
const timeout = requestParams.timeoutMs ?? requestParams.maxRuntimeMs ?? (requestParams.async === false ? DEFAULT_FOREGROUND_TIMEOUT_MS : undefined);
|
|
4053
4053
|
const workflowUsageBudget = validateUsageBudgetConfig(requestParams.usageBudget ?? deps.config.usageBudget, requestParams.usageBudget ? "usageBudget" : "config.usageBudget");
|
|
4054
4054
|
if (workflowUsageBudget.error) return buildRequestedModeError(requestParams, workflowUsageBudget.error);
|
|
4055
4055
|
const workflowCwd = resolveRequestedCwd(ctx.cwd, requestParams.cwd);
|
|
@@ -4076,8 +4076,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
4076
4076
|
state: "running",
|
|
4077
4077
|
startedAt,
|
|
4078
4078
|
lastUpdate: startedAt,
|
|
4079
|
-
deadlineAt: startedAt + timeout,
|
|
4080
|
-
timeoutMs: timeout,
|
|
4079
|
+
...(timeout !== undefined ? { deadlineAt: startedAt + timeout, timeoutMs: timeout } : {}),
|
|
4081
4080
|
cwd: ctx.cwd,
|
|
4082
4081
|
pid: process.pid,
|
|
4083
4082
|
steps: [],
|
|
@@ -4101,7 +4100,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
4101
4100
|
job.workflow = status.workflow;
|
|
4102
4101
|
}
|
|
4103
4102
|
};
|
|
4104
|
-
const workflowJob: AsyncJobState = { asyncId: workflowRunId, asyncDir, cwd: ctx.cwd, status: "running", sessionId: currentSessionId ?? undefined, mode: "workflow", agents: [], steps: [], startedAt, updatedAt: startedAt, timeoutMs: timeout, deadlineAt: startedAt + timeout, workflow: status.workflow };
|
|
4103
|
+
const workflowJob: AsyncJobState = { asyncId: workflowRunId, asyncDir, cwd: ctx.cwd, status: "running", sessionId: currentSessionId ?? undefined, mode: "workflow", agents: [], steps: [], startedAt, updatedAt: startedAt, ...(timeout !== undefined ? { timeoutMs: timeout, deadlineAt: startedAt + timeout } : {}), workflow: status.workflow };
|
|
4105
4104
|
deps.state.asyncJobs.set(workflowRunId, workflowJob);
|
|
4106
4105
|
deps.state.fleetJobs ??= new Map();
|
|
4107
4106
|
deps.state.fleetJobs.set(workflowRunId, workflowJob);
|
package/src/shared/types.ts
CHANGED
|
@@ -1007,7 +1007,7 @@ export interface Details {
|
|
|
1007
1007
|
console: Array<{ level: "log" | "info" | "warn" | "error"; text: string }>;
|
|
1008
1008
|
};
|
|
1009
1009
|
chatProgress?: {
|
|
1010
|
-
mode: "off" | "
|
|
1010
|
+
mode: "off" | "live-card";
|
|
1011
1011
|
repoRelation: "same" | "other";
|
|
1012
1012
|
repoLabel?: string;
|
|
1013
1013
|
};
|
|
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { keyText, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import { Key, matchesKey, type Component, type TUI } from "@earendil-works/pi-tui";
|
|
5
|
+
import { Key, matchesKey, truncateToWidth, type Component, type TUI } from "@earendil-works/pi-tui";
|
|
6
6
|
import { BUILTIN_AGENT_NAMES, discoverAgents } from "../agents/agents.ts";
|
|
7
7
|
import {
|
|
8
8
|
DEFAULT_PROVIDER_MODELS_MAX_AGE_DAYS,
|
|
@@ -281,7 +281,7 @@ class SubagentsStopSelector implements Component {
|
|
|
281
281
|
invalidate(): void {}
|
|
282
282
|
|
|
283
283
|
render(width: number): string[] {
|
|
284
|
-
const contentWidth = Math.max(
|
|
284
|
+
const contentWidth = Math.max(0, Math.min(this.width, Math.floor(width)));
|
|
285
285
|
const lines = [this.theme.bold("Stop subagent run"), this.theme.fg("dim", "Select a current-session async run to stop, or a scheduled run to cancel."), ""];
|
|
286
286
|
const maxRows = 10;
|
|
287
287
|
const start = Math.max(0, Math.min(this.selected - maxRows + 1, Math.max(0, this.targets.length - maxRows)));
|
|
@@ -305,7 +305,7 @@ class SubagentsStopSelector implements Component {
|
|
|
305
305
|
} else {
|
|
306
306
|
lines.push(this.theme.fg("dim", "↑↓/jk select · Enter confirm · Esc cancel"));
|
|
307
307
|
}
|
|
308
|
-
return lines;
|
|
308
|
+
return lines.map((line) => truncateToWidth(line, contentWidth));
|
|
309
309
|
}
|
|
310
310
|
}
|
|
311
311
|
|
package/src/tui/render.ts
CHANGED
|
@@ -1291,7 +1291,8 @@ function collapsedWidgetLineBudget(rows: number): number {
|
|
|
1291
1291
|
}
|
|
1292
1292
|
|
|
1293
1293
|
function paddedWidgetLine(line: string, width: number): string {
|
|
1294
|
-
|
|
1294
|
+
if (width <= 2) return " ".repeat(Math.max(0, width));
|
|
1295
|
+
const text = ` ${truncLine(line, width - 2)} `;
|
|
1295
1296
|
return `${text}${" ".repeat(Math.max(0, width - visibleWidth(text)))}`;
|
|
1296
1297
|
}
|
|
1297
1298
|
|
|
@@ -1352,7 +1353,7 @@ function buildWidgetComponent(jobs: AsyncJobState[], expanded: boolean): (tui: {
|
|
|
1352
1353
|
widgetRequestRender = requestRender;
|
|
1353
1354
|
const container = new Container();
|
|
1354
1355
|
container.render = (renderWidth: number): string[] => {
|
|
1355
|
-
const width =
|
|
1356
|
+
const width = Math.max(0, renderWidth - 2);
|
|
1356
1357
|
const frame = widgetAnimationFrame();
|
|
1357
1358
|
const lines = expanded
|
|
1358
1359
|
? buildWidgetLines(jobs, theme, width, true, frame)
|
|
@@ -3,7 +3,7 @@ import * as fs from "node:fs";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import type { Details } from "../shared/types.ts";
|
|
5
5
|
|
|
6
|
-
export const WORKFLOW_CHAT_PROGRESS_MODES = ["auto", "off", "
|
|
6
|
+
export const WORKFLOW_CHAT_PROGRESS_MODES = ["auto", "off", "live-card"] as const;
|
|
7
7
|
export type WorkflowChatProgressMode = typeof WORKFLOW_CHAT_PROGRESS_MODES[number];
|
|
8
8
|
export type ResolvedWorkflowChatProgressMode = Exclude<WorkflowChatProgressMode, "auto">;
|
|
9
9
|
|
|
@@ -86,7 +86,7 @@ export function resolveWorkflowChatProgress(input: ResolveWorkflowChatProgressIn
|
|
|
86
86
|
|
|
87
87
|
const requestedMode = requested.mode ?? "auto";
|
|
88
88
|
let mode: ResolvedWorkflowChatProgressMode;
|
|
89
|
-
if (requestedMode === "auto") mode = sameRepo
|
|
89
|
+
if (requestedMode === "auto") mode = sameRepo && !input.background ? "live-card" : "off";
|
|
90
90
|
else mode = requestedMode;
|
|
91
91
|
|
|
92
92
|
if (mode === "live-card" && !sameRepo) return { error: "chatProgress: 'live-card' is only available for workflowScript runs in the same Git repository." };
|
|
@@ -175,7 +175,7 @@ export class WorkflowScriptError extends Error {
|
|
|
175
175
|
|
|
176
176
|
export interface RunWorkflowScriptOptions {
|
|
177
177
|
script: string;
|
|
178
|
-
timeoutMs
|
|
178
|
+
timeoutMs?: number;
|
|
179
179
|
signal?: AbortSignal;
|
|
180
180
|
launch: (key: string, params: Record<string, unknown>, signal: AbortSignal) => Promise<WorkflowScriptChildResult>;
|
|
181
181
|
status: (keyOrRunId: string, signal: AbortSignal) => Promise<WorkflowScriptChildResult>;
|
|
@@ -261,7 +261,7 @@ function workflowStringMetadata(params: Record<string, unknown>): Pick<WorkflowS
|
|
|
261
261
|
|
|
262
262
|
export async function runWorkflowScript(options: RunWorkflowScriptOptions): Promise<WorkflowScriptResult> {
|
|
263
263
|
if (!options.script.trim()) throw new Error("workflowScript must not be empty.");
|
|
264
|
-
if (!Number.isInteger(options.timeoutMs) || options.timeoutMs < 1) throw new Error("workflow script timeout must be a positive integer.");
|
|
264
|
+
if (options.timeoutMs !== undefined && (!Number.isInteger(options.timeoutMs) || options.timeoutMs < 1)) throw new Error("workflow script timeout must be a positive integer.");
|
|
265
265
|
|
|
266
266
|
const worker = new Worker(WORKER_SOURCE, { eval: true });
|
|
267
267
|
const emits: unknown[] = [];
|
|
@@ -283,7 +283,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
283
283
|
const finish = (outcome: { value: unknown } | { error: Error }) => {
|
|
284
284
|
if (settled) return;
|
|
285
285
|
settled = true;
|
|
286
|
-
clearTimeout(timer);
|
|
286
|
+
if (timer) clearTimeout(timer);
|
|
287
287
|
options.signal?.removeEventListener("abort", onAbort);
|
|
288
288
|
void worker.terminate();
|
|
289
289
|
childController.abort("error" in outcome ? outcome.error : new Error("Workflow script completed; unawaited child launches are aborted."));
|
|
@@ -291,7 +291,9 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
291
291
|
else resolve({ value: outcome.value, ...partial() });
|
|
292
292
|
};
|
|
293
293
|
const onAbort = () => finish({ error: new Error("Workflow script aborted.") });
|
|
294
|
-
const timer =
|
|
294
|
+
const timer = options.timeoutMs === undefined
|
|
295
|
+
? undefined
|
|
296
|
+
: setTimeout(() => finish({ error: new Error(`Workflow script timed out after ${options.timeoutMs}ms.`) }), options.timeoutMs);
|
|
295
297
|
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
296
298
|
if (options.signal?.aborted) return onAbort();
|
|
297
299
|
|