pi-bro 0.25.1 → 0.26.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/settings.ts CHANGED
@@ -9,20 +9,22 @@ import {
9
9
  GROK_EFFORTS,
10
10
  CODEX_EFFORTS,
11
11
  MUSE_EFFORTS,
12
+ PI_EFFORTS,
12
13
  } from "./backend.ts";
13
14
  import { type BroMode, DEFAULT_BRO_MODE, MAX_PREFERENCES_CHARS, parseBroMode } from "./prompt.ts";
14
15
  import { errorMessage, isRecord } from "./util.ts";
15
16
 
16
17
  export const EFFORTS = ["default", "low", "medium", "high"] as const;
17
- export const BACKENDS = ["agy", "claude", "grok", "codex", "muse"] as const;
18
+ export const BACKENDS = ["agy", "claude", "grok", "codex", "muse", "pi"] as const;
18
19
  export type BackendName = (typeof BACKENDS)[number];
19
20
  export type BroEffort = "default" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
20
21
  export type AgyEffort = Exclude<BroEffort, "default" | "minimal" | "xhigh" | "max">;
21
22
  export type ClaudeEffort = (typeof CLAUDE_EFFORTS)[number];
22
- export { GROK_EFFORTS, CODEX_EFFORTS, MUSE_EFFORTS };
23
+ export { GROK_EFFORTS, CODEX_EFFORTS, MUSE_EFFORTS, PI_EFFORTS };
23
24
  export type GrokEffort = (typeof GROK_EFFORTS)[number];
24
25
  export type CodexEffort = (typeof CODEX_EFFORTS)[number];
25
26
  export type MuseEffort = (typeof MUSE_EFFORTS)[number];
27
+ export type PiEffort = (typeof PI_EFFORTS)[number];
26
28
 
27
29
  export const CAPABILITIES = ["explain", "show", "btw", "advisor", "review"] as const;
28
30
  export type Capability = (typeof CAPABILITIES)[number];
@@ -75,7 +77,7 @@ export const DEFAULT_SHOW_TURNS = 1;
75
77
 
76
78
  export function parseBackend(value: unknown, context: string): BackendName {
77
79
  if (typeof value !== "string" || !BACKENDS.some((backend) => backend === value)) {
78
- throw new Error(`${context} backend must be "agy", "claude", "grok", "codex", or "muse".`);
80
+ throw new Error(`${context} backend must be "agy", "claude", "grok", "codex", "muse", or "pi".`);
79
81
  }
80
82
  return value as BackendName;
81
83
  }
@@ -100,6 +102,12 @@ export function isMuseEffort(effort: unknown): effort is MuseEffort | "default"
100
102
  return effort === "default" || MUSE_EFFORTS.some((item) => item === effort);
101
103
  }
102
104
 
105
+ // The in-process Pi backend accepts the whole BroEffort range; createAgentSession clamps it to
106
+ // what the chosen physical model actually supports.
107
+ export function isPiEffort(effort: unknown): effort is BroEffort {
108
+ return effort === "default" || PI_EFFORTS.some((item) => item === effort);
109
+ }
110
+
103
111
  // Sonnet/opus aliases resolve case-insensitively; any other non-empty string passes
104
112
  // through untouched as an explicit user-entered model ID.
105
113
  export function resolveClaudeModel(input: string): string {
@@ -133,7 +141,18 @@ export function resolveMuseModel(input: string): string {
133
141
  return trimmed;
134
142
  }
135
143
 
136
- export type ExternalBackend = "claude" | "grok" | "codex" | "muse";
144
+ // Pi models are physical registry entries addressed as a non-empty "<provider>/<id>" pair;
145
+ // only the first "/" separates the two so model IDs containing slashes survive intact.
146
+ export function resolvePiModel(input: string): string {
147
+ const trimmed = input.trim();
148
+ const separator = trimmed.indexOf("/");
149
+ if (separator <= 0 || separator === trimmed.length - 1) {
150
+ throw new Error("Pi model must be a non-empty <provider>/<id> pair from your Pi model catalog.");
151
+ }
152
+ return trimmed;
153
+ }
154
+
155
+ export type ExternalBackend = "claude" | "grok" | "codex" | "muse" | "pi";
137
156
 
138
157
  export interface ExternalBackendMeta {
139
158
  readonly name: ExternalBackend;
@@ -144,6 +163,8 @@ export interface ExternalBackendMeta {
144
163
  readonly effortHelp: string;
145
164
  isEffort(effort: unknown): effort is BroEffort;
146
165
  resolveModel(input: string): string;
166
+ /** How this backend's stored model reads in the config list. Defaults to `prefix + model`. */
167
+ displayModel?(model: string): string;
147
168
  }
148
169
 
149
170
  export const EXTERNAL_BACKENDS: Record<ExternalBackend, ExternalBackendMeta> = {
@@ -187,8 +208,35 @@ export const EXTERNAL_BACKENDS: Record<ExternalBackend, ExternalBackendMeta> = {
187
208
  isEffort: isMuseEffort,
188
209
  resolveModel: resolveMuseModel,
189
210
  },
211
+ // Pi models are addressed by "<provider>/<id>" and resolved live from the host registry,
212
+ // so there are no fixed seed models and no prefix form. The `pi:` prefix is only the transport
213
+ // tag the picker uses to carry the choice back into applyModelChange; the stored and displayed
214
+ // value is the bare "<provider>/<id>".
215
+ pi: {
216
+ name: "pi",
217
+ label: "Pi",
218
+ prefix: "pi:",
219
+ efforts: PI_EFFORTS,
220
+ models: [],
221
+ effortHelp: '"default", "minimal", "low", "medium", "high", "xhigh", or "max"',
222
+ isEffort: isPiEffort,
223
+ resolveModel: resolvePiModel,
224
+ displayModel: (model) => model,
225
+ },
190
226
  };
191
227
 
228
+ /** The config-list text for a backend's stored model, using the backend's own display rule. */
229
+ export function externalModelDisplay(backend: ExternalBackend, model: string): string {
230
+ const meta = EXTERNAL_BACKENDS[backend];
231
+ return meta.displayModel ? meta.displayModel(model) : `${meta.prefix}${model}`;
232
+ }
233
+
234
+ // Narrows a BackendName to one that has an EXTERNAL_BACKENDS catalog entry. Agy is the only
235
+ // backend without one, so this is exactly "not Agy" and keeps index lookups type-safe.
236
+ export function isExternalBackend(backend: BackendName): backend is ExternalBackend {
237
+ return backend !== "agy";
238
+ }
239
+
192
240
  export function parseBackendOption(value: string): { backend: ExternalBackend; id: string } | undefined {
193
241
  for (const meta of Object.values(EXTERNAL_BACKENDS)) {
194
242
  if (value.startsWith(meta.prefix) && value.length > meta.prefix.length) {
@@ -198,6 +246,37 @@ export function parseBackendOption(value: string): { backend: ExternalBackend; i
198
246
  return undefined;
199
247
  }
200
248
 
249
+ // A single selectable Pi model: a physical, deduplicated "<provider>/<id>" entry.
250
+ export interface PiModelChoice {
251
+ /** Canonical "<provider>/<id>" value stored in settings. */
252
+ value: string;
253
+ /** Human label for the picker. */
254
+ label: string;
255
+ }
256
+
257
+ /** Builds the `value` for a Pi model, splitting on the first "/" only. */
258
+ export function piModelValue(providerId: string, modelId: string): string {
259
+ return `${providerId}/${modelId}`;
260
+ }
261
+
262
+ // Turns resolved scoped-model entries (or registry models) into deduplicated, physical-only choices.
263
+ // Models reported as "pi-virtual" are dropped: they route elsewhere at request time and cannot be
264
+ // driven as a standalone in-process session.
265
+ export function toPiModelChoices(
266
+ entries: readonly { model: { provider: string; id: string; name?: string; api?: string } }[],
267
+ ): PiModelChoice[] {
268
+ const seen = new Set<string>();
269
+ const choices: PiModelChoice[] = [];
270
+ for (const { model } of entries) {
271
+ if (model.api === "pi-virtual") continue;
272
+ const value = piModelValue(model.provider, model.id);
273
+ if (seen.has(value)) continue;
274
+ seen.add(value);
275
+ choices.push({ value, label: model.name ? `${model.name} · ${value}` : value });
276
+ }
277
+ return choices;
278
+ }
279
+
201
280
  export function parseModelEffortPair(value: unknown, context: string): ModelEffortPair {
202
281
  if (!isRecord(value) || typeof value.model !== "string" || !value.model.trim()) {
203
282
  throw new Error(`${context} must contain a model and effort set to "default", "low", "medium", or "high".`);
@@ -373,8 +452,8 @@ export function capabilityBackend(settings: BroSettings, capability: Capability)
373
452
  export function selectionForCapability(settings: BroSettings, capability: Capability): BackendSelection {
374
453
  const pair = capabilityPair(settings, capability);
375
454
  const backend = capabilityBackend(settings, capability);
376
- if (backend in EXTERNAL_BACKENDS) {
377
- const meta = EXTERNAL_BACKENDS[backend as ExternalBackend];
455
+ if (isExternalBackend(backend)) {
456
+ const meta = EXTERNAL_BACKENDS[backend];
378
457
  if (!meta.isEffort(pair.effort)) {
379
458
  throw new Error(`\`${pair.effort}\` is not supported on the ${meta.label} backend. Run \`/bro config\` to fix this.`);
380
459
  }