pi-pstack-adapter 0.8.0 → 0.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/status.ts CHANGED
@@ -1,7 +1,12 @@
1
1
  import { CERTIFIED } from "./certified.ts";
2
- import { CAPABILITY_LABELS, GENERAL_ROLE, MODEL_ROLES, PANEL_DEFAULT_ROLES, PANEL_ROLES, TIER_ORDER } from "./registry.ts";
2
+ import { CAPABILITY_LABELS, MODEL_ROLES, PANEL_DEFAULT_ROLES, PANEL_ROLES, TIER_ORDER } from "./registry.ts";
3
3
  import type { AdapterConfig, ModelSpec } from "./config.ts";
4
- import { panelEntries, splitModelSpec, trustWarning } from "./config.ts";
4
+ import { trustWarning } from "./config.ts";
5
+ import { type FilterOrigin, briefLine, describeOrigin, filterOf, isRestricting } from "./filter.ts";
6
+ import { type PanelSeat, type ResolutionContext, type ResolvedPanel, type RoleResolution, defaultSeats, resolvePanel, resolveRole } from "./pools.ts";
7
+
8
+ export { resolvePanel } from "./pools.ts";
9
+ export type { PanelSeat, ResolvedPanel } from "./pools.ts";
5
10
  import { DELEGATION_BACKENDS, delegationOrder } from "./delegation.ts";
6
11
  import type { PrerequisiteReport } from "./capabilities.ts";
7
12
  import type {
@@ -30,10 +35,16 @@ export interface StatusInput {
30
35
  readonly bootstrapped: boolean;
31
36
  /** Runtime findings that generation could not know, such as agent registration results. */
32
37
  readonly runtimeDiagnostics?: readonly Diagnostic[];
38
+ /** The model filter in force and where it came from. None when omitted. */
39
+ readonly filter?: FilterOrigin;
40
+ /** `provider/id` of the session's model, which an inheriting role runs on. */
41
+ readonly parentModel?: string;
33
42
  }
34
43
 
35
44
  export function renderStatus(input: StatusInput): string {
36
45
  const { config, source } = input;
46
+ const origin = input.filter ?? { kind: "none" };
47
+ const context: ResolutionContext = { filter: filterOf(origin), parentModel: input.parentModel };
37
48
  const lines: string[] = [];
38
49
 
39
50
  lines.push(`# ${config.namespace} adapter status`);
@@ -60,6 +71,7 @@ export function renderStatus(input: StatusInput): string {
60
71
  lines.push(`| Poteto Mode | ${input.modeActive ? "active" : "off"} |`);
61
72
  lines.push(`| delegation order | ${delegationOrder(config.delegation).map((backend) => backend.id).join(", then ")} |`);
62
73
  lines.push(`| delegation | ${describeDelegation(input.capabilities)} |`);
74
+ lines.push(`| model filter | ${describeOrigin(origin)} |`);
63
75
  lines.push("");
64
76
 
65
77
  const warning = trustWarning(source.trust, config);
@@ -90,25 +102,16 @@ export function renderStatus(input: StatusInput): string {
90
102
  lines.push("");
91
103
  lines.push(`| role | model | thinking |`);
92
104
  lines.push(`| --- | --- | --- |`);
93
- const extra = Object.keys(config.models)
94
- .filter((role) => !MODEL_ROLES.includes(role) && !PANEL_ROLES.includes(role))
95
- .sort();
96
- const general = config.models[GENERAL_ROLE];
97
- for (const role of [...MODEL_ROLES, ...extra]) {
98
- const configured = config.models[role];
99
- if (configured === undefined && general !== undefined) {
100
- const spec = splitModelSpec(general);
101
- lines.push(`| ${role} | ${spec.model} (from ${GENERAL_ROLE}) | ${spec.thinking ?? "backend default"} (from ${GENERAL_ROLE}) |`);
102
- continue;
103
- }
104
- if (configured === undefined) {
105
- lines.push(`| ${role} | inherit (parent session model) | inherit (parent session thinking) |`);
106
- continue;
107
- }
108
- const spec = splitModelSpec(configured);
109
- lines.push(`| ${role} | ${spec.model} | ${spec.thinking ?? "backend default"} |`);
105
+ for (const role of [...MODEL_ROLES, ...extraRoles(config)]) {
106
+ const [model, thinking] = describeRole(resolveRole(config, role, context), role);
107
+ lines.push(`| ${role} | ${model} | ${thinking} |`);
110
108
  }
111
109
  lines.push("");
110
+ const policy = renderModelPolicy(config, origin, input.parentModel);
111
+ if (policy) {
112
+ lines.push(policy);
113
+ lines.push("");
114
+ }
112
115
 
113
116
  lines.push(`## Panel roles`);
114
117
  lines.push("");
@@ -116,13 +119,13 @@ export function renderStatus(input: StatusInput): string {
116
119
  lines.push("");
117
120
  lines.push(`| panel | seats | source |`);
118
121
  lines.push(`| --- | --- | --- |`);
119
- const panels = PANEL_ROLES.map((panel) => resolvePanel(config, panel));
122
+ const panels = PANEL_ROLES.map((panel) => resolvePanel(config, panel, context));
120
123
  for (const panel of panels) {
121
124
  lines.push(`| ${panel.role} | ${panel.seats.map(describeSeat).join("; ")} | ${panel.configured ? "configured" : `unset, so ${PANEL_DEFAULT_ROLES.join(", ")}`} |`);
122
125
  }
123
126
  lines.push("");
124
127
  for (const panel of panels) {
125
- const warning = panelWarning(panel, config);
128
+ const warning = panelWarning(panel, config, context);
126
129
  if (warning) {
127
130
  lines.push(`> Warning. ${warning}`);
128
131
  lines.push("");
@@ -161,99 +164,156 @@ export function renderStatus(input: StatusInput): string {
161
164
  return lines.join("\n");
162
165
  }
163
166
 
164
- /** One seat of a panel. `spec` is undefined when the seat runs on the model of the agent that starts the panel. */
165
- export interface PanelSeat {
166
- readonly spec?: ModelSpec;
167
- /** The role a default seat came from, such as `general` for an unset `judgment`. */
168
- readonly from?: string;
167
+ function describeSpec(spec: ModelSpec): string {
168
+ return `${spec.model} at ${spec.thinking ?? "backend default"}`;
169
169
  }
170
170
 
171
- export interface ResolvedPanel {
172
- readonly role: string;
173
- readonly configured: boolean;
174
- readonly seats: readonly PanelSeat[];
171
+ function describeSeat(seat: PanelSeat): string {
172
+ const origin = seat.from ? ` (${seat.from})` : "";
173
+ switch (seat.kind) {
174
+ case "parent":
175
+ return `model of the agent that starts the panel${origin}`;
176
+ case "model":
177
+ return `${describeSpec(seat.spec)}${origin}`;
178
+ case "choice":
179
+ return `one of ${seat.choices.map(describeSpec).join(" or ")}, picked once for this seat${origin}`;
180
+ case "blocked":
181
+ return `BLOCKED, ${seat.reason}${origin}`;
182
+ }
175
183
  }
176
184
 
177
- /** A single role's model as delegation resolves it: the role, then `general`, then the parent. */
178
- function resolveRoleSeat(config: AdapterConfig, role: string): PanelSeat {
179
- const configured = config.models[role];
180
- if (configured !== undefined) return { spec: splitModelSpec(configured), from: role };
181
- const general = config.models[GENERAL_ROLE];
182
- if (general !== undefined) return { spec: splitModelSpec(general), from: GENERAL_ROLE };
183
- return { from: role };
184
- }
185
+ const PARENT_MODEL = "the model of the agent that starts the panel";
185
186
 
186
- export function resolvePanel(config: AdapterConfig, role: string): ResolvedPanel {
187
- const configured = config.models[role];
188
- if (configured !== undefined) {
189
- return {
190
- role,
191
- configured: true,
192
- seats: panelEntries(configured).map((entry) => (entry.kind === "inherit" ? {} : { spec: entry.spec })),
193
- };
187
+ /** The model a seat runs on, when it is already determined. */
188
+ function seatModel(seat: PanelSeat): string | undefined {
189
+ switch (seat.kind) {
190
+ case "parent":
191
+ return PARENT_MODEL;
192
+ case "model":
193
+ return seat.spec.model;
194
+ case "choice": {
195
+ const models = new Set(seat.choices.map((choice) => choice.model));
196
+ return models.size === 1 ? [...models][0] : undefined;
197
+ }
198
+ case "blocked":
199
+ return undefined;
194
200
  }
195
- return { role, configured: false, seats: defaultSeats(config) };
196
201
  }
197
202
 
198
- /** The seats an unset panel gets, which are also the default pool arena falls back to. */
199
- function defaultSeats(config: AdapterConfig): PanelSeat[] {
200
- return PANEL_DEFAULT_ROLES.map((name) => resolveRoleSeat(config, name));
201
- }
202
-
203
- function describeSeat(seat: PanelSeat): string {
204
- const origin = seat.from ? ` (${seat.from})` : "";
205
- if (!seat.spec) return `model of the agent that starts the panel${origin}`;
206
- return `${seat.spec.model} at ${seat.spec.thinking ?? "backend default"}${origin}`;
203
+ function seatModels(seats: readonly PanelSeat[]): Set<string> | undefined {
204
+ const models = new Set<string>();
205
+ for (const seat of seats) {
206
+ const model = seatModel(seat);
207
+ if (model !== undefined) models.add(model);
208
+ else if (seat.kind === "choice") return undefined;
209
+ }
210
+ return models;
207
211
  }
208
212
 
209
- const PARENT_MODEL = "the model of the agent that starts the panel";
210
-
211
213
  /**
212
214
  * Upstream's default panels span two model families. A panel whose seats all land on one model
213
215
  * lost that because of configuration, not because no other model exists, so say so where the
214
216
  * agent reads it. Families are left to the agent, which knows them better than a prefix list.
215
217
  */
216
- export function panelWarning(panel: ResolvedPanel, config: AdapterConfig): string | undefined {
217
- const models = new Set(panel.seats.map((seat) => seat.spec?.model ?? PARENT_MODEL));
218
+ export function panelWarning(panel: ResolvedPanel, config: AdapterConfig, context: ResolutionContext = {}): string | undefined {
219
+ const usable = panel.seats.filter((seat) => seat.kind !== "blocked");
220
+ const models = seatModels(usable);
218
221
  const pool = panel.role === "arena-cross-judge-pool";
219
222
  // Upstream lets a panel line hold one entry on purpose, so only the judge pool warns at one seat.
220
- if (models.size !== 1 || (panel.seats.length < 2 && !pool)) return undefined;
223
+ if (models === undefined || models.size !== 1 || (usable.length < 2 && !pool)) return undefined;
221
224
  const [model] = [...models];
222
225
  const named = model === PARENT_MODEL ? PARENT_MODEL : `\`${model}\``;
223
226
  const cause = panel.configured
224
227
  ? `its configured entries all name ${named}`
225
228
  : `it is unset, and ${PANEL_DEFAULT_ROLES.map((role) => `\`${role}\``).join(" and ")} both resolve to ${named}`;
226
- return `Every seat of \`${panel.role}\` runs on ${named}, because ${cause}, ${pool ? poolEffect(panel, config, model) : "so the panel has one model and no second family"}. This comes from the adapter configuration, not from which models are available. Run \`/skill:${config.namespace}-setup\` to put this panel on two model families, as upstream's defaults do.`;
229
+ return `Every seat of \`${panel.role}\` runs on ${named}, because ${cause}, ${pool ? poolEffect(panel, config, model, context) : "so the panel has one model and no second family"}. This comes from the adapter configuration, not from which models are available. Run \`/skill:${config.namespace}-setup\` to put this panel on two model families, as upstream's defaults do.`;
227
230
  }
228
231
 
229
232
  /**
230
233
  * Arena's judge is never from the session's family. When no pool entry qualifies, upstream tries
231
234
  * its default pool, which here is `judgment`, then `general`, before it skips the judge.
232
235
  */
233
- function poolEffect(panel: ResolvedPanel, config: AdapterConfig, model: string | undefined): string {
234
- const fallback = defaultSeats(config);
235
- const fallbackModels = new Set(fallback.map((seat) => seat.spec?.model ?? PARENT_MODEL));
236
+ function poolEffect(panel: ResolvedPanel, config: AdapterConfig, model: string | undefined, context: ResolutionContext): string {
237
+ const fallback = defaultSeats(config, context);
238
+ const fallbackModels = seatModels(fallback);
236
239
  const lead =
237
240
  model === PARENT_MODEL
238
241
  ? "and an `inherit` entry never judges, so arena"
239
242
  : `so a session on the family of \`${model}\` gets no judge from the pool, and arena`;
240
- const sameFallback = !panel.configured || (fallbackModels.size === 1 && fallbackModels.has(model ?? PARENT_MODEL));
243
+ const sameFallback = !panel.configured || (fallbackModels?.size === 1 && fallbackModels.has(model ?? PARENT_MODEL));
241
244
  return sameFallback
242
245
  ? `${lead} skips the judge`
243
246
  : `${lead} tries its default pool, ${fallback.map(describeSeat).join("; ")}, before it skips the judge`;
244
247
  }
245
248
 
246
249
  /** The panels a workflow reads, for its `check` report. */
247
- export function renderPanels(config: AdapterConfig, roles: readonly string[]): string {
250
+ export function renderPanels(config: AdapterConfig, roles: readonly string[], context: ResolutionContext = {}): string {
248
251
  const lines = roles.map((role) => {
249
- const panel = resolvePanel(config, role);
252
+ const panel = resolvePanel(config, role, context);
250
253
  const source = panel.configured ? "configured" : `unset, so ${PANEL_DEFAULT_ROLES.join(", ")}`;
251
- const warning = panelWarning(panel, config);
254
+ const warning = panelWarning(panel, config, context);
252
255
  return `- \`${role}\` (${source}): ${panel.seats.map(describeSeat).join("; ")}.${warning ? ` Warning. ${warning}` : ""}`;
253
256
  });
254
257
  return `Panels this workflow uses, one seat per entry:\n${lines.join("\n")}`;
255
258
  }
256
259
 
260
+ /** One `model roles` row: the model column and the thinking column. */
261
+ function describeRole(resolution: RoleResolution, role: string): readonly [string, string] {
262
+ const via = resolution.from === role ? "" : ` (from ${resolution.from})`;
263
+ switch (resolution.kind) {
264
+ case "inherit":
265
+ return ["inherit (parent session model)", "inherit (parent session thinking)"];
266
+ case "blocked": {
267
+ const excluded = resolution.choices.map((choice) => choice.spec.model);
268
+ return [`BLOCKED, ${resolution.reason}${excluded.length > 0 ? `: ${excluded.join(", ")}` : ""}. Do not delegate this role${via}`, "-"];
269
+ }
270
+ case "pool": {
271
+ const [only] = resolution.choices;
272
+ if (resolution.choices.length === 1 && only) return [`${only.spec.model}${via}`, `${only.spec.thinking ?? "backend default"}${via}`];
273
+ const eligible = resolution.choices.filter((choice) => choice.eligible).map((choice) => describeSpec(choice.spec));
274
+ const excluded = resolution.choices.filter((choice) => !choice.eligible).map((choice) => describeSpec(choice.spec));
275
+ const tail = excluded.length > 0 ? `; excluded by the model filter: ${excluded.join(", ")}` : "";
276
+ return [`pool, pick one of ${eligible.join(", ")}${tail}${via}`, `per choice${via}`];
277
+ }
278
+ }
279
+ }
280
+
281
+ /**
282
+ * The delegation rules a workflow's `check` report adds when pools, a model filter, or blocked roles
283
+ * are in play. Nothing when none of them is, so the report stays as it was.
284
+ */
285
+ export function renderModelPolicy(config: AdapterConfig, origin: FilterOrigin, parentModel: string | undefined): string | undefined {
286
+ const filter = filterOf(origin);
287
+ const context: ResolutionContext = { filter, parentModel };
288
+ const roles = [...MODEL_ROLES, ...extraRoles(config)];
289
+ const resolutions = roles.map((role) => [role, resolveRole(config, role, context)] as const);
290
+ const blocked = resolutions.filter(([, resolution]) => resolution.kind === "blocked");
291
+ const pools = resolutions.filter(([, resolution]) => resolution.kind === "pool" && resolution.choices.length > 1);
292
+ if (!isRestricting(filter) && pools.length === 0 && blocked.length === 0) return undefined;
293
+ const lines: string[] = [];
294
+ if (isRestricting(filter)) {
295
+ lines.push(`Model filter: ${describeOrigin(origin)}. The adapter blocks a delegation whose model it excludes, or whose model it cannot tell.`);
296
+ lines.push(`Put this line in each brief, verbatim: "${briefLine(filter)}"`);
297
+ }
298
+ if (pools.length > 0) {
299
+ lines.push(
300
+ `Pools: ${pools.map(([role, resolution]) => `\`${role}\` picks one of ${describeRole(resolution, role)[0].replace(/^pool, pick one of /, "")}`).join("; ")}. For each delegation, pick ONE eligible model from the role's pool by fit. A pool is not a retry list and not a panel: do not start the same task on several of its models, and when a delegation fails, report the failure instead of trying another model.`,
301
+ );
302
+ }
303
+ if (blocked.length > 0) {
304
+ lines.push(
305
+ `Blocked roles: ${blocked.map(([role, resolution]) => `\`${role}\` (${resolution.kind === "blocked" ? resolution.reason : ""})`).join("; ")}. Do not delegate work for a blocked role, and do not move it to \`general\` or your own model. Tell the user which role is blocked and why.`,
306
+ );
307
+ }
308
+ return lines.join("\n\n");
309
+ }
310
+
311
+ function extraRoles(config: AdapterConfig): string[] {
312
+ return Object.keys(config.models)
313
+ .filter((role) => !MODEL_ROLES.includes(role) && !PANEL_ROLES.includes(role))
314
+ .sort();
315
+ }
316
+
257
317
  function describeDelegation(capabilities: readonly CapabilityStatus[]): string {
258
318
  const backend = capabilities.find((capability) => capability.id === "delegation")?.backend;
259
319
  if (!backend) return "none available";
@@ -294,6 +354,8 @@ export function renderCheck(
294
354
  delegation?: CapabilityStatus,
295
355
  /** The workflow's panels, from `renderPanels`. */
296
356
  panels?: string,
357
+ /** Pools, the model filter, and blocked roles, from `renderModelPolicy`. */
358
+ policy?: string,
297
359
  ): string {
298
360
  const parts = [`\`/skill:${skill.generatedName}\` is tier **${skill.tier}**.`];
299
361
  if (entry.note) parts.push(entry.note);
@@ -313,6 +375,7 @@ export function renderCheck(
313
375
  );
314
376
  }
315
377
  if (panels && skill.tier !== "unsupported") parts.push(panels);
378
+ if (policy && skill.capabilities.includes("delegation") && skill.tier !== "unsupported") parts.push(policy);
316
379
  if (skill.tier === "experimental" && report.ok) {
317
380
  parts.push("Tier `experimental` means parts of this workflow have no Pi equivalent. Expect gaps.");
318
381
  }