pi-subagents 0.34.0 → 0.35.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/CHANGELOG.md +78 -9
- package/README.md +213 -32
- package/index.ts +1 -0
- package/install.mjs +1 -1
- package/package.json +23 -8
- package/prompts/review-loop.md +3 -1
- package/skills/pi-subagents/SKILL.md +87 -25
- package/src/agents/agent-management.ts +82 -15
- package/src/agents/agent-serializer.ts +19 -0
- package/src/agents/agents.ts +91 -49
- package/src/agents/frontmatter.ts +67 -13
- package/src/agents/skills.ts +25 -12
- package/src/api/background-work.ts +197 -0
- package/src/api/delegation.ts +158 -0
- package/src/extension/chain-validation.ts +165 -0
- package/src/extension/doctor.ts +15 -0
- package/src/extension/fanout-child.ts +3 -1
- package/src/extension/index.ts +65 -124
- package/src/extension/rpc.ts +10 -2
- package/src/extension/schemas.ts +18 -14
- package/src/extension/steering-notices.ts +35 -0
- package/src/extension/tool-description.ts +20 -9
- package/src/intercom/intercom-bridge.ts +3 -2
- package/src/intercom/native-supervisor-channel.ts +9 -1
- package/src/intercom/result-intercom.ts +4 -0
- package/src/runs/background/async-execution.ts +293 -44
- package/src/runs/background/async-job-tracker.ts +56 -9
- package/src/runs/background/async-resume.ts +159 -52
- package/src/runs/background/async-status.ts +25 -18
- package/src/runs/background/auto-drain.ts +67 -0
- package/src/runs/background/chain-root-attachment.ts +16 -8
- package/src/runs/background/control-channel.ts +260 -13
- package/src/runs/background/fleet-view.ts +23 -2
- package/src/runs/background/notify.ts +79 -10
- package/src/runs/background/result-watcher.ts +12 -9
- package/src/runs/background/run-id-resolver.ts +14 -2
- package/src/runs/background/run-status.ts +23 -15
- package/src/runs/background/scheduled-runs.ts +3 -0
- package/src/runs/background/stale-run-reconciler.ts +32 -10
- package/src/runs/background/steering.ts +237 -0
- package/src/runs/background/subagent-runner.ts +898 -236
- package/src/runs/background/subagent-wait.ts +484 -0
- package/src/runs/background/top-level-async.ts +2 -1
- package/src/runs/background/wait-config.ts +36 -0
- package/src/runs/background/wait-tool.ts +26 -0
- package/src/runs/foreground/async-steering-action.ts +230 -0
- package/src/runs/foreground/chain-clarify.ts +22 -6
- package/src/runs/foreground/chain-execution.ts +50 -32
- package/src/runs/foreground/execution.ts +308 -94
- package/src/runs/foreground/subagent-executor.ts +592 -268
- package/src/runs/shared/acceptance.ts +355 -97
- package/src/runs/shared/child-protocol.ts +121 -0
- package/src/runs/shared/completion-guard.ts +8 -127
- package/src/runs/shared/dynamic-fanout.ts +6 -4
- package/src/runs/shared/model-fallback.ts +36 -0
- package/src/runs/shared/nested-events.ts +9 -4
- package/src/runs/shared/nested-render.ts +4 -1
- package/src/runs/shared/parallel-utils.ts +7 -0
- package/src/runs/shared/pi-args.ts +34 -7
- package/src/runs/shared/pi-spawn.ts +18 -12
- package/src/runs/shared/session-lease.ts +279 -0
- package/src/runs/shared/single-output.ts +61 -6
- package/src/runs/shared/spawn-budget.ts +128 -0
- package/src/runs/shared/subagent-control.ts +10 -6
- package/src/runs/shared/subagent-prompt-runtime.ts +127 -26
- package/src/runs/shared/task-intent.ts +176 -0
- package/src/runs/shared/tool-availability.ts +65 -0
- package/src/runs/shared/turn-budget.ts +49 -4
- package/src/shared/atomic-json.ts +4 -1
- package/src/shared/fork-context.ts +28 -3
- package/src/shared/model-info.ts +7 -4
- package/src/shared/status-format.ts +7 -1
- package/src/shared/types.ts +203 -25
- package/src/shared/utils.ts +35 -7
- package/src/slash/delegation-adapters.ts +457 -0
- package/src/slash/delegation-request.ts +103 -0
- package/src/slash/prompt-template-bridge.ts +167 -344
- package/src/slash/slash-commands.ts +239 -6
- package/src/slash/subagents-admin.ts +428 -0
- package/src/slash/subagents-editor.ts +86 -0
- package/src/tui/fleet.ts +405 -0
- package/src/tui/render.ts +90 -16
- package/src/watchdog/change-signature.ts +127 -0
- package/src/watchdog/child-status.ts +205 -0
- package/src/watchdog/emission-guard.ts +123 -0
- package/src/watchdog/lsp-diagnostics.ts +532 -0
- package/src/watchdog/model-selection.ts +167 -0
- package/src/watchdog/register-child.ts +117 -0
- package/src/watchdog/register-main.ts +433 -0
- package/src/watchdog/render.ts +54 -0
- package/src/watchdog/review.ts +293 -0
- package/src/watchdog/runtime.ts +712 -0
- package/src/watchdog/settings.ts +528 -0
- package/src/watchdog/tool-actions.ts +155 -0
- package/src/watchdog/turn-delta.ts +161 -0
- package/src/watchdog/types.ts +188 -0
- package/src/watchdog/warning-format.ts +73 -0
- package/src/runs/background/wait.ts +0 -394
|
@@ -7,11 +7,13 @@ import {
|
|
|
7
7
|
type AsyncJobState,
|
|
8
8
|
type AsyncStartedEvent,
|
|
9
9
|
type ControlEvent,
|
|
10
|
+
type SteeringNotice,
|
|
10
11
|
type SubagentState,
|
|
11
12
|
POLL_INTERVAL_MS,
|
|
12
13
|
RESULTS_DIR,
|
|
13
14
|
SUBAGENT_CONTROL_EVENT,
|
|
14
15
|
SUBAGENT_CONTROL_INTERCOM_EVENT,
|
|
16
|
+
SUBAGENT_STEERING_NOTICE_EVENT,
|
|
15
17
|
} from "../../shared/types.ts";
|
|
16
18
|
import { readStatus } from "../../shared/utils.ts";
|
|
17
19
|
import { normalizeParallelGroups } from "./parallel-groups.ts";
|
|
@@ -23,6 +25,7 @@ interface AsyncJobTrackerOptions {
|
|
|
23
25
|
completionRetentionMs?: number;
|
|
24
26
|
pollIntervalMs?: number;
|
|
25
27
|
resultsDir?: string;
|
|
28
|
+
widgetEnabled?: boolean;
|
|
26
29
|
kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean;
|
|
27
30
|
now?: () => number;
|
|
28
31
|
}
|
|
@@ -30,9 +33,20 @@ interface AsyncJobTrackerOptions {
|
|
|
30
33
|
const CONTROL_EVENT_READ_CHUNK_BYTES = 64 * 1024;
|
|
31
34
|
const MAX_CONTROL_EVENT_LINE_BYTES = 1024 * 1024;
|
|
32
35
|
const CONTROL_EVENT_SCAN_WINDOW_BYTES = 2 * 1024 * 1024;
|
|
36
|
+
const MAX_RECENT_FLEET_JOBS = 20;
|
|
37
|
+
|
|
38
|
+
function rememberFleetJob(state: SubagentState, job: AsyncJobState): void {
|
|
39
|
+
state.fleetJobs ??= new Map();
|
|
40
|
+
state.fleetJobs.set(job.asyncId, job);
|
|
41
|
+
const terminal = [...state.fleetJobs.values()]
|
|
42
|
+
.filter((candidate) => candidate.status === "complete" || candidate.status === "failed" || candidate.status === "paused" || candidate.status === "stopped")
|
|
43
|
+
.sort((left, right) => (right.updatedAt ?? right.startedAt ?? 0) - (left.updatedAt ?? left.startedAt ?? 0));
|
|
44
|
+
for (const stale of terminal.slice(MAX_RECENT_FLEET_JOBS)) state.fleetJobs.delete(stale.asyncId);
|
|
45
|
+
}
|
|
33
46
|
|
|
34
47
|
export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: SubagentState, asyncDirRoot: string, options: AsyncJobTrackerOptions = {}): {
|
|
35
48
|
ensurePoller: () => void;
|
|
49
|
+
refreshWidget: (ctx: ExtensionContext) => void;
|
|
36
50
|
handleStarted: (data: unknown) => void;
|
|
37
51
|
handleComplete: (data: unknown) => void;
|
|
38
52
|
resetJobs: (ctx?: ExtensionContext) => void;
|
|
@@ -41,10 +55,12 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
41
55
|
const completionRetentionMs = options.completionRetentionMs ?? 10000;
|
|
42
56
|
const pollIntervalMs = options.pollIntervalMs ?? POLL_INTERVAL_MS;
|
|
43
57
|
const resultsDir = options.resultsDir ?? RESULTS_DIR;
|
|
58
|
+
const steeringNoticeSeen = new Map<string, number>();
|
|
44
59
|
const rerenderWidget = (ctx: ExtensionContext, jobs = Array.from(state.asyncJobs.values())) => {
|
|
45
|
-
renderWidget(ctx, jobs);
|
|
60
|
+
renderWidget(ctx, options.widgetEnabled === false ? [] : jobs);
|
|
46
61
|
ctx.ui.requestRender?.();
|
|
47
62
|
};
|
|
63
|
+
const refreshWidget = (ctx: ExtensionContext) => rerenderWidget(ctx);
|
|
48
64
|
const restoredControlEventCursor = (asyncDir: string) => {
|
|
49
65
|
try {
|
|
50
66
|
return fs.statSync(path.join(asyncDir, "events.jsonl")).size;
|
|
@@ -73,6 +89,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
73
89
|
currentPath: run.currentPath,
|
|
74
90
|
turnCount: run.turnCount,
|
|
75
91
|
toolCount: run.toolCount,
|
|
92
|
+
steering: run.steering,
|
|
76
93
|
mode: run.mode,
|
|
77
94
|
agents: visibleSteps.map((step) => step.agent),
|
|
78
95
|
currentStep: run.currentStep,
|
|
@@ -89,6 +106,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
89
106
|
timeoutMs: run.timeoutMs,
|
|
90
107
|
deadlineAt: run.deadlineAt,
|
|
91
108
|
timedOut: run.timedOut,
|
|
109
|
+
stopped: run.stopped,
|
|
92
110
|
turnBudget: run.turnBudget,
|
|
93
111
|
turnBudgetExceeded: run.turnBudgetExceeded,
|
|
94
112
|
wrapUpRequested: run.wrapUpRequested,
|
|
@@ -144,7 +162,24 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
144
162
|
console.error(`Ignoring malformed async control event in '${eventsPath}':`, error);
|
|
145
163
|
return;
|
|
146
164
|
}
|
|
147
|
-
if (!parsed || typeof parsed !== "object"
|
|
165
|
+
if (!parsed || typeof parsed !== "object") return;
|
|
166
|
+
if ((parsed as { type?: unknown }).type === "subagent.steering.notice") {
|
|
167
|
+
const notice = parsed as Partial<SteeringNotice>;
|
|
168
|
+
if (typeof notice.requestId !== "string" || typeof notice.runId !== "string" || (notice.state !== "failed" && notice.state !== "partial" && notice.state !== "recovered") || typeof notice.message !== "string") return;
|
|
169
|
+
if (typeof state.currentSessionId === "string" && notice.currentSessionId !== state.currentSessionId) return;
|
|
170
|
+
const key = `${notice.runId}:${notice.requestId}:${notice.state}`;
|
|
171
|
+
if (steeringNoticeSeen.has(key)) return;
|
|
172
|
+
const now = Date.now();
|
|
173
|
+
steeringNoticeSeen.set(key, now);
|
|
174
|
+
if (steeringNoticeSeen.size > 200) {
|
|
175
|
+
for (const [seenKey, seenAt] of steeringNoticeSeen) {
|
|
176
|
+
if (now - seenAt > 10 * 60 * 1000 || steeringNoticeSeen.size > 200) steeringNoticeSeen.delete(seenKey);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
pi.events.emit(SUBAGENT_STEERING_NOTICE_EVENT, { ...notice, source: "async", asyncDir: job.asyncDir, noticeText: notice.message });
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if ((parsed as { type?: unknown }).type !== "subagent.control") return;
|
|
148
183
|
const record = parsed as { event?: ControlEvent; channels?: string[]; childIntercomTarget?: string; noticeText?: string; intercom?: { to?: string; message?: string } };
|
|
149
184
|
if (!record.event || !Array.isArray(record.channels)) return;
|
|
150
185
|
const payload = {
|
|
@@ -269,7 +304,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
269
304
|
if (status) {
|
|
270
305
|
const previousStatus = job.status;
|
|
271
306
|
job.status = status.state;
|
|
272
|
-
if (job.status !== "complete" && job.status !== "failed" && job.status !== "paused") cancelCleanup(job.asyncId);
|
|
307
|
+
if (job.status !== "complete" && job.status !== "failed" && job.status !== "paused" && job.status !== "stopped") cancelCleanup(job.asyncId);
|
|
273
308
|
job.sessionId = status.sessionId ?? job.sessionId;
|
|
274
309
|
job.activityState = status.activityState;
|
|
275
310
|
job.lastActivityAt = status.lastActivityAt ?? job.lastActivityAt;
|
|
@@ -278,6 +313,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
278
313
|
job.currentPath = status.currentPath;
|
|
279
314
|
job.turnCount = status.turnCount ?? job.turnCount;
|
|
280
315
|
job.toolCount = status.toolCount ?? job.toolCount;
|
|
316
|
+
job.steering = status.steering ?? job.steering;
|
|
281
317
|
job.mode = status.mode;
|
|
282
318
|
job.currentStep = status.currentStep ?? job.currentStep;
|
|
283
319
|
job.chainStepCount = status.chainStepCount ?? job.chainStepCount;
|
|
@@ -308,12 +344,16 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
308
344
|
job.timeoutMs = status.timeoutMs ?? job.timeoutMs;
|
|
309
345
|
job.deadlineAt = status.deadlineAt ?? job.deadlineAt;
|
|
310
346
|
job.timedOut = status.timedOut ?? job.timedOut;
|
|
347
|
+
job.stopped = status.stopped ?? job.stopped;
|
|
311
348
|
job.turnBudget = status.turnBudget ?? job.turnBudget;
|
|
312
349
|
job.turnBudgetExceeded = status.turnBudgetExceeded ?? job.turnBudgetExceeded;
|
|
313
350
|
job.wrapUpRequested = status.wrapUpRequested ?? job.wrapUpRequested;
|
|
314
351
|
job.sessionFile = status.sessionFile ?? job.sessionFile;
|
|
315
|
-
if (
|
|
316
|
-
|
|
352
|
+
if (job.status === "complete" || job.status === "failed" || job.status === "paused" || job.status === "stopped") {
|
|
353
|
+
rememberFleetJob(state, job);
|
|
354
|
+
if (!nestedRefreshFailed && !hasLiveNestedDescendants(job.nestedChildren) && (previousStatus !== job.status || !state.cleanupTimers.has(job.asyncId))) {
|
|
355
|
+
scheduleCleanup(job.asyncId);
|
|
356
|
+
}
|
|
317
357
|
}
|
|
318
358
|
if (widgetRenderKey(job) !== widgetStateBefore) widgetChanged = true;
|
|
319
359
|
continue;
|
|
@@ -328,6 +368,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
328
368
|
job.status = "failed";
|
|
329
369
|
job.updatedAt = Date.now();
|
|
330
370
|
}
|
|
371
|
+
rememberFleetJob(state, job);
|
|
331
372
|
if (!hasLiveNestedDescendants(job.nestedChildren) && !state.cleanupTimers.has(job.asyncId)) {
|
|
332
373
|
scheduleCleanup(job.asyncId);
|
|
333
374
|
}
|
|
@@ -374,6 +415,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
374
415
|
turnBudget: info.turnBudget,
|
|
375
416
|
controlEventCursor: 0,
|
|
376
417
|
});
|
|
418
|
+
rememberFleetJob(state, state.asyncJobs.get(info.id)!);
|
|
377
419
|
ensurePoller();
|
|
378
420
|
if (state.lastUiContext) {
|
|
379
421
|
rerenderWidget(state.lastUiContext);
|
|
@@ -381,14 +423,15 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
381
423
|
};
|
|
382
424
|
|
|
383
425
|
const handleComplete = (data: unknown) => {
|
|
384
|
-
const result = data as { id?: string; success?: boolean; asyncDir?: string; sessionId?: string };
|
|
426
|
+
const result = data as { id?: string; success?: boolean; state?: AsyncJobState["status"]; asyncDir?: string; sessionId?: string; stopped?: boolean };
|
|
385
427
|
if (typeof state.currentSessionId === "string" && result.sessionId !== state.currentSessionId) return;
|
|
386
428
|
const asyncId = result.id;
|
|
387
429
|
if (!asyncId) return;
|
|
388
430
|
const job = state.asyncJobs.get(asyncId);
|
|
389
431
|
let nestedRefreshFailed = false;
|
|
390
432
|
if (job) {
|
|
391
|
-
job.status = result.success ? "complete" : "failed";
|
|
433
|
+
job.status = result.state ?? (result.success ? "complete" : "failed");
|
|
434
|
+
job.stopped = result.stopped ?? job.stopped;
|
|
392
435
|
job.updatedAt = Date.now();
|
|
393
436
|
if (result.asyncDir) job.asyncDir = result.asyncDir;
|
|
394
437
|
try {
|
|
@@ -398,6 +441,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
398
441
|
console.error(`Failed to refresh nested async descendants for '${job.asyncDir}':`, error);
|
|
399
442
|
}
|
|
400
443
|
}
|
|
444
|
+
if (job) rememberFleetJob(state, job);
|
|
401
445
|
if (state.lastUiContext) {
|
|
402
446
|
rerenderWidget(state.lastUiContext);
|
|
403
447
|
}
|
|
@@ -410,6 +454,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
410
454
|
}
|
|
411
455
|
state.cleanupTimers.clear();
|
|
412
456
|
state.asyncJobs.clear();
|
|
457
|
+
state.fleetJobs?.clear();
|
|
413
458
|
state.foregroundControls?.clear();
|
|
414
459
|
state.lastForegroundControlId = null;
|
|
415
460
|
state.resultFileCoalescer.clear();
|
|
@@ -430,12 +475,14 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
430
475
|
return;
|
|
431
476
|
}
|
|
432
477
|
for (const run of runs) {
|
|
433
|
-
|
|
478
|
+
const job = summaryToJob(run);
|
|
479
|
+
state.asyncJobs.set(run.id, job);
|
|
480
|
+
rememberFleetJob(state, job);
|
|
434
481
|
}
|
|
435
482
|
if (runs.length === 0) return;
|
|
436
483
|
ensurePoller();
|
|
437
484
|
if (state.lastUiContext?.hasUI) rerenderWidget(state.lastUiContext);
|
|
438
485
|
};
|
|
439
486
|
|
|
440
|
-
return { ensurePoller, handleStarted, handleComplete, resetJobs, restoreActiveJobs };
|
|
487
|
+
return { ensurePoller, refreshWidget, handleStarted, handleComplete, resetJobs, restoreActiveJobs };
|
|
441
488
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { ASYNC_DIR, RESULTS_DIR, type AsyncStatus, type
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { ASYNC_DIR, RESULTS_DIR, type AsyncStatus, type SteeringRecoveryDescriptor } from "../../shared/types.ts";
|
|
4
|
+
import type { AgentConfig } from "../../agents/agents.ts";
|
|
5
|
+
import { validateAcceptanceInput } from "../shared/acceptance.ts";
|
|
6
|
+
import { validateToolBudgetConfig } from "../shared/tool-budget.ts";
|
|
7
|
+
import { resolveTurnBudgetConfig } from "../shared/turn-budget.ts";
|
|
6
8
|
import { reconcileAsyncRun } from "./stale-run-reconciler.ts";
|
|
7
9
|
|
|
8
|
-
export const ASYNC_RESUME_INTERRUPT_SIGNAL: NodeJS.Signals = process.platform === "win32" ? "SIGBREAK" : "SIGUSR2";
|
|
9
|
-
|
|
10
10
|
export interface AsyncResumeParams {
|
|
11
11
|
id?: string;
|
|
12
12
|
runId?: string;
|
|
@@ -23,6 +23,7 @@ export interface AsyncResumeDeps {
|
|
|
23
23
|
|
|
24
24
|
export interface AsyncResumeOptions {
|
|
25
25
|
requireSessionFile?: boolean;
|
|
26
|
+
sessionId?: string;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
export type AsyncResumeTarget = {
|
|
@@ -32,49 +33,13 @@ export type AsyncResumeTarget = {
|
|
|
32
33
|
state: AsyncStatus["state"];
|
|
33
34
|
agent: string;
|
|
34
35
|
index: number;
|
|
35
|
-
intercomTarget: string;
|
|
36
36
|
cwd?: string;
|
|
37
37
|
sessionFile?: string;
|
|
38
|
+
model?: string;
|
|
39
|
+
thinking?: string;
|
|
40
|
+
recoveryDescriptor?: SteeringRecoveryDescriptor;
|
|
38
41
|
};
|
|
39
42
|
|
|
40
|
-
type KillFn = (pid: number, signal?: NodeJS.Signals | 0) => boolean;
|
|
41
|
-
|
|
42
|
-
export function interruptLiveAsyncResumeTarget(input: {
|
|
43
|
-
target: AsyncResumeTarget & { kind: "live" };
|
|
44
|
-
state?: Pick<SubagentState, "asyncJobs">;
|
|
45
|
-
kill?: KillFn;
|
|
46
|
-
now?: () => number;
|
|
47
|
-
resultsDir?: string;
|
|
48
|
-
}): { ok: true; asyncId: string } | { ok: false; message: string } {
|
|
49
|
-
const asyncId = input.target.runId;
|
|
50
|
-
if (!input.target.asyncDir) {
|
|
51
|
-
return { ok: false, message: `Async run ${asyncId} is live but does not have an async directory to interrupt.` };
|
|
52
|
-
}
|
|
53
|
-
const status = reconcileAsyncRun(input.target.asyncDir, { resultsDir: input.resultsDir, kill: input.kill, now: input.now }).status;
|
|
54
|
-
if (!status || status.state !== "running" || typeof status.pid !== "number") {
|
|
55
|
-
return { ok: false, message: `Async run ${asyncId} is live but no interrupt-capable runner pid was found.` };
|
|
56
|
-
}
|
|
57
|
-
try {
|
|
58
|
-
deliverInterruptRequest({
|
|
59
|
-
asyncDir: input.target.asyncDir,
|
|
60
|
-
pid: status.pid,
|
|
61
|
-
kill: input.kill,
|
|
62
|
-
signal: ASYNC_RESUME_INTERRUPT_SIGNAL,
|
|
63
|
-
now: input.now,
|
|
64
|
-
source: "async-resume",
|
|
65
|
-
});
|
|
66
|
-
const tracked = input.state?.asyncJobs.get(asyncId);
|
|
67
|
-
if (tracked) {
|
|
68
|
-
tracked.activityState = undefined;
|
|
69
|
-
tracked.updatedAt = input.now?.() ?? Date.now();
|
|
70
|
-
}
|
|
71
|
-
return { ok: true, asyncId };
|
|
72
|
-
} catch (error) {
|
|
73
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
74
|
-
return { ok: false, message: `Failed to interrupt async run ${asyncId}: ${message}` };
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
43
|
interface AsyncResultFile {
|
|
79
44
|
id?: string;
|
|
80
45
|
runId?: string;
|
|
@@ -83,8 +48,11 @@ interface AsyncResultFile {
|
|
|
83
48
|
state?: string;
|
|
84
49
|
success?: boolean;
|
|
85
50
|
cwd?: string;
|
|
51
|
+
sessionId?: string;
|
|
86
52
|
sessionFile?: string;
|
|
87
|
-
|
|
53
|
+
model?: string;
|
|
54
|
+
thinking?: string;
|
|
55
|
+
results?: Array<{ agent?: string; success?: boolean; sessionFile?: string; intercomTarget?: string; model?: string; thinking?: string }>;
|
|
88
56
|
}
|
|
89
57
|
|
|
90
58
|
export interface AsyncRunLocation {
|
|
@@ -122,9 +90,11 @@ function validateResultFile(value: unknown, resultPath: string): AsyncResultFile
|
|
|
122
90
|
const agent = validateOptionalString(child, "agent", resultPath, `results[${index}].agent`);
|
|
123
91
|
const sessionFile = validateOptionalString(child, "sessionFile", resultPath, `results[${index}].sessionFile`);
|
|
124
92
|
const intercomTarget = validateOptionalString(child, "intercomTarget", resultPath, `results[${index}].intercomTarget`);
|
|
93
|
+
const model = validateOptionalString(child, "model", resultPath, `results[${index}].model`);
|
|
94
|
+
const thinking = validateOptionalString(child, "thinking", resultPath, `results[${index}].thinking`);
|
|
125
95
|
const success = child.success;
|
|
126
96
|
if (success !== undefined && typeof success !== "boolean") throw new Error(`Invalid async result file '${resultPath}': results[${index}].success must be a boolean.`);
|
|
127
|
-
return { agent, sessionFile, intercomTarget, ...(typeof success === "boolean" ? { success } : {}) };
|
|
97
|
+
return { agent, sessionFile, intercomTarget, model, thinking, ...(typeof success === "boolean" ? { success } : {}) };
|
|
128
98
|
});
|
|
129
99
|
}
|
|
130
100
|
const success = data.success;
|
|
@@ -136,7 +106,10 @@ function validateResultFile(value: unknown, resultPath: string): AsyncResultFile
|
|
|
136
106
|
mode: validateOptionalString(data, "mode", resultPath),
|
|
137
107
|
state: validateOptionalString(data, "state", resultPath),
|
|
138
108
|
cwd: validateOptionalString(data, "cwd", resultPath),
|
|
109
|
+
sessionId: validateOptionalString(data, "sessionId", resultPath),
|
|
139
110
|
sessionFile: validateOptionalString(data, "sessionFile", resultPath),
|
|
111
|
+
model: validateOptionalString(data, "model", resultPath),
|
|
112
|
+
thinking: validateOptionalString(data, "thinking", resultPath),
|
|
140
113
|
...(typeof success === "boolean" ? { success } : {}),
|
|
141
114
|
...(results ? { results } : {}),
|
|
142
115
|
};
|
|
@@ -252,7 +225,7 @@ export function resolveAsyncRunLocation(params: AsyncResumeParams, asyncDirRoot:
|
|
|
252
225
|
}
|
|
253
226
|
|
|
254
227
|
function resultState(result: AsyncResultFile): AsyncStatus["state"] {
|
|
255
|
-
if (result.state === "complete" || result.state === "failed" || result.state === "paused" || result.state === "running" || result.state === "queued") {
|
|
228
|
+
if (result.state === "complete" || result.state === "failed" || result.state === "paused" || result.state === "stopped" || result.state === "running" || result.state === "queued") {
|
|
256
229
|
return result.state;
|
|
257
230
|
}
|
|
258
231
|
return result.success ? "complete" : "failed";
|
|
@@ -268,12 +241,106 @@ function validateStatusForResume(status: AsyncStatus | null, source: string): vo
|
|
|
268
241
|
if (!Array.isArray(status.steps)) throw new Error(`Invalid async status '${source}': steps must be an array.`);
|
|
269
242
|
status.steps.forEach((step, index) => {
|
|
270
243
|
if (!step || typeof step !== "object" || Array.isArray(step)) throw new Error(`Invalid async status '${source}': steps[${index}] must be an object.`);
|
|
271
|
-
|
|
272
|
-
if (
|
|
244
|
+
const stepRecord = step as Record<string, unknown>;
|
|
245
|
+
if (typeof stepRecord.agent !== "string") throw new Error(`Invalid async status '${source}': steps[${index}].agent must be a string.`);
|
|
246
|
+
if (stepRecord.sessionFile !== undefined && typeof stepRecord.sessionFile !== "string") throw new Error(`Invalid async status '${source}': steps[${index}].sessionFile must be a string.`);
|
|
247
|
+
if (stepRecord.model !== undefined && typeof stepRecord.model !== "string") throw new Error(`Invalid async status '${source}': steps[${index}].model must be a string.`);
|
|
248
|
+
if (stepRecord.thinking !== undefined && typeof stepRecord.thinking !== "string") throw new Error(`Invalid async status '${source}': steps[${index}].thinking must be a string.`);
|
|
273
249
|
});
|
|
274
250
|
}
|
|
275
251
|
}
|
|
276
252
|
|
|
253
|
+
export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): SteeringRecoveryDescriptor | undefined {
|
|
254
|
+
if (!asyncDir) return undefined;
|
|
255
|
+
const descriptorPath = path.join(asyncDir, "recovery-descriptor.json");
|
|
256
|
+
if (!fs.existsSync(descriptorPath)) return undefined;
|
|
257
|
+
let value: unknown;
|
|
258
|
+
try {
|
|
259
|
+
value = JSON.parse(fs.readFileSync(descriptorPath, "utf-8"));
|
|
260
|
+
} catch (error) {
|
|
261
|
+
throw new Error(`Failed to parse async recovery descriptor '${descriptorPath}': ${getErrorMessage(error)}`, { cause: error instanceof Error ? error : undefined });
|
|
262
|
+
}
|
|
263
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': expected an object.`);
|
|
264
|
+
const parsed = value as Record<string, unknown>;
|
|
265
|
+
const allowedFields = new Set([
|
|
266
|
+
"version", "sourceRunId", "agent", "sessionFile", "cwd", "model", "fallbackModels", "thinking", "tools", "extensions",
|
|
267
|
+
"subagentOnlyExtensions", "mcpDirectTools", "systemPrompt", "systemPromptMode", "inheritProjectContext", "inheritSkills", "skills",
|
|
268
|
+
"skillPath", "agentFilePath", "completionGuard", "memory", "outputPath", "outputMode", "acceptance", "sessionDir", "artifactConfig",
|
|
269
|
+
"artifactsDir", "maxOutput", "controlConfig", "absoluteDeadlineAt", "initialTurnBudget", "initialToolBudget", "maxSubagentDepth", "share",
|
|
270
|
+
]);
|
|
271
|
+
for (const field of Object.keys(parsed)) {
|
|
272
|
+
if (!allowedFields.has(field)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': unknown field '${field}'.`);
|
|
273
|
+
}
|
|
274
|
+
const requiredStrings = ["sourceRunId", "agent", "cwd", "systemPromptMode", "outputMode"] as const;
|
|
275
|
+
for (const field of requiredStrings) {
|
|
276
|
+
if (typeof parsed[field] !== "string" || !(parsed[field] as string).trim()) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${field} must be a non-empty string.`);
|
|
277
|
+
}
|
|
278
|
+
if (parsed.version !== 1) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': version must be 1.`);
|
|
279
|
+
if (parsed.systemPromptMode !== "append" && parsed.systemPromptMode !== "replace") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': systemPromptMode is invalid.`);
|
|
280
|
+
if (parsed.outputMode !== "inline" && parsed.outputMode !== "file-only") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': outputMode is invalid.`);
|
|
281
|
+
for (const field of ["inheritProjectContext", "inheritSkills", "share"] as const) {
|
|
282
|
+
if (typeof parsed[field] !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${field} must be a boolean.`);
|
|
283
|
+
}
|
|
284
|
+
if (!Number.isInteger(parsed.maxSubagentDepth) || (parsed.maxSubagentDepth as number) < 0) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': maxSubagentDepth must be a non-negative integer.`);
|
|
285
|
+
for (const field of ["fallbackModels", "tools", "extensions", "subagentOnlyExtensions", "mcpDirectTools", "skills", "skillPath"] as const) {
|
|
286
|
+
const item = parsed[field];
|
|
287
|
+
if (item !== undefined && (!Array.isArray(item) || item.some((entry) => typeof entry !== "string" || !entry.trim()))) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${field} must contain non-empty strings.`);
|
|
288
|
+
}
|
|
289
|
+
if (parsed.systemPrompt !== undefined && typeof parsed.systemPrompt !== "string") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': systemPrompt must be a string.`);
|
|
290
|
+
for (const field of ["sessionFile", "model", "thinking", "agentFilePath", "outputPath", "sessionDir", "artifactsDir"] as const) {
|
|
291
|
+
if (parsed[field] !== undefined && (typeof parsed[field] !== "string" || !(parsed[field] as string).trim())) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${field} must be a non-empty string.`);
|
|
292
|
+
}
|
|
293
|
+
if (parsed.completionGuard !== undefined && typeof parsed.completionGuard !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': completionGuard must be a boolean.`);
|
|
294
|
+
if (parsed.memory !== undefined) {
|
|
295
|
+
if (!parsed.memory || typeof parsed.memory !== "object" || Array.isArray(parsed.memory)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': memory must be an object.`);
|
|
296
|
+
const memory = parsed.memory as Record<string, unknown>;
|
|
297
|
+
if ((memory.scope !== "project" && memory.scope !== "user") || typeof memory.path !== "string" || !memory.path.trim()) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': memory is invalid.`);
|
|
298
|
+
}
|
|
299
|
+
if (parsed.absoluteDeadlineAt !== undefined && (!Number.isFinite(parsed.absoluteDeadlineAt) || (parsed.absoluteDeadlineAt as number) <= 0)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': absoluteDeadlineAt must be a positive timestamp.`);
|
|
300
|
+
if (parsed.initialTurnBudget !== undefined) {
|
|
301
|
+
const result = resolveTurnBudgetConfig(parsed.initialTurnBudget, "recoveryDescriptor.initialTurnBudget");
|
|
302
|
+
if (result.error) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${result.error}`);
|
|
303
|
+
}
|
|
304
|
+
if (parsed.initialToolBudget !== undefined) {
|
|
305
|
+
const result = validateToolBudgetConfig(parsed.initialToolBudget, "recoveryDescriptor.initialToolBudget");
|
|
306
|
+
if (result.error) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${result.error}`);
|
|
307
|
+
}
|
|
308
|
+
if (parsed.maxOutput !== undefined) {
|
|
309
|
+
if (!parsed.maxOutput || typeof parsed.maxOutput !== "object" || Array.isArray(parsed.maxOutput)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': maxOutput must be an object.`);
|
|
310
|
+
for (const field of ["bytes", "lines"] as const) {
|
|
311
|
+
const item = (parsed.maxOutput as Record<string, unknown>)[field];
|
|
312
|
+
if (item !== undefined && (!Number.isInteger(item) || (item as number) < 1)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': maxOutput.${field} must be a positive integer.`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if (parsed.artifactConfig !== undefined) {
|
|
316
|
+
if (!parsed.artifactConfig || typeof parsed.artifactConfig !== "object" || Array.isArray(parsed.artifactConfig)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': artifactConfig must be an object.`);
|
|
317
|
+
const artifact = parsed.artifactConfig as Record<string, unknown>;
|
|
318
|
+
for (const field of ["enabled", "includeInput", "includeOutput", "includeJsonl", "includeMetadata"] as const) {
|
|
319
|
+
if (typeof artifact[field] !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': artifactConfig.${field} must be a boolean.`);
|
|
320
|
+
}
|
|
321
|
+
if (artifact.includeTranscript !== undefined && typeof artifact.includeTranscript !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': artifactConfig.includeTranscript must be a boolean.`);
|
|
322
|
+
if (!Number.isInteger(artifact.cleanupDays) || (artifact.cleanupDays as number) < 0) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': artifactConfig.cleanupDays must be a non-negative integer.`);
|
|
323
|
+
}
|
|
324
|
+
if (parsed.controlConfig !== undefined) {
|
|
325
|
+
if (!parsed.controlConfig || typeof parsed.controlConfig !== "object" || Array.isArray(parsed.controlConfig)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': controlConfig must be an object.`);
|
|
326
|
+
const control = parsed.controlConfig as Record<string, unknown>;
|
|
327
|
+
if (typeof control.enabled !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': controlConfig.enabled must be a boolean.`);
|
|
328
|
+
for (const field of ["needsAttentionAfterMs", "activeNoticeAfterMs", "failedToolAttemptsBeforeAttention"] as const) {
|
|
329
|
+
if (!Number.isInteger(control[field]) || (control[field] as number) < 1) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': controlConfig.${field} must be a positive integer.`);
|
|
330
|
+
}
|
|
331
|
+
for (const field of ["activeNoticeAfterTurns", "activeNoticeAfterTokens"] as const) {
|
|
332
|
+
if (control[field] !== undefined && (!Number.isInteger(control[field]) || (control[field] as number) < 1)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': controlConfig.${field} must be a positive integer.`);
|
|
333
|
+
}
|
|
334
|
+
if (!Array.isArray(control.notifyOn) || control.notifyOn.some((item) => item !== "active_long_running" && item !== "needs_attention")) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': controlConfig.notifyOn is invalid.`);
|
|
335
|
+
if (!Array.isArray(control.notifyChannels) || control.notifyChannels.some((item) => item !== "event" && item !== "async" && item !== "intercom")) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': controlConfig.notifyChannels is invalid.`);
|
|
336
|
+
}
|
|
337
|
+
if (parsed.acceptance !== undefined) {
|
|
338
|
+
const errors = validateAcceptanceInput(parsed.acceptance, "recoveryDescriptor.acceptance");
|
|
339
|
+
if (errors.length) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${errors.join(" ")}`);
|
|
340
|
+
}
|
|
341
|
+
return parsed as unknown as SteeringRecoveryDescriptor;
|
|
342
|
+
}
|
|
343
|
+
|
|
277
344
|
function validateResumeSessionFile(runId: string, sessionFile: string): string {
|
|
278
345
|
if (path.extname(sessionFile) !== ".jsonl") throw new Error(`Async run '${runId}' session file must be a .jsonl file: ${sessionFile}`);
|
|
279
346
|
const resolved = path.resolve(sessionFile);
|
|
@@ -295,10 +362,16 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
|
|
|
295
362
|
: undefined;
|
|
296
363
|
const status = reconciliation?.status ?? null;
|
|
297
364
|
validateStatusForResume(status, location.asyncDir ? path.join(location.asyncDir, "status.json") : "status.json");
|
|
365
|
+
const recoveryDescriptor = readAsyncRecoveryDescriptor(location.asyncDir);
|
|
298
366
|
const result = location.resultPath ? readResultFile(location.resultPath) : undefined;
|
|
299
367
|
const runId = status?.runId ?? result?.runId ?? result?.id ?? location.resolvedId ?? (location.asyncDir ? path.basename(location.asyncDir) : "unknown");
|
|
368
|
+
if (options.sessionId && ((status && status.sessionId !== options.sessionId) || (result && result.sessionId !== options.sessionId))) {
|
|
369
|
+
throw new Error(`Async run '${runId}' was not found in the active session.`);
|
|
370
|
+
}
|
|
371
|
+
if (recoveryDescriptor && recoveryDescriptor.sourceRunId !== runId) throw new Error(`Async run '${runId}' has a recovery descriptor for a different source run.`);
|
|
300
372
|
const state = status?.state ?? (result ? resultState(result) : undefined);
|
|
301
373
|
if (!state) throw new Error(`Status file not found for async run '${runId}'.`);
|
|
374
|
+
if (state === "stopped") throw new Error(`Async run '${runId}' was stopped and cannot be resumed. Start a new run instead.`);
|
|
302
375
|
|
|
303
376
|
const statusSteps = status?.steps ?? [];
|
|
304
377
|
const resultSteps = result?.results ?? [];
|
|
@@ -319,9 +392,11 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
|
|
|
319
392
|
state,
|
|
320
393
|
agent: selectedStep.agent,
|
|
321
394
|
index: requestedIndex,
|
|
322
|
-
intercomTarget: resolveSubagentIntercomTarget(runId, selectedStep.agent, requestedIndex),
|
|
323
395
|
cwd: status?.cwd ?? result?.cwd,
|
|
324
396
|
sessionFile: selectedStep.sessionFile ?? status?.sessionFile ?? result?.sessionFile,
|
|
397
|
+
model: selectedStep.model,
|
|
398
|
+
thinking: selectedStep.thinking,
|
|
399
|
+
...(recoveryDescriptor ? { recoveryDescriptor } : {}),
|
|
325
400
|
};
|
|
326
401
|
}
|
|
327
402
|
if (selectedStep?.status === "pending") throw new Error(`Async run '${runId}' child ${requestedIndex} is pending and has not started yet. Wait for it to run or complete before resuming.`);
|
|
@@ -341,9 +416,11 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
|
|
|
341
416
|
state,
|
|
342
417
|
agent: selected.step.agent,
|
|
343
418
|
index: selected.index,
|
|
344
|
-
intercomTarget: resolveSubagentIntercomTarget(runId, selected.step.agent, selected.index),
|
|
345
419
|
cwd: status?.cwd ?? result?.cwd,
|
|
346
420
|
sessionFile: selected.step.sessionFile ?? status?.sessionFile ?? result?.sessionFile,
|
|
421
|
+
model: selected.step.model,
|
|
422
|
+
thinking: selected.step.thinking,
|
|
423
|
+
...(recoveryDescriptor ? { recoveryDescriptor } : {}),
|
|
347
424
|
};
|
|
348
425
|
}
|
|
349
426
|
}
|
|
@@ -356,11 +433,14 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
|
|
|
356
433
|
if (index < 0 || index >= stepCount) throw new Error(`Async run '${runId}' has ${stepCount} children. Index ${index} is out of range.`);
|
|
357
434
|
const agent = statusSteps[index]?.agent ?? resultSteps[index]?.agent ?? result?.agent;
|
|
358
435
|
if (!agent) throw new Error(`Could not determine child agent for async run '${runId}'.`);
|
|
436
|
+
if (recoveryDescriptor && recoveryDescriptor.agent !== agent) throw new Error(`Async run '${runId}' has a recovery descriptor for '${recoveryDescriptor.agent}', not '${agent}'.`);
|
|
359
437
|
const sessionFile = statusSteps[index]?.sessionFile
|
|
360
438
|
?? resultSteps[index]?.sessionFile
|
|
361
439
|
?? (stepCount === 1 ? status?.sessionFile ?? result?.sessionFile : undefined);
|
|
362
440
|
if (!sessionFile && requireSessionFile) throw new Error(`Async run '${runId}' child ${index} does not have a persisted session file to resume from.`);
|
|
363
441
|
const resolvedSessionFile = sessionFile ? validateResumeSessionFile(runId, sessionFile) : undefined;
|
|
442
|
+
const stepModel = statusSteps[index]?.model ?? resultSteps[index]?.model ?? (stepCount === 1 ? result?.model : undefined);
|
|
443
|
+
const stepThinking = statusSteps[index]?.thinking ?? resultSteps[index]?.thinking ?? (stepCount === 1 ? result?.thinking : undefined);
|
|
364
444
|
|
|
365
445
|
return {
|
|
366
446
|
kind: "revive",
|
|
@@ -369,9 +449,36 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
|
|
|
369
449
|
state,
|
|
370
450
|
agent,
|
|
371
451
|
index,
|
|
372
|
-
intercomTarget: resolveSubagentIntercomTarget(runId, agent, index),
|
|
373
452
|
cwd: status?.cwd ?? result?.cwd,
|
|
374
453
|
...(resolvedSessionFile ? { sessionFile: resolvedSessionFile } : {}),
|
|
454
|
+
...(stepModel ? { model: stepModel } : {}),
|
|
455
|
+
...(stepThinking ? { thinking: stepThinking } : {}),
|
|
456
|
+
...(recoveryDescriptor ? { recoveryDescriptor } : {}),
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export function applySteeringRecoveryAgentConfig(agentConfig: AgentConfig, descriptor: SteeringRecoveryDescriptor): AgentConfig {
|
|
461
|
+
return {
|
|
462
|
+
...agentConfig,
|
|
463
|
+
model: descriptor.model,
|
|
464
|
+
fallbackModels: descriptor.fallbackModels ? [...descriptor.fallbackModels] : undefined,
|
|
465
|
+
thinking: descriptor.thinking,
|
|
466
|
+
tools: descriptor.tools ? [...descriptor.tools] : undefined,
|
|
467
|
+
extensions: descriptor.extensions ? [...descriptor.extensions] : undefined,
|
|
468
|
+
subagentOnlyExtensions: descriptor.subagentOnlyExtensions ? [...descriptor.subagentOnlyExtensions] : undefined,
|
|
469
|
+
mcpDirectTools: descriptor.mcpDirectTools ? [...descriptor.mcpDirectTools] : undefined,
|
|
470
|
+
systemPrompt: descriptor.systemPrompt,
|
|
471
|
+
systemPromptMode: descriptor.systemPromptMode,
|
|
472
|
+
inheritProjectContext: descriptor.inheritProjectContext,
|
|
473
|
+
inheritSkills: descriptor.inheritSkills,
|
|
474
|
+
skills: descriptor.skills ? [...descriptor.skills] : undefined,
|
|
475
|
+
skillPath: descriptor.skillPath ? [...descriptor.skillPath] : undefined,
|
|
476
|
+
filePath: descriptor.agentFilePath,
|
|
477
|
+
completionGuard: descriptor.completionGuard,
|
|
478
|
+
memory: descriptor.memory ? { ...descriptor.memory } : undefined,
|
|
479
|
+
output: descriptor.outputPath,
|
|
480
|
+
toolBudget: descriptor.initialToolBudget,
|
|
481
|
+
maxSubagentDepth: descriptor.maxSubagentDepth,
|
|
375
482
|
};
|
|
376
483
|
}
|
|
377
484
|
|