@signalridge/pi-subagents 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +60 -0
- package/README.md +102 -4
- package/examples/agent-tool-description.md +2 -3
- package/package.json +1 -1
- package/src/agent-manager.ts +21 -0
- package/src/agent-runner.ts +57 -4
- package/src/agent-tiers.ts +297 -0
- package/src/custom-agents.ts +21 -0
- package/src/index.ts +40 -13
- package/src/invocation-config.ts +33 -8
- package/src/nested-tools.ts +8 -2
- package/src/schedule.ts +3 -0
- package/src/settings.ts +257 -5
- package/src/types.ts +21 -0
- package/src/ui/conversation-viewer.ts +9 -4
package/src/settings.ts
CHANGED
|
@@ -9,7 +9,41 @@ import type { WorkflowTier } from "@signalridge/pi-subagents-protocol";
|
|
|
9
9
|
import { NO_FALLBACK } from "./agent-types.js";
|
|
10
10
|
import type { JoinMode, ThinkingLevel } from "./types.js";
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
/** A tier's thinking value: a level, or `inherit` to keep the parent's. */
|
|
13
|
+
export type TierThinking = ThinkingLevel | "inherit";
|
|
14
|
+
|
|
15
|
+
/** Historical name for {@link TierThinking}, kept for the workflow tier types. */
|
|
16
|
+
export type WorkflowThinking = TierThinking;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* One user-named (model, thinking) pair for ordinary subagent spawns.
|
|
20
|
+
*
|
|
21
|
+
* Separate from {@link WorkflowTierProfile} because the key space differs: these
|
|
22
|
+
* names are whatever the user chose, while workflow tiers are the protocol's
|
|
23
|
+
* fixed small/medium/large. `description` is what the host agent reads in the
|
|
24
|
+
* tool description to decide between tiers, so it is prose about the job the
|
|
25
|
+
* tier is for, not about the model.
|
|
26
|
+
*/
|
|
27
|
+
export interface AgentTierProfile {
|
|
28
|
+
/** `inherit` keeps the parent model; other values are provider/model references. */
|
|
29
|
+
model: string;
|
|
30
|
+
/** `inherit` keeps the parent thinking level; other values are clamped natively. */
|
|
31
|
+
thinking: TierThinking;
|
|
32
|
+
/** Shown to the host agent. Defaults to the tier key when omitted. */
|
|
33
|
+
description?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Tier policy for ordinary subagent spawns, resolved by `agent-tiers.ts`. */
|
|
37
|
+
export interface AgentTiersSettings {
|
|
38
|
+
/** Applied when neither the caller nor the agent names a tier. */
|
|
39
|
+
defaultTier?: string;
|
|
40
|
+
/** A configured entry replaces the complete entry inherited from global settings. */
|
|
41
|
+
profiles?: Record<string, AgentTierProfile>;
|
|
42
|
+
/** Tombstones for malformed explicit profiles; not a user policy field. */
|
|
43
|
+
blockedProfiles?: string[];
|
|
44
|
+
/** Tombstone for a malformed defaultTier; not a user policy field. */
|
|
45
|
+
blockedDefaultTier?: boolean;
|
|
46
|
+
}
|
|
13
47
|
|
|
14
48
|
export interface WorkflowTierProfile {
|
|
15
49
|
/** `inherit` keeps the parent model; other values are provider/model references. */
|
|
@@ -42,6 +76,12 @@ export const DEFAULT_WORKFLOW_TIER_PROFILES: Readonly<Record<WorkflowTier, Workf
|
|
|
42
76
|
export interface SubagentsSettings {
|
|
43
77
|
/** Semantic model-plus-thinking profiles used by workflow-owned spawns. */
|
|
44
78
|
workflow?: WorkflowSettings;
|
|
79
|
+
/**
|
|
80
|
+
* User-named model tiers for ordinary subagent spawns. Independent of
|
|
81
|
+
* `workflow` above: the key space is arbitrary, and `pi-workflows` never reads
|
|
82
|
+
* these. See `agent-tiers.ts` for resolution and precedence.
|
|
83
|
+
*/
|
|
84
|
+
agentTiers?: AgentTiersSettings;
|
|
45
85
|
maxConcurrent?: number;
|
|
46
86
|
/**
|
|
47
87
|
* 0 = unlimited — the extension's single source of truth for that convention:
|
|
@@ -166,6 +206,8 @@ export interface SettingsAppliers {
|
|
|
166
206
|
setFallbackSubagent: (v: string | undefined) => void;
|
|
167
207
|
/** Optional because non-runtime settings tests and consumers need not apply workflow state. */
|
|
168
208
|
setWorkflow?: (settings: WorkflowSettings) => void;
|
|
209
|
+
/** Optional for the same reason as `setWorkflow`. */
|
|
210
|
+
setAgentTiers?: (settings: AgentTiersSettings) => void;
|
|
169
211
|
}
|
|
170
212
|
|
|
171
213
|
/** Emit callback — a subset of `pi.events.emit` to keep helpers testable. */
|
|
@@ -184,6 +226,11 @@ const VALID_THINKING_LEVELS: ReadonlySet<string> = new Set([
|
|
|
184
226
|
]);
|
|
185
227
|
const WORKFLOW_TIER_NAMES: readonly WorkflowTier[] = ["small", "medium", "large"];
|
|
186
228
|
const MAX_MODEL_REFERENCE_LENGTH = 512;
|
|
229
|
+
/** Mirrors MAX_AGENT_TIER_KEY_LENGTH in agent-tiers.ts; duplicated to keep settings dependency-free. */
|
|
230
|
+
const MAX_AGENT_TIER_KEY_LENGTH = 64;
|
|
231
|
+
/** Bounds a hand-edited config; far above any realistic number of tiers. */
|
|
232
|
+
const MAX_AGENT_TIER_PROFILES = 64;
|
|
233
|
+
const MAX_AGENT_TIER_DESCRIPTION_LENGTH = 512;
|
|
187
234
|
|
|
188
235
|
// Sanity ceilings — prevent hand-edited configs from asking for values that
|
|
189
236
|
// make no operational sense (e.g. 1e6 concurrent subagents). Permissive enough
|
|
@@ -250,6 +297,70 @@ function sanitizeWorkflow(raw: unknown): WorkflowSettings | undefined {
|
|
|
250
297
|
return out.defaultTier !== undefined || out.tiers !== undefined || out.blockedTiers !== undefined || out.blockedDefaultTier !== undefined ? out : undefined;
|
|
251
298
|
}
|
|
252
299
|
|
|
300
|
+
function isAgentTierKey(value: unknown): value is string {
|
|
301
|
+
return (
|
|
302
|
+
typeof value === "string" &&
|
|
303
|
+
value.length > 0 &&
|
|
304
|
+
value.length <= MAX_AGENT_TIER_KEY_LENGTH &&
|
|
305
|
+
value.trim() === value &&
|
|
306
|
+
!/\s/u.test(value)
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* A profile is all-or-nothing, for the same reason a workflow profile is: half
|
|
312
|
+
* a profile with an implied `inherit` makes a typo look like a policy choice.
|
|
313
|
+
* `description` is the one optional field — it is prose for the tool
|
|
314
|
+
* description, and its absence has an obvious meaning (use the key).
|
|
315
|
+
*/
|
|
316
|
+
function sanitizeAgentTierProfile(raw: unknown): AgentTierProfile | undefined {
|
|
317
|
+
if (!isRecord(raw)) return undefined;
|
|
318
|
+
const keys = Object.keys(raw);
|
|
319
|
+
if (keys.some((key) => key !== "model" && key !== "thinking" && key !== "description")) return undefined;
|
|
320
|
+
if (!Object.hasOwn(raw, "model") || !Object.hasOwn(raw, "thinking")) return undefined;
|
|
321
|
+
if (!validWorkflowModelReference(raw.model) || !validThinkingLevel(raw.thinking)) return undefined;
|
|
322
|
+
if (
|
|
323
|
+
Object.hasOwn(raw, "description") &&
|
|
324
|
+
(typeof raw.description !== "string" ||
|
|
325
|
+
raw.description.trim().length === 0 ||
|
|
326
|
+
raw.description.length > MAX_AGENT_TIER_DESCRIPTION_LENGTH)
|
|
327
|
+
) {
|
|
328
|
+
return undefined;
|
|
329
|
+
}
|
|
330
|
+
return {
|
|
331
|
+
model: raw.model.trim(),
|
|
332
|
+
thinking: raw.thinking,
|
|
333
|
+
...(typeof raw.description === "string" ? { description: raw.description.trim() } : {}),
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function sanitizeAgentTiers(raw: unknown): AgentTiersSettings | undefined {
|
|
338
|
+
if (!isRecord(raw)) return undefined;
|
|
339
|
+
const out: AgentTiersSettings = {};
|
|
340
|
+
if (isAgentTierKey(raw.defaultTier)) out.defaultTier = raw.defaultTier;
|
|
341
|
+
if (Object.hasOwn(raw, "defaultTier") && !isAgentTierKey(raw.defaultTier)) out.blockedDefaultTier = true;
|
|
342
|
+
if (raw.blockedDefaultTier === true) out.blockedDefaultTier = true;
|
|
343
|
+
if (Array.isArray(raw.blockedProfiles)) {
|
|
344
|
+
const blocked = [...new Set(raw.blockedProfiles.filter(isAgentTierKey))];
|
|
345
|
+
if (blocked.length > 0) out.blockedProfiles = blocked;
|
|
346
|
+
}
|
|
347
|
+
if (isRecord(raw.profiles)) {
|
|
348
|
+
const profiles: Record<string, AgentTierProfile> = {};
|
|
349
|
+
for (const key of Object.keys(raw.profiles).slice(0, MAX_AGENT_TIER_PROFILES)) {
|
|
350
|
+
if (!isAgentTierKey(key)) continue;
|
|
351
|
+
const profile = sanitizeAgentTierProfile(raw.profiles[key]);
|
|
352
|
+
if (profile) profiles[key] = profile;
|
|
353
|
+
}
|
|
354
|
+
if (Object.keys(profiles).length > 0) out.profiles = profiles;
|
|
355
|
+
}
|
|
356
|
+
return out.defaultTier !== undefined ||
|
|
357
|
+
out.profiles !== undefined ||
|
|
358
|
+
out.blockedProfiles !== undefined ||
|
|
359
|
+
out.blockedDefaultTier !== undefined
|
|
360
|
+
? out
|
|
361
|
+
: undefined;
|
|
362
|
+
}
|
|
363
|
+
|
|
253
364
|
function isWorkflowTierValue(value: unknown): value is WorkflowTier {
|
|
254
365
|
return value === "small" || value === "medium" || value === "large";
|
|
255
366
|
}
|
|
@@ -323,6 +434,8 @@ function sanitize(raw: unknown): SubagentsSettings {
|
|
|
323
434
|
|
|
324
435
|
const workflow = sanitizeWorkflow(r.workflow);
|
|
325
436
|
if (workflow) out.workflow = workflow;
|
|
437
|
+
const agentTiers = sanitizeAgentTiers(r.agentTiers);
|
|
438
|
+
if (agentTiers) out.agentTiers = agentTiers;
|
|
326
439
|
return out;
|
|
327
440
|
}
|
|
328
441
|
|
|
@@ -351,9 +464,28 @@ interface WorkflowSource {
|
|
|
351
464
|
blockedDefaultTier: boolean;
|
|
352
465
|
}
|
|
353
466
|
|
|
467
|
+
/**
|
|
468
|
+
* Presence of each agent-tier entry in one file, kept separate from the
|
|
469
|
+
* sanitized value so the merge can tell "the project did not mention this
|
|
470
|
+
* profile" from "the project mentioned it and it was malformed". Only the
|
|
471
|
+
* second may block the global entry, and neither may silently revive it.
|
|
472
|
+
*/
|
|
473
|
+
interface AgentTiersSource {
|
|
474
|
+
present: boolean;
|
|
475
|
+
object: boolean;
|
|
476
|
+
defaultTierPresent: boolean;
|
|
477
|
+
defaultTier?: string;
|
|
478
|
+
profilesPresent: boolean;
|
|
479
|
+
profilesObject: boolean;
|
|
480
|
+
profileEntries: Record<string, AgentTierProfile | undefined>;
|
|
481
|
+
blockedProfiles: string[];
|
|
482
|
+
blockedDefaultTier: boolean;
|
|
483
|
+
}
|
|
484
|
+
|
|
354
485
|
interface ReadSettings {
|
|
355
486
|
settings: SubagentsSettings;
|
|
356
487
|
workflow: WorkflowSource;
|
|
488
|
+
agentTiers: AgentTiersSource;
|
|
357
489
|
}
|
|
358
490
|
|
|
359
491
|
function emptyWorkflowSource(): WorkflowSource {
|
|
@@ -369,6 +501,116 @@ function emptyWorkflowSource(): WorkflowSource {
|
|
|
369
501
|
};
|
|
370
502
|
}
|
|
371
503
|
|
|
504
|
+
function emptyAgentTiersSource(): AgentTiersSource {
|
|
505
|
+
return {
|
|
506
|
+
present: false,
|
|
507
|
+
object: false,
|
|
508
|
+
defaultTierPresent: false,
|
|
509
|
+
blockedDefaultTier: false,
|
|
510
|
+
profilesPresent: false,
|
|
511
|
+
profilesObject: false,
|
|
512
|
+
profileEntries: {},
|
|
513
|
+
blockedProfiles: [],
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/** Preserve invalid agent-tier entry presence so project policy cannot resurrect a global entry. */
|
|
518
|
+
function agentTiersSource(raw: unknown): AgentTiersSource {
|
|
519
|
+
if (!isRecord(raw) || !Object.hasOwn(raw, "agentTiers")) return emptyAgentTiersSource();
|
|
520
|
+
const value = raw.agentTiers;
|
|
521
|
+
if (!isRecord(value)) {
|
|
522
|
+
return { ...emptyAgentTiersSource(), present: true, object: false, blockedDefaultTier: true };
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const source: AgentTiersSource = {
|
|
526
|
+
present: true,
|
|
527
|
+
object: true,
|
|
528
|
+
defaultTierPresent: Object.hasOwn(value, "defaultTier"),
|
|
529
|
+
blockedDefaultTier:
|
|
530
|
+
(Object.hasOwn(value, "defaultTier") && !isAgentTierKey(value.defaultTier)) || value.blockedDefaultTier === true,
|
|
531
|
+
...(isAgentTierKey(value.defaultTier) ? { defaultTier: value.defaultTier } : {}),
|
|
532
|
+
profilesPresent: Object.hasOwn(value, "profiles"),
|
|
533
|
+
profilesObject: isRecord(value.profiles),
|
|
534
|
+
profileEntries: {},
|
|
535
|
+
blockedProfiles: Array.isArray(value.blockedProfiles) ? value.blockedProfiles.filter(isAgentTierKey) : [],
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
if (isRecord(value.profiles)) {
|
|
539
|
+
for (const key of Object.keys(value.profiles).slice(0, MAX_AGENT_TIER_PROFILES)) {
|
|
540
|
+
if (!isAgentTierKey(key)) continue;
|
|
541
|
+
const profile = sanitizeAgentTierProfile(value.profiles[key]);
|
|
542
|
+
source.profileEntries[key] = profile;
|
|
543
|
+
if (!profile) source.blockedProfiles.push(key);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
source.blockedProfiles = [...new Set(source.blockedProfiles)];
|
|
547
|
+
return source;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Global supplies the catalogue, project edits it. A project profile replaces
|
|
552
|
+
* its global namesake whole — never field by field, which would let a project
|
|
553
|
+
* change a model while inheriting a thinking level nobody chose for that pair.
|
|
554
|
+
*/
|
|
555
|
+
function mergeAgentTierSources(
|
|
556
|
+
global: AgentTiersSource,
|
|
557
|
+
project: AgentTiersSource,
|
|
558
|
+
): AgentTiersSettings | undefined {
|
|
559
|
+
if (!global.present && !project.present) return undefined;
|
|
560
|
+
|
|
561
|
+
const merged: AgentTiersSettings = {};
|
|
562
|
+
if (project.present && !project.object) {
|
|
563
|
+
// The project said "agentTiers" and gave something that is not an object.
|
|
564
|
+
// Inheriting the global catalogue here would run models the project was
|
|
565
|
+
// trying to change, so the whole policy is blocked instead.
|
|
566
|
+
merged.blockedDefaultTier = true;
|
|
567
|
+
const inherited = [
|
|
568
|
+
...new Set([...Object.keys(global.profileEntries), ...global.blockedProfiles]),
|
|
569
|
+
].sort((a, b) => a.localeCompare(b));
|
|
570
|
+
if (inherited.length > 0) merged.blockedProfiles = inherited;
|
|
571
|
+
return merged;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
if (project.defaultTierPresent) {
|
|
575
|
+
if (project.defaultTier !== undefined) merged.defaultTier = project.defaultTier;
|
|
576
|
+
else merged.blockedDefaultTier = true;
|
|
577
|
+
} else {
|
|
578
|
+
if (global.defaultTier !== undefined) merged.defaultTier = global.defaultTier;
|
|
579
|
+
if (global.blockedDefaultTier || project.blockedDefaultTier) merged.blockedDefaultTier = true;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const blocked = new Set<string>([...global.blockedProfiles, ...project.blockedProfiles]);
|
|
583
|
+
const profiles: Record<string, AgentTierProfile> = {};
|
|
584
|
+
const keys = [...new Set([...Object.keys(global.profileEntries), ...Object.keys(project.profileEntries)])];
|
|
585
|
+
for (const key of keys) {
|
|
586
|
+
if (project.profilesPresent && !project.profilesObject) {
|
|
587
|
+
blocked.add(key);
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
if (Object.hasOwn(project.profileEntries, key)) {
|
|
591
|
+
const profile = project.profileEntries[key];
|
|
592
|
+
if (profile) {
|
|
593
|
+
profiles[key] = profile;
|
|
594
|
+
blocked.delete(key);
|
|
595
|
+
} else {
|
|
596
|
+
blocked.add(key);
|
|
597
|
+
}
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
const inherited = global.profileEntries[key];
|
|
601
|
+
if (inherited) profiles[key] = inherited;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (Object.keys(profiles).length > 0) merged.profiles = profiles;
|
|
605
|
+
if (blocked.size > 0) merged.blockedProfiles = [...blocked].sort((a, b) => a.localeCompare(b));
|
|
606
|
+
return merged.defaultTier !== undefined ||
|
|
607
|
+
merged.profiles !== undefined ||
|
|
608
|
+
merged.blockedProfiles !== undefined ||
|
|
609
|
+
merged.blockedDefaultTier !== undefined
|
|
610
|
+
? merged
|
|
611
|
+
: undefined;
|
|
612
|
+
}
|
|
613
|
+
|
|
372
614
|
/** Preserve invalid workflow entry presence so project policy cannot resurrect a global entry. */
|
|
373
615
|
function workflowSource(raw: unknown): WorkflowSource {
|
|
374
616
|
if (!isRecord(raw) || !Object.hasOwn(raw, "workflow")) return emptyWorkflowSource();
|
|
@@ -415,10 +657,12 @@ function workflowSource(raw: unknown): WorkflowSource {
|
|
|
415
657
|
}
|
|
416
658
|
|
|
417
659
|
function readSettingsFile(path: string): ReadSettings {
|
|
418
|
-
if (!existsSync(path))
|
|
660
|
+
if (!existsSync(path)) {
|
|
661
|
+
return { settings: {}, workflow: emptyWorkflowSource(), agentTiers: emptyAgentTiersSource() };
|
|
662
|
+
}
|
|
419
663
|
try {
|
|
420
664
|
const raw: unknown = JSON.parse(readFileSync(path, "utf-8"));
|
|
421
|
-
return { settings: sanitize(raw), workflow: workflowSource(raw) };
|
|
665
|
+
return { settings: sanitize(raw), workflow: workflowSource(raw), agentTiers: agentTiersSource(raw) };
|
|
422
666
|
} catch (err) {
|
|
423
667
|
const reason = err instanceof Error ? err.message : String(err);
|
|
424
668
|
console.warn(`[pi-subagents] Ignoring malformed settings at ${path}: ${reason}`);
|
|
@@ -431,6 +675,9 @@ function readSettingsFile(path: string): ReadSettings {
|
|
|
431
675
|
blockedTiers: [...WORKFLOW_TIER_NAMES],
|
|
432
676
|
blockedDefaultTier: true,
|
|
433
677
|
},
|
|
678
|
+
// An unreadable file may have held the tier catalogue; block rather than
|
|
679
|
+
// fall through to whatever global happens to define.
|
|
680
|
+
agentTiers: { ...emptyAgentTiersSource(), present: true, object: false, blockedDefaultTier: true },
|
|
434
681
|
};
|
|
435
682
|
}
|
|
436
683
|
}
|
|
@@ -444,13 +691,15 @@ function readSettingsFile(path: string): ReadSettings {
|
|
|
444
691
|
export function loadSettings(cwd: string = process.cwd()): SubagentsSettings {
|
|
445
692
|
const global = readSettingsFile(globalPath());
|
|
446
693
|
const project = readSettingsFile(projectPath(cwd));
|
|
447
|
-
const { workflow: _globalWorkflow, ...globalSettings } = global.settings;
|
|
448
|
-
const { workflow: _projectWorkflow, ...projectSettings } = project.settings;
|
|
694
|
+
const { workflow: _globalWorkflow, agentTiers: _globalAgentTiers, ...globalSettings } = global.settings;
|
|
695
|
+
const { workflow: _projectWorkflow, agentTiers: _projectAgentTiers, ...projectSettings } = project.settings;
|
|
449
696
|
const workflow = mergeWorkflowSources(global.workflow, project.workflow);
|
|
697
|
+
const agentTiers = mergeAgentTierSources(global.agentTiers, project.agentTiers);
|
|
450
698
|
return {
|
|
451
699
|
...globalSettings,
|
|
452
700
|
...projectSettings,
|
|
453
701
|
...(workflow ? { workflow } : {}),
|
|
702
|
+
...(agentTiers ? { agentTiers } : {}),
|
|
454
703
|
};
|
|
455
704
|
}
|
|
456
705
|
|
|
@@ -533,6 +782,9 @@ export function applySettings(s: SubagentsSettings, appliers: SettingsAppliers):
|
|
|
533
782
|
if (typeof s.fleetView === "boolean") appliers.setFleetView(s.fleetView);
|
|
534
783
|
if (typeof s.outputTranscript === "boolean") appliers.setOutputTranscript(s.outputTranscript);
|
|
535
784
|
if (s.workflow) appliers.setWorkflow?.(s.workflow);
|
|
785
|
+
// Applied unconditionally so a session that had tiers and no longer does gets
|
|
786
|
+
// the empty catalogue rather than keeping the previous one.
|
|
787
|
+
appliers.setAgentTiers?.(s.agentTiers ?? {});
|
|
536
788
|
}
|
|
537
789
|
|
|
538
790
|
/**
|
package/src/types.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import type { ThinkingLevel } from "@earendil-works/pi-ai";
|
|
6
6
|
import type { AgentSession } from "@earendil-works/pi-coding-agent";
|
|
7
7
|
import type { WorkflowTier } from "@signalridge/pi-subagents-protocol";
|
|
8
|
+
import type { AgentTierResolutionSnapshot } from "./agent-tiers.js";
|
|
8
9
|
import type { LifetimeUsage } from "./usage.js";
|
|
9
10
|
import type { WorkflowTierResolutionSnapshot } from "./workflow-tiers.js";
|
|
10
11
|
import type { WorktreeCleanupResult, WorktreeInfo } from "./worktree.js";
|
|
@@ -50,6 +51,14 @@ export interface AgentConfig {
|
|
|
50
51
|
excludeExtensions?: string[];
|
|
51
52
|
/** true = inherit all, string[] = only listed, false = none */
|
|
52
53
|
skills: true | string[] | false;
|
|
54
|
+
/**
|
|
55
|
+
* This agent's default model tier, from `tier:` in its frontmatter. A tier
|
|
56
|
+
* names a (model, thinking) pair in `agentTiers.profiles`; a tier passed at
|
|
57
|
+
* the call site overrides it. When set, it takes precedence over the legacy
|
|
58
|
+
* `model`/`thinking` fields below, which stay only for agents written before
|
|
59
|
+
* tiers existed.
|
|
60
|
+
*/
|
|
61
|
+
agentTier?: string;
|
|
53
62
|
model?: string;
|
|
54
63
|
thinking?: ThinkingLevel;
|
|
55
64
|
maxTurns?: number;
|
|
@@ -171,6 +180,15 @@ export interface AgentInvocation {
|
|
|
171
180
|
tier?: WorkflowTier;
|
|
172
181
|
/** Immutable model/thinking resolution captured by pi-subagents. */
|
|
173
182
|
tierSnapshot?: WorkflowTierResolutionSnapshot;
|
|
183
|
+
/**
|
|
184
|
+
* User-named tier applied to an ordinary spawn. Deliberately a separate field
|
|
185
|
+
* from `tier` above: that one is the workflow protocol's small/medium/large
|
|
186
|
+
* union, and widening it to hold arbitrary names would take the protocol's
|
|
187
|
+
* exhaustiveness with it.
|
|
188
|
+
*/
|
|
189
|
+
agentTier?: string;
|
|
190
|
+
/** Immutable model/thinking resolution for `agentTier`. */
|
|
191
|
+
agentTierSnapshot?: AgentTierResolutionSnapshot;
|
|
174
192
|
maxTurns?: number;
|
|
175
193
|
isolated?: boolean;
|
|
176
194
|
inheritContext?: boolean;
|
|
@@ -188,6 +206,7 @@ export type AgentRecordSnapshot = Omit<Readonly<AgentRecord>, "session" | "abort
|
|
|
188
206
|
readonly pendingSteers?: readonly string[];
|
|
189
207
|
readonly invocation?: Readonly<AgentInvocation> & {
|
|
190
208
|
readonly tierSnapshot?: Readonly<NonNullable<AgentInvocation["tierSnapshot"]>>;
|
|
209
|
+
readonly agentTierSnapshot?: Readonly<NonNullable<AgentInvocation["agentTierSnapshot"]>>;
|
|
191
210
|
};
|
|
192
211
|
};
|
|
193
212
|
|
|
@@ -234,6 +253,8 @@ export interface ScheduledSubagent {
|
|
|
234
253
|
// spawn params (subset of Agent tool params; no inherit_context, no resume)
|
|
235
254
|
subagent_type: SubagentType;
|
|
236
255
|
prompt: string;
|
|
256
|
+
/** Model tier key, resolved at fire time against the then-current catalogue. */
|
|
257
|
+
tier?: string;
|
|
237
258
|
model?: string;
|
|
238
259
|
thinking?: ThinkingLevel;
|
|
239
260
|
max_turns?: number;
|
|
@@ -177,11 +177,16 @@ export class ConversationViewer implements Component {
|
|
|
177
177
|
const fitted = truncateToWidth(pad(content, innerW), innerW, "...", true);
|
|
178
178
|
return ` ${fitted} `;
|
|
179
179
|
};
|
|
180
|
-
|
|
181
|
-
|
|
180
|
+
// Two full-width rules, top and bottom, and none in between. The overlay
|
|
181
|
+
// floats over the transcript, so without them there is no telling where the
|
|
182
|
+
// agent's conversation ends and the parent's resumes. An inner rule would be
|
|
183
|
+
// a third horizontal line competing with the two that mark the boundary, and
|
|
184
|
+
// a four-sided box would cost two columns on every row for the same job.
|
|
185
|
+
const rule = () => th.fg("border", "─".repeat(Math.max(0, width)));
|
|
182
186
|
const hrMid = row("");
|
|
183
187
|
|
|
184
|
-
lines.push(
|
|
188
|
+
lines.push(rule());
|
|
189
|
+
lines.push(row(th.bold("Agent conversation")));
|
|
185
190
|
const name = getDisplayName(this.record.type);
|
|
186
191
|
const modeLabel = getPromptModeLabel(this.record.type);
|
|
187
192
|
const modeTag = modeLabel ? th.fg("dim", `mode ${modeLabel}`) : undefined;
|
|
@@ -256,7 +261,7 @@ export class ConversationViewer implements Component {
|
|
|
256
261
|
const footerGap = Math.max(1, innerW - visibleWidth(footerLeft) - visibleWidth(footerRight));
|
|
257
262
|
lines.push(row(footerLeft + " ".repeat(footerGap) + footerRight));
|
|
258
263
|
}
|
|
259
|
-
lines.push(
|
|
264
|
+
lines.push(rule());
|
|
260
265
|
|
|
261
266
|
return lines;
|
|
262
267
|
}
|