pi-squire 0.14.0 → 0.15.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/child.ts CHANGED
@@ -7,10 +7,10 @@
7
7
  // exits too (the parent gets the error and the session file stays); only a
8
8
  // run the user stopped never exits. A run that ends on a usage limit first
9
9
  // moves to the agent's next fallback, from `PI_DELEGATE_FALLBACK`, and
10
- // continues there; it exits only when no fallback is left. The user
10
+ // continues there; it exits only when no fallback is left. An entry that is
11
+ // an alias resolves to one model through Pi's own `--model` resolver. The user
11
12
  // taking over — typing here, or stopping a run — turns it off for the rest of
12
- // the session, with a notice; `/auto-exit` turns it back on. Modelled on
13
- // edxeth/pi-subagents' `auto-exit` (README "Child lifecycle", `cf6dbf4`).
13
+ // the session, with a notice; `/auto-exit` turns it back on.
14
14
  //
15
15
  // It also takes over the parent's task request on `message:inbound` and sends
16
16
  // it as a user prompt, so the task starts through the same path as a typed
@@ -18,7 +18,14 @@
18
18
  // for custom-message runs (earendil-works/pi#5581); this takeover remains the
19
19
  // package's pinned startup delivery path. The provider still arms the
20
20
  // automatic answer for a request a listener took over.
21
- import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
21
+ import type { Api, Model } from "@earendil-works/pi-ai";
22
+ import {
23
+ type ExtensionAPI,
24
+ type ExtensionContext,
25
+ type ModelRegistry,
26
+ type ModelRuntime,
27
+ resolveCliModel,
28
+ } from "@earendil-works/pi-coding-agent";
22
29
 
23
30
  import { activeMark, classify, type Response, recordLimit } from "./limits.ts";
24
31
  import { STATUS_KEY } from "./results.ts";
@@ -55,14 +62,47 @@ function recordUsageLimit(last: Message, ctx: ExtensionContext, response?: Respo
55
62
  }
56
63
  }
57
64
 
58
- /** Switch to the first of `fallback` that is usable and not marked, taking each off the list; the one switched to, if any. */
59
- async function switchModel(pi: ExtensionAPI, ctx: ExtensionContext, fallback: string[]): Promise<string | undefined> {
65
+ /**
66
+ * The one model a fallback entry names, as Pi's `--model` resolves it: an
67
+ * exact `provider/id`, or a bare id or alias such as `sonnet`. Undefined when
68
+ * the registry has no such model; a custom model id Pi would make up for an
69
+ * unknown one is not used.
70
+ */
71
+ function resolveEntry(registry: ModelRegistry, entry: string): Model<Api> | undefined {
72
+ // The resolver reads only these two from the runtime; the registry is the extension's view of it.
73
+ const runtime = {
74
+ getModels: () => registry.getAll(),
75
+ hasConfiguredAuth: (provider: string) =>
76
+ registry.getAll().some((model) => model.provider === provider && registry.hasConfiguredAuth(model)),
77
+ } as unknown as ModelRuntime;
78
+ try {
79
+ const { model } = resolveCliModel({ cliModel: entry, modelRuntime: runtime });
80
+ return model === undefined ? undefined : registry.find(model.provider, model.id);
81
+ } catch {
82
+ return undefined; // an entry Pi cannot read is passed over like one that names no model
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Switch to the first model `fallback` names that is usable, not marked and
88
+ * not tried yet, taking each entry off the list in order. Returns the
89
+ * `provider/id` switched to, if any.
90
+ */
91
+ async function switchModel(
92
+ pi: ExtensionAPI,
93
+ ctx: ExtensionContext,
94
+ fallback: string[],
95
+ tried: Set<string>,
96
+ ): Promise<string | undefined> {
60
97
  for (let next = fallback.shift(); next !== undefined; next = fallback.shift()) {
61
- const slash = next.indexOf("/");
62
- const model = ctx.modelRegistry.find(next.slice(0, slash), next.slice(slash + 1));
63
- if (model === undefined || !ctx.modelRegistry.hasConfiguredAuth(model)) continue;
98
+ const model = resolveEntry(ctx.modelRegistry, next);
99
+ if (model === undefined) continue;
100
+ const name = `${model.provider}/${model.id}`;
101
+ if (tried.has(name)) continue;
102
+ tried.add(name);
103
+ if (!ctx.modelRegistry.hasConfiguredAuth(model)) continue;
64
104
  if (activeMark(ctx.modelRegistry, model.provider, model.id) !== undefined) continue;
65
- if (await pi.setModel(model)) return next;
105
+ if (await pi.setModel(model)) return name;
66
106
  }
67
107
  return undefined;
68
108
  }
@@ -88,19 +128,27 @@ function registerTakeover(pi: ExtensionAPI, parent: string): void {
88
128
  /**
89
129
  * A usage limit or an unavailable endpoint moves the run to the next fallback
90
130
  * that is usable and not marked, and continues it in this session; each
91
- * fallback is tried once. Returns the setter for what ended the last run, as
131
+ * model is tried once, and never the one that just failed. Returns the setter
132
+ * for what ended the last run: the failed model as `provider/id`, and why as
92
133
  * `<provider/model> <what happened>`.
93
134
  */
94
- function registerFallback(pi: ExtensionAPI): (failure: string | undefined) => void {
95
- /** The models still to move to on a usage limit, in order. */
96
- const fallback = (process.env[FALLBACK_ENV] ?? "").split(",").filter((model) => model.includes("/"));
97
- let failure: string | undefined;
135
+ function registerFallback(pi: ExtensionAPI): (failure: Failure | undefined) => void {
136
+ /** The entries still to move to on a usage limit, in order: models or aliases. */
137
+ const fallback = (process.env[FALLBACK_ENV] ?? "")
138
+ .split(",")
139
+ .map((entry) => entry.trim())
140
+ .filter((entry) => entry !== "");
141
+ /** The models already switched to or failed on, as `provider/id`. */
142
+ const tried = new Set<string>();
143
+ let failure: Failure | undefined;
98
144
  pi.on("agent_before_settle", async (_event, ctx) => {
99
145
  const from = failure;
100
146
  failure = undefined;
101
- const next = from === undefined ? undefined : await switchModel(pi, ctx, fallback);
147
+ if (from === undefined) return;
148
+ tried.add(from.model);
149
+ const next = await switchModel(pi, ctx, fallback, tried);
102
150
  if (next === undefined) return;
103
- const content = `[delegate] ${from}, so this session switched to ${next}. Carry on with the task from where it stopped.`;
151
+ const content = `[delegate] ${from.reason}, so this session switched to ${next}. Carry on with the task from where it stopped.`;
104
152
  return { continue: true, entries: [{ type: "custom_message", customType: "delegate", content, display: true }] };
105
153
  });
106
154
  return (value) => {
@@ -108,14 +156,17 @@ function registerFallback(pi: ExtensionAPI): (failure: string | undefined) => vo
108
156
  };
109
157
  }
110
158
 
111
- /** Why a failed run should move to a fallback, as `<provider/model> <what happened>`; undefined when it should not. */
112
- function fallbackReason(last: Message | undefined): string | undefined {
159
+ /** A failed run that should move to a fallback: the model it failed on, and why. */
160
+ type Failure = { model: string; reason: string };
161
+
162
+ /** Why a failed run should move to a fallback, with `reason` as `<provider/model> <what happened>`; undefined when it should not. */
163
+ function fallbackReason(last: Message | undefined): Failure | undefined {
113
164
  const error = last?.errorMessage;
114
165
  if (typeof error !== "string") return undefined;
115
166
  const model = `${String(last?.provider)}/${String(last?.model)}`;
116
- if (classify(error) !== undefined) return `${model} hit a usage limit`;
167
+ if (classify(error) !== undefined) return { model, reason: `${model} hit a usage limit` };
117
168
  // Pi has already retried an unavailable endpoint; it marks nothing, but this run moves on.
118
- if (/unavailable/i.test(error)) return `${model} was unavailable (${error})`;
169
+ if (/unavailable/i.test(error)) return { model, reason: `${model} was unavailable (${error})` };
119
170
  return undefined;
120
171
  }
121
172