pi-goala 0.3.0 → 0.3.2
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 +21 -2
- package/README.md +35 -11
- package/docs/architecture.md +8 -5
- package/docs/configuration.md +13 -3
- package/extensions/goala/config.ts +5 -4
- package/extensions/goala/index.ts +9 -22
- package/extensions/goala/routing.ts +28 -0
- package/extensions/goala/setup.ts +252 -47
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.2 (2026-07-26)
|
|
4
|
+
|
|
5
|
+
- Document how `AGENTS.md` and Pi skills customise project and personal
|
|
6
|
+
workflows around Goala's lifecycle.
|
|
7
|
+
- Describe planning, execution, and verification models as roles rather than
|
|
8
|
+
incorrectly presenting the repair loop as a separate phase.
|
|
9
|
+
- Clarify the fallback executor activation threshold and distinguish it from
|
|
10
|
+
unavailable-model fallback.
|
|
11
|
+
- Add direct and extension-level routing tests for every model role, including
|
|
12
|
+
the fallback activation boundary.
|
|
13
|
+
|
|
14
|
+
## 0.3.1 (2026-07-26)
|
|
15
|
+
|
|
16
|
+
- Make each role default to keeping its current model configuration.
|
|
17
|
+
- Separate reasoning-effort changes from provider/model changes.
|
|
18
|
+
- Select provider, then a filtered model, while skipping provider selection
|
|
19
|
+
when only one authenticated provider is available.
|
|
20
|
+
- Review, edit, save, or cancel the complete configuration atomically.
|
|
21
|
+
|
|
3
22
|
## 0.3.0 (2026-07-26)
|
|
4
23
|
|
|
5
|
-
- Add interactive per-
|
|
24
|
+
- Add interactive per-role provider, model, and reasoning selection from Pi's
|
|
6
25
|
authenticated available-model registry.
|
|
7
26
|
- Support mixed-provider planner, executor, step-verifier, final-verifier, and
|
|
8
27
|
repeated-repair roles.
|
|
@@ -61,7 +80,7 @@
|
|
|
61
80
|
|
|
62
81
|
## 0.1.0
|
|
63
82
|
|
|
64
|
-
- Add persistent goal, plan, execution, verification, and bounded repair
|
|
83
|
+
- Add persistent goal, plan, execution, verification, and bounded repair loops.
|
|
65
84
|
- Add configurable planner, executor, verifier, and repair model roles.
|
|
66
85
|
- Add fresh planning/execution sessions and logical verifier context isolation.
|
|
67
86
|
- Add verifier-gated local episodic memory with SQLite FTS5 retrieval.
|
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.2
|
|
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.2
|
|
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
|
|
|
@@ -317,6 +317,7 @@ Use this hierarchy:
|
|
|
317
317
|
| Detailed architecture, domain rules, and decisions | Versioned repository docs, linked from `AGENTS.md` |
|
|
318
318
|
| Product direction and future milestones | `PROJECT_GOAL.md` or `ROADMAP.md` |
|
|
319
319
|
| Personal defaults that apply to every repository | `~/.pi/agent/AGENTS.md` |
|
|
320
|
+
| Repeatable procedures such as commit/PR, release, deployment, or framework workflows | Pi skills, referenced from `AGENTS.md` when mandatory |
|
|
320
321
|
| Distilled experience from successfully completed work | Verified Goala episodic memory |
|
|
321
322
|
|
|
322
323
|
Pi loads repository and global `AGENTS.md` files into every fresh Goala phase.
|
|
@@ -357,6 +358,22 @@ can include it as an evidence-backed episode finding. Important decisions
|
|
|
357
358
|
should still be committed to the repository; future memory is bounded,
|
|
358
359
|
relevance-ranked, and deliberately treated as untrusted evidence.
|
|
359
360
|
|
|
361
|
+
### Customise your workflow
|
|
362
|
+
|
|
363
|
+
Goala works with a standard Pi installation and provides the goal, planning,
|
|
364
|
+
execution, verification, and memory lifecycle. It does not prescribe every
|
|
365
|
+
project procedure.
|
|
366
|
+
|
|
367
|
+
Put concise, always-applicable rules in the repository's `AGENTS.md`, and put
|
|
368
|
+
personal defaults that apply across repositories in
|
|
369
|
+
`~/.pi/agent/AGENTS.md`. Use Pi skills for detailed, repeatable procedures such
|
|
370
|
+
as commit and pull-request handling, releases, deployments, migrations, or
|
|
371
|
+
framework-specific workflows.
|
|
372
|
+
|
|
373
|
+
When a skill is mandatory for particular work, reference it from `AGENTS.md`.
|
|
374
|
+
This keeps the instructions loaded into every phase short while allowing the
|
|
375
|
+
full procedure to be loaded only when relevant.
|
|
376
|
+
|
|
360
377
|
### When direction changes
|
|
361
378
|
|
|
362
379
|
Use `/goal revise <feedback>` when the current checkpoint needs rework. Use
|
|
@@ -370,21 +387,28 @@ new bounded goal when the desired outcome has materially changed.
|
|
|
370
387
|
|
|
371
388
|
The recommended OpenAI Codex preset is:
|
|
372
389
|
|
|
373
|
-
|
|
|
390
|
+
| Model role | Model | Reasoning |
|
|
374
391
|
|---|---|---:|
|
|
375
|
-
|
|
|
376
|
-
|
|
|
377
|
-
| Optional step
|
|
378
|
-
|
|
|
379
|
-
|
|
|
392
|
+
| Planner | `gpt-5.6-sol` | medium |
|
|
393
|
+
| Executor | `gpt-5.6-luna` | medium |
|
|
394
|
+
| Optional step verifier | `gpt-5.6-luna` | medium |
|
|
395
|
+
| Final verifier | `gpt-5.6-sol` | medium |
|
|
396
|
+
| Fallback executor | `gpt-5.6-terra` | medium |
|
|
397
|
+
|
|
398
|
+
Repeated repair is not a separate lifecycle phase. It loops back into
|
|
399
|
+
execution after verification fails. The fallback executor replaces the normal
|
|
400
|
+
executor only after the configured number of failed verification attempts.
|
|
380
401
|
|
|
381
402
|
If those models are unavailable, Goala can use the model that was active
|
|
382
403
|
when Pi started. Run `/goala-setup current` to persist that portable
|
|
383
404
|
single-model configuration. Run `/goala-setup custom` to choose an
|
|
384
405
|
authenticated provider/model and reasoning level separately for planning,
|
|
385
406
|
execution, step verification, final verification, and repeated repair. The
|
|
386
|
-
interactive
|
|
387
|
-
|
|
407
|
+
interactive wizard defaults to keeping each current role. It can change
|
|
408
|
+
reasoning alone or select a provider followed by one of that provider's
|
|
409
|
+
available models. Provider selection is skipped when only one is authenticated.
|
|
410
|
+
A final summary can be edited, saved, or cancelled; nothing is written before
|
|
411
|
+
save.
|
|
388
412
|
|
|
389
413
|
Advanced users can edit the same per-role profiles in
|
|
390
414
|
`~/.pi/agent/pi-goala/config.json`.
|
package/docs/architecture.md
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
Goala is one Pi extension with three responsibilities:
|
|
4
4
|
|
|
5
5
|
1. persist a goal and its acceptance contract;
|
|
6
|
-
2. route work through
|
|
6
|
+
2. route work through planning, execution, independent verification, and a
|
|
7
|
+
bounded repair loop;
|
|
7
8
|
3. retrieve and promote bounded verified memory.
|
|
8
9
|
|
|
9
10
|
## Module boundaries
|
|
@@ -13,7 +14,7 @@ own one concern:
|
|
|
13
14
|
|
|
14
15
|
| Module | Responsibility |
|
|
15
16
|
|---|---|
|
|
16
|
-
| `index.ts` | Commands, phase transitions,
|
|
17
|
+
| `index.ts` | Commands, phase transitions, and event orchestration |
|
|
17
18
|
| `tools.ts` | Tool schemas and state transitions caused by tool submissions |
|
|
18
19
|
| `session.ts` | Fresh-session handoff and logical phase context slicing |
|
|
19
20
|
| `sources.ts` | Safe project-local source resolution, hashing, and drift detection |
|
|
@@ -23,6 +24,7 @@ own one concern:
|
|
|
23
24
|
| `workflow.ts` | Goal-state types, normalization, and invariants |
|
|
24
25
|
| `memory.ts` | Verified episodic storage and retrieval |
|
|
25
26
|
| `recovery.ts` | Discovery of unfinished goals in saved Pi sessions |
|
|
27
|
+
| `routing.ts` | Model-role selection and fallback-executor activation |
|
|
26
28
|
| `setup.ts` | Interactive available-model selection for each lifecycle role |
|
|
27
29
|
| `config.ts` | Namespaced configuration and model-role presets |
|
|
28
30
|
|
|
@@ -124,9 +126,10 @@ step verification, and a balanced fallback after repeated repair. Independent
|
|
|
124
126
|
step context and the final strong verifier preserve the trust boundary without
|
|
125
127
|
paying the strongest-model cost at every human checkpoint. A portable
|
|
126
128
|
current-model preset is available for other providers. The advanced setup
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
129
|
+
defaults to retaining each role, supports reasoning-only changes, and filters
|
|
130
|
+
models after provider selection. It stores provider, model, and reasoning
|
|
131
|
+
independently for every role only after final review; mixed-provider workflows
|
|
132
|
+
therefore use the same phase-routing path as the bundled preset.
|
|
130
133
|
|
|
131
134
|
If a configured model cannot be resolved and current-model fallback is
|
|
132
135
|
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
|
|
|
@@ -102,6 +105,13 @@ leaves the existing configuration unchanged.
|
|
|
102
105
|
}
|
|
103
106
|
```
|
|
104
107
|
|
|
108
|
+
`fallbackExecutor` is not a separate lifecycle phase. It replaces the normal
|
|
109
|
+
executor inside the repair loop after repeated verification failures.
|
|
110
|
+
`afterRepairCycle` is the number of failed verification attempts that activates
|
|
111
|
+
it. With the default value of `2`, the first repair still uses the normal
|
|
112
|
+
executor; after the repaired work fails verification again, subsequent
|
|
113
|
+
execution uses the fallback executor.
|
|
114
|
+
|
|
105
115
|
Restart Pi or use `/reload` after manually editing the file.
|
|
106
116
|
|
|
107
117
|
Version 1 configuration used one top-level `provider`. Goala reads that schema,
|
|
@@ -218,7 +218,7 @@ export const GOALA_SETUP_PRESETS: readonly GoalaSetupPreset[] = [
|
|
|
218
218
|
},
|
|
219
219
|
{
|
|
220
220
|
id: "current",
|
|
221
|
-
label: "Use the current model for every
|
|
221
|
+
label: "Use the current model for every role",
|
|
222
222
|
create: (currentModel) =>
|
|
223
223
|
currentModel
|
|
224
224
|
? currentModelConfig(currentModel.provider, currentModel.id)
|
|
@@ -250,13 +250,14 @@ export function formatConfig(config: GoalaConfig): string {
|
|
|
250
250
|
`Executor: ${formatProfile(config.executor)}`,
|
|
251
251
|
`Step verifier: ${formatProfile(config.stepVerifier)}`,
|
|
252
252
|
`Verifier: ${formatProfile(config.verifier)}`,
|
|
253
|
-
`Fallback: ${formatProfile(config.fallbackExecutor)}
|
|
253
|
+
`Fallback executor: ${formatProfile(config.fallbackExecutor)}`,
|
|
254
|
+
`Fallback executor activates after: ${config.fallbackExecutor.afterRepairCycle} failed verification attempts`,
|
|
254
255
|
`Review policy: ${config.reviewPolicy}`,
|
|
255
256
|
`Memory: ${config.memory.enabled ? "enabled" : "disabled"}; auto-recall ${config.memory.autoRecall ? "on" : "off"}`,
|
|
256
|
-
`
|
|
257
|
+
`Unavailable-model fallback to current Pi model: ${config.allowCurrentModelFallback ? "allowed" : "disabled"}`,
|
|
257
258
|
].join("\n");
|
|
258
259
|
}
|
|
259
260
|
|
|
260
261
|
function formatProfile(profile: ModelProfile): string {
|
|
261
|
-
return `${profile.provider}/${profile.model}
|
|
262
|
+
return `${profile.provider}/${profile.model} · ${profile.thinkingLevel} reasoning`;
|
|
262
263
|
}
|
|
@@ -10,7 +10,6 @@ import {
|
|
|
10
10
|
loadConfig,
|
|
11
11
|
writeConfig,
|
|
12
12
|
type GoalaConfig,
|
|
13
|
-
type ModelProfile,
|
|
14
13
|
type ReviewPolicy,
|
|
15
14
|
type ThinkingLevel,
|
|
16
15
|
} from "./config.ts";
|
|
@@ -37,6 +36,7 @@ import {
|
|
|
37
36
|
findRecoverableGoals,
|
|
38
37
|
formatRecoveryStatus,
|
|
39
38
|
} from "./recovery.ts";
|
|
39
|
+
import { modelProfileForPhase } from "./routing.ts";
|
|
40
40
|
import {
|
|
41
41
|
moveToFreshSession,
|
|
42
42
|
slicePhaseContext,
|
|
@@ -102,23 +102,6 @@ export default function goala(pi: ExtensionAPI): void {
|
|
|
102
102
|
displayPlan(pi, state);
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
function profileForPhase(): ModelProfile {
|
|
106
|
-
if (
|
|
107
|
-
state.phase === "planning" ||
|
|
108
|
-
state.phase === "awaiting-execution" ||
|
|
109
|
-
state.phase === "awaiting-review"
|
|
110
|
-
) return config.planner;
|
|
111
|
-
if (state.phase === "verifying-step") return config.stepVerifier;
|
|
112
|
-
if (state.phase === "verifying") return config.verifier;
|
|
113
|
-
if (
|
|
114
|
-
state.phase === "executing" &&
|
|
115
|
-
state.repairCycles >= config.fallbackExecutor.afterRepairCycle
|
|
116
|
-
) {
|
|
117
|
-
return config.fallbackExecutor;
|
|
118
|
-
}
|
|
119
|
-
return config.executor;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
105
|
async function statusForSession(
|
|
123
106
|
ctx: ExtensionCommandContext,
|
|
124
107
|
): Promise<string> {
|
|
@@ -143,7 +126,11 @@ export default function goala(pi: ExtensionAPI): void {
|
|
|
143
126
|
return true;
|
|
144
127
|
}
|
|
145
128
|
|
|
146
|
-
const profile =
|
|
129
|
+
const profile = modelProfileForPhase(
|
|
130
|
+
config,
|
|
131
|
+
state.phase,
|
|
132
|
+
state.repairCycles,
|
|
133
|
+
);
|
|
147
134
|
let model = ctx.modelRegistry.find(profile.provider, profile.model);
|
|
148
135
|
if (!model && config.allowCurrentModelFallback) {
|
|
149
136
|
const fallback = sessionDefaultModel ?? (ctx.model
|
|
@@ -563,12 +550,12 @@ Do not edit files or rely on executor claims. Finish with submit_step_verificati
|
|
|
563
550
|
if (!choice && ctx.hasUI) {
|
|
564
551
|
const selected = await ctx.ui.select("Goala setup", [
|
|
565
552
|
...GOALA_SETUP_PRESETS.map((preset) => preset.label),
|
|
566
|
-
"Configure each
|
|
553
|
+
"Configure each role",
|
|
567
554
|
"Show current configuration",
|
|
568
555
|
]);
|
|
569
556
|
choice = selected === "Show current configuration"
|
|
570
557
|
? "status"
|
|
571
|
-
: selected === "Configure each
|
|
558
|
+
: selected === "Configure each role"
|
|
572
559
|
? "custom"
|
|
573
560
|
: GOALA_SETUP_PRESETS.find((preset) => preset.label === selected)?.id ?? "";
|
|
574
561
|
}
|
|
@@ -605,7 +592,7 @@ Do not edit files or rely on executor claims. Finish with submit_step_verificati
|
|
|
605
592
|
);
|
|
606
593
|
if (missing.length > 0) {
|
|
607
594
|
ctx.ui.notify(
|
|
608
|
-
`
|
|
595
|
+
`Configuration not saved; unavailable model(s): ${missing.map((model) => `${model.provider}/${model.id}`).join(", ")}.`,
|
|
609
596
|
"warning",
|
|
610
597
|
);
|
|
611
598
|
return;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
GoalaConfig,
|
|
3
|
+
ModelProfile,
|
|
4
|
+
} from "./config.ts";
|
|
5
|
+
import type { Phase } from "./workflow.ts";
|
|
6
|
+
|
|
7
|
+
export function modelProfileForPhase(
|
|
8
|
+
config: GoalaConfig,
|
|
9
|
+
phase: Phase,
|
|
10
|
+
repairCycles: number,
|
|
11
|
+
): ModelProfile {
|
|
12
|
+
if (
|
|
13
|
+
phase === "planning" ||
|
|
14
|
+
phase === "awaiting-execution" ||
|
|
15
|
+
phase === "awaiting-review"
|
|
16
|
+
) {
|
|
17
|
+
return config.planner;
|
|
18
|
+
}
|
|
19
|
+
if (phase === "verifying-step") return config.stepVerifier;
|
|
20
|
+
if (phase === "verifying") return config.verifier;
|
|
21
|
+
if (
|
|
22
|
+
phase === "executing" &&
|
|
23
|
+
repairCycles >= config.fallbackExecutor.afterRepairCycle
|
|
24
|
+
) {
|
|
25
|
+
return config.fallbackExecutor;
|
|
26
|
+
}
|
|
27
|
+
return config.executor;
|
|
28
|
+
}
|
|
@@ -12,12 +12,16 @@ 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" },
|
|
18
22
|
{ key: "stepVerifier", label: "Step verifier" },
|
|
19
23
|
{ key: "verifier", label: "Final verifier" },
|
|
20
|
-
{ key: "fallbackExecutor", label: "
|
|
24
|
+
{ key: "fallbackExecutor", label: "Fallback executor" },
|
|
21
25
|
];
|
|
22
26
|
|
|
23
27
|
const THINKING_LEVELS: readonly ThinkingLevel[] = [
|
|
@@ -30,8 +34,235 @@ 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_ROLE = "Edit a role";
|
|
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 activation =
|
|
229
|
+
role.key === "fallbackExecutor"
|
|
230
|
+
? `\nActivates after ${config.fallbackExecutor.afterRepairCycle} failed verification attempts`
|
|
231
|
+
: "";
|
|
232
|
+
const action = await ctx.ui.select(
|
|
233
|
+
`${role.label}${activation}\nCurrent: ${profileLabel(ctx, available, current)}`,
|
|
234
|
+
actions,
|
|
235
|
+
);
|
|
236
|
+
if (!action) return false;
|
|
237
|
+
if (action === KEEP_CURRENT) return true;
|
|
238
|
+
|
|
239
|
+
if (action === CHANGE_REASONING) {
|
|
240
|
+
const thinkingLevel = await selectThinkingLevel(ctx, role.label, current);
|
|
241
|
+
if (!thinkingLevel) return false;
|
|
242
|
+
setProfile(config, role.key, { ...current, thinkingLevel });
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const profile = await changeModel(ctx, available, role.label, current);
|
|
247
|
+
if (!profile) return false;
|
|
248
|
+
setProfile(config, role.key, profile);
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function reviewSummary(
|
|
253
|
+
ctx: ExtensionCommandContext,
|
|
254
|
+
available: AvailableModel[],
|
|
255
|
+
config: GoalaConfig,
|
|
256
|
+
): string {
|
|
257
|
+
return MODEL_ROLES.map(
|
|
258
|
+
(role) => {
|
|
259
|
+
const label =
|
|
260
|
+
role.key === "fallbackExecutor"
|
|
261
|
+
? `${role.label} (after ${config.fallbackExecutor.afterRepairCycle} failed verification attempts)`
|
|
262
|
+
: role.label;
|
|
263
|
+
return `${label}: ${profileLabel(ctx, available, config[role.key])}`;
|
|
264
|
+
},
|
|
265
|
+
).join("\n");
|
|
35
266
|
}
|
|
36
267
|
|
|
37
268
|
export async function selectModelRoles(
|
|
@@ -39,7 +270,7 @@ export async function selectModelRoles(
|
|
|
39
270
|
current: GoalaConfig,
|
|
40
271
|
): Promise<GoalaConfig | undefined> {
|
|
41
272
|
if (!ctx.hasUI) {
|
|
42
|
-
ctx.ui.notify("Per-
|
|
273
|
+
ctx.ui.notify("Per-role model setup requires interactive Pi.", "warning");
|
|
43
274
|
return undefined;
|
|
44
275
|
}
|
|
45
276
|
|
|
@@ -59,51 +290,25 @@ export async function selectModelRoles(
|
|
|
59
290
|
|
|
60
291
|
const proposed = structuredClone(current);
|
|
61
292
|
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,
|
|
293
|
+
if (!(await configureRole(ctx, available, proposed, role))) return undefined;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
while (true) {
|
|
297
|
+
const action = await ctx.ui.select(
|
|
298
|
+
`Review Goala configuration\n${reviewSummary(ctx, available, proposed)}`,
|
|
299
|
+
[SAVE_CONFIGURATION, EDIT_ROLE, CANCEL],
|
|
79
300
|
);
|
|
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
|
-
}
|
|
301
|
+
if (!action || action === CANCEL) return undefined;
|
|
302
|
+
if (action === SAVE_CONFIGURATION) return proposed;
|
|
93
303
|
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
afterRepairCycle: current.fallbackExecutor.afterRepairCycle,
|
|
103
|
-
};
|
|
104
|
-
} else {
|
|
105
|
-
proposed[role.key] = profile;
|
|
304
|
+
const selectedRole = await ctx.ui.select(
|
|
305
|
+
"Choose a role to edit",
|
|
306
|
+
MODEL_ROLES.map((role) => role.label),
|
|
307
|
+
);
|
|
308
|
+
if (!selectedRole) return undefined;
|
|
309
|
+
const role = MODEL_ROLES.find((candidate) => candidate.label === selectedRole);
|
|
310
|
+
if (!role || !(await configureRole(ctx, available, proposed, role))) {
|
|
311
|
+
return undefined;
|
|
106
312
|
}
|
|
107
313
|
}
|
|
108
|
-
return proposed;
|
|
109
314
|
}
|