pi-goala 0.3.0 → 0.3.1
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 +8 -0
- package/README.md +8 -5
- package/docs/architecture.md +4 -3
- package/docs/configuration.md +6 -3
- package/extensions/goala/index.ts +1 -1
- package/extensions/goala/setup.ts +241 -45
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.1 (2026-07-26)
|
|
4
|
+
|
|
5
|
+
- Make each phase default to keeping its current model configuration.
|
|
6
|
+
- Separate reasoning-effort changes from provider/model changes.
|
|
7
|
+
- Select provider, then a filtered model, while skipping provider selection
|
|
8
|
+
when only one authenticated provider is available.
|
|
9
|
+
- Review, edit, save, or cancel the complete configuration atomically.
|
|
10
|
+
|
|
3
11
|
## 0.3.0 (2026-07-26)
|
|
4
12
|
|
|
5
13
|
- Add interactive per-phase provider, model, and reasoning selection from Pi's
|
package/README.md
CHANGED
|
@@ -39,7 +39,7 @@ pi install npm:pi-goala
|
|
|
39
39
|
Pin a release for reproducible team installations:
|
|
40
40
|
|
|
41
41
|
```text
|
|
42
|
-
pi install npm:pi-goala@0.3.
|
|
42
|
+
pi install npm:pi-goala@0.3.1
|
|
43
43
|
```
|
|
44
44
|
|
|
45
45
|
Pi's package command both downloads Goala and registers its extension. A plain
|
|
@@ -49,7 +49,7 @@ supported installation path.
|
|
|
49
49
|
From GitHub or a local checkout:
|
|
50
50
|
|
|
51
51
|
```text
|
|
52
|
-
pi install git:github.com/barryking/pi-goala@v0.3.
|
|
52
|
+
pi install git:github.com/barryking/pi-goala@v0.3.1
|
|
53
53
|
pi install /absolute/path/to/pi-goala
|
|
54
54
|
```
|
|
55
55
|
|
|
@@ -115,7 +115,7 @@ Useful commands:
|
|
|
115
115
|
/memory retire <id> Exclude an obsolete episode from recall
|
|
116
116
|
/memory restore <id> Restore a retired episode
|
|
117
117
|
/goala-setup Choose a model preset
|
|
118
|
-
/goala-setup custom
|
|
118
|
+
/goala-setup custom Review or change each role's model and reasoning
|
|
119
119
|
/goala-setup status Show effective configuration
|
|
120
120
|
```
|
|
121
121
|
|
|
@@ -383,8 +383,11 @@ when Pi started. Run `/goala-setup current` to persist that portable
|
|
|
383
383
|
single-model configuration. Run `/goala-setup custom` to choose an
|
|
384
384
|
authenticated provider/model and reasoning level separately for planning,
|
|
385
385
|
execution, step verification, final verification, and repeated repair. The
|
|
386
|
-
interactive
|
|
387
|
-
|
|
386
|
+
interactive wizard defaults to keeping each current role. It can change
|
|
387
|
+
reasoning alone or select a provider followed by one of that provider's
|
|
388
|
+
available models. Provider selection is skipped when only one is authenticated.
|
|
389
|
+
A final summary can be edited, saved, or cancelled; nothing is written before
|
|
390
|
+
save.
|
|
388
391
|
|
|
389
392
|
Advanced users can edit the same per-role profiles in
|
|
390
393
|
`~/.pi/agent/pi-goala/config.json`.
|
package/docs/architecture.md
CHANGED
|
@@ -124,9 +124,10 @@ step verification, and a balanced fallback after repeated repair. Independent
|
|
|
124
124
|
step context and the final strong verifier preserve the trust boundary without
|
|
125
125
|
paying the strongest-model cost at every human checkpoint. A portable
|
|
126
126
|
current-model preset is available for other providers. The advanced setup
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
127
|
+
defaults to retaining each role, supports reasoning-only changes, and filters
|
|
128
|
+
models after provider selection. It stores provider, model, and reasoning
|
|
129
|
+
independently for every role only after final review; mixed-provider workflows
|
|
130
|
+
therefore use the same phase-routing path as the bundled preset.
|
|
130
131
|
|
|
131
132
|
If a configured model cannot be resolved and current-model fallback is
|
|
132
133
|
enabled, Goala uses the model that was active when the extension session
|
package/docs/configuration.md
CHANGED
|
@@ -51,9 +51,12 @@ and restore the backup to `~/.pi/agent/`.
|
|
|
51
51
|
|
|
52
52
|
`openai` selects the tested Sol/Luna/Terra model split when those models are
|
|
53
53
|
available. `current` assigns the model active at Pi startup to every role.
|
|
54
|
-
`custom`
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
`custom` reviews each role and defaults to keeping its current configuration.
|
|
55
|
+
For a change, it can adjust reasoning alone or select a provider followed by
|
|
56
|
+
one of that provider's authenticated, available models. Provider selection is
|
|
57
|
+
skipped when only one provider is available. The final summary can be edited,
|
|
58
|
+
saved, or cancelled. Cancelling any prompt leaves the existing configuration
|
|
59
|
+
unchanged.
|
|
57
60
|
|
|
58
61
|
## Full schema
|
|
59
62
|
|
|
@@ -605,7 +605,7 @@ Do not edit files or rely on executor claims. Finish with submit_step_verificati
|
|
|
605
605
|
);
|
|
606
606
|
if (missing.length > 0) {
|
|
607
607
|
ctx.ui.notify(
|
|
608
|
-
`
|
|
608
|
+
`Configuration not saved; unavailable model(s): ${missing.map((model) => `${model.provider}/${model.id}`).join(", ")}.`,
|
|
609
609
|
"warning",
|
|
610
610
|
);
|
|
611
611
|
return;
|
|
@@ -12,6 +12,10 @@ type ModelRole =
|
|
|
12
12
|
| "verifier"
|
|
13
13
|
| "fallbackExecutor";
|
|
14
14
|
|
|
15
|
+
type AvailableModel = ReturnType<
|
|
16
|
+
ExtensionCommandContext["modelRegistry"]["getAvailable"]
|
|
17
|
+
>[number];
|
|
18
|
+
|
|
15
19
|
const MODEL_ROLES: ReadonlyArray<{ key: ModelRole; label: string }> = [
|
|
16
20
|
{ key: "planner", label: "Planner" },
|
|
17
21
|
{ key: "executor", label: "Executor" },
|
|
@@ -30,8 +34,226 @@ const THINKING_LEVELS: readonly ThinkingLevel[] = [
|
|
|
30
34
|
"max",
|
|
31
35
|
];
|
|
32
36
|
|
|
33
|
-
|
|
34
|
-
|
|
37
|
+
const KEEP_CURRENT = "Keep current";
|
|
38
|
+
const CHANGE_REASONING = "Change reasoning effort";
|
|
39
|
+
const CHANGE_MODEL = "Change provider or model";
|
|
40
|
+
const SAVE_CONFIGURATION = "Save configuration";
|
|
41
|
+
const EDIT_PHASE = "Edit a phase";
|
|
42
|
+
const CANCEL = "Cancel";
|
|
43
|
+
|
|
44
|
+
function titleCase(value: string): string {
|
|
45
|
+
return `${value.charAt(0).toUpperCase()}${value.slice(1)}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function thinkingLabel(level: ThinkingLevel): string {
|
|
49
|
+
if (level === "off") return "Off";
|
|
50
|
+
if (level === "xhigh") return "Extra high";
|
|
51
|
+
if (level === "max") return "Maximum";
|
|
52
|
+
return titleCase(level);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function providerLabel(
|
|
56
|
+
ctx: ExtensionCommandContext,
|
|
57
|
+
provider: string,
|
|
58
|
+
): string {
|
|
59
|
+
const displayName = ctx.modelRegistry.getProviderDisplayName(provider);
|
|
60
|
+
return displayName === provider ? provider : `${displayName} (${provider})`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function modelLabel(model: AvailableModel): string {
|
|
64
|
+
return model.name && model.name !== model.id
|
|
65
|
+
? `${model.name} (${model.id})`
|
|
66
|
+
: model.id;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function profileLabel(
|
|
70
|
+
ctx: ExtensionCommandContext,
|
|
71
|
+
available: AvailableModel[],
|
|
72
|
+
profile: ModelProfile,
|
|
73
|
+
): string {
|
|
74
|
+
const model = available.find(
|
|
75
|
+
(candidate) =>
|
|
76
|
+
candidate.provider === profile.provider && candidate.id === profile.model,
|
|
77
|
+
);
|
|
78
|
+
const reasoning =
|
|
79
|
+
profile.thinkingLevel === "off"
|
|
80
|
+
? "Reasoning off"
|
|
81
|
+
: `${thinkingLabel(profile.thinkingLevel)} reasoning`;
|
|
82
|
+
return [
|
|
83
|
+
providerLabel(ctx, profile.provider),
|
|
84
|
+
model ? modelLabel(model) : `${profile.model} (unavailable)`,
|
|
85
|
+
reasoning,
|
|
86
|
+
].join(" / ");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function setProfile(
|
|
90
|
+
config: GoalaConfig,
|
|
91
|
+
role: ModelRole,
|
|
92
|
+
profile: ModelProfile,
|
|
93
|
+
): void {
|
|
94
|
+
if (role === "fallbackExecutor") {
|
|
95
|
+
config.fallbackExecutor = {
|
|
96
|
+
...profile,
|
|
97
|
+
afterRepairCycle: config.fallbackExecutor.afterRepairCycle,
|
|
98
|
+
};
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
config[role] = profile;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function selectThinkingLevel(
|
|
105
|
+
ctx: ExtensionCommandContext,
|
|
106
|
+
roleLabel: string,
|
|
107
|
+
profile: ModelProfile,
|
|
108
|
+
): Promise<ThinkingLevel | undefined> {
|
|
109
|
+
const ordered = [
|
|
110
|
+
profile.thinkingLevel,
|
|
111
|
+
...THINKING_LEVELS.filter((level) => level !== profile.thinkingLevel),
|
|
112
|
+
];
|
|
113
|
+
const choices = ordered.map((level, index) =>
|
|
114
|
+
index === 0 ? `${thinkingLabel(level)} (current)` : thinkingLabel(level),
|
|
115
|
+
);
|
|
116
|
+
const selected = await ctx.ui.select(
|
|
117
|
+
`${roleLabel} reasoning effort\nCurrent: ${thinkingLabel(profile.thinkingLevel)}`,
|
|
118
|
+
choices,
|
|
119
|
+
);
|
|
120
|
+
if (!selected) return undefined;
|
|
121
|
+
return ordered[choices.indexOf(selected)];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function selectProvider(
|
|
125
|
+
ctx: ExtensionCommandContext,
|
|
126
|
+
available: AvailableModel[],
|
|
127
|
+
roleLabel: string,
|
|
128
|
+
currentProvider: string,
|
|
129
|
+
): Promise<string | undefined> {
|
|
130
|
+
const providers = [...new Set(available.map((model) => model.provider))].sort(
|
|
131
|
+
(left, right) => {
|
|
132
|
+
if (left === currentProvider) return -1;
|
|
133
|
+
if (right === currentProvider) return 1;
|
|
134
|
+
return providerLabel(ctx, left).localeCompare(providerLabel(ctx, right));
|
|
135
|
+
},
|
|
136
|
+
);
|
|
137
|
+
if (providers.length === 1) return providers[0];
|
|
138
|
+
|
|
139
|
+
const choices = providers.map((provider, index) =>
|
|
140
|
+
index === 0 && provider === currentProvider
|
|
141
|
+
? `${providerLabel(ctx, provider)} (current)`
|
|
142
|
+
: providerLabel(ctx, provider),
|
|
143
|
+
);
|
|
144
|
+
const selected = await ctx.ui.select(
|
|
145
|
+
`${roleLabel} provider\nCurrent: ${providerLabel(ctx, currentProvider)}`,
|
|
146
|
+
choices,
|
|
147
|
+
);
|
|
148
|
+
if (!selected) return undefined;
|
|
149
|
+
return providers[choices.indexOf(selected)];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function selectModel(
|
|
153
|
+
ctx: ExtensionCommandContext,
|
|
154
|
+
available: AvailableModel[],
|
|
155
|
+
roleLabel: string,
|
|
156
|
+
provider: string,
|
|
157
|
+
current: ModelProfile,
|
|
158
|
+
): Promise<AvailableModel | undefined> {
|
|
159
|
+
const models = available
|
|
160
|
+
.filter((model) => model.provider === provider)
|
|
161
|
+
.sort((left, right) => {
|
|
162
|
+
const leftCurrent =
|
|
163
|
+
left.provider === current.provider && left.id === current.model;
|
|
164
|
+
const rightCurrent =
|
|
165
|
+
right.provider === current.provider && right.id === current.model;
|
|
166
|
+
if (leftCurrent !== rightCurrent) return leftCurrent ? -1 : 1;
|
|
167
|
+
return modelLabel(left).localeCompare(modelLabel(right));
|
|
168
|
+
});
|
|
169
|
+
const choices = models.map((model, index) =>
|
|
170
|
+
index === 0 &&
|
|
171
|
+
model.provider === current.provider &&
|
|
172
|
+
model.id === current.model
|
|
173
|
+
? `${modelLabel(model)} (current)`
|
|
174
|
+
: modelLabel(model),
|
|
175
|
+
);
|
|
176
|
+
const selected = await ctx.ui.select(
|
|
177
|
+
`${roleLabel} model\nProvider: ${providerLabel(ctx, provider)}`,
|
|
178
|
+
choices,
|
|
179
|
+
);
|
|
180
|
+
if (!selected) return undefined;
|
|
181
|
+
return models[choices.indexOf(selected)];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function changeModel(
|
|
185
|
+
ctx: ExtensionCommandContext,
|
|
186
|
+
available: AvailableModel[],
|
|
187
|
+
roleLabel: string,
|
|
188
|
+
current: ModelProfile,
|
|
189
|
+
): Promise<ModelProfile | undefined> {
|
|
190
|
+
const provider = await selectProvider(
|
|
191
|
+
ctx,
|
|
192
|
+
available,
|
|
193
|
+
roleLabel,
|
|
194
|
+
current.provider,
|
|
195
|
+
);
|
|
196
|
+
if (!provider) return undefined;
|
|
197
|
+
const model = await selectModel(ctx, available, roleLabel, provider, current);
|
|
198
|
+
if (!model) return undefined;
|
|
199
|
+
|
|
200
|
+
const profile: ModelProfile = {
|
|
201
|
+
provider: model.provider,
|
|
202
|
+
model: model.id,
|
|
203
|
+
thinkingLevel: model.reasoning ? current.thinkingLevel : "off",
|
|
204
|
+
};
|
|
205
|
+
if (!model.reasoning) return profile;
|
|
206
|
+
|
|
207
|
+
const thinkingLevel = await selectThinkingLevel(ctx, roleLabel, profile);
|
|
208
|
+
if (!thinkingLevel) return undefined;
|
|
209
|
+
return { ...profile, thinkingLevel };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function configureRole(
|
|
213
|
+
ctx: ExtensionCommandContext,
|
|
214
|
+
available: AvailableModel[],
|
|
215
|
+
config: GoalaConfig,
|
|
216
|
+
role: { key: ModelRole; label: string },
|
|
217
|
+
): Promise<boolean> {
|
|
218
|
+
const current = config[role.key];
|
|
219
|
+
const currentModel = available.find(
|
|
220
|
+
(model) =>
|
|
221
|
+
model.provider === current.provider && model.id === current.model,
|
|
222
|
+
);
|
|
223
|
+
const actions = [
|
|
224
|
+
KEEP_CURRENT,
|
|
225
|
+
...(currentModel?.reasoning ? [CHANGE_REASONING] : []),
|
|
226
|
+
CHANGE_MODEL,
|
|
227
|
+
];
|
|
228
|
+
const action = await ctx.ui.select(
|
|
229
|
+
`${role.label}\nCurrent: ${profileLabel(ctx, available, current)}`,
|
|
230
|
+
actions,
|
|
231
|
+
);
|
|
232
|
+
if (!action) return false;
|
|
233
|
+
if (action === KEEP_CURRENT) return true;
|
|
234
|
+
|
|
235
|
+
if (action === CHANGE_REASONING) {
|
|
236
|
+
const thinkingLevel = await selectThinkingLevel(ctx, role.label, current);
|
|
237
|
+
if (!thinkingLevel) return false;
|
|
238
|
+
setProfile(config, role.key, { ...current, thinkingLevel });
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const profile = await changeModel(ctx, available, role.label, current);
|
|
243
|
+
if (!profile) return false;
|
|
244
|
+
setProfile(config, role.key, profile);
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function reviewSummary(
|
|
249
|
+
ctx: ExtensionCommandContext,
|
|
250
|
+
available: AvailableModel[],
|
|
251
|
+
config: GoalaConfig,
|
|
252
|
+
): string {
|
|
253
|
+
return MODEL_ROLES.map(
|
|
254
|
+
(role) =>
|
|
255
|
+
`${role.label}: ${profileLabel(ctx, available, config[role.key])}`,
|
|
256
|
+
).join("\n");
|
|
35
257
|
}
|
|
36
258
|
|
|
37
259
|
export async function selectModelRoles(
|
|
@@ -59,51 +281,25 @@ export async function selectModelRoles(
|
|
|
59
281
|
|
|
60
282
|
const proposed = structuredClone(current);
|
|
61
283
|
for (const role of MODEL_ROLES) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
return `${left.provider}/${left.id}`.localeCompare(`${right.provider}/${right.id}`);
|
|
70
|
-
});
|
|
71
|
-
const choices = ordered.map((model) => {
|
|
72
|
-
const provider = ctx.modelRegistry.getProviderDisplayName(model.provider);
|
|
73
|
-
const name = model.name && model.name !== model.id ? `${model.name} · ` : "";
|
|
74
|
-
return `${provider} · ${name}${model.provider}/${model.id}`;
|
|
75
|
-
});
|
|
76
|
-
const selected = await ctx.ui.select(
|
|
77
|
-
`${role.label} model\nCurrent: ${profileLabel(existing)}`,
|
|
78
|
-
choices,
|
|
284
|
+
if (!(await configureRole(ctx, available, proposed, role))) return undefined;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
while (true) {
|
|
288
|
+
const action = await ctx.ui.select(
|
|
289
|
+
`Review Goala configuration\n${reviewSummary(ctx, available, proposed)}`,
|
|
290
|
+
[SAVE_CONFIGURATION, EDIT_PHASE, CANCEL],
|
|
79
291
|
);
|
|
80
|
-
if (!
|
|
81
|
-
|
|
82
|
-
if (!model) return undefined;
|
|
83
|
-
|
|
84
|
-
let thinkingLevel: ThinkingLevel = "off";
|
|
85
|
-
if (model.reasoning) {
|
|
86
|
-
const selectedThinking = await ctx.ui.select(
|
|
87
|
-
`${role.label} reasoning\n${model.provider}/${model.id}`,
|
|
88
|
-
[...THINKING_LEVELS],
|
|
89
|
-
);
|
|
90
|
-
if (!selectedThinking) return undefined;
|
|
91
|
-
thinkingLevel = selectedThinking as ThinkingLevel;
|
|
92
|
-
}
|
|
292
|
+
if (!action || action === CANCEL) return undefined;
|
|
293
|
+
if (action === SAVE_CONFIGURATION) return proposed;
|
|
93
294
|
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
afterRepairCycle: current.fallbackExecutor.afterRepairCycle,
|
|
103
|
-
};
|
|
104
|
-
} else {
|
|
105
|
-
proposed[role.key] = profile;
|
|
295
|
+
const selectedRole = await ctx.ui.select(
|
|
296
|
+
"Choose a phase to edit",
|
|
297
|
+
MODEL_ROLES.map((role) => role.label),
|
|
298
|
+
);
|
|
299
|
+
if (!selectedRole) return undefined;
|
|
300
|
+
const role = MODEL_ROLES.find((candidate) => candidate.label === selectedRole);
|
|
301
|
+
if (!role || !(await configureRole(ctx, available, proposed, role))) {
|
|
302
|
+
return undefined;
|
|
106
303
|
}
|
|
107
304
|
}
|
|
108
|
-
return proposed;
|
|
109
305
|
}
|