@signalridge/pi-subagents 1.8.0 → 1.9.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/src/types.ts CHANGED
@@ -4,13 +4,11 @@
4
4
 
5
5
  import type { ThinkingLevel } from "@earendil-works/pi-ai";
6
6
  import type { AgentSession } from "@earendil-works/pi-coding-agent";
7
- import type { WorkflowTier } from "@signalridge/pi-subagents-protocol";
8
7
  import type { AgentTierResolutionSnapshot } from "./agent-tiers.js";
9
8
  import type { LifetimeUsage } from "./usage.js";
10
- import type { WorkflowTierResolutionSnapshot } from "./workflow-tiers.js";
11
9
  import type { WorktreeCleanupResult, WorktreeInfo } from "./worktree.js";
12
10
 
13
- export type { ThinkingLevel, WorkflowTier };
11
+ export type { ThinkingLevel };
14
12
 
15
13
  /** Agent type: any string name (built-in defaults or user-defined). */
16
14
  export type SubagentType = string;
@@ -237,18 +235,18 @@ export interface ResumableAgentEntry {
237
235
  }
238
236
 
239
237
  export interface AgentInvocation {
240
- /** Short display name, e.g. "haiku" — only set when different from parent. */
238
+ /**
239
+ * Short display name, e.g. "haiku". Set only when the spawn is not simply
240
+ * running the parent's model: an untiered spawn that resolved a different one,
241
+ * or a tier whose profile pins one. A tier that inherits leaves this absent,
242
+ * which is how the UI says "same model as the parent".
243
+ */
241
244
  modelName?: string;
242
245
  thinking?: ThinkingLevel;
243
- /** Semantic workflow tier selected by the workflow definition. */
244
- tier?: WorkflowTier;
245
- /** Immutable model/thinking resolution captured by pi-subagents. */
246
- tierSnapshot?: WorkflowTierResolutionSnapshot;
247
246
  /**
248
- * User-named tier applied to an ordinary spawn. Deliberately a separate field
249
- * from `tier` above: that one is the workflow protocol's small/medium/large
250
- * union, and widening it to hold arbitrary names would take the protocol's
251
- * exhaustiveness with it.
247
+ * The tier applied to this spawn, from any caller — the Agent tool, nested
248
+ * delegation, the scheduler, or a managed workflow call. All four name a key
249
+ * from the one `agentTiers` catalogue.
252
250
  */
253
251
  agentTier?: string;
254
252
  /** Immutable model/thinking resolution for `agentTier`. */
@@ -269,7 +267,6 @@ export type AgentRecordSnapshot = Omit<Readonly<AgentRecord>, "session" | "abort
269
267
  readonly lifetimeUsage: Readonly<LifetimeUsage>;
270
268
  readonly pendingSteers?: readonly string[];
271
269
  readonly invocation?: Readonly<AgentInvocation> & {
272
- readonly tierSnapshot?: Readonly<NonNullable<AgentInvocation["tierSnapshot"]>>;
273
270
  readonly agentTierSnapshot?: Readonly<NonNullable<AgentInvocation["agentTierSnapshot"]>>;
274
271
  };
275
272
  };
@@ -174,7 +174,7 @@ export function buildInvocationTags(
174
174
  ): { modelName?: string; tags: string[] } {
175
175
  const tags: string[] = [];
176
176
  if (!invocation) return { tags };
177
- if (invocation.tier) tags.push(`tier: ${invocation.tier}`);
177
+ if (invocation.agentTier) tags.push(`tier: ${invocation.agentTier}`);
178
178
  if (invocation.thinking) tags.push(`thinking: ${invocation.thinking}`);
179
179
  if (invocation.isolated) tags.push("isolated");
180
180
  if (invocation.isolation === "worktree") tags.push("worktree");
@@ -1,202 +0,0 @@
1
- /** Resolution and audit snapshots for semantic workflow model tiers. */
2
- import {
3
- type Api,
4
- clampThinkingLevel,
5
- getSupportedThinkingLevels,
6
- type Model,
7
- } from "@earendil-works/pi-ai";
8
- import type { WorkflowTier } from "@signalridge/pi-subagents-protocol";
9
- import { type ModelRegistry, resolveModel } from "./model-resolver.js";
10
- import {
11
- DEFAULT_WORKFLOW_TIER_PROFILES,
12
- type WorkflowSettings,
13
- type WorkflowThinking,
14
- type WorkflowTierProfile,
15
- } from "./settings.js";
16
- import type { AgentConfig, ThinkingLevel } from "./types.js";
17
-
18
- let workflowSettings: WorkflowSettings = {};
19
-
20
- export type { WorkflowTier };
21
-
22
- export type WorkflowResolutionSource = "frontmatter" | "tier" | "parent";
23
-
24
- /** Durable, JSON-safe explanation of the policy used for one managed spawn. */
25
- export interface WorkflowTierResolutionSnapshot {
26
- tier: WorkflowTier;
27
- /** Effective provider/model id, after resolving or inheriting the parent. */
28
- model?: string;
29
- /** Effective thinking level; omitted when the selected model supports no level. */
30
- thinking?: ThinkingLevel;
31
- configuredModel?: string;
32
- configuredThinking?: WorkflowThinking;
33
- requestedThinking?: ThinkingLevel;
34
- modelSource: WorkflowResolutionSource;
35
- thinkingSource: WorkflowResolutionSource;
36
- /** True when pi-ai lowered the requested level for the selected model. */
37
- clamped?: boolean;
38
- diagnostic?: string;
39
- }
40
-
41
- export interface WorkflowTierResolution {
42
- model?: Model<Api>;
43
- thinkingLevel?: ThinkingLevel;
44
- snapshot?: WorkflowTierResolutionSnapshot;
45
- }
46
-
47
- export interface ResolveWorkflowTierInput {
48
- /** Explicit semantic tier from a workflow task. */
49
- tier?: WorkflowTier;
50
- /** Settings are passed by pi-subagents; workflows never resolve policy. */
51
- settings?: WorkflowSettings;
52
- agentConfig?: AgentConfig;
53
- /** Direct model objects take precedence over agent frontmatter and tiers. */
54
- directModel?: Model<Api>;
55
- /** Direct model references take precedence over agent frontmatter and tiers. */
56
- modelOverride?: string;
57
- thinkingOverride?: ThinkingLevel;
58
- parentModel?: Model<Api>;
59
- parentThinking?: ThinkingLevel;
60
- modelRegistry: ModelRegistry<Model<Api>>;
61
- }
62
-
63
- export function isWorkflowTier(value: unknown): value is WorkflowTier {
64
- return value === "small" || value === "medium" || value === "large";
65
- }
66
-
67
- export function getWorkflowSettings(): WorkflowSettings {
68
- return structuredClone(workflowSettings);
69
- }
70
-
71
- export function setWorkflowSettings(settings: WorkflowSettings): void {
72
- workflowSettings = structuredClone(settings);
73
- }
74
-
75
-
76
- function effectiveModelId(model: Model<Api> | undefined): string | undefined {
77
- return model ? `${model.provider}/${model.id}` : undefined;
78
- }
79
-
80
- /**
81
- * Resolve model and thinking independently, preserving agent frontmatter as the
82
- * authoritative override. A tier only fills fields omitted by frontmatter; the
83
- * parent session is the final fallback. An unavailable model explicitly selected
84
- * by a tier fails closed instead of silently changing the model.
85
- */
86
- function isCompleteProfile(value: unknown): value is WorkflowTierProfile {
87
- if (!value || typeof value !== "object" || Array.isArray(value)) return false;
88
- const profile = value as Record<string, unknown>;
89
- return (
90
- typeof profile.model === "string" &&
91
- profile.model.length > 0 &&
92
- typeof profile.thinking === "string" &&
93
- ["inherit", "minimal", "low", "medium", "high", "xhigh", "max"].includes(profile.thinking)
94
- );
95
- }
96
-
97
- /**
98
- * Resolve model and thinking independently, preserving agent frontmatter as the
99
- * authoritative override. A tier only fills fields omitted by frontmatter; the
100
- * parent session is the final fallback. The configured default tier is applied
101
- * only inside pi-subagents, never by the workflow package.
102
- */
103
- export function resolveWorkflowTier(input: ResolveWorkflowTierInput): WorkflowTierResolution {
104
- if (input.tier !== undefined && !isWorkflowTier(input.tier)) {
105
- throw new Error("workflow tier must be one of small, medium, or large");
106
- }
107
-
108
- const workflow = input.settings ?? workflowSettings;
109
- if (input.tier === undefined && workflow.blockedDefaultTier) {
110
- throw new Error("workflow defaultTier is blocked by malformed configuration");
111
- }
112
- const tier = input.tier ?? workflow.defaultTier;
113
- if (tier !== undefined && !isWorkflowTier(tier)) {
114
- throw new Error("workflow defaultTier must be one of small, medium, or large");
115
- }
116
-
117
- const defaultProfile = tier === undefined ? undefined : DEFAULT_WORKFLOW_TIER_PROFILES[tier];
118
- const configuredProfile = tier === undefined ? undefined : workflow.tiers?.[tier];
119
- if (tier !== undefined && workflow.blockedTiers?.includes(tier)) {
120
- throw new Error(`workflow tier "${tier}" is blocked by malformed configuration`);
121
- }
122
- if (configuredProfile !== undefined && !isCompleteProfile(configuredProfile)) {
123
- // This should be unreachable after settings validation. Throwing here keeps
124
- // a forged in-process settings object from degrading into parent policy.
125
- throw new Error(`workflow tier "${tier}" has an incomplete model/thinking profile`);
126
- }
127
- const profile = tier === undefined
128
- ? undefined
129
- : { ...defaultProfile, ...(configuredProfile ?? {}) };
130
-
131
- const tierConfiguredModel = profile?.model;
132
- const configuredModel = input.modelOverride ?? input.agentConfig?.model ?? tierConfiguredModel;
133
- const modelSource: WorkflowResolutionSource = input.directModel !== undefined || input.modelOverride !== undefined || input.agentConfig?.model !== undefined
134
- ? "frontmatter"
135
- : configuredModel !== undefined && configuredModel !== "inherit"
136
- ? "tier"
137
- : "parent";
138
- let model = input.directModel ?? input.parentModel;
139
- let diagnostic: string | undefined;
140
-
141
- if (input.directModel === undefined && configuredModel !== undefined && configuredModel !== "inherit") {
142
- const resolved = resolveModel(configuredModel, input.modelRegistry);
143
- if (typeof resolved === "string") {
144
- const tierOwnsModel =
145
- input.modelOverride === undefined &&
146
- input.agentConfig?.model === undefined &&
147
- tierConfiguredModel !== undefined &&
148
- tierConfiguredModel !== "inherit";
149
- if (tierOwnsModel) {
150
- throw new Error(`workflow tier "${tier}" has an unavailable model: ${resolved}`);
151
- }
152
- diagnostic = `${resolved} Falling back to the parent model.`;
153
- } else {
154
- model = resolved;
155
- }
156
- }
157
-
158
- const tierConfiguredThinking = profile?.thinking;
159
- const configuredThinking = input.thinkingOverride ?? input.agentConfig?.thinking ?? tierConfiguredThinking;
160
- const thinkingSource: WorkflowResolutionSource = input.thinkingOverride !== undefined || input.agentConfig?.thinking !== undefined
161
- ? "frontmatter"
162
- : tierConfiguredThinking !== undefined && tierConfiguredThinking !== "inherit"
163
- ? "tier"
164
- : "parent";
165
- const requestedThinking = configuredThinking === "inherit" ? input.parentThinking : configuredThinking ?? input.parentThinking;
166
- let thinkingLevel = requestedThinking;
167
- let clamped = false;
168
-
169
- if (model && requestedThinking) {
170
- const supported = getSupportedThinkingLevels(model);
171
- const clampedLevel = clampThinkingLevel(model, requestedThinking);
172
- if (clampedLevel !== requestedThinking) {
173
- clamped = true;
174
- const supportedText = supported.join(", ");
175
- const clampDiagnostic = `Thinking level "${requestedThinking}" is not supported by ${effectiveModelId(model) ?? "the selected model"}; using "${clampedLevel}" (supported: ${supportedText}).`;
176
- diagnostic = diagnostic ? `${diagnostic} ${clampDiagnostic}` : clampDiagnostic;
177
- }
178
- // "off" is a ModelThinkingLevel sentinel, not a ThinkingLevel accepted by
179
- // AgentSession options. Omitting the option preserves the provider's off behavior.
180
- thinkingLevel = clampedLevel === "off" ? undefined : clampedLevel as ThinkingLevel;
181
- }
182
-
183
- if (tier === undefined) return { model, thinkingLevel };
184
-
185
- const snapshot: WorkflowTierResolutionSnapshot = {
186
- tier,
187
- ...(effectiveModelId(model) ? { model: effectiveModelId(model) } : {}),
188
- ...(thinkingLevel ? { thinking: thinkingLevel } : {}),
189
- ...(configuredModel !== undefined ? { configuredModel } : {}),
190
- ...(configuredThinking !== undefined ? { configuredThinking } : {}),
191
- ...(requestedThinking !== undefined ? { requestedThinking } : {}),
192
- modelSource,
193
- thinkingSource,
194
- ...(clamped ? { clamped: true } : {}),
195
- ...(diagnostic ? { diagnostic } : {}),
196
- };
197
-
198
- return { model, thinkingLevel, snapshot };
199
- }
200
-
201
- /** Compatibility alias for callers that use the shorter tier terminology. */
202
- export const resolveTier = resolveWorkflowTier;