@nredd/pi-subagents 0.22.0-nredd.6 → 0.22.0-nredd.8
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/README.md +160 -43
- package/package.json +8 -8
- package/src/agent-file-toggle.ts +6 -4
- package/src/agent-manager.ts +418 -118
- package/src/agent-runner.ts +155 -62
- package/src/cross-extension-rpc.ts +11 -36
- package/src/custom-agents.ts +9 -5
- package/src/default-agents.ts +2 -4
- package/src/enabled-models.ts +8 -7
- package/src/enterprise-local.ts +5 -5
- package/src/home-paths.ts +27 -0
- package/src/index.ts +231 -107
- package/src/invocation-config.ts +8 -35
- package/src/memory.ts +9 -9
- package/src/mention-clone.ts +9 -5
- package/src/model-resolver.ts +41 -11
- package/src/model-scope.ts +2 -1
- package/src/model-selection.ts +159 -0
- package/src/model-tiers.ts +171 -0
- package/src/nested-tools.ts +31 -26
- package/src/output-file.ts +23 -1
- package/src/package-resources.ts +25 -10
- package/src/process-budget.ts +346 -0
- package/src/quota-admission.ts +21 -9
- package/src/resource-loader.ts +51 -0
- package/src/schedule.ts +41 -19
- package/src/settings.ts +111 -8
- package/src/skill-loader.ts +8 -7
- package/src/stale-ctx.ts +16 -0
- package/src/types.ts +33 -0
- package/src/ui/agent-row.ts +13 -3
- package/src/ui/agent-widget.ts +15 -4
- package/src/ui/conversation-viewer.ts +6 -3
- package/src/ui/fallback-flag.ts +90 -0
- package/src/ui/fleet-list.ts +363 -53
- package/src/ui/fleet-state.ts +154 -0
- package/src/ui/notification-view.ts +18 -2
- package/src/ui/workflow-card.ts +5 -1
- package/src/ui/workflow-dialog.ts +10 -0
- package/src/workflow/host.ts +43 -45
- package/src/workflow/journal.ts +13 -1
- package/src/workflow/progress.ts +4 -0
- package/src/workflow/runtime.ts +25 -8
- package/src/workflow/saved.ts +19 -17
- package/src/workflow/task.ts +10 -0
- package/src/workflow/worker-source.ts +156 -38
package/src/index.ts
CHANGED
|
@@ -12,13 +12,13 @@
|
|
|
12
12
|
|
|
13
13
|
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
14
14
|
import { isAbsolute, join } from "node:path";
|
|
15
|
-
import { defineTool, type ExtensionAPI, type ExtensionCommandContext, type ExtensionContext, type ExtensionFactory,
|
|
15
|
+
import { defineTool, type ExtensionAPI, type ExtensionCommandContext, type ExtensionContext, type ExtensionFactory, getSettingsListTheme } from "@earendil-works/pi-coding-agent";
|
|
16
16
|
import { Container, isKeyRelease, Key, matchesKey, type ScrollViewScrollbar, type SettingItem, SettingsList, Spacer, Text } from "@earendil-works/pi-tui";
|
|
17
17
|
import { nanoid } from "nanoid";
|
|
18
18
|
import { Type } from "typebox";
|
|
19
19
|
import { abortable } from "./abortable.js";
|
|
20
20
|
import { hasAgentBadge, renderAgentName } from "./agent-color.js";
|
|
21
|
-
import { buildNewAgentFile, disableInContent, enableInContent, isEmptyStub, locateAgentFile, personalAgentsDir, projectAgentsDir, serializeAgentFile } from "./agent-file-toggle.js";
|
|
21
|
+
import { buildNewAgentFile, disableInContent, enableInContent, isEmptyStub, locateAgentFile as locateAgentFileAtHome, personalAgentsDir as personalAgentsDirAtHome, projectAgentsDir, serializeAgentFile } from "./agent-file-toggle.js";
|
|
22
22
|
import { AgentManager, describeEvicted, isTopLevelAgent, markConsumed } from "./agent-manager.js";
|
|
23
23
|
import { getAgentConversation, getDefaultMaxTurns, getGraceTurns, getRememberAgents, normalizeMaxTurns, resolveEffectiveMaxTurns, SUBAGENT_TOOL_NAMES, setDefaultMaxTurns, setGraceTurns, setRememberAgents, steerAgent } from "./agent-runner.js";
|
|
24
24
|
import { rowDescription, sourceIndicator, sourceLegend } from "./agent-source-badge.js";
|
|
@@ -39,15 +39,19 @@ import {
|
|
|
39
39
|
} from "./detached-results.js";
|
|
40
40
|
import { readEnabledModels, resolveEnabledModels } from "./enabled-models.js";
|
|
41
41
|
import { GroupJoinManager } from "./group-join.js";
|
|
42
|
+
import { resolveAgentDir } from "./home-paths.js";
|
|
42
43
|
import { cwdParam, isolationParam, resolveAgentInvocationConfig, resolveCwdParam, resolveJoinMode } from "./invocation-config.js";
|
|
43
44
|
import { describeMention, handleBase, isReservedHandle, parseMention, resolveHandleToType, stripAgentPrefix } from "./mention.js";
|
|
44
45
|
import { runMentionClone } from "./mention-clone.js";
|
|
45
46
|
import { applyModelCallEvent, type ModelCallEvent } from "./model-call.js";
|
|
46
|
-
import {
|
|
47
|
-
import {
|
|
47
|
+
import { type ModelFallback, type ModelRegistry, setAgentModels, setModelAliases, setModelScopeProvider } from "./model-resolver.js";
|
|
48
|
+
import { isScopeModelsEnabled, setScopeModelsEnabled } from "./model-scope.js";
|
|
49
|
+
import { admitModelSelection, describeSelection, modelRequest, modelSelectionNote, selectModel } from "./model-selection.js";
|
|
50
|
+
import { tierDisclosure } from "./model-tiers.js";
|
|
48
51
|
import { getMaxSubagentDepth, setMaxSubagentDepth } from "./nested-tools.js";
|
|
49
|
-
import { createOutputFilePath, ensureOutputFile, getOutputTranscriptDefault, sessionTaskDir, setOutputTranscriptDefault, streamToOutputFile, writeInitialEntry } from "./output-file.js";
|
|
52
|
+
import { appendModelFallbackEntry, appendModelTierEntry, createOutputFilePath, ensureOutputFile, getOutputTranscriptDefault, sessionTaskDir, setOutputTranscriptDefault, streamToOutputFile, writeInitialEntry } from "./output-file.js";
|
|
50
53
|
import { getPackageAgentsGate, getPackageWorkflowsGate, invalidatePackageCache, isProjectTrusted, packageNameForPath, setPackageAgentsGate, setPackageWorkflowsGate, setProjectTrusted } from "./package-resources.js";
|
|
54
|
+
import { getBudgetPolicy, setBudgetPolicy } from "./process-budget.js";
|
|
51
55
|
import { checkAdmission, describeBlock, mayPark, modelRef, QUOTA_RESTART_GRACE_MS } from "./quota-admission.js";
|
|
52
56
|
import { SubagentScheduler } from "./schedule.js";
|
|
53
57
|
import { resolveStorePath, ScheduleStore } from "./schedule-store.js";
|
|
@@ -74,6 +78,7 @@ import {
|
|
|
74
78
|
} from "./ui/agent-widget.js";
|
|
75
79
|
import { ConversationViewer, getConversationOverlayOptions, type ViewerBackgroundAction } from "./ui/conversation-viewer.js";
|
|
76
80
|
import { FleetList, type FleetUICtx, type FleetWorkflow } from "./ui/fleet-list.js";
|
|
81
|
+
import { FLEET_STATE_ENTRY, FLEET_STATE_VERSION } from "./ui/fleet-state.js";
|
|
77
82
|
import { renderNotification } from "./ui/notification-view.js";
|
|
78
83
|
import { showSchedulesMenu } from "./ui/schedule-menu.js";
|
|
79
84
|
import { selectItem } from "./ui/select-item.js";
|
|
@@ -203,6 +208,7 @@ function formatTaskNotification(record: AgentRecord, resultMaxLen: number, showC
|
|
|
203
208
|
record.outputFile ? `<output-file>${escapeXml(record.outputFile)}</output-file>` : null,
|
|
204
209
|
`<status>${escapeXml(status)}</status>`,
|
|
205
210
|
`<summary>Agent "${escapeXml(record.description)}" ${record.status}${getStatusNote(record.status)}</summary>`,
|
|
211
|
+
modelSelectionNote(record) ? `<model-note>${escapeXml(modelSelectionNote(record))}</model-note>` : null,
|
|
206
212
|
`<result>${escapeXml(resultPreview)}</result>`,
|
|
207
213
|
`<usage><total_tokens>${totalTokens}</total_tokens><tool_uses>${record.toolUses}</tool_uses>${ctxXml}${compactXml}${costXml}<duration_ms>${durationMs}</duration_ms></usage>`,
|
|
208
214
|
`</task-notification>`,
|
|
@@ -211,7 +217,7 @@ function formatTaskNotification(record: AgentRecord, resultMaxLen: number, showC
|
|
|
211
217
|
|
|
212
218
|
/** Build AgentDetails from a base + record-specific fields. */
|
|
213
219
|
function buildDetails(
|
|
214
|
-
base: Pick<AgentDetails, "displayName" | "description" | "subagentType" | "modelName" | "tags">,
|
|
220
|
+
base: Pick<AgentDetails, "displayName" | "description" | "subagentType" | "modelName" | "modelNote" | "modelFallback" | "modelTier" | "tags">,
|
|
215
221
|
record: { toolUses: number; startedAt: number; completedAt?: number; status: string; error?: string; id?: string; session?: any; outputFile?: string; lifetimeUsage: LifetimeUsage },
|
|
216
222
|
activity?: AgentActivity,
|
|
217
223
|
overrides?: Partial<AgentDetails>,
|
|
@@ -254,6 +260,8 @@ function buildNotificationDetails(record: AgentRecord, resultMaxLen: number, act
|
|
|
254
260
|
durationMs: record.completedAt ? record.completedAt - record.startedAt : 0,
|
|
255
261
|
outputFile: record.outputFile,
|
|
256
262
|
error: record.error,
|
|
263
|
+
modelNote: modelSelectionNote(record) || undefined,
|
|
264
|
+
modelTier: record.modelTier,
|
|
257
265
|
resultPreview: record.result
|
|
258
266
|
? record.result.length > resultMaxLen
|
|
259
267
|
? record.result.slice(0, resultMaxLen) + "…"
|
|
@@ -286,12 +294,18 @@ function buildNotificationDetails(record: AgentRecord, resultMaxLen: number, act
|
|
|
286
294
|
* agents and those with `extensions: false`.
|
|
287
295
|
*/
|
|
288
296
|
export function formatToolsSuffix(cfg: AgentConfig | undefined): string {
|
|
289
|
-
const
|
|
297
|
+
const denied = cfg?.disallowedTools ?? [];
|
|
298
|
+
const tools = denied.length > 0
|
|
299
|
+
? (cfg?.builtinToolNames ?? BUILTIN_TOOL_NAMES).filter((tool) => !denied.includes(tool))
|
|
300
|
+
: cfg?.builtinToolNames;
|
|
301
|
+
// Only claim what is known at registration: extensions may supply command
|
|
302
|
+
// tools (including ssh_exec), so missing bash is not proof of no remote access.
|
|
303
|
+
const shellNote = tools && !tools.includes("bash") ? "; no built-in shell: cannot run bash commands" : "";
|
|
290
304
|
// `isolated` overrides extensions to false in the runner, so both mean the
|
|
291
305
|
// agent has no extension tools.
|
|
292
306
|
const noExtensionTools = cfg?.isolated === true || cfg?.extensions === false;
|
|
293
307
|
if (tools && tools.length === 0) {
|
|
294
|
-
return noExtensionTools ? "none" : "no built-ins, extension tools only";
|
|
308
|
+
return (noExtensionTools ? "none" : "no built-ins, extension tools only") + shellNote;
|
|
295
309
|
}
|
|
296
310
|
const isFullSet = !tools
|
|
297
311
|
|| (tools.length === BUILTIN_TOOL_NAMES.length && BUILTIN_TOOL_NAMES.every((t) => tools.includes(t)));
|
|
@@ -299,7 +313,7 @@ export function formatToolsSuffix(cfg: AgentConfig | undefined): string {
|
|
|
299
313
|
// Extension tools resolve at run time, so they cannot be enumerated here, but
|
|
300
314
|
// omitting the fact reads as "built-ins only": an agent that can search the
|
|
301
315
|
// web through an extension then looks web-less, and the work gets routed away.
|
|
302
|
-
return noExtensionTools ? builtins : `${builtins} + extension tools
|
|
316
|
+
return (noExtensionTools ? builtins : `${builtins} + extension tools`) + shellNote;
|
|
303
317
|
}
|
|
304
318
|
|
|
305
319
|
/** CLI flag that runs a workflow script at session start. */
|
|
@@ -319,6 +333,8 @@ export type { Clock, TimerHandle } from "./clock.js";
|
|
|
319
333
|
|
|
320
334
|
/** Optional host dependencies, shared by the scheduler and completion delivery. */
|
|
321
335
|
export interface SubagentsExtensionOptions {
|
|
336
|
+
/** Home for every user-scoped read and child run. Omitted uses host defaults at call time. */
|
|
337
|
+
homeDir?: string;
|
|
322
338
|
clock?: Clock;
|
|
323
339
|
}
|
|
324
340
|
|
|
@@ -331,11 +347,29 @@ export default createSubagentsExtension();
|
|
|
331
347
|
|
|
332
348
|
function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions) {
|
|
333
349
|
const clock = options.clock ?? realClock;
|
|
350
|
+
const getAgentDir = () => resolveAgentDir(options.homeDir);
|
|
351
|
+
const personalAgentsDir = () => personalAgentsDirAtHome(options.homeDir);
|
|
352
|
+
const locateAgentFile: typeof locateAgentFileAtHome = (name, path, cwd, source) =>
|
|
353
|
+
locateAgentFileAtHome(name, path, cwd, source, options.homeDir);
|
|
334
354
|
// Child AgentSessions load normal extensions. Re-entering this extension there
|
|
335
355
|
// would create another manager and leak handlers. Nested orchestration is
|
|
336
356
|
// injected as scoped custom tools by the existing manager instead.
|
|
337
357
|
if (inChildSessionContext()) return;
|
|
338
358
|
|
|
359
|
+
// A factory throw is only a load warning. Check shared-state compatibility
|
|
360
|
+
// before registering anything, and make an incompatible reload fail the
|
|
361
|
+
// bind through the host's fatal session_start path instead of loading half
|
|
362
|
+
// an extension (or continuing without the process-wide budget).
|
|
363
|
+
const StartupError = hostStartupErrorClass();
|
|
364
|
+
try {
|
|
365
|
+
getBudgetPolicy();
|
|
366
|
+
} catch (error) {
|
|
367
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
368
|
+
if (!StartupError) throw new Error(message + " This host also lacks ExtensionStartupError; upgrade pi before retrying.", { cause: error });
|
|
369
|
+
pi.on("session_start", async () => { throw new StartupError(message, { cause: error }); });
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
339
373
|
// ---- Register custom notification renderer ----
|
|
340
374
|
// Core collapses a custom message to this renderer's first line and draws the
|
|
341
375
|
// ▸/▾ gutter itself; see ui/notification-view.ts.
|
|
@@ -368,16 +402,15 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
|
|
|
368
402
|
});
|
|
369
403
|
|
|
370
404
|
// Fuzzy model names ("opus") prefer what the user enabled in pi's settings.
|
|
371
|
-
setModelScopeProvider(registry => resolveEnabledModels(readEnabledModels(process.cwd()), registry, process.cwd()));
|
|
405
|
+
setModelScopeProvider(registry => resolveEnabledModels(readEnabledModels(process.cwd(), options.homeDir), registry, process.cwd(), options.homeDir));
|
|
372
406
|
|
|
373
407
|
// Read directly rather than waiting for applyAndEmitLoaded below: these decide
|
|
374
408
|
// the initial load, which happens hundreds of lines before settings are applied.
|
|
375
|
-
const bootSettings = loadSettings(process.cwd());
|
|
409
|
+
const bootSettings = loadSettings(process.cwd(), options.homeDir);
|
|
376
410
|
let strictAgentFiles = bootSettings.strictAgentFiles === true;
|
|
377
411
|
// Strict agent files fail a bind by throwing the host's `ExtensionStartupError`
|
|
378
412
|
// from `session_start`. A host without that class would silently degrade to
|
|
379
413
|
// warnings, so refuse here, before any project agent definition is read.
|
|
380
|
-
const StartupError = hostStartupErrorClass();
|
|
381
414
|
if (strictAgentFiles && !StartupError) {
|
|
382
415
|
throw new Error(
|
|
383
416
|
"[pi-subagents] strictAgentFiles requires a pi host that exports ExtensionStartupError, which this host lacks. " +
|
|
@@ -398,7 +431,7 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
|
|
|
398
431
|
|
|
399
432
|
/** Reload agents from package/global/project agent dirs and merge with defaults (called on init and each Agent invocation). */
|
|
400
433
|
const reloadCustomAgents = (strict = false) => {
|
|
401
|
-
const userAgents = loadCustomAgents(process.cwd(), strict);
|
|
434
|
+
const userAgents = loadCustomAgents(process.cwd(), strict, { homeDir: options.homeDir });
|
|
402
435
|
registerAgents(userAgents);
|
|
403
436
|
};
|
|
404
437
|
|
|
@@ -557,6 +590,8 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
|
|
|
557
590
|
durationMs,
|
|
558
591
|
tokens,
|
|
559
592
|
usage,
|
|
593
|
+
// Present only on a fallback, so the payload keeps its shape otherwise.
|
|
594
|
+
...fallbackFields(record),
|
|
560
595
|
};
|
|
561
596
|
}
|
|
562
597
|
|
|
@@ -566,6 +601,26 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
|
|
|
566
601
|
*/
|
|
567
602
|
let detachedSessionFile: string | undefined;
|
|
568
603
|
|
|
604
|
+
/** `modelFallback` + `modelNote` event fields, or nothing. Only what is KNOWN when the event fires. */
|
|
605
|
+
function fallbackFields(record: AgentRecord | undefined): Record<string, unknown> {
|
|
606
|
+
return {
|
|
607
|
+
...(record?.modelTier ? { modelTier: record.modelTier } : {}),
|
|
608
|
+
...(record?.modelFallback ? { modelFallback: record.modelFallback } : {}),
|
|
609
|
+
...(modelSelectionNote(record) ? { modelNote: modelSelectionNote(record) } : {}),
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/** The `subagents:record` history entry; the ONE builder, shared by live completion and the detached spool. */
|
|
614
|
+
function historyRecord(record: AgentRecord): Record<string, unknown> {
|
|
615
|
+
return {
|
|
616
|
+
id: record.id, type: record.type, description: record.description,
|
|
617
|
+
status: record.status, result: record.result, error: record.error,
|
|
618
|
+
startedAt: record.startedAt, completedAt: record.completedAt,
|
|
619
|
+
...(record.modelFallback ? { modelFallback: record.modelFallback } : {}),
|
|
620
|
+
...(record.modelTier ? { modelTier: record.modelTier } : {}),
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
|
|
569
624
|
function spoolDetachedResult(sessionFile: string, record: AgentRecord) {
|
|
570
625
|
if (record.resultConsumed) return;
|
|
571
626
|
const footer = record.outputFile ? `\nFull transcript available at: ${record.outputFile}` : "";
|
|
@@ -574,11 +629,7 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
|
|
|
574
629
|
id: record.id,
|
|
575
630
|
content: formatTaskNotification(record, 500, showCost) + footer,
|
|
576
631
|
details: buildNotificationDetails(record, 500),
|
|
577
|
-
record:
|
|
578
|
-
id: record.id, type: record.type, description: record.description,
|
|
579
|
-
status: record.status, result: record.result, error: record.error,
|
|
580
|
-
startedAt: record.startedAt, completedAt: record.completedAt,
|
|
581
|
-
},
|
|
632
|
+
record: historyRecord(record),
|
|
582
633
|
});
|
|
583
634
|
} catch { /* spool unwritable: the transcript file still has the result */ }
|
|
584
635
|
}
|
|
@@ -590,7 +641,11 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
|
|
|
590
641
|
|
|
591
642
|
function deliverDetachedResult(result: DetachedResult) {
|
|
592
643
|
try {
|
|
593
|
-
if (result.record)
|
|
644
|
+
if (result.record) {
|
|
645
|
+
pi.appendEntry("subagents:record", result.record);
|
|
646
|
+
fleet.ingestFailure(result.record);
|
|
647
|
+
fleet.update();
|
|
648
|
+
}
|
|
594
649
|
const details = result.details as NotificationDetails;
|
|
595
650
|
outbox.add({
|
|
596
651
|
ids: [details.id, ...(details.others?.map(other => other.id) ?? [])],
|
|
@@ -617,6 +672,7 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
|
|
|
617
672
|
return;
|
|
618
673
|
}
|
|
619
674
|
|
|
675
|
+
if (!record.resultConsumed) fleet.ingestFailure(record);
|
|
620
676
|
// Emit lifecycle event based on terminal status
|
|
621
677
|
const isError = record.status === "error" || record.status === "stopped" || record.status === "aborted";
|
|
622
678
|
const eventData = buildEventData(record);
|
|
@@ -627,11 +683,7 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
|
|
|
627
683
|
}
|
|
628
684
|
|
|
629
685
|
// Persist final record for cross-extension history reconstruction
|
|
630
|
-
pi.appendEntry("subagents:record",
|
|
631
|
-
id: record.id, type: record.type, description: record.description,
|
|
632
|
-
status: record.status, result: record.result, error: record.error,
|
|
633
|
-
startedAt: record.startedAt, completedAt: record.completedAt,
|
|
634
|
-
});
|
|
686
|
+
pi.appendEntry("subagents:record", historyRecord(record));
|
|
635
687
|
|
|
636
688
|
// Skip notification if result was already consumed via get_subagent_result
|
|
637
689
|
if (record.resultConsumed) {
|
|
@@ -671,6 +723,7 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
|
|
|
671
723
|
id: record.id,
|
|
672
724
|
type: record.type,
|
|
673
725
|
description: record.description,
|
|
726
|
+
...fallbackFields(record),
|
|
674
727
|
});
|
|
675
728
|
}, (record, info) => {
|
|
676
729
|
if (!isTopLevelAgent(record)) return;
|
|
@@ -682,6 +735,7 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
|
|
|
682
735
|
reason: info.reason,
|
|
683
736
|
tokensBefore: info.tokensBefore,
|
|
684
737
|
compactionCount: record.compactionCount,
|
|
738
|
+
...fallbackFields(record),
|
|
685
739
|
});
|
|
686
740
|
}, (record, usage) => {
|
|
687
741
|
// Every assistant message from every agent — nested included, exactly once.
|
|
@@ -717,6 +771,7 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
|
|
|
717
771
|
// clamped it to the model, or an agent file outranked it), so a ledger
|
|
718
772
|
// can show `low (asked minimal)` instead of silently relabelling.
|
|
719
773
|
...(invocation?.requestedThinking !== undefined ? { requestedThinking: invocation.requestedThinking } : {}),
|
|
774
|
+
...fallbackFields(record),
|
|
720
775
|
depth: record.depth ?? 1,
|
|
721
776
|
...(record.parentAgentId !== undefined ? { parentAgentId: record.parentAgentId } : {}),
|
|
722
777
|
...(record.workflowId !== undefined ? { workflowId: record.workflowId } : {}),
|
|
@@ -725,7 +780,7 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
|
|
|
725
780
|
}, (record, message) => {
|
|
726
781
|
if (!isTopLevelAgent(record)) return;
|
|
727
782
|
pi.events.emit("subagents:steered", { id: record.id, message });
|
|
728
|
-
}, clock);
|
|
783
|
+
}, clock, options.homeDir);
|
|
729
784
|
|
|
730
785
|
// Expose manager via Symbol.for() global registry for cross-package access.
|
|
731
786
|
// Standard Node.js pattern for cross-package singletons (used by OpenTelemetry, etc.).
|
|
@@ -800,6 +855,13 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
|
|
|
800
855
|
// awaits nothing. A forged `blocking` would charge it to the foreground
|
|
801
856
|
// pool and could defer it behind a queue whose gate nobody is holding.
|
|
802
857
|
delete safeOptions.blocking;
|
|
858
|
+
// The spawn path's own disclosure; a caller-supplied one could hide a
|
|
859
|
+
// fallback or invent one. The runner reports the real outcome itself.
|
|
860
|
+
delete safeOptions.modelFallback;
|
|
861
|
+
delete safeOptions.modelSelection;
|
|
862
|
+
delete safeOptions.modelTier;
|
|
863
|
+
delete safeOptions.previousModelTier;
|
|
864
|
+
delete safeOptions.previousModelFallback;
|
|
803
865
|
return spawnResolved(piRef, ctxRef, type, prompt, safeOptions);
|
|
804
866
|
};
|
|
805
867
|
|
|
@@ -833,6 +895,7 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
|
|
|
833
895
|
|
|
834
896
|
// --- Cross-extension RPC via pi.events ---
|
|
835
897
|
let currentCtx: ExtensionContext | undefined;
|
|
898
|
+
let unwatchFleetSchedules: (() => void) | undefined;
|
|
836
899
|
// RPC handlers + the `subagents:ready` broadcast are wired on `session_start`
|
|
837
900
|
// (a bound lifecycle event), not at factory time. pi runs every extension
|
|
838
901
|
// factory before the `extensions:` filter and only fires lifecycle events for
|
|
@@ -848,7 +911,7 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
|
|
|
848
911
|
// Session-scoped: store is constructed inside session_start once sessionId
|
|
849
912
|
// is available. Mirrors pi-chonky-tasks's session-scoped task store —
|
|
850
913
|
// schedules reset on /new, restore on /resume.
|
|
851
|
-
const scheduler = new SubagentScheduler({ clock: options.clock });
|
|
914
|
+
const scheduler = new SubagentScheduler({ clock: options.clock, homeDir: options.homeDir });
|
|
852
915
|
|
|
853
916
|
function startScheduler(ctx: ExtensionContext) {
|
|
854
917
|
try {
|
|
@@ -870,6 +933,7 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
|
|
|
870
933
|
// bound session_start, so a filtered-out activation never advertises (#142).
|
|
871
934
|
pi.on("session_start", async (_event, ctx) => {
|
|
872
935
|
currentCtx = ctx;
|
|
936
|
+
restoreFleetDisclosure(ctx);
|
|
873
937
|
// Pi resolves project trust before the first session starts, so this is the
|
|
874
938
|
// earliest honest answer. The activation-time load defaulted to untrusted,
|
|
875
939
|
// so a trusted project's `packages[]` only becomes visible now.
|
|
@@ -897,6 +961,7 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
|
|
|
897
961
|
widget.setUICtx(ctx.ui);
|
|
898
962
|
fleet.setUICtx(ctx.ui as any);
|
|
899
963
|
watchHandoffKey(ctx.ui);
|
|
964
|
+
unwatchFleetSchedules ??= pi.events.on("subagents:scheduled", () => fleet.update());
|
|
900
965
|
}
|
|
901
966
|
manager.clearCompleted(true);
|
|
902
967
|
const sessionFile = ctx.sessionManager?.getSessionFile?.();
|
|
@@ -911,6 +976,7 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
|
|
|
911
976
|
// fires once per activation, but a double-bind must not leak listeners.
|
|
912
977
|
if (!rpcHandle) {
|
|
913
978
|
rpcHandle = registerRpcHandlers({
|
|
979
|
+
homeDir: options.homeDir,
|
|
914
980
|
events: pi.events,
|
|
915
981
|
pi,
|
|
916
982
|
getCtx: () => currentCtx,
|
|
@@ -930,6 +996,7 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
|
|
|
930
996
|
if (!record || record.parentAgentId) return false;
|
|
931
997
|
if (record.status === "running" || record.status === "queued") return false;
|
|
932
998
|
markConsumed(record, clock.now());
|
|
999
|
+
fleet.acknowledgeFailure(record.id);
|
|
933
1000
|
outbox.cancel(record.id);
|
|
934
1001
|
return true;
|
|
935
1002
|
},
|
|
@@ -942,6 +1009,7 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
|
|
|
942
1009
|
pi.events.emit("subagents:ready", {});
|
|
943
1010
|
}
|
|
944
1011
|
if (isSchedulingEnabled() && !scheduler.isActive()) startScheduler(ctx);
|
|
1012
|
+
fleet.update(); // restored quota-parked jobs have no agent record
|
|
945
1013
|
// Stack `@handle` suggestions on pi's built-in autocomplete. Registered at
|
|
946
1014
|
// most once per activation: pi appends wrappers to a list it never prunes,
|
|
947
1015
|
// so a second call would layer a duplicate provider on the first. TUI only
|
|
@@ -1102,6 +1170,9 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
|
|
|
1102
1170
|
description: entry.description,
|
|
1103
1171
|
reclaim: { handle: entry.handle, alias: entry.alias },
|
|
1104
1172
|
resumeSessionFile: entry.sessionFile,
|
|
1173
|
+
// Historical disclosures never replace the continuation's current selection.
|
|
1174
|
+
...(entry.modelFallback ? { previousModelFallback: entry.modelFallback } : {}),
|
|
1175
|
+
...(entry.modelTier ? { previousModelTier: entry.modelTier } : {}),
|
|
1105
1176
|
isBackground: true,
|
|
1106
1177
|
});
|
|
1107
1178
|
// The agent may still be starting — wait, so a startup failure lands in
|
|
@@ -1155,7 +1226,7 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
|
|
|
1155
1226
|
// Not awaited: the clone runs a full model turn, and prompt() is blocked
|
|
1156
1227
|
// until this hook returns. The user gets their prompt back immediately
|
|
1157
1228
|
// and the agent appears in the widget when it starts.
|
|
1158
|
-
void runMentionClone({ ctx, type, message: mention.message, agentTool: registeredAgentTool })
|
|
1229
|
+
void runMentionClone({ ctx, type, message: mention.message, agentTool: registeredAgentTool, homeDir: options.homeDir })
|
|
1159
1230
|
.then(async (result) => {
|
|
1160
1231
|
if (result.spawned) return;
|
|
1161
1232
|
// A clone that could not run must not swallow the mention: start the
|
|
@@ -1181,7 +1252,7 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
|
|
|
1181
1252
|
}
|
|
1182
1253
|
|
|
1183
1254
|
try {
|
|
1184
|
-
// Nothing else to pass:
|
|
1255
|
+
// Nothing else to pass: the manager resolves model; runAgent resolves thinking, max turns and
|
|
1185
1256
|
// `isolated` from the agent's own config when the spawn omits them, and the
|
|
1186
1257
|
// manager's onStart/onComplete callbacks own the widget, the fleet list
|
|
1187
1258
|
// and the completion notification — the same contract the scheduler and
|
|
@@ -1213,6 +1284,7 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
|
|
|
1213
1284
|
// to this session (see detached-results.ts). Only agents that cannot outlive the
|
|
1214
1285
|
// session are aborted.
|
|
1215
1286
|
pi.on("session_shutdown", async (event: { reason?: string }) => {
|
|
1287
|
+
fleet.flushState(); // appendEntry must run before this activation loses its context
|
|
1216
1288
|
const sessionFile = currentCtx?.sessionManager?.getSessionFile?.();
|
|
1217
1289
|
unwatchDetached?.();
|
|
1218
1290
|
unwatchDetached = undefined;
|
|
@@ -1262,6 +1334,8 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
|
|
|
1262
1334
|
widget.dispose();
|
|
1263
1335
|
outbox.dispose();
|
|
1264
1336
|
fleet.dispose();
|
|
1337
|
+
unwatchFleetSchedules?.();
|
|
1338
|
+
unwatchFleetSchedules = undefined;
|
|
1265
1339
|
unsubHandoffKey?.();
|
|
1266
1340
|
unsubHandoffKey = undefined;
|
|
1267
1341
|
handoffKeyUi = undefined;
|
|
@@ -1334,6 +1408,7 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
|
|
|
1334
1408
|
type: record.type,
|
|
1335
1409
|
description: record.description,
|
|
1336
1410
|
isBackground: true,
|
|
1411
|
+
...fallbackFields(record),
|
|
1337
1412
|
});
|
|
1338
1413
|
}
|
|
1339
1414
|
}
|
|
@@ -1398,6 +1473,17 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
|
|
|
1398
1473
|
getFullscreenScrollbar, backgroundActionFor, isShowModelEnabled,
|
|
1399
1474
|
// Agents the previous instance of this session left running (/reload, /new, ...).
|
|
1400
1475
|
() => listRunningDetached(currentCtx?.sessionManager?.getSessionFile?.()), clock);
|
|
1476
|
+
fleet.setParkedSource(() => scheduler.list().filter(job => job.enabled && job.quotaParked && job.lastStatus !== "running").length);
|
|
1477
|
+
/** Replay UI-only state from the active branch, like workflow/goal custom snapshots. */
|
|
1478
|
+
function restoreFleetDisclosure(ctx: ExtensionContext): void {
|
|
1479
|
+
fleet.restoreSession(() => ctx.sessionManager?.getBranch?.() ?? [], state => {
|
|
1480
|
+
pi.appendEntry(FLEET_STATE_ENTRY, { ...state, version: FLEET_STATE_VERSION });
|
|
1481
|
+
});
|
|
1482
|
+
}
|
|
1483
|
+
pi.on("session_tree", (_event, ctx) => {
|
|
1484
|
+
restoreFleetDisclosure(ctx);
|
|
1485
|
+
fleet.update();
|
|
1486
|
+
});
|
|
1401
1487
|
/** What the Agent tool row reads at render time, so its summary stays live. */
|
|
1402
1488
|
const agentRowLive: AgentRowLive = {
|
|
1403
1489
|
getRecord: (id) => manager.getRecord(id),
|
|
@@ -1570,6 +1656,9 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
|
|
|
1570
1656
|
if (opts.outputTranscript) {
|
|
1571
1657
|
existing.outputFile = createOutputFilePath(ctx.cwd, id, ctx.sessionManager.getSessionId());
|
|
1572
1658
|
ensureOutputFile(existing.outputFile);
|
|
1659
|
+
// A fresh transcript for a later resume must still say what the agent ran on.
|
|
1660
|
+
if (existing.modelFallback) appendModelFallbackEntry(existing.outputFile, id, existing.modelFallback);
|
|
1661
|
+
if (existing.modelTier) appendModelTierEntry(existing.outputFile, id, existing.modelTier);
|
|
1573
1662
|
}
|
|
1574
1663
|
// Anchor streaming past the turns already on disk, captured BEFORE the
|
|
1575
1664
|
// run starts. The resumed prompt lands as an ordinary user message at
|
|
@@ -1628,6 +1717,7 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
|
|
|
1628
1717
|
type: existing.type,
|
|
1629
1718
|
description: existing.description,
|
|
1630
1719
|
isBackground: true,
|
|
1720
|
+
...fallbackFields(existing),
|
|
1631
1721
|
});
|
|
1632
1722
|
|
|
1633
1723
|
return record;
|
|
@@ -1683,6 +1773,13 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
|
|
|
1683
1773
|
return name.replace(/-\d{8}$/, "");
|
|
1684
1774
|
}
|
|
1685
1775
|
|
|
1776
|
+
/** The "queued" line of a background result; names the process budget only when it is on. */
|
|
1777
|
+
const queuedPositionLine = (): string => {
|
|
1778
|
+
const { maxActive } = getBudgetPolicy();
|
|
1779
|
+
const budget = maxActive > 0 ? `, ${maxActive} process-wide` : "";
|
|
1780
|
+
return `Position: queued (max ${manager.getMaxConcurrent()} concurrent${budget})\n`;
|
|
1781
|
+
};
|
|
1782
|
+
|
|
1686
1783
|
// Apply persisted settings on startup and emit `subagents:settings_loaded`.
|
|
1687
1784
|
// Global + project merged; missing → defaults; corrupt file emits a warning
|
|
1688
1785
|
// to stderr and falls back to defaults.
|
|
@@ -1690,6 +1787,7 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
|
|
|
1690
1787
|
{
|
|
1691
1788
|
setMaxConcurrent: (n) => manager.setMaxConcurrent(n),
|
|
1692
1789
|
setMaxConcurrentForeground: (n) => manager.setMaxConcurrentForeground(n),
|
|
1790
|
+
setBudgetPolicy,
|
|
1693
1791
|
setDefaultMaxTurns,
|
|
1694
1792
|
setGraceTurns,
|
|
1695
1793
|
setDefaultJoinMode,
|
|
@@ -1718,6 +1816,8 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
|
|
|
1718
1816
|
setPackageWorkflows: setPackageWorkflowsGate,
|
|
1719
1817
|
},
|
|
1720
1818
|
(event, payload) => pi.events.emit(event, payload),
|
|
1819
|
+
process.cwd(),
|
|
1820
|
+
options.homeDir,
|
|
1721
1821
|
);
|
|
1722
1822
|
|
|
1723
1823
|
// ---- Agent tool ----
|
|
@@ -1800,7 +1900,7 @@ If the target is already known, use a direct tool — \`read\` for a known path,
|
|
|
1800
1900
|
- Use steer_subagent to send mid-run messages to a running background agent.
|
|
1801
1901
|
- Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, etc.), since it is not aware of the user's intent.
|
|
1802
1902
|
- If an agent's description says it should be used proactively, try to use it without the user having to ask for it first.
|
|
1803
|
-
- Use model to
|
|
1903
|
+
- Use model to select a configured tier (e.g. "@cheap"), an exact "provider/modelId", or a fuzzy tool-call input (e.g. "haiku").
|
|
1804
1904
|
- Use thinking to control extended thinking level.
|
|
1805
1905
|
- Use inherit_context if the agent needs the parent conversation history.${isolationGuideline}${scheduleGuideline}
|
|
1806
1906
|
|
|
@@ -1902,7 +2002,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1902
2002
|
model: Type.Optional(
|
|
1903
2003
|
Type.String({
|
|
1904
2004
|
description:
|
|
1905
|
-
'Optional model override. Accepts "provider/modelId" or fuzzy name (e.g. "haiku"
|
|
2005
|
+
'Optional model override. Accepts a configured tier (e.g. "@cheap"), "provider/modelId", or fuzzy name (e.g. "haiku"). Explicit values win over agentModels and the agent definition.',
|
|
1906
2006
|
}),
|
|
1907
2007
|
),
|
|
1908
2008
|
thinking: Type.Optional(
|
|
@@ -2018,7 +2118,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2018
2118
|
// foreground path. Resume deliberately doesn't: it replays the stored
|
|
2019
2119
|
// session and ignores `subagent_type` entirely, so a note about type
|
|
2020
2120
|
// substitution would be describing something that didn't happen.
|
|
2021
|
-
const
|
|
2121
|
+
const typeNote = dispatch.ok && dispatch.fellBackFrom !== undefined
|
|
2022
2122
|
? `Note: Unknown agent type "${dispatch.fellBackFrom}" — using ${resolveType(subagentType) ? subagentType : "the fallback agent config"}.\n\n`
|
|
2023
2123
|
: "";
|
|
2024
2124
|
|
|
@@ -2028,36 +2128,22 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2028
2128
|
const customConfig = getAgentConfig(subagentType);
|
|
2029
2129
|
|
|
2030
2130
|
const resolvedConfig = resolveAgentInvocationConfig(customConfig, params, {
|
|
2031
|
-
agentType: subagentType,
|
|
2032
2131
|
worktreeAllowed: isWorktreeIsolationEnabled(),
|
|
2033
2132
|
defaultRunInBackground: getBackgroundByDefault(),
|
|
2034
2133
|
});
|
|
2035
2134
|
|
|
2036
|
-
//
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
if (resolvedConfig.modelFromParams) return textResult(resolved);
|
|
2042
|
-
// config-specified: silent fallback to parent
|
|
2043
|
-
} else {
|
|
2044
|
-
model = resolved;
|
|
2045
|
-
}
|
|
2046
|
-
}
|
|
2047
|
-
|
|
2048
|
-
// Scope validation: the effective resolved model is checked against the
|
|
2049
|
-
// user's enabledModels list. Policy (hard error vs warn-and-proceed) lives
|
|
2050
|
-
// in model-scope.ts so the nested delegation tools apply the same rule.
|
|
2051
|
-
const scopeVerdict = checkModelScope({
|
|
2052
|
-
model,
|
|
2053
|
-
cwd: ctx.cwd,
|
|
2054
|
-
modelRegistry: ctx.modelRegistry,
|
|
2055
|
-
callerSupplied: resolvedConfig.modelFromParams,
|
|
2056
|
-
agentLabel: customConfig?.displayName ?? subagentType,
|
|
2057
|
-
modelInput: resolvedConfig.modelInput,
|
|
2135
|
+
// A live resume keeps its record's selection, ignoring new model inputs.
|
|
2136
|
+
const selection = params.resume ? undefined : selectModel({
|
|
2137
|
+
homeDir: options.homeDir,
|
|
2138
|
+
agentType: subagentType, definitionModel: customConfig?.model, paramModel: params.model,
|
|
2139
|
+
parentModel: ctx.model, registry: ctx.modelRegistry, cwd: ctx.cwd, agentLabel: customConfig?.displayName,
|
|
2058
2140
|
});
|
|
2059
|
-
if (
|
|
2060
|
-
|
|
2141
|
+
if (typeof selection === "string") return { ...textResult(selection), isError: true };
|
|
2142
|
+
const tierBlock = selection?.tierBinding && !params.schedule ? await admitModelSelection(selection, ctx.modelRegistry, pi.events) : undefined;
|
|
2143
|
+
const model = selection?.model;
|
|
2144
|
+
const modelFallback = selection?.fallback;
|
|
2145
|
+
|
|
2146
|
+
if (selection?.scopeWarning) ctx.ui.notify(selection.scopeWarning, "warning");
|
|
2061
2147
|
|
|
2062
2148
|
const thinking = resolvedConfig.thinking;
|
|
2063
2149
|
const inheritContext = resolvedConfig.inheritContext;
|
|
@@ -2066,7 +2152,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2066
2152
|
const isolation = resolvedConfig.isolation;
|
|
2067
2153
|
// Undefined = the session cwd. The manager validates it (absolute,
|
|
2068
2154
|
// existing directory) at spawn and again at start.
|
|
2069
|
-
const cwd = resolveCwdParam(params.cwd, ctx.cwd);
|
|
2155
|
+
const cwd = resolveCwdParam(params.cwd, ctx.cwd, options.homeDir);
|
|
2070
2156
|
// Whether this spawn writes its .output transcript. Per-agent
|
|
2071
2157
|
// frontmatter (`output_transcript`) wins; otherwise the project/global
|
|
2072
2158
|
// default applies. `attachTranscript` below is the SOLE gate — every
|
|
@@ -2084,17 +2170,12 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2084
2170
|
// parent's model used to show the level with nothing to attach it to.
|
|
2085
2171
|
// This is the pre-session snapshot — agent-manager overwrites it with the
|
|
2086
2172
|
// effective values the moment a session reports them.
|
|
2087
|
-
const { modelName, modelId } =
|
|
2088
|
-
? isVirtualModel(model) ? describeVirtualModel(model) : describeModel(model)
|
|
2089
|
-
: { modelName: undefined, modelId: undefined };
|
|
2173
|
+
const { modelName, modelId } = describeSelection(selection, ctx.modelRegistry);
|
|
2090
2174
|
const effectiveMaxTurns = normalizeMaxTurns(resolvedConfig.maxTurns ?? getDefaultMaxTurns());
|
|
2091
2175
|
const agentInvocation: AgentInvocation = {
|
|
2092
2176
|
modelName,
|
|
2093
2177
|
modelId,
|
|
2094
2178
|
thinking,
|
|
2095
|
-
// Only set where the agent file outranked the caller, so the surfaces can
|
|
2096
|
-
// disclose a parameter that was accepted but could not take effect (#182).
|
|
2097
|
-
requestedThinking: resolvedConfig.overridden?.thinking,
|
|
2098
2179
|
// Explicit value only — the default fallback would just add noise.
|
|
2099
2180
|
// Normalize so `0` (unlimited) doesn't surface as a misleading "max turns: 0".
|
|
2100
2181
|
maxTurns: normalizeMaxTurns(resolvedConfig.maxTurns),
|
|
@@ -2107,11 +2188,33 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2107
2188
|
const modeLabel = getPromptModeLabel(subagentType);
|
|
2108
2189
|
const { tags: invocationTags } = buildInvocationTags(agentInvocation);
|
|
2109
2190
|
const agentTags = modeLabel ? [modeLabel, ...invocationTags] : invocationTags;
|
|
2191
|
+
/**
|
|
2192
|
+
* The lead-in for a result: the type note, then the model note. The model
|
|
2193
|
+
* note is read off the record the shared spawn path created. Before a
|
|
2194
|
+
* record exists (scheduling) it is this call's own resolution, the only
|
|
2195
|
+
* one there is. Never recomputed for a resume, which runs on its old session.
|
|
2196
|
+
*/
|
|
2197
|
+
const modelNoteOf = (rec: AgentRecord | undefined): string | undefined =>
|
|
2198
|
+
modelSelectionNote(rec ?? { modelFallback, modelTier: selection?.tier }) || undefined;
|
|
2199
|
+
const modelFallbackOf = (rec: AgentRecord | undefined): ModelFallback | undefined =>
|
|
2200
|
+
rec ? rec.modelFallback : modelFallback;
|
|
2201
|
+
/** A resume's lead-in: the record's note only (the type note describes a spawn, not a resume). */
|
|
2202
|
+
const resumeNoteFor = (rec: AgentRecord): string => {
|
|
2203
|
+
const note = modelNoteOf(rec);
|
|
2204
|
+
return note ? `${note}\n\n` : "";
|
|
2205
|
+
};
|
|
2206
|
+
const fallbackNoteFor = (rec: AgentRecord | undefined): string => {
|
|
2207
|
+
const modelNote = modelNoteOf(rec);
|
|
2208
|
+
return typeNote + (modelNote ? `${modelNote}\n\n` : "");
|
|
2209
|
+
};
|
|
2110
2210
|
const detailBase = {
|
|
2111
2211
|
displayName,
|
|
2112
2212
|
description: params.description,
|
|
2113
2213
|
subagentType,
|
|
2114
2214
|
modelName,
|
|
2215
|
+
modelNote: modelNoteOf(undefined),
|
|
2216
|
+
modelFallback: modelFallbackOf(undefined),
|
|
2217
|
+
modelTier: selection?.tier ? tierDisclosure(selection.tier) : undefined,
|
|
2115
2218
|
tags: agentTags.length > 0 ? agentTags : undefined,
|
|
2116
2219
|
};
|
|
2117
2220
|
|
|
@@ -2129,7 +2232,10 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2129
2232
|
* buildInvocationTags would silently drop `twin`.
|
|
2130
2233
|
*/
|
|
2131
2234
|
const detailBaseFor = (rec: AgentRecord | undefined): typeof detailBase => {
|
|
2132
|
-
|
|
2235
|
+
// A record's disclosure outranks this call's, even before its session
|
|
2236
|
+
// reports an invocation: resume reuses the old session, so this call's
|
|
2237
|
+
// config says nothing about what ran.
|
|
2238
|
+
if (!rec?.invocation) return rec ? { ...detailBase, modelNote: modelNoteOf(rec), modelFallback: modelFallbackOf(rec), modelTier: rec.modelTier } : detailBase;
|
|
2133
2239
|
const type = rec.type;
|
|
2134
2240
|
const { modelName: recModelName, tags } = buildInvocationTags(rec.invocation);
|
|
2135
2241
|
const recModeLabel = getPromptModeLabel(type);
|
|
@@ -2139,6 +2245,9 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2139
2245
|
description: rec.description,
|
|
2140
2246
|
subagentType: type,
|
|
2141
2247
|
modelName: recModelName,
|
|
2248
|
+
modelNote: modelNoteOf(rec),
|
|
2249
|
+
modelFallback: modelFallbackOf(rec),
|
|
2250
|
+
modelTier: rec.modelTier,
|
|
2142
2251
|
tags: recTags.length > 0 ? recTags : undefined,
|
|
2143
2252
|
};
|
|
2144
2253
|
};
|
|
@@ -2172,7 +2281,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2172
2281
|
// at fire time, and the original is what a user edits.
|
|
2173
2282
|
subagent_type: requestedType,
|
|
2174
2283
|
prompt: params.prompt as string,
|
|
2175
|
-
model: params.model as string | undefined,
|
|
2284
|
+
model: selection?.requested?.startsWith("@") ? selection.requested : params.model as string | undefined,
|
|
2176
2285
|
thinking: thinking,
|
|
2177
2286
|
max_turns: effectiveMaxTurns,
|
|
2178
2287
|
isolated: isolated,
|
|
@@ -2180,7 +2289,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2180
2289
|
});
|
|
2181
2290
|
const next = scheduler.getNextRun(job.id);
|
|
2182
2291
|
return textResult(
|
|
2183
|
-
`${
|
|
2292
|
+
`${fallbackNoteFor(undefined)}Scheduled "${job.name}" (id: ${job.id}, type: ${job.scheduleType}). ` +
|
|
2184
2293
|
`Next run: ${next ?? "(unknown)"}. ` +
|
|
2185
2294
|
`Manage via /agents → Scheduled jobs.`,
|
|
2186
2295
|
);
|
|
@@ -2206,19 +2315,21 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2206
2315
|
// a background spawn. Previously run_in_background was silently ignored
|
|
2207
2316
|
// on resume (this branch returned before the background branch below),
|
|
2208
2317
|
// so a resumed agent always blocked the main loop until it finished.
|
|
2318
|
+
// A detached resume hands control back while the record stays
|
|
2319
|
+
// "running", and parallel tool calls can resume one agent twice in a
|
|
2320
|
+
// turn, so nothing stops the model from resuming an agent mid-run.
|
|
2321
|
+
// manager.resume() refuses that on both paths (it would orphan the live
|
|
2322
|
+
// run's abort controller); say why here, where the model can act on it,
|
|
2323
|
+
// instead of letting it read as a generic failure.
|
|
2324
|
+
if (existing.status === "running" || existing.status === "queued") {
|
|
2325
|
+
return textResult(
|
|
2326
|
+
`Agent "${params.resume}" is still ${existing.status} — it can only be resumed once its current run finishes.\n` +
|
|
2327
|
+
`Use steer_subagent to send it a message mid-run, or get_subagent_result to wait for it.`,
|
|
2328
|
+
);
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2209
2331
|
if (runInBackground) {
|
|
2210
2332
|
const id = existing.id;
|
|
2211
|
-
// A detached resume hands control back while the record stays
|
|
2212
|
-
// "running", so nothing stops the model from resuming the same agent
|
|
2213
|
-
// again mid-run. manager.resume() refuses that (it would orphan the
|
|
2214
|
-
// live run's abort controller); say why here, where the model can act
|
|
2215
|
-
// on it, instead of letting it read as a generic failure.
|
|
2216
|
-
if (existing.status === "running" || existing.status === "queued") {
|
|
2217
|
-
return textResult(
|
|
2218
|
-
`Agent "${params.resume}" is still ${existing.status} — it can only be resumed once its current run finishes.\n` +
|
|
2219
|
-
`Use steer_subagent to send it a message mid-run, or get_subagent_result to wait for it.`,
|
|
2220
|
-
);
|
|
2221
|
-
}
|
|
2222
2333
|
|
|
2223
2334
|
const record = await startBackgroundResume(ctx, existing, params.prompt, {
|
|
2224
2335
|
outputTranscript,
|
|
@@ -2231,11 +2342,11 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2231
2342
|
|
|
2232
2343
|
const isQueued = record.status === "queued";
|
|
2233
2344
|
return textResult(
|
|
2234
|
-
|
|
2345
|
+
`${resumeNoteFor(record)}Agent ${isQueued ? "queued" : "resumed"} in background.\n` +
|
|
2235
2346
|
`Agent ID: ${id}\n` +
|
|
2236
2347
|
`Type: ${existing.type}\n` +
|
|
2237
2348
|
(record.outputFile ? `Output file: ${record.outputFile}\n` : "") +
|
|
2238
|
-
(isQueued ?
|
|
2349
|
+
(isQueued ? queuedPositionLine() : "") +
|
|
2239
2350
|
`\nYou will be notified when this agent completes.\n` +
|
|
2240
2351
|
`Use get_subagent_result to retrieve full results, or steer_subagent to send it messages.`,
|
|
2241
2352
|
{ ...detailBaseFor(record), toolUses: record.toolUses, tokens: "", durationMs: 0, status: "background" as const, agentId: id },
|
|
@@ -2273,10 +2384,10 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2273
2384
|
// A failed resume surfaces the error, plus any partial output THIS
|
|
2274
2385
|
// resume produced (never the previous turn's answer, #144).
|
|
2275
2386
|
if (record.status === "error") {
|
|
2276
|
-
return textResult(
|
|
2387
|
+
return textResult(`${resumeNoteFor(record)}Agent failed: ${record.error}${partialOutputSuffix(record)}`, buildDetails(detailBaseFor(record), record, resumeState));
|
|
2277
2388
|
}
|
|
2278
2389
|
return textResult(
|
|
2279
|
-
record.result?.trim() || "No output."
|
|
2390
|
+
`${resumeNoteFor(record)}${record.result?.trim() || "No output."}`,
|
|
2280
2391
|
buildDetails(detailBaseFor(record), record, resumeState),
|
|
2281
2392
|
);
|
|
2282
2393
|
}
|
|
@@ -2287,12 +2398,12 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2287
2398
|
const backgroundResult = (id: string, record: AgentRecord | undefined) => {
|
|
2288
2399
|
const isQueued = record?.status === "queued";
|
|
2289
2400
|
return textResult(
|
|
2290
|
-
`${
|
|
2401
|
+
`${fallbackNoteFor(record)}Agent ${isQueued ? "queued" : "started"} in background.\n` +
|
|
2291
2402
|
`Agent ID: ${id}\n` +
|
|
2292
2403
|
`Type: ${displayName}\n` +
|
|
2293
2404
|
`Description: ${params.description}\n` +
|
|
2294
2405
|
(record?.outputFile ? `Output file: ${record.outputFile}\n` : "") +
|
|
2295
|
-
(isQueued ?
|
|
2406
|
+
(isQueued ? queuedPositionLine() : "") +
|
|
2296
2407
|
`\nYou will be notified when this agent completes.\n` +
|
|
2297
2408
|
`Use get_subagent_result to retrieve full results, or steer_subagent to send it messages.\n` +
|
|
2298
2409
|
`Do not duplicate this agent's work.`,
|
|
@@ -2306,13 +2417,14 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2306
2417
|
// exhaustion policy says fail now (`wait: false`). Anything else fails
|
|
2307
2418
|
// now, with the reset, rather than starting on an exhausted provider.
|
|
2308
2419
|
const dispatchModel = model ?? ctx.model;
|
|
2309
|
-
const quotaBlock =
|
|
2420
|
+
const quotaBlock = selection?.tierBinding ? tierBlock
|
|
2421
|
+
: dispatchModel ? await checkAdmission(pi.events, modelRef(dispatchModel), undefined, clock) : undefined;
|
|
2310
2422
|
if (quotaBlock) {
|
|
2311
2423
|
// A parked job is a scheduled job, which has no `cwd` to carry.
|
|
2312
2424
|
const canPark = runInBackground && !inheritContext && cwd === undefined && mayPark(quotaBlock)
|
|
2313
2425
|
&& isSchedulingEnabled() && scheduler.isActive();
|
|
2314
2426
|
if (!canPark || !mayPark(quotaBlock)) {
|
|
2315
|
-
throw new Error(`${describeBlock(quotaBlock)} The agent was not started.`);
|
|
2427
|
+
throw new Error(`${selection?.tier ? `${modelNoteOf(undefined)}\n` : ""}${describeBlock(quotaBlock)} The agent was not started.`);
|
|
2316
2428
|
}
|
|
2317
2429
|
const job = scheduler.addJob({
|
|
2318
2430
|
name: `${params.description} (quota ${nanoid(4)})`,
|
|
@@ -2320,7 +2432,9 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2320
2432
|
schedule: new Date(quotaBlock.resetAt + QUOTA_RESTART_GRACE_MS).toISOString(),
|
|
2321
2433
|
subagent_type: requestedType,
|
|
2322
2434
|
prompt: params.prompt as string,
|
|
2323
|
-
|
|
2435
|
+
// Keep the request, not today's configured/parent pick. Fire-time
|
|
2436
|
+
// selection must still apply agentModels and aliases.
|
|
2437
|
+
model: selection?.requested?.startsWith("@") ? selection.requested : params.model,
|
|
2324
2438
|
thinking,
|
|
2325
2439
|
max_turns: effectiveMaxTurns,
|
|
2326
2440
|
isolated,
|
|
@@ -2328,7 +2442,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2328
2442
|
quotaParked: true,
|
|
2329
2443
|
});
|
|
2330
2444
|
return textResult(
|
|
2331
|
-
`${
|
|
2445
|
+
`${fallbackNoteFor(undefined)}${describeBlock(quotaBlock)} Queued "${job.name}" (id: ${job.id}) to start at ` +
|
|
2332
2446
|
`${scheduler.getNextRun(job.id) ?? job.schedule}. Manage via /agents → Scheduled jobs.`,
|
|
2333
2447
|
);
|
|
2334
2448
|
}
|
|
@@ -2354,6 +2468,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2354
2468
|
// tool call failed, whereas a returned message reads to the model as a
|
|
2355
2469
|
// subagent that ran and reported this (#179).
|
|
2356
2470
|
id = manager.spawn(pi, ctx, subagentType, params.prompt, {
|
|
2471
|
+
modelSelection: selection,
|
|
2357
2472
|
description: params.description,
|
|
2358
2473
|
name: params.name as string | undefined,
|
|
2359
2474
|
model,
|
|
@@ -2399,6 +2514,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2399
2514
|
type: subagentType,
|
|
2400
2515
|
description: params.description,
|
|
2401
2516
|
isBackground: true,
|
|
2517
|
+
...fallbackFields(manager.getRecord(id)),
|
|
2402
2518
|
});
|
|
2403
2519
|
|
|
2404
2520
|
return backgroundResult(id, record);
|
|
@@ -2507,6 +2623,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2507
2623
|
let fgResult: { record: AgentRecord } | undefined;
|
|
2508
2624
|
try {
|
|
2509
2625
|
fgResult = await Promise.race([manager.spawnAndWait(pi, ctx, subagentType, params.prompt, {
|
|
2626
|
+
modelSelection: selection,
|
|
2510
2627
|
description: params.description,
|
|
2511
2628
|
name: params.name as string | undefined,
|
|
2512
2629
|
model,
|
|
@@ -2570,7 +2687,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2570
2687
|
|
|
2571
2688
|
if (record.status === "error") {
|
|
2572
2689
|
// Error headline + any partial output the run produced before failing.
|
|
2573
|
-
return textResult(`${
|
|
2690
|
+
return textResult(`${fallbackNoteFor(record)}Agent failed: ${record.error}${partialOutputSuffix(record)}`, details);
|
|
2574
2691
|
}
|
|
2575
2692
|
|
|
2576
2693
|
const durationMs = (record.completedAt ?? Date.now()) - record.startedAt;
|
|
@@ -2581,7 +2698,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2581
2698
|
if (costText) statsParts.push(costText);
|
|
2582
2699
|
}
|
|
2583
2700
|
return textResult(
|
|
2584
|
-
`${
|
|
2701
|
+
`${fallbackNoteFor(record)}Agent completed in ${formatMs(durationMs)} (${statsParts.join(", ")})${getForegroundOutcomeNote(record.status)}.\n\n` +
|
|
2585
2702
|
(record.result?.trim() || "No output."),
|
|
2586
2703
|
details,
|
|
2587
2704
|
);
|
|
@@ -2671,6 +2788,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2671
2788
|
startedAt: task.startTime,
|
|
2672
2789
|
...(task.endTime !== undefined ? { completedAt: task.endTime } : {}),
|
|
2673
2790
|
tokens: task.totalTokens,
|
|
2791
|
+
observationKey: task,
|
|
2674
2792
|
}));
|
|
2675
2793
|
}
|
|
2676
2794
|
|
|
@@ -2687,6 +2805,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2687
2805
|
args: task.args,
|
|
2688
2806
|
signal: task.abortController.signal,
|
|
2689
2807
|
host: createWorkflowHost({
|
|
2808
|
+
homeDir: options.homeDir,
|
|
2690
2809
|
pi,
|
|
2691
2810
|
ctx,
|
|
2692
2811
|
manager,
|
|
@@ -2844,6 +2963,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2844
2963
|
? { scriptPath: resumeFrom.scriptPath }
|
|
2845
2964
|
: params,
|
|
2846
2965
|
ctx.cwd,
|
|
2966
|
+
options.homeDir,
|
|
2847
2967
|
);
|
|
2848
2968
|
if (!resolved.ok) return textResult(resolved.message);
|
|
2849
2969
|
|
|
@@ -3129,7 +3249,9 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
3129
3249
|
let output =
|
|
3130
3250
|
`Agent: ${record.id}\n` +
|
|
3131
3251
|
`Type: ${displayName} | Status: ${record.status}${getStatusNote(record.status)} | ${statsParts.join(" | ")}\n` +
|
|
3132
|
-
`Description: ${record.description}\n
|
|
3252
|
+
`Description: ${record.description}\n` +
|
|
3253
|
+
(modelSelectionNote(record) ? `${modelSelectionNote(record)}\n` : "") +
|
|
3254
|
+
`\n`;
|
|
3133
3255
|
|
|
3134
3256
|
if (record.status === "running") {
|
|
3135
3257
|
output += "Agent is still running. Use wait: true or check back later.";
|
|
@@ -3142,6 +3264,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
3142
3264
|
// Mark result as consumed — suppresses the completion notification
|
|
3143
3265
|
if (record.status !== "running" && record.status !== "queued") {
|
|
3144
3266
|
markConsumed(record, clock.now());
|
|
3267
|
+
fleet.acknowledgeFailure(record.id);
|
|
3145
3268
|
outbox.cancel(record.id);
|
|
3146
3269
|
}
|
|
3147
3270
|
|
|
@@ -3224,19 +3347,21 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
3224
3347
|
|
|
3225
3348
|
function getModelLabel(type: string, registry?: ModelRegistry): string {
|
|
3226
3349
|
const cfg = getAgentConfig(type);
|
|
3227
|
-
|
|
3228
|
-
|
|
3350
|
+
const input = modelRequest(type, cfg?.model).requested;
|
|
3351
|
+
if (!input) return "inherit";
|
|
3352
|
+
const label = getModelLabelFromConfig(input);
|
|
3229
3353
|
if (!registry) return label;
|
|
3230
|
-
const
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3354
|
+
const selection = selectModel({ homeDir: options.homeDir, agentType: type, definitionModel: cfg?.model, parentModel: undefined, registry, cwd: currentCtx?.cwd ?? process.cwd() });
|
|
3355
|
+
if (typeof selection === "string" || selection.outcome === "fallback" || !selection.model) {
|
|
3356
|
+
return `${label} (unavailable, fallback: inherit)`;
|
|
3357
|
+
}
|
|
3358
|
+
const resolved = selection.model;
|
|
3234
3359
|
// Surface what it actually resolved to when that differs from the config —
|
|
3235
3360
|
// e.g. a provider fallback or a looser version pin. Cosmetic separator/date
|
|
3236
3361
|
// differences are normalized away so an effectively-identical match stays quiet.
|
|
3237
3362
|
const resolvedFull = `${resolved.provider}/${resolved.id}`;
|
|
3238
3363
|
const norm = (s: string) => s.toLowerCase().replace(/\./g, "-").replace(/-\d{8}$/, "");
|
|
3239
|
-
if (norm(
|
|
3364
|
+
if (norm(input) === norm(resolvedFull)) return label;
|
|
3240
3365
|
return `${label} (→ ${resolvedFull.replace(/-\d{8}$/, "")})`;
|
|
3241
3366
|
}
|
|
3242
3367
|
|
|
@@ -3316,7 +3441,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
3316
3441
|
*/
|
|
3317
3442
|
function packageNameOf(cfg: AgentConfig | undefined): string | undefined {
|
|
3318
3443
|
if (cfg?.source !== "package" || !cfg.sourcePath) return undefined;
|
|
3319
|
-
return packageNameForPath(cfg.sourcePath, process.cwd());
|
|
3444
|
+
return packageNameForPath(cfg.sourcePath, process.cwd(), "agents", { homeDir: options.homeDir });
|
|
3320
3445
|
}
|
|
3321
3446
|
|
|
3322
3447
|
async function showAllAgentsList(ctx: ExtensionCommandContext) {
|
|
@@ -3370,8 +3495,9 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
3370
3495
|
};
|
|
3371
3496
|
});
|
|
3372
3497
|
|
|
3373
|
-
|
|
3374
|
-
|
|
3498
|
+
const cfg = selected ? getAgentConfig(selected) : undefined;
|
|
3499
|
+
if (selected && cfg) {
|
|
3500
|
+
await showAgentDetail(ctx, selected, cfg);
|
|
3375
3501
|
await showAllAgentsList(ctx);
|
|
3376
3502
|
}
|
|
3377
3503
|
}
|
|
@@ -3425,13 +3551,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
3425
3551
|
);
|
|
3426
3552
|
}
|
|
3427
3553
|
|
|
3428
|
-
async function showAgentDetail(ctx: ExtensionCommandContext, name: string) {
|
|
3429
|
-
const cfg = getAgentConfig(name);
|
|
3430
|
-
if (!cfg) {
|
|
3431
|
-
ctx.ui.notify(`Agent config not found for "${name}".`, "warning");
|
|
3432
|
-
return;
|
|
3433
|
-
}
|
|
3434
|
-
|
|
3554
|
+
async function showAgentDetail(ctx: ExtensionCommandContext, name: string, cfg: AgentConfig) {
|
|
3435
3555
|
const isPackage = cfg.source === "package";
|
|
3436
3556
|
const file = locateAgentFile(name, cfg.sourcePath, undefined, cfg.source);
|
|
3437
3557
|
const isDefault = cfg.isDefault === true;
|
|
@@ -3838,7 +3958,11 @@ Write the file using the write tool. Only write the file, nothing else.`;
|
|
|
3838
3958
|
// goes away. undefined is dropped by JSON.stringify.
|
|
3839
3959
|
fallbackSubagent: getFallbackSubagent(),
|
|
3840
3960
|
// Global-only (see loadSettings): never written into the project file.
|
|
3961
|
+
maxActiveSubagents: undefined,
|
|
3962
|
+
nestedSubagentReserve: undefined,
|
|
3841
3963
|
modelAliases: undefined,
|
|
3964
|
+
modelTiers: undefined,
|
|
3965
|
+
modelResolution: undefined,
|
|
3842
3966
|
agentModels: undefined,
|
|
3843
3967
|
reportUsage: isReportUsageEnabled(),
|
|
3844
3968
|
showCost: isShowCostEnabled(),
|