@oh-my-pi/pi-coding-agent 16.4.4 → 16.4.6
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 +62 -0
- package/dist/cli.js +3799 -3729
- package/dist/types/async/job-manager.d.ts +8 -0
- package/dist/types/cli/bench-cli.d.ts +1 -7
- package/dist/types/cli/usage-cli.d.ts +1 -0
- package/dist/types/commands/usage.d.ts +7 -0
- package/dist/types/config/settings-schema.d.ts +19 -9
- package/dist/types/config/settings.d.ts +3 -2
- package/dist/types/discovery/helpers.d.ts +2 -2
- package/dist/types/extensibility/extensions/types.d.ts +36 -0
- package/dist/types/irc/bus.d.ts +4 -0
- package/dist/types/modes/components/__tests__/pause-screen.test.d.ts +1 -0
- package/dist/types/modes/components/ask-dialog.d.ts +27 -0
- package/dist/types/modes/components/custom-editor.d.ts +3 -8
- package/dist/types/modes/components/index.d.ts +2 -1
- package/dist/types/modes/components/model-browser.d.ts +100 -0
- package/dist/types/modes/components/model-hub.d.ts +52 -0
- package/dist/types/modes/components/pause-screen.d.ts +43 -0
- package/dist/types/modes/components/session-selector.d.ts +13 -0
- package/dist/types/modes/components/tool-execution.d.ts +2 -0
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -1
- package/dist/types/modes/controllers/input-controller.d.ts +2 -0
- package/dist/types/modes/interactive-mode.d.ts +3 -0
- package/dist/types/modes/queue-input.d.ts +8 -0
- package/dist/types/modes/shared.d.ts +1 -1
- package/dist/types/modes/types.d.ts +4 -0
- package/dist/types/session/agent-session.d.ts +1 -1
- package/dist/types/session/agent-storage.d.ts +57 -0
- package/dist/types/session/session-context.d.ts +9 -0
- package/dist/types/task/executor.d.ts +26 -13
- package/dist/types/task/index.d.ts +12 -11
- package/dist/types/task/label.d.ts +4 -0
- package/dist/types/task/repair-args.d.ts +8 -8
- package/dist/types/task/types.d.ts +31 -56
- package/dist/types/tools/ask.d.ts +12 -0
- package/dist/types/tools/conflict-detect.d.ts +17 -1
- package/dist/types/tools/job.d.ts +16 -0
- package/package.json +12 -12
- package/scripts/build-binary.ts +0 -1
- package/scripts/compile-binary.ts +4 -3
- package/src/async/job-manager.ts +9 -0
- package/src/cli/bench-cli.ts +7 -26
- package/src/cli/usage-cli.ts +11 -0
- package/src/commands/usage.ts +13 -2
- package/src/commit/agentic/tools/analyze-file.ts +2 -3
- package/src/config/settings-schema.ts +18 -7
- package/src/config/settings.ts +13 -4
- package/src/discovery/helpers.ts +3 -4
- package/src/extensibility/custom-tools/loader.ts +70 -37
- package/src/extensibility/extensions/types.ts +46 -0
- package/src/irc/bus.ts +61 -20
- package/src/modes/components/__tests__/pause-screen.test.ts +143 -0
- package/src/modes/components/advisor-config.ts +32 -22
- package/src/modes/components/ask-dialog.ts +888 -0
- package/src/modes/components/custom-editor.test.ts +58 -1
- package/src/modes/components/custom-editor.ts +42 -11
- package/src/modes/components/index.ts +2 -1
- package/src/modes/components/model-browser.ts +769 -0
- package/src/modes/components/model-hub.ts +2002 -0
- package/src/modes/components/pause-screen.ts +208 -0
- package/src/modes/components/session-selector.ts +299 -42
- package/src/modes/components/tool-execution.ts +2 -0
- package/src/modes/components/usage-row.ts +5 -6
- package/src/modes/controllers/event-controller.ts +8 -2
- package/src/modes/controllers/extension-ui-controller.ts +252 -5
- package/src/modes/controllers/input-controller.ts +140 -6
- package/src/modes/controllers/selector-controller.ts +160 -97
- package/src/modes/controllers/tan-command-controller.ts +1 -1
- package/src/modes/controllers/todo-command-controller.ts +1 -2
- package/src/modes/interactive-mode.ts +8 -0
- package/src/modes/queue-input.ts +132 -0
- package/src/modes/shared.ts +1 -1
- package/src/modes/theme/theme.ts +3 -3
- package/src/modes/types.ts +4 -0
- package/src/modes/utils/ui-helpers.ts +50 -24
- package/src/prompts/agents/scout.md +0 -1
- package/src/prompts/agents/task.md +1 -1
- package/src/prompts/system/subagent-system-prompt.md +1 -5
- package/src/prompts/system/subagent-yield-reminder.md +10 -0
- package/src/prompts/system/task-label.md +23 -0
- package/src/prompts/tools/job.md +1 -1
- package/src/prompts/tools/task-summary.md +3 -0
- package/src/prompts/tools/task.md +17 -18
- package/src/session/agent-session.ts +186 -49
- package/src/session/agent-storage.ts +330 -3
- package/src/session/history-storage.ts +1 -34
- package/src/session/session-context.test.ts +73 -0
- package/src/session/session-context.ts +43 -26
- package/src/slash-commands/builtin-registry.ts +18 -0
- package/src/task/agents.ts +2 -0
- package/src/task/executor.ts +159 -46
- package/src/task/index.ts +377 -239
- package/src/task/label.ts +38 -0
- package/src/task/render.ts +74 -22
- package/src/task/repair-args.ts +20 -31
- package/src/task/spawn-policy.test.ts +4 -4
- package/src/task/types.ts +46 -66
- package/src/tools/ask.ts +233 -40
- package/src/tools/conflict-detect.ts +102 -5
- package/src/tools/index.ts +1 -0
- package/src/tools/irc.ts +20 -11
- package/src/tools/job.ts +158 -18
- package/src/tools/write.ts +70 -6
- package/src/vibe/runtime.ts +1 -1
- package/src/web/search/providers/browser-headers.ts +30 -13
- package/dist/types/modes/components/model-selector.d.ts +0 -37
- package/dist/types/tools/bash-command-fixup.d.ts +0 -3
- package/src/modes/components/model-selector.ts +0 -1291
- package/src/tools/bash-command-fixup.ts +0 -4
package/src/task/executor.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import path from "node:path";
|
|
8
|
-
import type { AgentEvent, AgentIdentity, AgentTelemetryConfig
|
|
8
|
+
import type { AgentEvent, AgentIdentity, AgentTelemetryConfig } from "@oh-my-pi/pi-agent-core";
|
|
9
9
|
import { recordHandoff, resolveTelemetry } from "@oh-my-pi/pi-agent-core";
|
|
10
10
|
import type { Api, Model, ServiceTierByFamily, Usage } from "@oh-my-pi/pi-ai";
|
|
11
11
|
import { logger, popLoopPhase, prompt, pushLoopPhase, untilAborted } from "@oh-my-pi/pi-utils";
|
|
@@ -42,6 +42,7 @@ import type { AuthStorage } from "../session/auth-storage";
|
|
|
42
42
|
import { SKILL_PROMPT_MESSAGE_TYPE, USER_INTERRUPT_LABEL } from "../session/messages";
|
|
43
43
|
import { SessionManager } from "../session/session-manager";
|
|
44
44
|
import { truncateTail } from "../session/streaming-output";
|
|
45
|
+
import type { ConfiguredThinkingLevel } from "../thinking";
|
|
45
46
|
import type { ContextFileEntry, ToolSession } from "../tools";
|
|
46
47
|
import { resolveEvalBackends } from "../tools/eval-backends";
|
|
47
48
|
import { isIrcEnabled } from "../tools/irc";
|
|
@@ -56,15 +57,14 @@ import { ToolAbortError } from "../tools/tool-errors";
|
|
|
56
57
|
import type { EventBus } from "../utils/event-bus";
|
|
57
58
|
import { buildNamedToolChoice } from "../utils/tool-choice";
|
|
58
59
|
import type { WorkspaceTree } from "../workspace-tree";
|
|
60
|
+
import { generateTaskLabel } from "./label";
|
|
59
61
|
import { subprocessToolRegistry } from "./subprocess-tool-registry";
|
|
60
62
|
import {
|
|
61
63
|
type AgentDefinition,
|
|
62
64
|
type AgentProgress,
|
|
63
65
|
MAX_OUTPUT_BYTES,
|
|
64
66
|
MAX_OUTPUT_LINES,
|
|
65
|
-
oneLineLabel,
|
|
66
67
|
type ReviewFinding,
|
|
67
|
-
resolveSubagentDisplayName,
|
|
68
68
|
type SingleResult,
|
|
69
69
|
TASK_SUBAGENT_EVENT_CHANNEL,
|
|
70
70
|
TASK_SUBAGENT_LIFECYCLE_CHANNEL,
|
|
@@ -79,23 +79,27 @@ export type { YieldItem } from "./types";
|
|
|
79
79
|
const MCP_CALL_TIMEOUT_MS = 60_000;
|
|
80
80
|
|
|
81
81
|
/**
|
|
82
|
-
* Soft per-agent request budgets (assistant requests per run).
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
* `
|
|
82
|
+
* Soft per-agent request budgets (assistant requests per run). Crossing the
|
|
83
|
+
* budget injects a wrap-up steering notice (`task.softRequestBudgetNotice`,
|
|
84
|
+
* on by default). At 1.5x the budget the free-running turn is stopped and the
|
|
85
|
+
* agent is driven to one forced final `yield` so partial findings come back
|
|
86
|
+
* as a real report; only if it still refuses to yield within
|
|
87
|
+
* {@link BUDGET_STOP_GRACE_REQUESTS} more requests is the run hard-aborted.
|
|
88
|
+
* The `default` key applies to agents without an explicit entry and can be
|
|
89
|
+
* overridden via the `task.softRequestBudget` setting (0 disables the guard).
|
|
89
90
|
*/
|
|
90
91
|
export const SOFT_REQUEST_BUDGET: Record<string, number> = {
|
|
91
|
-
scout:
|
|
92
|
-
sonic:
|
|
93
|
-
default:
|
|
92
|
+
scout: 100,
|
|
93
|
+
sonic: 100,
|
|
94
|
+
default: 200,
|
|
94
95
|
};
|
|
95
96
|
|
|
96
|
-
/**
|
|
97
|
-
export
|
|
98
|
-
|
|
97
|
+
/** Extra requests allowed after a budget stop for the forced yield to land before the run is hard-aborted. */
|
|
98
|
+
export const BUDGET_STOP_GRACE_REQUESTS = 5;
|
|
99
|
+
|
|
100
|
+
/** Steering notice injected when a subagent crosses its soft request budget. */
|
|
101
|
+
export function buildBudgetNotice(requests: number, budget: number): string {
|
|
102
|
+
return `[budget notice] You have used ${requests} requests in this run (soft budget: ${budget}). Wrap up now: finish the current step and yield your final report. At ${Math.ceil(budget * 1.5)} requests the run is force-stopped and you will be asked to yield whatever you have.`;
|
|
99
103
|
}
|
|
100
104
|
|
|
101
105
|
/** Flatten whitespace and clip salvage text for the cancelled-child summary line. */
|
|
@@ -287,9 +291,8 @@ export interface ExecutorOptions {
|
|
|
287
291
|
* the session did not start with a plan (or while plan mode is still active).
|
|
288
292
|
*/
|
|
289
293
|
planReference?: { path: string; content: string };
|
|
294
|
+
/** Pre-set UI label (e.g. eval bridge label). When absent, a tiny-model label is generated from the assignment. */
|
|
290
295
|
description?: string;
|
|
291
|
-
/** Specialist role/expertise for this spawn; drives the system-prompt preamble, display name, and telemetry identity. */
|
|
292
|
-
role?: string;
|
|
293
296
|
index: number;
|
|
294
297
|
id: string;
|
|
295
298
|
parentToolCallId?: string;
|
|
@@ -306,7 +309,7 @@ export interface ExecutorOptions {
|
|
|
306
309
|
* if the resolved subagent model has no working credentials. See #985.
|
|
307
310
|
*/
|
|
308
311
|
parentActiveModelPattern?: string;
|
|
309
|
-
thinkingLevel?:
|
|
312
|
+
thinkingLevel?: ConfiguredThinkingLevel;
|
|
310
313
|
outputSchema?: unknown;
|
|
311
314
|
/**
|
|
312
315
|
* Caller supplied a schema that supersedes the agent's native output prompt.
|
|
@@ -800,7 +803,7 @@ export function createSubagentSettings(
|
|
|
800
803
|
});
|
|
801
804
|
}
|
|
802
805
|
|
|
803
|
-
type AbortReason = "signal" | "terminate" | "timeout" | "budget";
|
|
806
|
+
export type AbortReason = "signal" | "terminate" | "timeout" | "budget";
|
|
804
807
|
|
|
805
808
|
/** Inputs for the run monitor driving one subagent assignment. */
|
|
806
809
|
interface RunMonitorArgs {
|
|
@@ -810,6 +813,10 @@ interface RunMonitorArgs {
|
|
|
810
813
|
task: string;
|
|
811
814
|
assignment?: string;
|
|
812
815
|
description?: string;
|
|
816
|
+
/** Parent model registry for tiny-model label generation; absent → skip labeling. */
|
|
817
|
+
modelRegistry?: ModelRegistry;
|
|
818
|
+
/** Parent settings for tiny-model label generation. */
|
|
819
|
+
settings?: Settings;
|
|
813
820
|
modelOverride?: string | string[];
|
|
814
821
|
signal?: AbortSignal;
|
|
815
822
|
onProgress?: (progress: AgentProgress) => void;
|
|
@@ -838,6 +845,12 @@ interface SubagentRunMonitor {
|
|
|
838
845
|
hasUsage(): boolean;
|
|
839
846
|
yieldCalled(): boolean;
|
|
840
847
|
runtimeLimitExceeded(): boolean;
|
|
848
|
+
/** True once the soft-budget stop fired: the free-running turn was aborted and the run is being driven to a forced final yield. */
|
|
849
|
+
budgetStopRequested(): boolean;
|
|
850
|
+
/** Resolves when the budget-stop session abort has settled (immediately when no stop fired). */
|
|
851
|
+
waitForBudgetStop(): Promise<void>;
|
|
852
|
+
/** The abort kind for this run, when an abort was requested. */
|
|
853
|
+
abortKind(): AbortReason | undefined;
|
|
841
854
|
/** True when the abort carries a precise external reason (signal / wall-clock / budget). */
|
|
842
855
|
hasExplicitAbortReason(): boolean;
|
|
843
856
|
/** Whether the (attempted) abort counts as a cancelled run rather than an internal failure. */
|
|
@@ -927,6 +940,8 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
|
|
|
927
940
|
let hasUsage = false;
|
|
928
941
|
let budgetSteerSent = false;
|
|
929
942
|
let budgetLimitExceeded = false;
|
|
943
|
+
let budgetStopRequested = false;
|
|
944
|
+
let budgetStopAbortPromise: Promise<void> | undefined;
|
|
930
945
|
let lastAssistantSalvageText: string | undefined;
|
|
931
946
|
let activeSessionAbortPromise: Promise<void> | undefined;
|
|
932
947
|
|
|
@@ -965,6 +980,24 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
|
|
|
965
980
|
void abortActiveSession();
|
|
966
981
|
};
|
|
967
982
|
|
|
983
|
+
// Soft-budget stop: cancel the free-running turn WITHOUT aborting the
|
|
984
|
+
// monitor, so driveSessionToYield can still drive one forced final yield.
|
|
985
|
+
// Deliberately not routed through abortActiveSession(): that memoizes its
|
|
986
|
+
// promise, and a later hard abort (grace exhausted) must be able to abort
|
|
987
|
+
// the session again.
|
|
988
|
+
const requestBudgetStop = () => {
|
|
989
|
+
if (budgetStopRequested || abortSent || resolved) return;
|
|
990
|
+
budgetStopRequested = true;
|
|
991
|
+
const session = activeSession;
|
|
992
|
+
budgetStopAbortPromise = session
|
|
993
|
+
? session.abort().catch(error => {
|
|
994
|
+
logger.debug("Subagent budget-stop abort failed", {
|
|
995
|
+
error: error instanceof Error ? error.message : String(error),
|
|
996
|
+
});
|
|
997
|
+
})
|
|
998
|
+
: Promise.resolve();
|
|
999
|
+
};
|
|
1000
|
+
|
|
968
1001
|
// Handle abort signal
|
|
969
1002
|
if (signal) {
|
|
970
1003
|
signal.addEventListener(
|
|
@@ -1010,6 +1043,9 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
|
|
|
1010
1043
|
return `Subagent runtime limit exceeded (task.maxRuntimeMs=${maxRuntimeMs})`;
|
|
1011
1044
|
}
|
|
1012
1045
|
if (budgetLimitExceeded) {
|
|
1046
|
+
return `Soft request budget exceeded (${progress.requests} requests; budget ${softRequestBudget}) — agent did not yield when force-stopped`;
|
|
1047
|
+
}
|
|
1048
|
+
if (budgetStopRequested) {
|
|
1013
1049
|
return `Soft request budget exceeded (${progress.requests} requests; budget ${softRequestBudget})`;
|
|
1014
1050
|
}
|
|
1015
1051
|
return resolveSignalAbortReason();
|
|
@@ -1066,6 +1102,27 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
|
|
|
1066
1102
|
}, PROGRESS_COALESCE_MS - elapsed);
|
|
1067
1103
|
};
|
|
1068
1104
|
|
|
1105
|
+
// The task wire schema carries no description: when the caller didn't pre-set
|
|
1106
|
+
// a UI label (e.g. the eval bridge's `label`), compress the assignment into a
|
|
1107
|
+
// tiny-model one-sentence label off the spawn's critical path. Best-effort —
|
|
1108
|
+
// a late label still lands via the finalize-time reads of `progress.description`;
|
|
1109
|
+
// failures just leave the label unset.
|
|
1110
|
+
const labelSource = assignment?.trim();
|
|
1111
|
+
if (!args.description && args.modelRegistry && args.settings && labelSource) {
|
|
1112
|
+
generateTaskLabel(labelSource, args.modelRegistry, args.settings, id)
|
|
1113
|
+
.then(label => {
|
|
1114
|
+
if (!label || abortSignal.aborted || progress.description) return;
|
|
1115
|
+
progress.description = label;
|
|
1116
|
+
if (!resolved) scheduleProgress();
|
|
1117
|
+
})
|
|
1118
|
+
.catch(err => {
|
|
1119
|
+
logger.debug("Subagent label generation failed", {
|
|
1120
|
+
id,
|
|
1121
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1122
|
+
});
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1069
1126
|
const getMessageContent = (message: unknown): unknown => {
|
|
1070
1127
|
if (!isRecord(message) || !("content" in message)) {
|
|
1071
1128
|
return undefined;
|
|
@@ -1323,14 +1380,26 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
|
|
|
1323
1380
|
}
|
|
1324
1381
|
}
|
|
1325
1382
|
if (softRequestBudget > 0 && !abortSent && !yieldCallPending) {
|
|
1326
|
-
|
|
1327
|
-
|
|
1383
|
+
const stopThreshold = softRequestBudget * 1.5;
|
|
1384
|
+
if (budgetStopRequested) {
|
|
1385
|
+
// Grace window after the stop: the forced yield needs a
|
|
1386
|
+
// request or two; a child that keeps burning requests
|
|
1387
|
+
// instead of yielding is hard-aborted.
|
|
1388
|
+
if (progress.requests >= stopThreshold + BUDGET_STOP_GRACE_REQUESTS) {
|
|
1389
|
+
requestAbort("budget");
|
|
1390
|
+
}
|
|
1391
|
+
} else if (progress.requests >= stopThreshold) {
|
|
1392
|
+
requestBudgetStop();
|
|
1328
1393
|
} else if (softRequestBudgetNotice && !budgetSteerSent && progress.requests >= softRequestBudget) {
|
|
1329
1394
|
budgetSteerSent = true;
|
|
1330
1395
|
const steerSession = activeSession;
|
|
1331
1396
|
if (steerSession) {
|
|
1332
|
-
|
|
1333
|
-
|
|
1397
|
+
// Build the notice now (the count at crossing time), but send
|
|
1398
|
+
// behind an async boundary: a synchronously-throwing send must
|
|
1399
|
+
// never take down event processing (which escalates to terminate).
|
|
1400
|
+
const notice = buildBudgetNotice(progress.requests, softRequestBudget);
|
|
1401
|
+
void Promise.resolve()
|
|
1402
|
+
.then(() => steerSession.sendUserMessage(notice, { deliverAs: "steer" }))
|
|
1334
1403
|
.catch(err => {
|
|
1335
1404
|
logger.warn("Subagent budget steer failed", {
|
|
1336
1405
|
error: err instanceof Error ? err.message : String(err),
|
|
@@ -1481,7 +1550,13 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
|
|
|
1481
1550
|
hasUsage: () => hasUsage,
|
|
1482
1551
|
yieldCalled: () => yieldCalled,
|
|
1483
1552
|
runtimeLimitExceeded: () => runtimeLimitExceeded,
|
|
1484
|
-
hasExplicitAbortReason: () =>
|
|
1553
|
+
hasExplicitAbortReason: () =>
|
|
1554
|
+
abortReason === "signal" || runtimeLimitExceeded || budgetLimitExceeded || budgetStopRequested,
|
|
1555
|
+
budgetStopRequested: () => budgetStopRequested,
|
|
1556
|
+
waitForBudgetStop: () => budgetStopAbortPromise ?? Promise.resolve(),
|
|
1557
|
+
// A soft stop that never escalated still identifies as a budget abort so
|
|
1558
|
+
// the lifecycle can park the agent as resumable instead of killing it.
|
|
1559
|
+
abortKind: () => abortReason ?? (budgetStopRequested ? "budget" : undefined),
|
|
1485
1560
|
isAbortedRun: () =>
|
|
1486
1561
|
abortReason === "signal" || runtimeLimitExceeded || budgetLimitExceeded || abortReason === undefined,
|
|
1487
1562
|
requestAbort,
|
|
@@ -1529,7 +1604,9 @@ const MAX_YIELD_RETRIES = 3;
|
|
|
1529
1604
|
/**
|
|
1530
1605
|
* Drive one assignment through a live session: send the prompt, wait for idle,
|
|
1531
1606
|
* remind the agent to `yield` (up to {@link MAX_YIELD_RETRIES} times), then
|
|
1532
|
-
* classify the terminal assistant state.
|
|
1607
|
+
* classify the terminal assistant state. A soft-budget stop short-circuits the
|
|
1608
|
+
* reminder ladder into a single forced final yield so partial findings still
|
|
1609
|
+
* come back as a real report.
|
|
1533
1610
|
*/
|
|
1534
1611
|
async function driveSessionToYield(
|
|
1535
1612
|
session: AgentSession,
|
|
@@ -1570,13 +1647,30 @@ async function driveSessionToYield(
|
|
|
1570
1647
|
};
|
|
1571
1648
|
|
|
1572
1649
|
try {
|
|
1573
|
-
|
|
1574
|
-
|
|
1650
|
+
try {
|
|
1651
|
+
await awaitAbortable(session.prompt(task, { attribution: "agent" }));
|
|
1652
|
+
await awaitAbortable(session.waitForIdle());
|
|
1653
|
+
} catch (err) {
|
|
1654
|
+
// A budget stop cancels the free-running turn by aborting the
|
|
1655
|
+
// session, which can surface here as a rejected prompt. Swallow it
|
|
1656
|
+
// and drive the forced final yield below; real caller/timeout
|
|
1657
|
+
// aborts (monitor signal) and genuine failures keep the old path.
|
|
1658
|
+
if (!monitor.budgetStopRequested() || abortSignal.aborted) throw err;
|
|
1659
|
+
}
|
|
1575
1660
|
|
|
1576
1661
|
const reminderToolChoice = buildNamedToolChoice("yield", session.model);
|
|
1577
1662
|
|
|
1578
1663
|
let retryCount = 0;
|
|
1579
1664
|
while (!monitor.yieldCalled() && retryCount < MAX_YIELD_RETRIES && !abortSignal.aborted) {
|
|
1665
|
+
// A budget stop collapses the reminder ladder to a single forced
|
|
1666
|
+
// final yield: wait for the stop's session abort to settle, then
|
|
1667
|
+
// prompt once with the wrap-up reminder + named tool choice.
|
|
1668
|
+
const budgetStop = monitor.budgetStopRequested();
|
|
1669
|
+
if (budgetStop) {
|
|
1670
|
+
retryCount = MAX_YIELD_RETRIES - 1;
|
|
1671
|
+
await monitor.waitForBudgetStop();
|
|
1672
|
+
if (monitor.yieldCalled() || abortSignal.aborted) break;
|
|
1673
|
+
}
|
|
1580
1674
|
// Skip reminders when the model returned a terminal error (e.g.
|
|
1581
1675
|
// rate-limit cap hit, auth failure). Re-prompting would just
|
|
1582
1676
|
// hit the same wall, multiplying the failure noise without
|
|
@@ -1588,6 +1682,7 @@ async function driveSessionToYield(
|
|
|
1588
1682
|
const reminder = prompt.render(submitReminderTemplate, {
|
|
1589
1683
|
retryCount,
|
|
1590
1684
|
maxRetries: MAX_YIELD_RETRIES,
|
|
1685
|
+
budgetStop,
|
|
1591
1686
|
});
|
|
1592
1687
|
|
|
1593
1688
|
const isFinalRetry = retryCount >= MAX_YIELD_RETRIES;
|
|
@@ -1642,6 +1737,14 @@ async function driveSessionToYield(
|
|
|
1642
1737
|
error ??= lastAssistant.errorMessage || "Subagent failed";
|
|
1643
1738
|
}
|
|
1644
1739
|
}
|
|
1740
|
+
|
|
1741
|
+
// A budget-stopped run that still produced no yield is a budget abort:
|
|
1742
|
+
// surface the precise reason instead of a generic missing-yield failure.
|
|
1743
|
+
if (!monitor.yieldCalled() && monitor.budgetStopRequested() && !aborted) {
|
|
1744
|
+
aborted = true;
|
|
1745
|
+
abortReasonText ??= monitor.resolveAbortReasonText();
|
|
1746
|
+
exitCode = 1;
|
|
1747
|
+
}
|
|
1645
1748
|
} catch (err) {
|
|
1646
1749
|
if (abortSignal.aborted && monitor.yieldCalled() && !monitor.runtimeLimitExceeded()) {
|
|
1647
1750
|
exitCode = 0;
|
|
@@ -1672,7 +1775,6 @@ interface FinalizeRunArgs {
|
|
|
1672
1775
|
agent: AgentDefinition;
|
|
1673
1776
|
task: string;
|
|
1674
1777
|
assignment?: string;
|
|
1675
|
-
description?: string;
|
|
1676
1778
|
modelOverride?: string | string[];
|
|
1677
1779
|
outputSchema?: unknown;
|
|
1678
1780
|
signal?: AbortSignal;
|
|
@@ -1789,7 +1891,7 @@ async function finalizeRunResult(args: FinalizeRunArgs): Promise<SingleResult> {
|
|
|
1789
1891
|
parentToolCallId: args.parentToolCallId,
|
|
1790
1892
|
detached: args.detached,
|
|
1791
1893
|
agentSource: agent.source,
|
|
1792
|
-
description:
|
|
1894
|
+
description: progress.description,
|
|
1793
1895
|
status: progress.status as "completed" | "failed" | "aborted",
|
|
1794
1896
|
sessionFile: args.sessionFile,
|
|
1795
1897
|
index,
|
|
@@ -1803,7 +1905,7 @@ async function finalizeRunResult(args: FinalizeRunArgs): Promise<SingleResult> {
|
|
|
1803
1905
|
agentSource: agent.source,
|
|
1804
1906
|
task,
|
|
1805
1907
|
assignment,
|
|
1806
|
-
description:
|
|
1908
|
+
description: progress.description,
|
|
1807
1909
|
lastIntent: progress.lastIntent,
|
|
1808
1910
|
exitCode,
|
|
1809
1911
|
output: truncatedOutput,
|
|
@@ -1827,10 +1929,19 @@ async function finalizeRunResult(args: FinalizeRunArgs): Promise<SingleResult> {
|
|
|
1827
1929
|
};
|
|
1828
1930
|
}
|
|
1829
1931
|
|
|
1932
|
+
/**
|
|
1933
|
+
* Settle a subagent's registry lifecycle after a run: terminal teardown for
|
|
1934
|
+
* hard aborts, unregister for one-shot helpers, park for isolated runs, and
|
|
1935
|
+
* idle + lifecycle adoption for kept-alive agents. A soft-budget abort on a
|
|
1936
|
+
* kept-alive, revivable agent is treated as a self-inflicted stop rather than
|
|
1937
|
+
* a kill — the agent stays interrogable and resumable (irc wake / revival).
|
|
1938
|
+
*/
|
|
1830
1939
|
export async function finalizeSubagentLifecycle(args: {
|
|
1831
1940
|
id: string;
|
|
1832
1941
|
session: AgentSession;
|
|
1833
1942
|
aborted: boolean;
|
|
1943
|
+
/** Which watchdog (if any) requested the abort; decides revivability. */
|
|
1944
|
+
abortKind?: AbortReason;
|
|
1834
1945
|
keepAlive: boolean;
|
|
1835
1946
|
isolated: boolean;
|
|
1836
1947
|
agentIdleTtlMs: number;
|
|
@@ -1845,8 +1956,12 @@ export async function finalizeSubagentLifecycle(args: {
|
|
|
1845
1956
|
}
|
|
1846
1957
|
};
|
|
1847
1958
|
|
|
1848
|
-
|
|
1849
|
-
|
|
1959
|
+
// A budget abort leaves a consistent session with its transcript on disk;
|
|
1960
|
+
// caller signals, wall-clock timeouts (possible stream hang), and internal
|
|
1961
|
+
// terminations are genuine kills and stay terminal.
|
|
1962
|
+
const resumableAbort =
|
|
1963
|
+
args.abortKind === "budget" && args.keepAlive && !args.isolated && args.reviveSession !== null;
|
|
1964
|
+
if (args.aborted && !resumableAbort) {
|
|
1850
1965
|
registry.setStatus(args.id, "aborted");
|
|
1851
1966
|
await disposeSession();
|
|
1852
1967
|
return;
|
|
@@ -1974,7 +2089,6 @@ export async function runSubagentFollowUpTurn(options: FollowUpTurnOptions): Pro
|
|
|
1974
2089
|
id,
|
|
1975
2090
|
agent,
|
|
1976
2091
|
task: message,
|
|
1977
|
-
description: options.description,
|
|
1978
2092
|
signal,
|
|
1979
2093
|
artifactsDir: options.artifactsDir,
|
|
1980
2094
|
eventBus: options.eventBus,
|
|
@@ -2046,12 +2160,6 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
2046
2160
|
options.parentServiceTier,
|
|
2047
2161
|
);
|
|
2048
2162
|
const maxRecursionDepth = settings.get("task.maxRecursionDepth") ?? 2;
|
|
2049
|
-
// Tailored specialist identity for this spawn. `subagentRole` is the full
|
|
2050
|
-
// (trimmed) role text fed to the system-prompt preamble; `subagentDisplayName`
|
|
2051
|
-
// is the label-normalized form the registry/roster show, falling back to the
|
|
2052
|
-
// agent type name when no role was given.
|
|
2053
|
-
const subagentRole = options.role?.trim() || undefined;
|
|
2054
|
-
const subagentDisplayName = resolveSubagentDisplayName(options.role, agent.name);
|
|
2055
2163
|
const maxRuntimeMs = Math.max(
|
|
2056
2164
|
0,
|
|
2057
2165
|
Math.trunc(Number(options.maxRuntimeMs ?? settings.get("task.maxRuntimeMs") ?? 0) || 0),
|
|
@@ -2117,6 +2225,8 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
2117
2225
|
task,
|
|
2118
2226
|
assignment,
|
|
2119
2227
|
description: options.description,
|
|
2228
|
+
modelRegistry: options.modelRegistry,
|
|
2229
|
+
settings,
|
|
2120
2230
|
modelOverride,
|
|
2121
2231
|
signal,
|
|
2122
2232
|
onProgress,
|
|
@@ -2252,7 +2362,11 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
2252
2362
|
? formatModelSelectorValue(formatModelStringWithRouting(model), resolvedThinkingLevel)
|
|
2253
2363
|
: formatModelStringWithRouting(model);
|
|
2254
2364
|
}
|
|
2255
|
-
|
|
2365
|
+
// Precedence: explicit `:level` suffix on the resolved model pattern >
|
|
2366
|
+
// agent-definition default (e.g. task's `auto`) > pattern-derived level.
|
|
2367
|
+
const effectiveThinkingLevel = explicitThinkingLevel
|
|
2368
|
+
? resolvedThinkingLevel
|
|
2369
|
+
: (thinkingLevel ?? resolvedThinkingLevel);
|
|
2256
2370
|
resolvedAt = performance.now();
|
|
2257
2371
|
|
|
2258
2372
|
const effectiveCwd = worktree ?? cwd;
|
|
@@ -2280,8 +2394,8 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
2280
2394
|
const subagentAgentIdentity: AgentIdentity | undefined = options.parentTelemetry
|
|
2281
2395
|
? {
|
|
2282
2396
|
id,
|
|
2283
|
-
name:
|
|
2284
|
-
description:
|
|
2397
|
+
name: agent.name,
|
|
2398
|
+
description: agent.description,
|
|
2285
2399
|
}
|
|
2286
2400
|
: undefined;
|
|
2287
2401
|
const subagentTelemetry: AgentTelemetryConfig | undefined =
|
|
@@ -2337,7 +2451,6 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
2337
2451
|
systemPrompt: defaultPrompt => {
|
|
2338
2452
|
const subagentPrompt = prompt.render(subagentSystemPromptTemplate, {
|
|
2339
2453
|
agent: agent.systemPrompt,
|
|
2340
|
-
role: subagentRole ? oneLineLabel(subagentRole) : "",
|
|
2341
2454
|
context: options.context?.trim() ?? "",
|
|
2342
2455
|
planReference: options.planReference?.content ?? "",
|
|
2343
2456
|
planReferencePath: options.planReference?.path ?? "",
|
|
@@ -2360,7 +2473,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
2360
2473
|
parentTaskPrefix: id,
|
|
2361
2474
|
parentAgentId: options.parentAgentId,
|
|
2362
2475
|
agentId: id,
|
|
2363
|
-
agentDisplayName:
|
|
2476
|
+
agentDisplayName: agent.name,
|
|
2364
2477
|
enableLsp: lspEnabled,
|
|
2365
2478
|
skipPythonPreflight,
|
|
2366
2479
|
enableMCP,
|
|
@@ -2571,6 +2684,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
2571
2684
|
id,
|
|
2572
2685
|
session,
|
|
2573
2686
|
aborted,
|
|
2687
|
+
abortKind: monitor.abortKind(),
|
|
2574
2688
|
keepAlive: options.keepAlive !== false,
|
|
2575
2689
|
isolated: worktree !== undefined,
|
|
2576
2690
|
agentIdleTtlMs,
|
|
@@ -2629,7 +2743,6 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
2629
2743
|
agent,
|
|
2630
2744
|
task,
|
|
2631
2745
|
assignment,
|
|
2632
|
-
description: options.description,
|
|
2633
2746
|
modelOverride,
|
|
2634
2747
|
outputSchema,
|
|
2635
2748
|
signal,
|