pi-subagents 0.21.2 → 0.21.4
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 +15 -0
- package/README.md +23 -13
- package/package.json +15 -1
- package/skills/pi-subagents/SKILL.md +32 -12
- package/src/agents/agent-management.ts +58 -16
- package/src/agents/agent-serializer.ts +4 -1
- package/src/agents/agents.ts +39 -30
- package/src/agents/chain-serializer.ts +16 -3
- package/src/agents/identity.ts +30 -0
- package/src/extension/control-notices.ts +92 -0
- package/src/extension/index.ts +22 -41
- package/src/extension/schemas.ts +11 -2
- package/src/manager-ui/agent-manager-chain-detail.ts +4 -0
- package/src/manager-ui/agent-manager-detail.ts +4 -0
- package/src/manager-ui/agent-manager-edit.ts +25 -6
- package/src/manager-ui/agent-manager.ts +28 -8
- package/src/runs/background/async-execution.ts +12 -2
- package/src/runs/background/subagent-runner.ts +12 -10
- package/src/runs/foreground/chain-clarify.ts +2 -0
- package/src/runs/foreground/chain-execution.ts +36 -0
- package/src/runs/foreground/execution.ts +26 -4
- package/src/runs/foreground/subagent-executor.ts +26 -1
- package/src/runs/shared/parallel-utils.ts +1 -0
- package/src/runs/shared/single-output.ts +47 -3
- package/src/shared/settings.ts +9 -3
- package/src/shared/types.ts +13 -0
- package/src/slash/slash-commands.ts +6 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ChainConfig, ChainStepConfig } from "./agents.ts";
|
|
2
|
+
import { buildRuntimeName, frontmatterNameForConfig, parsePackageName } from "./identity.ts";
|
|
2
3
|
import { parseFrontmatter } from "./frontmatter.ts";
|
|
3
4
|
|
|
4
5
|
function parseStepBody(agent: string, sectionBody: string): ChainStepConfig {
|
|
@@ -19,6 +20,10 @@ function parseStepBody(agent: string, sectionBody: string): ChainStepConfig {
|
|
|
19
20
|
else if (rawValue) step.output = rawValue;
|
|
20
21
|
continue;
|
|
21
22
|
}
|
|
23
|
+
if (key === "outputmode") {
|
|
24
|
+
if (rawValue === "inline" || rawValue === "file-only") step.outputMode = rawValue;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
22
27
|
if (key === "reads") {
|
|
23
28
|
if (rawValue === "false") {
|
|
24
29
|
step.reads = false;
|
|
@@ -75,14 +80,20 @@ export function parseChain(content: string, source: "user" | "project", filePath
|
|
|
75
80
|
steps.push(parseStepBody(agent, sectionBody));
|
|
76
81
|
}
|
|
77
82
|
|
|
83
|
+
const localName = frontmatter.name;
|
|
84
|
+
const parsedPackage = parsePackageName(frontmatter.package, `Chain '${localName}' package`);
|
|
85
|
+
if (parsedPackage.error) throw new Error(parsedPackage.error);
|
|
86
|
+
const packageName = parsedPackage.packageName;
|
|
78
87
|
const extraFields: Record<string, string> = {};
|
|
79
88
|
for (const [key, value] of Object.entries(frontmatter)) {
|
|
80
|
-
if (key === "name" || key === "description") continue;
|
|
89
|
+
if (key === "name" || key === "package" || key === "description") continue;
|
|
81
90
|
extraFields[key] = value;
|
|
82
91
|
}
|
|
83
92
|
|
|
84
93
|
return {
|
|
85
|
-
name:
|
|
94
|
+
name: buildRuntimeName(localName, packageName),
|
|
95
|
+
localName,
|
|
96
|
+
packageName,
|
|
86
97
|
description: frontmatter.description,
|
|
87
98
|
source,
|
|
88
99
|
filePath,
|
|
@@ -94,7 +105,8 @@ export function parseChain(content: string, source: "user" | "project", filePath
|
|
|
94
105
|
export function serializeChain(config: ChainConfig): string {
|
|
95
106
|
const lines: string[] = [];
|
|
96
107
|
lines.push("---");
|
|
97
|
-
lines.push(`name: ${config
|
|
108
|
+
lines.push(`name: ${frontmatterNameForConfig(config)}`);
|
|
109
|
+
if (config.packageName) lines.push(`package: ${config.packageName}`);
|
|
98
110
|
lines.push(`description: ${config.description}`);
|
|
99
111
|
if (config.extraFields) {
|
|
100
112
|
for (const [key, value] of Object.entries(config.extraFields)) {
|
|
@@ -109,6 +121,7 @@ export function serializeChain(config: ChainConfig): string {
|
|
|
109
121
|
lines.push(`## ${step.agent}`);
|
|
110
122
|
if (step.output === false) lines.push("output: false");
|
|
111
123
|
else if (step.output) lines.push(`output: ${step.output}`);
|
|
124
|
+
if (step.outputMode) lines.push(`outputMode: ${step.outputMode}`);
|
|
112
125
|
if (step.reads === false) lines.push("reads: false");
|
|
113
126
|
else if (Array.isArray(step.reads) && step.reads.length > 0) lines.push(`reads: ${step.reads.join(", ")}`);
|
|
114
127
|
if (step.model) lines.push(`model: ${step.model}`);
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { AgentConfig, ChainConfig } from "./agents.ts";
|
|
2
|
+
|
|
3
|
+
const IDENTIFIER_PATTERN = /^[a-z0-9][a-z0-9-]*(?:\.[a-z0-9][a-z0-9-]*)*$/;
|
|
4
|
+
|
|
5
|
+
function normalizePackageName(value: string | undefined): string | undefined {
|
|
6
|
+
const trimmed = value?.trim();
|
|
7
|
+
if (!trimmed) return undefined;
|
|
8
|
+
return trimmed.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9.-]/g, "").replace(/-+/g, "-").replace(/\.+/g, ".").replace(/(?:^[-.]+|[-.]+$)/g, "");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function parsePackageName(value: unknown, label = "package"): { packageName?: string; error?: string } {
|
|
12
|
+
if (value === undefined || value === false || value === "") return { packageName: undefined };
|
|
13
|
+
if (typeof value !== "string") return { error: `${label} must be a string or false when provided.` };
|
|
14
|
+
const packageName = normalizePackageName(value);
|
|
15
|
+
if (!packageName || !IDENTIFIER_PATTERN.test(packageName)) return { error: `${label} is invalid after sanitization.` };
|
|
16
|
+
return { packageName };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function buildRuntimeName(localName: string, packageName?: string): string {
|
|
20
|
+
const trimmedPackage = packageName?.trim();
|
|
21
|
+
return trimmedPackage ? `${trimmedPackage}.${localName}` : localName;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function frontmatterNameForConfig(config: Pick<AgentConfig | ChainConfig, "name" | "localName" | "packageName">): string {
|
|
25
|
+
if (config.localName) return config.localName;
|
|
26
|
+
if (config.packageName && config.name.startsWith(`${config.packageName}.`)) {
|
|
27
|
+
return config.name.slice(config.packageName.length + 1);
|
|
28
|
+
}
|
|
29
|
+
return config.name;
|
|
30
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { controlNotificationKey, formatControlNoticeMessage } from "../runs/shared/subagent-control.ts";
|
|
3
|
+
import type { ControlEvent, SubagentState } from "../shared/types.ts";
|
|
4
|
+
|
|
5
|
+
export const SUBAGENT_CONTROL_MESSAGE_TYPE = "subagent_control_notice";
|
|
6
|
+
|
|
7
|
+
export interface SubagentControlMessageDetails {
|
|
8
|
+
event: ControlEvent;
|
|
9
|
+
source?: "foreground" | "async";
|
|
10
|
+
asyncDir?: string;
|
|
11
|
+
childIntercomTarget?: string;
|
|
12
|
+
noticeText?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function controlNoticeTarget(details: SubagentControlMessageDetails): string | undefined {
|
|
16
|
+
return details.childIntercomTarget;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function formatSubagentControlNotice(details: SubagentControlMessageDetails, content?: string): string {
|
|
20
|
+
return details.noticeText ?? content ?? formatControlNoticeMessage(details.event, controlNoticeTarget(details));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function noticeTimerKey(details: SubagentControlMessageDetails): string {
|
|
24
|
+
const childIntercomTarget = controlNoticeTarget(details);
|
|
25
|
+
return `${details.event.runId}:${controlNotificationKey(details.event, childIntercomTarget)}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function clearPendingForegroundControlNotices(state: SubagentState, runId?: string): void {
|
|
29
|
+
const pending = state.pendingForegroundControlNotices;
|
|
30
|
+
if (!pending) return;
|
|
31
|
+
for (const [key, timer] of pending) {
|
|
32
|
+
if (runId !== undefined && !key.startsWith(`${runId}:`)) continue;
|
|
33
|
+
clearTimeout(timer);
|
|
34
|
+
pending.delete(key);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function deliverControlNotice(input: {
|
|
39
|
+
pi: Pick<ExtensionAPI, "sendMessage">;
|
|
40
|
+
visibleControlNotices: Set<string>;
|
|
41
|
+
details: SubagentControlMessageDetails;
|
|
42
|
+
}): void {
|
|
43
|
+
const childIntercomTarget = controlNoticeTarget(input.details);
|
|
44
|
+
const key = controlNotificationKey(input.details.event, childIntercomTarget);
|
|
45
|
+
if (input.visibleControlNotices.has(key)) return;
|
|
46
|
+
input.visibleControlNotices.add(key);
|
|
47
|
+
const noticeText = input.details.noticeText ?? formatControlNoticeMessage(input.details.event, childIntercomTarget);
|
|
48
|
+
input.pi.sendMessage(
|
|
49
|
+
{
|
|
50
|
+
customType: SUBAGENT_CONTROL_MESSAGE_TYPE,
|
|
51
|
+
content: noticeText,
|
|
52
|
+
display: true,
|
|
53
|
+
details: { ...input.details, childIntercomTarget, noticeText },
|
|
54
|
+
},
|
|
55
|
+
{ triggerTurn: input.details.source !== "foreground" },
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isForegroundNoticeStillActionable(state: SubagentState, details: SubagentControlMessageDetails): boolean {
|
|
60
|
+
const control = state.foregroundControls.get(details.event.runId);
|
|
61
|
+
if (!control) return false;
|
|
62
|
+
if (control.currentAgent && control.currentAgent !== details.event.agent) return false;
|
|
63
|
+
if (details.event.index !== undefined && control.currentIndex !== details.event.index) return false;
|
|
64
|
+
return control.currentActivityState === "needs_attention";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function handleSubagentControlNotice(input: {
|
|
68
|
+
pi: Pick<ExtensionAPI, "sendMessage">;
|
|
69
|
+
state: SubagentState;
|
|
70
|
+
visibleControlNotices: Set<string>;
|
|
71
|
+
details: SubagentControlMessageDetails;
|
|
72
|
+
foregroundDelayMs?: number;
|
|
73
|
+
}): void {
|
|
74
|
+
if (!input.details?.event || input.details.event.type === "active_long_running") return;
|
|
75
|
+
if (input.details.source !== "foreground") {
|
|
76
|
+
deliverControlNotice(input);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const pending = input.state.pendingForegroundControlNotices ?? new Map<string, ReturnType<typeof setTimeout>>();
|
|
81
|
+
input.state.pendingForegroundControlNotices = pending;
|
|
82
|
+
const timerKey = noticeTimerKey(input.details);
|
|
83
|
+
const existing = pending.get(timerKey);
|
|
84
|
+
if (existing) clearTimeout(existing);
|
|
85
|
+
const timer = setTimeout(() => {
|
|
86
|
+
pending.delete(timerKey);
|
|
87
|
+
if (!isForegroundNoticeStillActionable(input.state, input.details)) return;
|
|
88
|
+
deliverControlNotice(input);
|
|
89
|
+
}, input.foregroundDelayMs ?? 1000);
|
|
90
|
+
timer.unref?.();
|
|
91
|
+
pending.set(timerKey, timer);
|
|
92
|
+
}
|
package/src/extension/index.ts
CHANGED
|
@@ -25,7 +25,6 @@ import { renderWidget, renderSubagentResult, stopResultAnimations, stopWidgetAni
|
|
|
25
25
|
import { SubagentParams } from "./schemas.ts";
|
|
26
26
|
import { createSubagentExecutor } from "../runs/foreground/subagent-executor.ts";
|
|
27
27
|
import { createAsyncJobTracker } from "../runs/background/async-job-tracker.ts";
|
|
28
|
-
import { controlNotificationKey, formatControlNoticeMessage } from "../runs/shared/subagent-control.ts";
|
|
29
28
|
import { createResultWatcher } from "../runs/background/result-watcher.ts";
|
|
30
29
|
import { registerSlashCommands } from "../slash/slash-commands.ts";
|
|
31
30
|
import { registerPromptTemplateDelegationBridge } from "../slash/prompt-template-bridge.ts";
|
|
@@ -36,7 +35,6 @@ import registerSubagentNotify, { type SubagentNotifyDetails } from "../runs/back
|
|
|
36
35
|
import { SUBAGENT_CHILD_ENV } from "../runs/shared/pi-args.ts";
|
|
37
36
|
import { formatDuration, shortenPath } from "../shared/formatters.ts";
|
|
38
37
|
import {
|
|
39
|
-
type ControlEvent,
|
|
40
38
|
type Details,
|
|
41
39
|
type ExtensionConfig,
|
|
42
40
|
type SubagentState,
|
|
@@ -49,6 +47,13 @@ import {
|
|
|
49
47
|
SUBAGENT_CONTROL_EVENT,
|
|
50
48
|
WIDGET_KEY,
|
|
51
49
|
} from "../shared/types.ts";
|
|
50
|
+
import {
|
|
51
|
+
clearPendingForegroundControlNotices,
|
|
52
|
+
formatSubagentControlNotice,
|
|
53
|
+
handleSubagentControlNotice,
|
|
54
|
+
SUBAGENT_CONTROL_MESSAGE_TYPE,
|
|
55
|
+
type SubagentControlMessageDetails,
|
|
56
|
+
} from "./control-notices.ts";
|
|
52
57
|
|
|
53
58
|
/**
|
|
54
59
|
* Derive subagent session base directory from parent session file.
|
|
@@ -153,24 +158,6 @@ function createSlashResultComponent(
|
|
|
153
158
|
return container;
|
|
154
159
|
}
|
|
155
160
|
|
|
156
|
-
const SUBAGENT_CONTROL_MESSAGE_TYPE = "subagent_control_notice";
|
|
157
|
-
|
|
158
|
-
interface SubagentControlMessageDetails {
|
|
159
|
-
event: ControlEvent;
|
|
160
|
-
source?: "foreground" | "async";
|
|
161
|
-
asyncDir?: string;
|
|
162
|
-
childIntercomTarget?: string;
|
|
163
|
-
noticeText?: string;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function controlNoticeTarget(details: SubagentControlMessageDetails): string | undefined {
|
|
167
|
-
return details.childIntercomTarget;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function formatSubagentControlNotice(details: SubagentControlMessageDetails, content?: string): string {
|
|
171
|
-
return details.noticeText ?? content ?? formatControlNoticeMessage(details.event, controlNoticeTarget(details));
|
|
172
|
-
}
|
|
173
|
-
|
|
174
161
|
function parseSubagentNotifyContent(content: string): SubagentNotifyDetails | undefined {
|
|
175
162
|
const lines = content.split("\n");
|
|
176
163
|
const header = lines[0] ?? "";
|
|
@@ -259,6 +246,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
259
246
|
asyncJobs: new Map(),
|
|
260
247
|
foregroundControls: new Map(),
|
|
261
248
|
lastForegroundControlId: null,
|
|
249
|
+
pendingForegroundControlNotices: new Map(),
|
|
262
250
|
cleanupTimers: new Map(),
|
|
263
251
|
lastUiContext: null,
|
|
264
252
|
poller: null,
|
|
@@ -283,6 +271,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
283
271
|
const runtimeCleanup = () => {
|
|
284
272
|
stopWidgetAnimation();
|
|
285
273
|
stopResultAnimations();
|
|
274
|
+
clearPendingForegroundControlNotices(state);
|
|
286
275
|
if (state.poller) {
|
|
287
276
|
clearInterval(state.poller);
|
|
288
277
|
state.poller = null;
|
|
@@ -419,11 +408,11 @@ Example: { chain: [{agent:"agent-a", task:"Analyze {task}"}, {agent:"agent-b", t
|
|
|
419
408
|
|
|
420
409
|
MANAGEMENT (use action field, omit agent/task/chain/tasks):
|
|
421
410
|
• { action: "list" } - discover executable agents/chains
|
|
422
|
-
• { action: "get", agent: "name" } - full detail
|
|
423
|
-
• { action: "create", config: { name, systemPrompt, systemPromptMode, inheritProjectContext, inheritSkills, defaultContext, ... } }
|
|
424
|
-
• { action: "update", agent: "
|
|
425
|
-
• { action: "delete", agent: "
|
|
426
|
-
• Use chainName for chain operations
|
|
411
|
+
• { action: "get", agent: "name" } - full detail; packaged agents use dotted runtime names like "package.agent"
|
|
412
|
+
• { action: "create", config: { name: "custom-agent", package: "code-analysis", systemPrompt, systemPromptMode, inheritProjectContext, inheritSkills, defaultContext, ... } }
|
|
413
|
+
• { action: "update", agent: "code-analysis.custom-agent", config: { package: "analysis", ... } } - merge
|
|
414
|
+
• { action: "delete", agent: "code-analysis.custom-agent" }
|
|
415
|
+
• Use chainName for chain operations; packaged chains also use dotted runtime names
|
|
427
416
|
|
|
428
417
|
CONTROL:
|
|
429
418
|
• { action: "status", id: "..." } - inspect an async/background run by id or prefix
|
|
@@ -497,22 +486,12 @@ DIAGNOSTICS:
|
|
|
497
486
|
const visibleControlNotices = existingVisibleControlNotices instanceof Set ? existingVisibleControlNotices as Set<string> : new Set<string>();
|
|
498
487
|
globalStore[controlNoticeSeenStoreKey] = visibleControlNotices;
|
|
499
488
|
const controlEventHandler = (payload: unknown) => {
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
const noticeText = details.noticeText ?? formatControlNoticeMessage(details.event, childIntercomTarget);
|
|
507
|
-
pi.sendMessage(
|
|
508
|
-
{
|
|
509
|
-
customType: SUBAGENT_CONTROL_MESSAGE_TYPE,
|
|
510
|
-
content: noticeText,
|
|
511
|
-
display: true,
|
|
512
|
-
details: { ...details, childIntercomTarget, noticeText },
|
|
513
|
-
},
|
|
514
|
-
{ triggerTurn: true },
|
|
515
|
-
);
|
|
489
|
+
handleSubagentControlNotice({
|
|
490
|
+
pi,
|
|
491
|
+
state,
|
|
492
|
+
visibleControlNotices,
|
|
493
|
+
details: payload as SubagentControlMessageDetails,
|
|
494
|
+
});
|
|
516
495
|
};
|
|
517
496
|
const eventUnsubscribes = [
|
|
518
497
|
pi.events.on(SUBAGENT_ASYNC_STARTED_EVENT, handleStarted),
|
|
@@ -547,6 +526,7 @@ DIAGNOSTICS:
|
|
|
547
526
|
state.currentSessionId = ctx.sessionManager.getSessionFile() ?? `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
548
527
|
state.lastUiContext = ctx;
|
|
549
528
|
cleanupSessionArtifacts(ctx);
|
|
529
|
+
clearPendingForegroundControlNotices(state);
|
|
550
530
|
resetJobs(ctx);
|
|
551
531
|
restoreSlashFinalSnapshots(ctx.sessionManager.getEntries());
|
|
552
532
|
};
|
|
@@ -569,6 +549,7 @@ DIAGNOSTICS:
|
|
|
569
549
|
stopResultWatcher();
|
|
570
550
|
if (state.poller) clearInterval(state.poller);
|
|
571
551
|
state.poller = null;
|
|
552
|
+
clearPendingForegroundControlNotices(state);
|
|
572
553
|
for (const timer of state.cleanupTimers.values()) {
|
|
573
554
|
clearTimeout(timer);
|
|
574
555
|
}
|
package/src/extension/schemas.ts
CHANGED
|
@@ -22,6 +22,11 @@ const OutputOverride = Type.Unsafe({
|
|
|
22
22
|
description: "Output filename/path (string), or false to disable file output",
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
+
const OutputModeOverride = Type.String({
|
|
26
|
+
enum: ["inline", "file-only"],
|
|
27
|
+
description: "Return saved output inline (default) or only a concise file reference. file-only requires output to be a path.",
|
|
28
|
+
});
|
|
29
|
+
|
|
25
30
|
const ReadsOverride = Type.Unsafe({
|
|
26
31
|
anyOf: [
|
|
27
32
|
{ type: "array", items: { type: "string" } },
|
|
@@ -36,6 +41,7 @@ const TaskItem = Type.Object({
|
|
|
36
41
|
cwd: Type.Optional(Type.String()),
|
|
37
42
|
count: Type.Optional(Type.Integer({ minimum: 1, description: "Repeat this parallel task N times with the same settings." })),
|
|
38
43
|
output: Type.Optional(OutputOverride),
|
|
44
|
+
outputMode: Type.Optional(OutputModeOverride),
|
|
39
45
|
reads: Type.Optional(ReadsOverride),
|
|
40
46
|
progress: Type.Optional(Type.Boolean({ description: "Enable progress.md tracking for this task" })),
|
|
41
47
|
model: Type.Optional(Type.String({ description: "Override model for this task (e.g. 'google/gemini-3-pro')" })),
|
|
@@ -49,6 +55,7 @@ const ParallelTaskSchema = Type.Object({
|
|
|
49
55
|
cwd: Type.Optional(Type.String()),
|
|
50
56
|
count: Type.Optional(Type.Integer({ minimum: 1, description: "Repeat this parallel task N times with the same settings." })),
|
|
51
57
|
output: Type.Optional(OutputOverride),
|
|
58
|
+
outputMode: Type.Optional(OutputModeOverride),
|
|
52
59
|
reads: Type.Optional(ReadsOverride),
|
|
53
60
|
progress: Type.Optional(Type.Boolean({ description: "Enable progress.md tracking in {chain_dir}" })),
|
|
54
61
|
skill: Type.Optional(SkillOverride),
|
|
@@ -63,6 +70,7 @@ const ChainItem = Type.Object({
|
|
|
63
70
|
})),
|
|
64
71
|
cwd: Type.Optional(Type.String()),
|
|
65
72
|
output: Type.Optional(OutputOverride),
|
|
73
|
+
outputMode: Type.Optional(OutputModeOverride),
|
|
66
74
|
reads: Type.Optional(ReadsOverride),
|
|
67
75
|
progress: Type.Optional(Type.Boolean({ description: "Enable progress.md tracking in {chain_dir}" })),
|
|
68
76
|
skill: Type.Optional(SkillOverride),
|
|
@@ -119,9 +127,9 @@ export const SubagentParams = Type.Object({
|
|
|
119
127
|
{ type: "object", additionalProperties: true },
|
|
120
128
|
{ type: "string" },
|
|
121
129
|
],
|
|
122
|
-
description: "Agent or chain config for create/update. Agent: name, description, scope ('user'|'project', default 'user'), systemPrompt, systemPromptMode, inheritProjectContext, inheritSkills, defaultContext ('fresh'|'fork'), model, tools (comma-separated), extensions (comma-separated), skills (comma-separated), thinking, output, reads, progress, maxSubagentDepth. Chain: name, description, scope, steps (array of {agent, task?, output?, reads?, model?, skill?, progress?}). Presence of 'steps' creates a chain instead of an agent. String values must be valid JSON."
|
|
130
|
+
description: "Agent or chain config for create/update. Agent: name, package (optional namespace; runtime name becomes package.name), description, scope ('user'|'project', default 'user'), systemPrompt, systemPromptMode, inheritProjectContext, inheritSkills, defaultContext ('fresh'|'fork'), model, tools (comma-separated), extensions (comma-separated), skills (comma-separated), thinking, output, reads, progress, maxSubagentDepth. Chain: name, package, description, scope, steps (array of {agent, task?, output?, outputMode?, reads?, model?, skill?, progress?}). Presence of 'steps' creates a chain instead of an agent. String values must be valid JSON."
|
|
123
131
|
})),
|
|
124
|
-
tasks: Type.Optional(Type.Array(TaskItem, { description: "PARALLEL mode: [{agent, task, count?, output?, reads?, progress?}, ...]" })),
|
|
132
|
+
tasks: Type.Optional(Type.Array(TaskItem, { description: "PARALLEL mode: [{agent, task, count?, output?, outputMode?, reads?, progress?}, ...]" })),
|
|
125
133
|
concurrency: Type.Optional(Type.Integer({ minimum: 1, description: "Top-level PARALLEL mode only: max concurrent tasks. Defaults to config.parallel.concurrency or 4." })),
|
|
126
134
|
worktree: Type.Optional(Type.Boolean({
|
|
127
135
|
description: "Create isolated git worktrees for each parallel task. " +
|
|
@@ -154,6 +162,7 @@ export const SubagentParams = Type.Object({
|
|
|
154
162
|
],
|
|
155
163
|
description: "Output file for single agent (string), or false to disable. Relative paths resolve against cwd.",
|
|
156
164
|
})),
|
|
165
|
+
outputMode: Type.Optional(OutputModeOverride),
|
|
157
166
|
skill: Type.Optional(SkillOverride),
|
|
158
167
|
model: Type.Optional(Type.String({ description: "Override model for single agent (e.g. 'anthropic/claude-sonnet-4')" })),
|
|
159
168
|
});
|
|
@@ -50,6 +50,10 @@ function buildChainDetailLines(chain: ChainConfig, width: number): string[] {
|
|
|
50
50
|
const steps = chain.steps as DetailChainStep[];
|
|
51
51
|
const dependencyMap = buildDependencyMap(steps);
|
|
52
52
|
lines.push(truncateToWidth(chain.description, contentWidth));
|
|
53
|
+
if (chain.packageName) {
|
|
54
|
+
lines.push(truncateToWidth(`Local name: ${chain.localName ?? chain.name}`, contentWidth));
|
|
55
|
+
lines.push(truncateToWidth(`Package: ${chain.packageName}`, contentWidth));
|
|
56
|
+
}
|
|
53
57
|
lines.push("");
|
|
54
58
|
lines.push(truncateToWidth(`File: ${formatPath(chain.filePath)}`, contentWidth));
|
|
55
59
|
lines.push("");
|
|
@@ -61,6 +61,10 @@ function buildDetailLines(
|
|
|
61
61
|
const defaultContext = agent.defaultContext ?? "auto";
|
|
62
62
|
const maxSubagentDepth = agent.maxSubagentDepth !== undefined ? String(agent.maxSubagentDepth) : "(default)";
|
|
63
63
|
|
|
64
|
+
if (agent.packageName) {
|
|
65
|
+
lines.push(renderFieldLine("Local name:", agent.localName ?? agent.name, contentWidth, theme));
|
|
66
|
+
lines.push(renderFieldLine("Package:", agent.packageName, contentWidth, theme));
|
|
67
|
+
}
|
|
64
68
|
lines.push(renderFieldLine("Model:", agent.model ?? "default", contentWidth, theme));
|
|
65
69
|
lines.push(renderFieldLine("Prompt mode:", agent.systemPromptMode, contentWidth, theme));
|
|
66
70
|
lines.push(renderFieldLine("Project ctx:", agent.inheritProjectContext ? "on" : "off", contentWidth, theme));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
import { matchesKey, truncateToWidth } from "@mariozechner/pi-tui";
|
|
3
|
-
import { defaultSystemPromptMode, type AgentConfig, type AgentDefaultContext, type BuiltinAgentOverrideBase } from "../agents/agents.ts";
|
|
3
|
+
import { buildRuntimeName, defaultSystemPromptMode, frontmatterNameForConfig, parsePackageName, type AgentConfig, type AgentDefaultContext, type BuiltinAgentOverrideBase } from "../agents/agents.ts";
|
|
4
4
|
import { createEditorState, ensureCursorVisible, getCursorDisplayPos, handleEditorInput, renderEditor, wrapText } from "../tui/text-editor.ts";
|
|
5
5
|
import type { TextEditorState } from "../tui/text-editor.ts";
|
|
6
6
|
import { pad, row, renderHeader, renderFooter, formatScrollInfo } from "../tui/render-helpers.ts";
|
|
@@ -26,7 +26,7 @@ interface CreateEditStateOptions {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
|
|
29
|
-
const FIELD_ORDER = ["name", "description", "model", "fallbackModels", "thinking", "systemPromptMode", "inheritProjectContext", "inheritSkills", "defaultContext", "tools", "extensions", "skills", "output", "reads", "progress", "interactive", "prompt"] as const;
|
|
29
|
+
const FIELD_ORDER = ["name", "package", "description", "model", "fallbackModels", "thinking", "systemPromptMode", "inheritProjectContext", "inheritSkills", "defaultContext", "tools", "extensions", "skills", "output", "reads", "progress", "interactive", "prompt"] as const;
|
|
30
30
|
type ThinkingLevel = typeof THINKING_LEVELS[number];
|
|
31
31
|
const PROMPT_VIEWPORT_HEIGHT = 16;
|
|
32
32
|
const MODEL_SELECTOR_HEIGHT = 10;
|
|
@@ -94,12 +94,13 @@ export function createEditState(draft: AgentConfig, isNew: boolean, models: Mode
|
|
|
94
94
|
function renderFieldValue(field: EditField, state: EditState): string {
|
|
95
95
|
const draft = state.draft;
|
|
96
96
|
switch (field) {
|
|
97
|
-
case "name": return draft
|
|
97
|
+
case "name": return frontmatterNameForConfig(draft);
|
|
98
|
+
case "package": return draft.packageName ?? "";
|
|
98
99
|
case "description": return draft.description;
|
|
99
100
|
case "model": return draft.model ?? "default";
|
|
100
101
|
case "fallbackModels": return draft.fallbackModels && draft.fallbackModels.length > 0 ? draft.fallbackModels.join(", ") : "";
|
|
101
102
|
case "thinking": return draft.thinking ?? "off";
|
|
102
|
-
case "systemPromptMode": return draft.systemPromptMode ?? defaultSystemPromptMode(draft
|
|
103
|
+
case "systemPromptMode": return draft.systemPromptMode ?? defaultSystemPromptMode(frontmatterNameForConfig(draft));
|
|
103
104
|
case "inheritProjectContext": return draft.inheritProjectContext ? "on" : "off";
|
|
104
105
|
case "inheritSkills": return draft.inheritSkills ? "on" : "off";
|
|
105
106
|
case "defaultContext": return draft.defaultContext ?? "auto";
|
|
@@ -118,14 +119,32 @@ function renderFieldValue(field: EditField, state: EditState): string {
|
|
|
118
119
|
function applyFieldValue(field: EditField, state: EditState, value: string): void {
|
|
119
120
|
const draft = state.draft;
|
|
120
121
|
switch (field) {
|
|
121
|
-
case "name":
|
|
122
|
+
case "name": {
|
|
123
|
+
const localName = value.trim();
|
|
124
|
+
draft.localName = localName || undefined;
|
|
125
|
+
draft.name = localName ? buildRuntimeName(localName, draft.packageName) : "";
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
case "package": {
|
|
129
|
+
const parsed = parsePackageName(value, "package");
|
|
130
|
+
if (parsed.error) {
|
|
131
|
+
state.error = parsed.error;
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
const packageName = parsed.packageName;
|
|
135
|
+
draft.packageName = packageName;
|
|
136
|
+
const localName = frontmatterNameForConfig(draft).trim();
|
|
137
|
+
draft.name = localName ? buildRuntimeName(localName, packageName) : "";
|
|
138
|
+
state.error = undefined;
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
122
141
|
case "description": draft.description = value.trim(); break;
|
|
123
142
|
case "model": draft.model = value.trim() || undefined; break;
|
|
124
143
|
case "fallbackModels": draft.fallbackModels = parseCommaList(value); break;
|
|
125
144
|
case "systemPromptMode": {
|
|
126
145
|
const trimmed = value.trim();
|
|
127
146
|
if (trimmed === "") {
|
|
128
|
-
draft.systemPromptMode = defaultSystemPromptMode(draft
|
|
147
|
+
draft.systemPromptMode = defaultSystemPromptMode(frontmatterNameForConfig(draft));
|
|
129
148
|
break;
|
|
130
149
|
}
|
|
131
150
|
if (trimmed === "append" || trimmed === "replace") {
|
|
@@ -8,7 +8,9 @@ import {
|
|
|
8
8
|
defaultInheritProjectContext,
|
|
9
9
|
defaultInheritSkills,
|
|
10
10
|
defaultSystemPromptMode,
|
|
11
|
+
buildRuntimeName,
|
|
11
12
|
discoverAgentsAll,
|
|
13
|
+
frontmatterNameForConfig,
|
|
12
14
|
removeBuiltinAgentOverride,
|
|
13
15
|
saveBuiltinAgentOverride,
|
|
14
16
|
type AgentConfig,
|
|
@@ -238,10 +240,19 @@ export class AgentManagerComponent implements Component {
|
|
|
238
240
|
catch (err) { this.statusMessage = { text: err instanceof Error ? err.message : "Failed to load chain file.", type: "error" }; this.screen = "list"; }
|
|
239
241
|
}
|
|
240
242
|
|
|
243
|
+
private runtimeNameExistsInScope(kind: "agent" | "chain", scope: "user" | "project", name: string, excludePath?: string): boolean {
|
|
244
|
+
const discovered = discoverAgentsAll(this.agentData.cwd);
|
|
245
|
+
if (kind === "agent") {
|
|
246
|
+
const agents = scope === "user" ? discovered.user : discovered.project;
|
|
247
|
+
return agents.some((agent) => agent.name === name && agent.filePath !== excludePath);
|
|
248
|
+
}
|
|
249
|
+
return discovered.chains.some((chain) => chain.source === scope && chain.name === name && chain.filePath !== excludePath);
|
|
250
|
+
}
|
|
251
|
+
|
|
241
252
|
private enterNameInput(mode: NameInputState["mode"], sourceId?: string, template?: AgentTemplate): void {
|
|
242
253
|
const allowProject = Boolean(this.agentData.projectDir); let initial = ""; let scope: "user" | "project" = "user";
|
|
243
|
-
if (mode === "clone-agent" && sourceId) { const entry = this.getAgentEntry(sourceId); if (entry) { initial = `${entry.config
|
|
244
|
-
if (mode === "clone-chain" && sourceId) { const entry = this.getChainEntry(sourceId); if (entry) { initial = `${entry.config
|
|
254
|
+
if (mode === "clone-agent" && sourceId) { const entry = this.getAgentEntry(sourceId); if (entry) { initial = `${frontmatterNameForConfig(entry.config)}-copy`; scope = entry.config.source === "project" ? "project" : "user"; } }
|
|
255
|
+
if (mode === "clone-chain" && sourceId) { const entry = this.getChainEntry(sourceId); if (entry) { initial = `${frontmatterNameForConfig(entry.config)}-copy`; scope = entry.config.source === "project" ? "project" : "user"; } }
|
|
245
256
|
if (mode === "new-agent" && template && template.name !== "Blank") initial = slugTemplateName(template.name);
|
|
246
257
|
this.nameInputState = { mode, editor: createEditorState(initial), scope, allowProject, sourceId, template }; this.screen = "name-input";
|
|
247
258
|
}
|
|
@@ -269,7 +280,16 @@ export class AgentManagerComponent implements Component {
|
|
|
269
280
|
return false;
|
|
270
281
|
}
|
|
271
282
|
}
|
|
272
|
-
|
|
283
|
+
let localName = frontmatterNameForConfig(edit.draft).trim();
|
|
284
|
+
if (!edit.draft.packageName && edit.draft.name !== entry.config.name) localName = edit.draft.name.trim();
|
|
285
|
+
if (!localName || !edit.draft.description) { edit.error = "Name and description are required."; return false; }
|
|
286
|
+
edit.draft.localName = localName;
|
|
287
|
+
edit.draft.name = buildRuntimeName(localName, edit.draft.packageName);
|
|
288
|
+
const draftScope = edit.draft.source === "project" ? "project" : "user";
|
|
289
|
+
if (this.runtimeNameExistsInScope("agent", draftScope, edit.draft.name, entry.config.filePath)) {
|
|
290
|
+
edit.error = `An agent named '${edit.draft.name}' already exists in ${draftScope} scope.`;
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
273
293
|
let filePath = entry.config.filePath;
|
|
274
294
|
if (entry.isNew) {
|
|
275
295
|
const dir = edit.draft.source === "project" ? this.agentData.projectDir : this.agentData.userDir;
|
|
@@ -404,15 +424,15 @@ export class AgentManagerComponent implements Component {
|
|
|
404
424
|
const sourceEntry = this.getChainEntry(state.sourceId); if (!sourceEntry) { this.screen = "list"; this.tui.requestRender(); return; }
|
|
405
425
|
const dir = state.scope === "project" ? this.agentData.projectDir : this.agentData.userDir;
|
|
406
426
|
if (!dir) { state.error = "Project agents directory not found."; this.tui.requestRender(); return; }
|
|
407
|
-
const filePath = path.join(dir, `${name}.chain.md`); if (fs.existsSync(filePath)) { state.error = "A chain with that name already exists."; this.tui.requestRender(); return; }
|
|
408
|
-
try { const cloned = cloneChainConfig({ ...sourceEntry.config, name, source: state.scope, filePath }); fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(filePath, serializeChain(cloned), "utf-8"); const added: ChainEntry = { id: `c${this.nextId++}`, kind: "chain", config: cloned }; this.chains.push(added); this.nameInputState = null; this.enterChainDetail(added); this.tui.requestRender(); return; }
|
|
427
|
+
const filePath = path.join(dir, `${name}.chain.md`); if (fs.existsSync(filePath) || this.runtimeNameExistsInScope("chain", state.scope, name)) { state.error = "A chain with that name already exists."; this.tui.requestRender(); return; }
|
|
428
|
+
try { const cloned = cloneChainConfig({ ...sourceEntry.config, name, localName: name, packageName: undefined, source: state.scope, filePath }); fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(filePath, serializeChain(cloned), "utf-8"); const added: ChainEntry = { id: `c${this.nextId++}`, kind: "chain", config: cloned }; this.chains.push(added); this.nameInputState = null; this.enterChainDetail(added); this.tui.requestRender(); return; }
|
|
409
429
|
catch (err) { state.error = err instanceof Error ? err.message : "Failed to clone chain."; this.tui.requestRender(); return; }
|
|
410
430
|
}
|
|
411
431
|
if (state.mode === "new-chain") {
|
|
412
432
|
const dir = state.scope === "project" ? this.agentData.projectDir : this.agentData.userDir;
|
|
413
433
|
if (!dir) { state.error = "Directory not found."; this.tui.requestRender(); return; }
|
|
414
|
-
const filePath = path.join(dir, `${name}.chain.md`); if (fs.existsSync(filePath)) { state.error = "A chain with that name already exists."; this.tui.requestRender(); return; }
|
|
415
|
-
const config: ChainConfig = { name, description: "Describe this chain", source: state.scope, filePath, steps: [{ agent: "agent-name", task: "{task}" }] };
|
|
434
|
+
const filePath = path.join(dir, `${name}.chain.md`); if (fs.existsSync(filePath) || this.runtimeNameExistsInScope("chain", state.scope, name)) { state.error = "A chain with that name already exists."; this.tui.requestRender(); return; }
|
|
435
|
+
const config: ChainConfig = { name, localName: name, description: "Describe this chain", source: state.scope, filePath, steps: [{ agent: "agent-name", task: "{task}" }] };
|
|
416
436
|
try { fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(filePath, serializeChain(config), "utf-8"); const entry: ChainEntry = { id: `c${this.nextId++}`, kind: "chain", config }; this.chains.push(entry); this.nameInputState = null; this.enterChainEdit(entry); }
|
|
417
437
|
catch (err) { state.error = err instanceof Error ? err.message : "Failed to create chain."; }
|
|
418
438
|
this.tui.requestRender(); return;
|
|
@@ -438,7 +458,7 @@ export class AgentManagerComponent implements Component {
|
|
|
438
458
|
}
|
|
439
459
|
const dir = state.scope === "project" ? this.agentData.projectDir : this.agentData.userDir;
|
|
440
460
|
if (!dir) { state.error = "Project agents directory not found."; this.tui.requestRender(); return; }
|
|
441
|
-
const filePath = path.join(dir, `${name}.md`); const config: AgentConfig = { ...baseConfig, name, source: state.scope, filePath };
|
|
461
|
+
const filePath = path.join(dir, `${name}.md`); const config: AgentConfig = { ...baseConfig, name, localName: name, packageName: undefined, source: state.scope, filePath };
|
|
442
462
|
const entry: AgentEntry = { id: `a${this.nextId++}`, kind: "agent", config, isNew: true };
|
|
443
463
|
this.agents.push(entry); this.nameInputState = null; this.enterEdit(entry); this.tui.requestRender();
|
|
444
464
|
}
|
|
@@ -11,7 +11,7 @@ import { createRequire } from "node:module";
|
|
|
11
11
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
12
12
|
import type { AgentConfig } from "../../agents/agents.ts";
|
|
13
13
|
import { applyThinkingSuffix } from "../shared/pi-args.ts";
|
|
14
|
-
import { injectSingleOutputInstruction, resolveSingleOutputPath } from "../shared/single-output.ts";
|
|
14
|
+
import { injectSingleOutputInstruction, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
|
|
15
15
|
import { buildChainInstructions, isParallelStep, resolveStepBehavior, writeInitialProgressFile, type ChainStep, type ResolvedStepBehavior, type SequentialStep, type StepOverrides } from "../../shared/settings.ts";
|
|
16
16
|
import type { RunnerStep } from "../shared/parallel-utils.ts";
|
|
17
17
|
import { resolvePiPackageRoot } from "../shared/pi-spawn.ts";
|
|
@@ -98,6 +98,7 @@ interface AsyncSingleParams {
|
|
|
98
98
|
sessionFile?: string;
|
|
99
99
|
skills?: string[];
|
|
100
100
|
output?: string | false;
|
|
101
|
+
outputMode?: "inline" | "file-only";
|
|
101
102
|
modelOverride?: string;
|
|
102
103
|
availableModels?: AvailableModelInfo[];
|
|
103
104
|
maxSubagentDepth: number;
|
|
@@ -170,6 +171,7 @@ function formatAsyncStartError(mode: "single" | "chain", message: string): Async
|
|
|
170
171
|
const UNAVAILABLE_SUBAGENT_SKILL_ERROR = "Skills not found: pi-subagents";
|
|
171
172
|
|
|
172
173
|
class UnavailableSubagentSkillError extends Error {}
|
|
174
|
+
class AsyncStartValidationError extends Error {}
|
|
173
175
|
|
|
174
176
|
/**
|
|
175
177
|
* Execute a chain asynchronously
|
|
@@ -232,6 +234,7 @@ export function executeAsyncChain(
|
|
|
232
234
|
const stepSkillInput = normalizeSkillInput(s.skill);
|
|
233
235
|
return {
|
|
234
236
|
...(s.output !== undefined ? { output: s.output } : {}),
|
|
237
|
+
...(s.outputMode !== undefined ? { outputMode: s.outputMode } : {}),
|
|
235
238
|
...(s.reads !== undefined ? { reads: s.reads } : {}),
|
|
236
239
|
...(s.progress !== undefined ? { progress: s.progress } : {}),
|
|
237
240
|
...(stepSkillInput !== undefined ? { skills: stepSkillInput } : {}),
|
|
@@ -258,6 +261,8 @@ export function executeAsyncChain(
|
|
|
258
261
|
if (behavior.progress) progressInstructionCreated = true;
|
|
259
262
|
const progressInstructions = buildChainInstructions({ ...behavior, output: false, reads: false }, runnerCwd, isFirstProgressAgent);
|
|
260
263
|
const outputPath = resolveSingleOutputPath(behavior.output, ctx.cwd, instructionCwd);
|
|
264
|
+
const validationError = validateFileOnlyOutputMode(behavior.outputMode, outputPath, `Async step (${s.agent})`);
|
|
265
|
+
if (validationError) throw new AsyncStartValidationError(validationError);
|
|
261
266
|
const task = injectSingleOutputInstruction(`${readInstructions.prefix}${s.task ?? "{previous}"}${progressInstructions.suffix}`, outputPath);
|
|
262
267
|
|
|
263
268
|
const primaryModel = resolveModelCandidate(behavior.model ?? a.model, availableModels, ctx.currentModelProvider);
|
|
@@ -278,6 +283,7 @@ export function executeAsyncChain(
|
|
|
278
283
|
inheritSkills: a.inheritSkills,
|
|
279
284
|
skills: resolvedSkills.map((r) => r.name),
|
|
280
285
|
outputPath,
|
|
286
|
+
outputMode: behavior.outputMode,
|
|
281
287
|
sessionFile,
|
|
282
288
|
maxSubagentDepth: resolveChildMaxSubagentDepth(maxSubagentDepth, a.maxSubagentDepth),
|
|
283
289
|
};
|
|
@@ -323,7 +329,7 @@ export function executeAsyncChain(
|
|
|
323
329
|
return buildSeqStep(s as SequentialStep, nextSessionFile());
|
|
324
330
|
});
|
|
325
331
|
} catch (error) {
|
|
326
|
-
if (error instanceof UnavailableSubagentSkillError) return formatAsyncStartError("chain", error.message);
|
|
332
|
+
if (error instanceof UnavailableSubagentSkillError || error instanceof AsyncStartValidationError) return formatAsyncStartError("chain", error.message);
|
|
327
333
|
throw error;
|
|
328
334
|
}
|
|
329
335
|
let childTargetIndex = 0;
|
|
@@ -470,6 +476,9 @@ export function executeAsyncSingle(
|
|
|
470
476
|
}
|
|
471
477
|
|
|
472
478
|
const outputPath = resolveSingleOutputPath(params.output, ctx.cwd, runnerCwd);
|
|
479
|
+
const outputMode = params.outputMode ?? "inline";
|
|
480
|
+
const validationError = validateFileOnlyOutputMode(outputMode, outputPath, `Async single run (${agent})`);
|
|
481
|
+
if (validationError) return formatAsyncStartError("single", validationError);
|
|
473
482
|
const taskWithOutputInstruction = injectSingleOutputInstruction(task, outputPath);
|
|
474
483
|
let spawnResult: { pid?: number; error?: string } = {};
|
|
475
484
|
try {
|
|
@@ -494,6 +503,7 @@ export function executeAsyncSingle(
|
|
|
494
503
|
inheritSkills: agentConfig.inheritSkills,
|
|
495
504
|
skills: resolvedSkills.map((r) => r.name),
|
|
496
505
|
outputPath,
|
|
506
|
+
outputMode,
|
|
497
507
|
sessionFile,
|
|
498
508
|
maxSubagentDepth: resolveChildMaxSubagentDepth(maxSubagentDepth, agentConfig.maxSubagentDepth),
|
|
499
509
|
},
|