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/CHANGELOG.md +7 -0
- package/README.md +66 -1
- package/VERSION +1 -1
- package/adapter-skills/setup.md +40 -8
- package/index.ts +197 -20
- package/package.json +1 -1
- package/src/config.ts +96 -8
- package/src/filter.ts +150 -0
- package/src/gate.ts +139 -0
- package/src/generate.ts +28 -2
- package/src/pools.ts +104 -0
- package/src/status.ts +132 -69
package/src/status.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { CERTIFIED } from "./certified.ts";
|
|
2
|
-
import { CAPABILITY_LABELS,
|
|
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 {
|
|
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
|
|
94
|
-
|
|
95
|
-
.
|
|
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
|
-
|
|
165
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
|
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 || (
|
|
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 =
|
|
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
|
|
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
|
}
|