@nredd/pi-subagents 0.22.0-nredd.7 → 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 +124 -37
- package/package.json +8 -8
- package/src/agent-file-toggle.ts +6 -4
- package/src/agent-manager.ts +131 -28
- 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 +191 -90
- package/src/invocation-config.ts +3 -16
- 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 +22 -26
- package/src/output-file.ts +23 -1
- package/src/package-resources.ts +25 -10
- package/src/process-budget.ts +6 -3
- package/src/quota-admission.ts +21 -9
- package/src/resource-loader.ts +51 -0
- package/src/schedule.ts +36 -18
- package/src/settings.ts +42 -12
- package/src/skill-loader.ts +8 -7
- package/src/stale-ctx.ts +16 -0
- package/src/types.ts +27 -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,14 +39,17 @@ 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";
|
|
51
54
|
import { getBudgetPolicy, setBudgetPolicy } from "./process-budget.js";
|
|
52
55
|
import { checkAdmission, describeBlock, mayPark, modelRef, QUOTA_RESTART_GRACE_MS } from "./quota-admission.js";
|
|
@@ -75,6 +78,7 @@ import {
|
|
|
75
78
|
} from "./ui/agent-widget.js";
|
|
76
79
|
import { ConversationViewer, getConversationOverlayOptions, type ViewerBackgroundAction } from "./ui/conversation-viewer.js";
|
|
77
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";
|
|
78
82
|
import { renderNotification } from "./ui/notification-view.js";
|
|
79
83
|
import { showSchedulesMenu } from "./ui/schedule-menu.js";
|
|
80
84
|
import { selectItem } from "./ui/select-item.js";
|
|
@@ -204,6 +208,7 @@ function formatTaskNotification(record: AgentRecord, resultMaxLen: number, showC
|
|
|
204
208
|
record.outputFile ? `<output-file>${escapeXml(record.outputFile)}</output-file>` : null,
|
|
205
209
|
`<status>${escapeXml(status)}</status>`,
|
|
206
210
|
`<summary>Agent "${escapeXml(record.description)}" ${record.status}${getStatusNote(record.status)}</summary>`,
|
|
211
|
+
modelSelectionNote(record) ? `<model-note>${escapeXml(modelSelectionNote(record))}</model-note>` : null,
|
|
207
212
|
`<result>${escapeXml(resultPreview)}</result>`,
|
|
208
213
|
`<usage><total_tokens>${totalTokens}</total_tokens><tool_uses>${record.toolUses}</tool_uses>${ctxXml}${compactXml}${costXml}<duration_ms>${durationMs}</duration_ms></usage>`,
|
|
209
214
|
`</task-notification>`,
|
|
@@ -212,7 +217,7 @@ function formatTaskNotification(record: AgentRecord, resultMaxLen: number, showC
|
|
|
212
217
|
|
|
213
218
|
/** Build AgentDetails from a base + record-specific fields. */
|
|
214
219
|
function buildDetails(
|
|
215
|
-
base: Pick<AgentDetails, "displayName" | "description" | "subagentType" | "modelName" | "tags">,
|
|
220
|
+
base: Pick<AgentDetails, "displayName" | "description" | "subagentType" | "modelName" | "modelNote" | "modelFallback" | "modelTier" | "tags">,
|
|
216
221
|
record: { toolUses: number; startedAt: number; completedAt?: number; status: string; error?: string; id?: string; session?: any; outputFile?: string; lifetimeUsage: LifetimeUsage },
|
|
217
222
|
activity?: AgentActivity,
|
|
218
223
|
overrides?: Partial<AgentDetails>,
|
|
@@ -255,6 +260,8 @@ function buildNotificationDetails(record: AgentRecord, resultMaxLen: number, act
|
|
|
255
260
|
durationMs: record.completedAt ? record.completedAt - record.startedAt : 0,
|
|
256
261
|
outputFile: record.outputFile,
|
|
257
262
|
error: record.error,
|
|
263
|
+
modelNote: modelSelectionNote(record) || undefined,
|
|
264
|
+
modelTier: record.modelTier,
|
|
258
265
|
resultPreview: record.result
|
|
259
266
|
? record.result.length > resultMaxLen
|
|
260
267
|
? record.result.slice(0, resultMaxLen) + "…"
|
|
@@ -287,12 +294,18 @@ function buildNotificationDetails(record: AgentRecord, resultMaxLen: number, act
|
|
|
287
294
|
* agents and those with `extensions: false`.
|
|
288
295
|
*/
|
|
289
296
|
export function formatToolsSuffix(cfg: AgentConfig | undefined): string {
|
|
290
|
-
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" : "";
|
|
291
304
|
// `isolated` overrides extensions to false in the runner, so both mean the
|
|
292
305
|
// agent has no extension tools.
|
|
293
306
|
const noExtensionTools = cfg?.isolated === true || cfg?.extensions === false;
|
|
294
307
|
if (tools && tools.length === 0) {
|
|
295
|
-
return noExtensionTools ? "none" : "no built-ins, extension tools only";
|
|
308
|
+
return (noExtensionTools ? "none" : "no built-ins, extension tools only") + shellNote;
|
|
296
309
|
}
|
|
297
310
|
const isFullSet = !tools
|
|
298
311
|
|| (tools.length === BUILTIN_TOOL_NAMES.length && BUILTIN_TOOL_NAMES.every((t) => tools.includes(t)));
|
|
@@ -300,7 +313,7 @@ export function formatToolsSuffix(cfg: AgentConfig | undefined): string {
|
|
|
300
313
|
// Extension tools resolve at run time, so they cannot be enumerated here, but
|
|
301
314
|
// omitting the fact reads as "built-ins only": an agent that can search the
|
|
302
315
|
// web through an extension then looks web-less, and the work gets routed away.
|
|
303
|
-
return noExtensionTools ? builtins : `${builtins} + extension tools
|
|
316
|
+
return (noExtensionTools ? builtins : `${builtins} + extension tools`) + shellNote;
|
|
304
317
|
}
|
|
305
318
|
|
|
306
319
|
/** CLI flag that runs a workflow script at session start. */
|
|
@@ -320,6 +333,8 @@ export type { Clock, TimerHandle } from "./clock.js";
|
|
|
320
333
|
|
|
321
334
|
/** Optional host dependencies, shared by the scheduler and completion delivery. */
|
|
322
335
|
export interface SubagentsExtensionOptions {
|
|
336
|
+
/** Home for every user-scoped read and child run. Omitted uses host defaults at call time. */
|
|
337
|
+
homeDir?: string;
|
|
323
338
|
clock?: Clock;
|
|
324
339
|
}
|
|
325
340
|
|
|
@@ -332,6 +347,10 @@ export default createSubagentsExtension();
|
|
|
332
347
|
|
|
333
348
|
function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions) {
|
|
334
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);
|
|
335
354
|
// Child AgentSessions load normal extensions. Re-entering this extension there
|
|
336
355
|
// would create another manager and leak handlers. Nested orchestration is
|
|
337
356
|
// injected as scoped custom tools by the existing manager instead.
|
|
@@ -383,11 +402,11 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
|
|
|
383
402
|
});
|
|
384
403
|
|
|
385
404
|
// Fuzzy model names ("opus") prefer what the user enabled in pi's settings.
|
|
386
|
-
setModelScopeProvider(registry => resolveEnabledModels(readEnabledModels(process.cwd()), registry, process.cwd()));
|
|
405
|
+
setModelScopeProvider(registry => resolveEnabledModels(readEnabledModels(process.cwd(), options.homeDir), registry, process.cwd(), options.homeDir));
|
|
387
406
|
|
|
388
407
|
// Read directly rather than waiting for applyAndEmitLoaded below: these decide
|
|
389
408
|
// the initial load, which happens hundreds of lines before settings are applied.
|
|
390
|
-
const bootSettings = loadSettings(process.cwd());
|
|
409
|
+
const bootSettings = loadSettings(process.cwd(), options.homeDir);
|
|
391
410
|
let strictAgentFiles = bootSettings.strictAgentFiles === true;
|
|
392
411
|
// Strict agent files fail a bind by throwing the host's `ExtensionStartupError`
|
|
393
412
|
// from `session_start`. A host without that class would silently degrade to
|
|
@@ -412,7 +431,7 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
|
|
|
412
431
|
|
|
413
432
|
/** Reload agents from package/global/project agent dirs and merge with defaults (called on init and each Agent invocation). */
|
|
414
433
|
const reloadCustomAgents = (strict = false) => {
|
|
415
|
-
const userAgents = loadCustomAgents(process.cwd(), strict);
|
|
434
|
+
const userAgents = loadCustomAgents(process.cwd(), strict, { homeDir: options.homeDir });
|
|
416
435
|
registerAgents(userAgents);
|
|
417
436
|
};
|
|
418
437
|
|
|
@@ -571,6 +590,8 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
|
|
|
571
590
|
durationMs,
|
|
572
591
|
tokens,
|
|
573
592
|
usage,
|
|
593
|
+
// Present only on a fallback, so the payload keeps its shape otherwise.
|
|
594
|
+
...fallbackFields(record),
|
|
574
595
|
};
|
|
575
596
|
}
|
|
576
597
|
|
|
@@ -580,6 +601,26 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
|
|
|
580
601
|
*/
|
|
581
602
|
let detachedSessionFile: string | undefined;
|
|
582
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
|
+
|
|
583
624
|
function spoolDetachedResult(sessionFile: string, record: AgentRecord) {
|
|
584
625
|
if (record.resultConsumed) return;
|
|
585
626
|
const footer = record.outputFile ? `\nFull transcript available at: ${record.outputFile}` : "";
|
|
@@ -588,11 +629,7 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
|
|
|
588
629
|
id: record.id,
|
|
589
630
|
content: formatTaskNotification(record, 500, showCost) + footer,
|
|
590
631
|
details: buildNotificationDetails(record, 500),
|
|
591
|
-
record:
|
|
592
|
-
id: record.id, type: record.type, description: record.description,
|
|
593
|
-
status: record.status, result: record.result, error: record.error,
|
|
594
|
-
startedAt: record.startedAt, completedAt: record.completedAt,
|
|
595
|
-
},
|
|
632
|
+
record: historyRecord(record),
|
|
596
633
|
});
|
|
597
634
|
} catch { /* spool unwritable: the transcript file still has the result */ }
|
|
598
635
|
}
|
|
@@ -604,7 +641,11 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
|
|
|
604
641
|
|
|
605
642
|
function deliverDetachedResult(result: DetachedResult) {
|
|
606
643
|
try {
|
|
607
|
-
if (result.record)
|
|
644
|
+
if (result.record) {
|
|
645
|
+
pi.appendEntry("subagents:record", result.record);
|
|
646
|
+
fleet.ingestFailure(result.record);
|
|
647
|
+
fleet.update();
|
|
648
|
+
}
|
|
608
649
|
const details = result.details as NotificationDetails;
|
|
609
650
|
outbox.add({
|
|
610
651
|
ids: [details.id, ...(details.others?.map(other => other.id) ?? [])],
|
|
@@ -631,6 +672,7 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
|
|
|
631
672
|
return;
|
|
632
673
|
}
|
|
633
674
|
|
|
675
|
+
if (!record.resultConsumed) fleet.ingestFailure(record);
|
|
634
676
|
// Emit lifecycle event based on terminal status
|
|
635
677
|
const isError = record.status === "error" || record.status === "stopped" || record.status === "aborted";
|
|
636
678
|
const eventData = buildEventData(record);
|
|
@@ -641,11 +683,7 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
|
|
|
641
683
|
}
|
|
642
684
|
|
|
643
685
|
// Persist final record for cross-extension history reconstruction
|
|
644
|
-
pi.appendEntry("subagents:record",
|
|
645
|
-
id: record.id, type: record.type, description: record.description,
|
|
646
|
-
status: record.status, result: record.result, error: record.error,
|
|
647
|
-
startedAt: record.startedAt, completedAt: record.completedAt,
|
|
648
|
-
});
|
|
686
|
+
pi.appendEntry("subagents:record", historyRecord(record));
|
|
649
687
|
|
|
650
688
|
// Skip notification if result was already consumed via get_subagent_result
|
|
651
689
|
if (record.resultConsumed) {
|
|
@@ -685,6 +723,7 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
|
|
|
685
723
|
id: record.id,
|
|
686
724
|
type: record.type,
|
|
687
725
|
description: record.description,
|
|
726
|
+
...fallbackFields(record),
|
|
688
727
|
});
|
|
689
728
|
}, (record, info) => {
|
|
690
729
|
if (!isTopLevelAgent(record)) return;
|
|
@@ -696,6 +735,7 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
|
|
|
696
735
|
reason: info.reason,
|
|
697
736
|
tokensBefore: info.tokensBefore,
|
|
698
737
|
compactionCount: record.compactionCount,
|
|
738
|
+
...fallbackFields(record),
|
|
699
739
|
});
|
|
700
740
|
}, (record, usage) => {
|
|
701
741
|
// Every assistant message from every agent — nested included, exactly once.
|
|
@@ -731,6 +771,7 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
|
|
|
731
771
|
// clamped it to the model, or an agent file outranked it), so a ledger
|
|
732
772
|
// can show `low (asked minimal)` instead of silently relabelling.
|
|
733
773
|
...(invocation?.requestedThinking !== undefined ? { requestedThinking: invocation.requestedThinking } : {}),
|
|
774
|
+
...fallbackFields(record),
|
|
734
775
|
depth: record.depth ?? 1,
|
|
735
776
|
...(record.parentAgentId !== undefined ? { parentAgentId: record.parentAgentId } : {}),
|
|
736
777
|
...(record.workflowId !== undefined ? { workflowId: record.workflowId } : {}),
|
|
@@ -739,7 +780,7 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
|
|
|
739
780
|
}, (record, message) => {
|
|
740
781
|
if (!isTopLevelAgent(record)) return;
|
|
741
782
|
pi.events.emit("subagents:steered", { id: record.id, message });
|
|
742
|
-
}, clock);
|
|
783
|
+
}, clock, options.homeDir);
|
|
743
784
|
|
|
744
785
|
// Expose manager via Symbol.for() global registry for cross-package access.
|
|
745
786
|
// Standard Node.js pattern for cross-package singletons (used by OpenTelemetry, etc.).
|
|
@@ -814,6 +855,13 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
|
|
|
814
855
|
// awaits nothing. A forged `blocking` would charge it to the foreground
|
|
815
856
|
// pool and could defer it behind a queue whose gate nobody is holding.
|
|
816
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;
|
|
817
865
|
return spawnResolved(piRef, ctxRef, type, prompt, safeOptions);
|
|
818
866
|
};
|
|
819
867
|
|
|
@@ -847,6 +895,7 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
|
|
|
847
895
|
|
|
848
896
|
// --- Cross-extension RPC via pi.events ---
|
|
849
897
|
let currentCtx: ExtensionContext | undefined;
|
|
898
|
+
let unwatchFleetSchedules: (() => void) | undefined;
|
|
850
899
|
// RPC handlers + the `subagents:ready` broadcast are wired on `session_start`
|
|
851
900
|
// (a bound lifecycle event), not at factory time. pi runs every extension
|
|
852
901
|
// factory before the `extensions:` filter and only fires lifecycle events for
|
|
@@ -862,7 +911,7 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
|
|
|
862
911
|
// Session-scoped: store is constructed inside session_start once sessionId
|
|
863
912
|
// is available. Mirrors pi-chonky-tasks's session-scoped task store —
|
|
864
913
|
// schedules reset on /new, restore on /resume.
|
|
865
|
-
const scheduler = new SubagentScheduler({ clock: options.clock });
|
|
914
|
+
const scheduler = new SubagentScheduler({ clock: options.clock, homeDir: options.homeDir });
|
|
866
915
|
|
|
867
916
|
function startScheduler(ctx: ExtensionContext) {
|
|
868
917
|
try {
|
|
@@ -884,6 +933,7 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
|
|
|
884
933
|
// bound session_start, so a filtered-out activation never advertises (#142).
|
|
885
934
|
pi.on("session_start", async (_event, ctx) => {
|
|
886
935
|
currentCtx = ctx;
|
|
936
|
+
restoreFleetDisclosure(ctx);
|
|
887
937
|
// Pi resolves project trust before the first session starts, so this is the
|
|
888
938
|
// earliest honest answer. The activation-time load defaulted to untrusted,
|
|
889
939
|
// so a trusted project's `packages[]` only becomes visible now.
|
|
@@ -911,6 +961,7 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
|
|
|
911
961
|
widget.setUICtx(ctx.ui);
|
|
912
962
|
fleet.setUICtx(ctx.ui as any);
|
|
913
963
|
watchHandoffKey(ctx.ui);
|
|
964
|
+
unwatchFleetSchedules ??= pi.events.on("subagents:scheduled", () => fleet.update());
|
|
914
965
|
}
|
|
915
966
|
manager.clearCompleted(true);
|
|
916
967
|
const sessionFile = ctx.sessionManager?.getSessionFile?.();
|
|
@@ -925,6 +976,7 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
|
|
|
925
976
|
// fires once per activation, but a double-bind must not leak listeners.
|
|
926
977
|
if (!rpcHandle) {
|
|
927
978
|
rpcHandle = registerRpcHandlers({
|
|
979
|
+
homeDir: options.homeDir,
|
|
928
980
|
events: pi.events,
|
|
929
981
|
pi,
|
|
930
982
|
getCtx: () => currentCtx,
|
|
@@ -944,6 +996,7 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
|
|
|
944
996
|
if (!record || record.parentAgentId) return false;
|
|
945
997
|
if (record.status === "running" || record.status === "queued") return false;
|
|
946
998
|
markConsumed(record, clock.now());
|
|
999
|
+
fleet.acknowledgeFailure(record.id);
|
|
947
1000
|
outbox.cancel(record.id);
|
|
948
1001
|
return true;
|
|
949
1002
|
},
|
|
@@ -956,6 +1009,7 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
|
|
|
956
1009
|
pi.events.emit("subagents:ready", {});
|
|
957
1010
|
}
|
|
958
1011
|
if (isSchedulingEnabled() && !scheduler.isActive()) startScheduler(ctx);
|
|
1012
|
+
fleet.update(); // restored quota-parked jobs have no agent record
|
|
959
1013
|
// Stack `@handle` suggestions on pi's built-in autocomplete. Registered at
|
|
960
1014
|
// most once per activation: pi appends wrappers to a list it never prunes,
|
|
961
1015
|
// so a second call would layer a duplicate provider on the first. TUI only
|
|
@@ -1116,6 +1170,9 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
|
|
|
1116
1170
|
description: entry.description,
|
|
1117
1171
|
reclaim: { handle: entry.handle, alias: entry.alias },
|
|
1118
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 } : {}),
|
|
1119
1176
|
isBackground: true,
|
|
1120
1177
|
});
|
|
1121
1178
|
// The agent may still be starting — wait, so a startup failure lands in
|
|
@@ -1169,7 +1226,7 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
|
|
|
1169
1226
|
// Not awaited: the clone runs a full model turn, and prompt() is blocked
|
|
1170
1227
|
// until this hook returns. The user gets their prompt back immediately
|
|
1171
1228
|
// and the agent appears in the widget when it starts.
|
|
1172
|
-
void runMentionClone({ ctx, type, message: mention.message, agentTool: registeredAgentTool })
|
|
1229
|
+
void runMentionClone({ ctx, type, message: mention.message, agentTool: registeredAgentTool, homeDir: options.homeDir })
|
|
1173
1230
|
.then(async (result) => {
|
|
1174
1231
|
if (result.spawned) return;
|
|
1175
1232
|
// A clone that could not run must not swallow the mention: start the
|
|
@@ -1195,7 +1252,7 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
|
|
|
1195
1252
|
}
|
|
1196
1253
|
|
|
1197
1254
|
try {
|
|
1198
|
-
// Nothing else to pass:
|
|
1255
|
+
// Nothing else to pass: the manager resolves model; runAgent resolves thinking, max turns and
|
|
1199
1256
|
// `isolated` from the agent's own config when the spawn omits them, and the
|
|
1200
1257
|
// manager's onStart/onComplete callbacks own the widget, the fleet list
|
|
1201
1258
|
// and the completion notification — the same contract the scheduler and
|
|
@@ -1227,6 +1284,7 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
|
|
|
1227
1284
|
// to this session (see detached-results.ts). Only agents that cannot outlive the
|
|
1228
1285
|
// session are aborted.
|
|
1229
1286
|
pi.on("session_shutdown", async (event: { reason?: string }) => {
|
|
1287
|
+
fleet.flushState(); // appendEntry must run before this activation loses its context
|
|
1230
1288
|
const sessionFile = currentCtx?.sessionManager?.getSessionFile?.();
|
|
1231
1289
|
unwatchDetached?.();
|
|
1232
1290
|
unwatchDetached = undefined;
|
|
@@ -1276,6 +1334,8 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
|
|
|
1276
1334
|
widget.dispose();
|
|
1277
1335
|
outbox.dispose();
|
|
1278
1336
|
fleet.dispose();
|
|
1337
|
+
unwatchFleetSchedules?.();
|
|
1338
|
+
unwatchFleetSchedules = undefined;
|
|
1279
1339
|
unsubHandoffKey?.();
|
|
1280
1340
|
unsubHandoffKey = undefined;
|
|
1281
1341
|
handoffKeyUi = undefined;
|
|
@@ -1348,6 +1408,7 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
|
|
|
1348
1408
|
type: record.type,
|
|
1349
1409
|
description: record.description,
|
|
1350
1410
|
isBackground: true,
|
|
1411
|
+
...fallbackFields(record),
|
|
1351
1412
|
});
|
|
1352
1413
|
}
|
|
1353
1414
|
}
|
|
@@ -1412,6 +1473,17 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
|
|
|
1412
1473
|
getFullscreenScrollbar, backgroundActionFor, isShowModelEnabled,
|
|
1413
1474
|
// Agents the previous instance of this session left running (/reload, /new, ...).
|
|
1414
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
|
+
});
|
|
1415
1487
|
/** What the Agent tool row reads at render time, so its summary stays live. */
|
|
1416
1488
|
const agentRowLive: AgentRowLive = {
|
|
1417
1489
|
getRecord: (id) => manager.getRecord(id),
|
|
@@ -1584,6 +1656,9 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
|
|
|
1584
1656
|
if (opts.outputTranscript) {
|
|
1585
1657
|
existing.outputFile = createOutputFilePath(ctx.cwd, id, ctx.sessionManager.getSessionId());
|
|
1586
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);
|
|
1587
1662
|
}
|
|
1588
1663
|
// Anchor streaming past the turns already on disk, captured BEFORE the
|
|
1589
1664
|
// run starts. The resumed prompt lands as an ordinary user message at
|
|
@@ -1642,6 +1717,7 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
|
|
|
1642
1717
|
type: existing.type,
|
|
1643
1718
|
description: existing.description,
|
|
1644
1719
|
isBackground: true,
|
|
1720
|
+
...fallbackFields(existing),
|
|
1645
1721
|
});
|
|
1646
1722
|
|
|
1647
1723
|
return record;
|
|
@@ -1740,6 +1816,8 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
|
|
|
1740
1816
|
setPackageWorkflows: setPackageWorkflowsGate,
|
|
1741
1817
|
},
|
|
1742
1818
|
(event, payload) => pi.events.emit(event, payload),
|
|
1819
|
+
process.cwd(),
|
|
1820
|
+
options.homeDir,
|
|
1743
1821
|
);
|
|
1744
1822
|
|
|
1745
1823
|
// ---- Agent tool ----
|
|
@@ -1822,7 +1900,7 @@ If the target is already known, use a direct tool — \`read\` for a known path,
|
|
|
1822
1900
|
- Use steer_subagent to send mid-run messages to a running background agent.
|
|
1823
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.
|
|
1824
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.
|
|
1825
|
-
- 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").
|
|
1826
1904
|
- Use thinking to control extended thinking level.
|
|
1827
1905
|
- Use inherit_context if the agent needs the parent conversation history.${isolationGuideline}${scheduleGuideline}
|
|
1828
1906
|
|
|
@@ -1924,7 +2002,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1924
2002
|
model: Type.Optional(
|
|
1925
2003
|
Type.String({
|
|
1926
2004
|
description:
|
|
1927
|
-
'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.',
|
|
1928
2006
|
}),
|
|
1929
2007
|
),
|
|
1930
2008
|
thinking: Type.Optional(
|
|
@@ -2040,7 +2118,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2040
2118
|
// foreground path. Resume deliberately doesn't: it replays the stored
|
|
2041
2119
|
// session and ignores `subagent_type` entirely, so a note about type
|
|
2042
2120
|
// substitution would be describing something that didn't happen.
|
|
2043
|
-
const
|
|
2121
|
+
const typeNote = dispatch.ok && dispatch.fellBackFrom !== undefined
|
|
2044
2122
|
? `Note: Unknown agent type "${dispatch.fellBackFrom}" — using ${resolveType(subagentType) ? subagentType : "the fallback agent config"}.\n\n`
|
|
2045
2123
|
: "";
|
|
2046
2124
|
|
|
@@ -2050,36 +2128,22 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2050
2128
|
const customConfig = getAgentConfig(subagentType);
|
|
2051
2129
|
|
|
2052
2130
|
const resolvedConfig = resolveAgentInvocationConfig(customConfig, params, {
|
|
2053
|
-
agentType: subagentType,
|
|
2054
2131
|
worktreeAllowed: isWorktreeIsolationEnabled(),
|
|
2055
2132
|
defaultRunInBackground: getBackgroundByDefault(),
|
|
2056
2133
|
});
|
|
2057
2134
|
|
|
2058
|
-
//
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
if (resolvedConfig.modelFromParams) return textResult(resolved);
|
|
2064
|
-
// config-specified: silent fallback to parent
|
|
2065
|
-
} else {
|
|
2066
|
-
model = resolved;
|
|
2067
|
-
}
|
|
2068
|
-
}
|
|
2069
|
-
|
|
2070
|
-
// Scope validation: the effective resolved model is checked against the
|
|
2071
|
-
// user's enabledModels list. Policy (hard error vs warn-and-proceed) lives
|
|
2072
|
-
// in model-scope.ts so the nested delegation tools apply the same rule.
|
|
2073
|
-
const scopeVerdict = checkModelScope({
|
|
2074
|
-
model,
|
|
2075
|
-
cwd: ctx.cwd,
|
|
2076
|
-
modelRegistry: ctx.modelRegistry,
|
|
2077
|
-
callerSupplied: resolvedConfig.modelFromParams,
|
|
2078
|
-
agentLabel: customConfig?.displayName ?? subagentType,
|
|
2079
|
-
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,
|
|
2080
2140
|
});
|
|
2081
|
-
if (
|
|
2082
|
-
|
|
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");
|
|
2083
2147
|
|
|
2084
2148
|
const thinking = resolvedConfig.thinking;
|
|
2085
2149
|
const inheritContext = resolvedConfig.inheritContext;
|
|
@@ -2088,7 +2152,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2088
2152
|
const isolation = resolvedConfig.isolation;
|
|
2089
2153
|
// Undefined = the session cwd. The manager validates it (absolute,
|
|
2090
2154
|
// existing directory) at spawn and again at start.
|
|
2091
|
-
const cwd = resolveCwdParam(params.cwd, ctx.cwd);
|
|
2155
|
+
const cwd = resolveCwdParam(params.cwd, ctx.cwd, options.homeDir);
|
|
2092
2156
|
// Whether this spawn writes its .output transcript. Per-agent
|
|
2093
2157
|
// frontmatter (`output_transcript`) wins; otherwise the project/global
|
|
2094
2158
|
// default applies. `attachTranscript` below is the SOLE gate — every
|
|
@@ -2106,9 +2170,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2106
2170
|
// parent's model used to show the level with nothing to attach it to.
|
|
2107
2171
|
// This is the pre-session snapshot — agent-manager overwrites it with the
|
|
2108
2172
|
// effective values the moment a session reports them.
|
|
2109
|
-
const { modelName, modelId } =
|
|
2110
|
-
? isVirtualModel(model) ? describeVirtualModel(model) : describeModel(model)
|
|
2111
|
-
: { modelName: undefined, modelId: undefined };
|
|
2173
|
+
const { modelName, modelId } = describeSelection(selection, ctx.modelRegistry);
|
|
2112
2174
|
const effectiveMaxTurns = normalizeMaxTurns(resolvedConfig.maxTurns ?? getDefaultMaxTurns());
|
|
2113
2175
|
const agentInvocation: AgentInvocation = {
|
|
2114
2176
|
modelName,
|
|
@@ -2126,11 +2188,33 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2126
2188
|
const modeLabel = getPromptModeLabel(subagentType);
|
|
2127
2189
|
const { tags: invocationTags } = buildInvocationTags(agentInvocation);
|
|
2128
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
|
+
};
|
|
2129
2210
|
const detailBase = {
|
|
2130
2211
|
displayName,
|
|
2131
2212
|
description: params.description,
|
|
2132
2213
|
subagentType,
|
|
2133
2214
|
modelName,
|
|
2215
|
+
modelNote: modelNoteOf(undefined),
|
|
2216
|
+
modelFallback: modelFallbackOf(undefined),
|
|
2217
|
+
modelTier: selection?.tier ? tierDisclosure(selection.tier) : undefined,
|
|
2134
2218
|
tags: agentTags.length > 0 ? agentTags : undefined,
|
|
2135
2219
|
};
|
|
2136
2220
|
|
|
@@ -2148,7 +2232,10 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2148
2232
|
* buildInvocationTags would silently drop `twin`.
|
|
2149
2233
|
*/
|
|
2150
2234
|
const detailBaseFor = (rec: AgentRecord | undefined): typeof detailBase => {
|
|
2151
|
-
|
|
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;
|
|
2152
2239
|
const type = rec.type;
|
|
2153
2240
|
const { modelName: recModelName, tags } = buildInvocationTags(rec.invocation);
|
|
2154
2241
|
const recModeLabel = getPromptModeLabel(type);
|
|
@@ -2158,6 +2245,9 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2158
2245
|
description: rec.description,
|
|
2159
2246
|
subagentType: type,
|
|
2160
2247
|
modelName: recModelName,
|
|
2248
|
+
modelNote: modelNoteOf(rec),
|
|
2249
|
+
modelFallback: modelFallbackOf(rec),
|
|
2250
|
+
modelTier: rec.modelTier,
|
|
2161
2251
|
tags: recTags.length > 0 ? recTags : undefined,
|
|
2162
2252
|
};
|
|
2163
2253
|
};
|
|
@@ -2191,7 +2281,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2191
2281
|
// at fire time, and the original is what a user edits.
|
|
2192
2282
|
subagent_type: requestedType,
|
|
2193
2283
|
prompt: params.prompt as string,
|
|
2194
|
-
model: params.model as string | undefined,
|
|
2284
|
+
model: selection?.requested?.startsWith("@") ? selection.requested : params.model as string | undefined,
|
|
2195
2285
|
thinking: thinking,
|
|
2196
2286
|
max_turns: effectiveMaxTurns,
|
|
2197
2287
|
isolated: isolated,
|
|
@@ -2199,7 +2289,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2199
2289
|
});
|
|
2200
2290
|
const next = scheduler.getNextRun(job.id);
|
|
2201
2291
|
return textResult(
|
|
2202
|
-
`${
|
|
2292
|
+
`${fallbackNoteFor(undefined)}Scheduled "${job.name}" (id: ${job.id}, type: ${job.scheduleType}). ` +
|
|
2203
2293
|
`Next run: ${next ?? "(unknown)"}. ` +
|
|
2204
2294
|
`Manage via /agents → Scheduled jobs.`,
|
|
2205
2295
|
);
|
|
@@ -2252,7 +2342,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2252
2342
|
|
|
2253
2343
|
const isQueued = record.status === "queued";
|
|
2254
2344
|
return textResult(
|
|
2255
|
-
|
|
2345
|
+
`${resumeNoteFor(record)}Agent ${isQueued ? "queued" : "resumed"} in background.\n` +
|
|
2256
2346
|
`Agent ID: ${id}\n` +
|
|
2257
2347
|
`Type: ${existing.type}\n` +
|
|
2258
2348
|
(record.outputFile ? `Output file: ${record.outputFile}\n` : "") +
|
|
@@ -2294,10 +2384,10 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2294
2384
|
// A failed resume surfaces the error, plus any partial output THIS
|
|
2295
2385
|
// resume produced (never the previous turn's answer, #144).
|
|
2296
2386
|
if (record.status === "error") {
|
|
2297
|
-
return textResult(
|
|
2387
|
+
return textResult(`${resumeNoteFor(record)}Agent failed: ${record.error}${partialOutputSuffix(record)}`, buildDetails(detailBaseFor(record), record, resumeState));
|
|
2298
2388
|
}
|
|
2299
2389
|
return textResult(
|
|
2300
|
-
record.result?.trim() || "No output."
|
|
2390
|
+
`${resumeNoteFor(record)}${record.result?.trim() || "No output."}`,
|
|
2301
2391
|
buildDetails(detailBaseFor(record), record, resumeState),
|
|
2302
2392
|
);
|
|
2303
2393
|
}
|
|
@@ -2308,7 +2398,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2308
2398
|
const backgroundResult = (id: string, record: AgentRecord | undefined) => {
|
|
2309
2399
|
const isQueued = record?.status === "queued";
|
|
2310
2400
|
return textResult(
|
|
2311
|
-
`${
|
|
2401
|
+
`${fallbackNoteFor(record)}Agent ${isQueued ? "queued" : "started"} in background.\n` +
|
|
2312
2402
|
`Agent ID: ${id}\n` +
|
|
2313
2403
|
`Type: ${displayName}\n` +
|
|
2314
2404
|
`Description: ${params.description}\n` +
|
|
@@ -2327,13 +2417,14 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2327
2417
|
// exhaustion policy says fail now (`wait: false`). Anything else fails
|
|
2328
2418
|
// now, with the reset, rather than starting on an exhausted provider.
|
|
2329
2419
|
const dispatchModel = model ?? ctx.model;
|
|
2330
|
-
const quotaBlock =
|
|
2420
|
+
const quotaBlock = selection?.tierBinding ? tierBlock
|
|
2421
|
+
: dispatchModel ? await checkAdmission(pi.events, modelRef(dispatchModel), undefined, clock) : undefined;
|
|
2331
2422
|
if (quotaBlock) {
|
|
2332
2423
|
// A parked job is a scheduled job, which has no `cwd` to carry.
|
|
2333
2424
|
const canPark = runInBackground && !inheritContext && cwd === undefined && mayPark(quotaBlock)
|
|
2334
2425
|
&& isSchedulingEnabled() && scheduler.isActive();
|
|
2335
2426
|
if (!canPark || !mayPark(quotaBlock)) {
|
|
2336
|
-
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.`);
|
|
2337
2428
|
}
|
|
2338
2429
|
const job = scheduler.addJob({
|
|
2339
2430
|
name: `${params.description} (quota ${nanoid(4)})`,
|
|
@@ -2341,7 +2432,9 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2341
2432
|
schedule: new Date(quotaBlock.resetAt + QUOTA_RESTART_GRACE_MS).toISOString(),
|
|
2342
2433
|
subagent_type: requestedType,
|
|
2343
2434
|
prompt: params.prompt as string,
|
|
2344
|
-
|
|
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,
|
|
2345
2438
|
thinking,
|
|
2346
2439
|
max_turns: effectiveMaxTurns,
|
|
2347
2440
|
isolated,
|
|
@@ -2349,7 +2442,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2349
2442
|
quotaParked: true,
|
|
2350
2443
|
});
|
|
2351
2444
|
return textResult(
|
|
2352
|
-
`${
|
|
2445
|
+
`${fallbackNoteFor(undefined)}${describeBlock(quotaBlock)} Queued "${job.name}" (id: ${job.id}) to start at ` +
|
|
2353
2446
|
`${scheduler.getNextRun(job.id) ?? job.schedule}. Manage via /agents → Scheduled jobs.`,
|
|
2354
2447
|
);
|
|
2355
2448
|
}
|
|
@@ -2375,6 +2468,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2375
2468
|
// tool call failed, whereas a returned message reads to the model as a
|
|
2376
2469
|
// subagent that ran and reported this (#179).
|
|
2377
2470
|
id = manager.spawn(pi, ctx, subagentType, params.prompt, {
|
|
2471
|
+
modelSelection: selection,
|
|
2378
2472
|
description: params.description,
|
|
2379
2473
|
name: params.name as string | undefined,
|
|
2380
2474
|
model,
|
|
@@ -2420,6 +2514,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2420
2514
|
type: subagentType,
|
|
2421
2515
|
description: params.description,
|
|
2422
2516
|
isBackground: true,
|
|
2517
|
+
...fallbackFields(manager.getRecord(id)),
|
|
2423
2518
|
});
|
|
2424
2519
|
|
|
2425
2520
|
return backgroundResult(id, record);
|
|
@@ -2528,6 +2623,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2528
2623
|
let fgResult: { record: AgentRecord } | undefined;
|
|
2529
2624
|
try {
|
|
2530
2625
|
fgResult = await Promise.race([manager.spawnAndWait(pi, ctx, subagentType, params.prompt, {
|
|
2626
|
+
modelSelection: selection,
|
|
2531
2627
|
description: params.description,
|
|
2532
2628
|
name: params.name as string | undefined,
|
|
2533
2629
|
model,
|
|
@@ -2591,7 +2687,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2591
2687
|
|
|
2592
2688
|
if (record.status === "error") {
|
|
2593
2689
|
// Error headline + any partial output the run produced before failing.
|
|
2594
|
-
return textResult(`${
|
|
2690
|
+
return textResult(`${fallbackNoteFor(record)}Agent failed: ${record.error}${partialOutputSuffix(record)}`, details);
|
|
2595
2691
|
}
|
|
2596
2692
|
|
|
2597
2693
|
const durationMs = (record.completedAt ?? Date.now()) - record.startedAt;
|
|
@@ -2602,7 +2698,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2602
2698
|
if (costText) statsParts.push(costText);
|
|
2603
2699
|
}
|
|
2604
2700
|
return textResult(
|
|
2605
|
-
`${
|
|
2701
|
+
`${fallbackNoteFor(record)}Agent completed in ${formatMs(durationMs)} (${statsParts.join(", ")})${getForegroundOutcomeNote(record.status)}.\n\n` +
|
|
2606
2702
|
(record.result?.trim() || "No output."),
|
|
2607
2703
|
details,
|
|
2608
2704
|
);
|
|
@@ -2692,6 +2788,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2692
2788
|
startedAt: task.startTime,
|
|
2693
2789
|
...(task.endTime !== undefined ? { completedAt: task.endTime } : {}),
|
|
2694
2790
|
tokens: task.totalTokens,
|
|
2791
|
+
observationKey: task,
|
|
2695
2792
|
}));
|
|
2696
2793
|
}
|
|
2697
2794
|
|
|
@@ -2708,6 +2805,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2708
2805
|
args: task.args,
|
|
2709
2806
|
signal: task.abortController.signal,
|
|
2710
2807
|
host: createWorkflowHost({
|
|
2808
|
+
homeDir: options.homeDir,
|
|
2711
2809
|
pi,
|
|
2712
2810
|
ctx,
|
|
2713
2811
|
manager,
|
|
@@ -2865,6 +2963,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2865
2963
|
? { scriptPath: resumeFrom.scriptPath }
|
|
2866
2964
|
: params,
|
|
2867
2965
|
ctx.cwd,
|
|
2966
|
+
options.homeDir,
|
|
2868
2967
|
);
|
|
2869
2968
|
if (!resolved.ok) return textResult(resolved.message);
|
|
2870
2969
|
|
|
@@ -3150,7 +3249,9 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
3150
3249
|
let output =
|
|
3151
3250
|
`Agent: ${record.id}\n` +
|
|
3152
3251
|
`Type: ${displayName} | Status: ${record.status}${getStatusNote(record.status)} | ${statsParts.join(" | ")}\n` +
|
|
3153
|
-
`Description: ${record.description}\n
|
|
3252
|
+
`Description: ${record.description}\n` +
|
|
3253
|
+
(modelSelectionNote(record) ? `${modelSelectionNote(record)}\n` : "") +
|
|
3254
|
+
`\n`;
|
|
3154
3255
|
|
|
3155
3256
|
if (record.status === "running") {
|
|
3156
3257
|
output += "Agent is still running. Use wait: true or check back later.";
|
|
@@ -3163,6 +3264,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
3163
3264
|
// Mark result as consumed — suppresses the completion notification
|
|
3164
3265
|
if (record.status !== "running" && record.status !== "queued") {
|
|
3165
3266
|
markConsumed(record, clock.now());
|
|
3267
|
+
fleet.acknowledgeFailure(record.id);
|
|
3166
3268
|
outbox.cancel(record.id);
|
|
3167
3269
|
}
|
|
3168
3270
|
|
|
@@ -3245,19 +3347,21 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
3245
3347
|
|
|
3246
3348
|
function getModelLabel(type: string, registry?: ModelRegistry): string {
|
|
3247
3349
|
const cfg = getAgentConfig(type);
|
|
3248
|
-
|
|
3249
|
-
|
|
3350
|
+
const input = modelRequest(type, cfg?.model).requested;
|
|
3351
|
+
if (!input) return "inherit";
|
|
3352
|
+
const label = getModelLabelFromConfig(input);
|
|
3250
3353
|
if (!registry) return label;
|
|
3251
|
-
const
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
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;
|
|
3255
3359
|
// Surface what it actually resolved to when that differs from the config —
|
|
3256
3360
|
// e.g. a provider fallback or a looser version pin. Cosmetic separator/date
|
|
3257
3361
|
// differences are normalized away so an effectively-identical match stays quiet.
|
|
3258
3362
|
const resolvedFull = `${resolved.provider}/${resolved.id}`;
|
|
3259
3363
|
const norm = (s: string) => s.toLowerCase().replace(/\./g, "-").replace(/-\d{8}$/, "");
|
|
3260
|
-
if (norm(
|
|
3364
|
+
if (norm(input) === norm(resolvedFull)) return label;
|
|
3261
3365
|
return `${label} (→ ${resolvedFull.replace(/-\d{8}$/, "")})`;
|
|
3262
3366
|
}
|
|
3263
3367
|
|
|
@@ -3337,7 +3441,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
3337
3441
|
*/
|
|
3338
3442
|
function packageNameOf(cfg: AgentConfig | undefined): string | undefined {
|
|
3339
3443
|
if (cfg?.source !== "package" || !cfg.sourcePath) return undefined;
|
|
3340
|
-
return packageNameForPath(cfg.sourcePath, process.cwd());
|
|
3444
|
+
return packageNameForPath(cfg.sourcePath, process.cwd(), "agents", { homeDir: options.homeDir });
|
|
3341
3445
|
}
|
|
3342
3446
|
|
|
3343
3447
|
async function showAllAgentsList(ctx: ExtensionCommandContext) {
|
|
@@ -3391,8 +3495,9 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
3391
3495
|
};
|
|
3392
3496
|
});
|
|
3393
3497
|
|
|
3394
|
-
|
|
3395
|
-
|
|
3498
|
+
const cfg = selected ? getAgentConfig(selected) : undefined;
|
|
3499
|
+
if (selected && cfg) {
|
|
3500
|
+
await showAgentDetail(ctx, selected, cfg);
|
|
3396
3501
|
await showAllAgentsList(ctx);
|
|
3397
3502
|
}
|
|
3398
3503
|
}
|
|
@@ -3446,13 +3551,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
3446
3551
|
);
|
|
3447
3552
|
}
|
|
3448
3553
|
|
|
3449
|
-
async function showAgentDetail(ctx: ExtensionCommandContext, name: string) {
|
|
3450
|
-
const cfg = getAgentConfig(name);
|
|
3451
|
-
if (!cfg) {
|
|
3452
|
-
ctx.ui.notify(`Agent config not found for "${name}".`, "warning");
|
|
3453
|
-
return;
|
|
3454
|
-
}
|
|
3455
|
-
|
|
3554
|
+
async function showAgentDetail(ctx: ExtensionCommandContext, name: string, cfg: AgentConfig) {
|
|
3456
3555
|
const isPackage = cfg.source === "package";
|
|
3457
3556
|
const file = locateAgentFile(name, cfg.sourcePath, undefined, cfg.source);
|
|
3458
3557
|
const isDefault = cfg.isDefault === true;
|
|
@@ -3862,6 +3961,8 @@ Write the file using the write tool. Only write the file, nothing else.`;
|
|
|
3862
3961
|
maxActiveSubagents: undefined,
|
|
3863
3962
|
nestedSubagentReserve: undefined,
|
|
3864
3963
|
modelAliases: undefined,
|
|
3964
|
+
modelTiers: undefined,
|
|
3965
|
+
modelResolution: undefined,
|
|
3865
3966
|
agentModels: undefined,
|
|
3866
3967
|
reportUsage: isReportUsageEnabled(),
|
|
3867
3968
|
showCost: isShowCostEnabled(),
|