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/CHANGELOG.md +116 -1
- package/README.md +179 -70
- package/package.json +3 -3
- package/src/agents.ts +361 -111
- package/src/child.ts +72 -21
- package/src/index.ts +242 -66
- package/src/navigator.ts +1 -1
- package/src/new-delegate.ts +17 -8
- package/src/result-sessions.ts +39 -0
- package/src/results.ts +2 -0
- package/src/resume-checks.ts +45 -12
- package/src/resume.ts +7 -2
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.
|
|
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.
|
|
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 {
|
|
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
|
-
/**
|
|
59
|
-
|
|
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
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
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
|
-
*
|
|
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:
|
|
95
|
-
/** The
|
|
96
|
-
const fallback = (process.env[FALLBACK_ENV] ?? "")
|
|
97
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
112
|
-
|
|
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
|
|