@underactive/pi-topping-moa-fusion 0.4.5 → 0.5.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 +29 -0
- package/README.md +0 -3
- package/package.json +9 -6
- package/src/agents/discovery.ts +24 -88
- package/src/config/settings.ts +79 -69
- package/src/debate/runDebate.ts +6 -5
- package/src/moa/modelRuntime.ts +7 -23
- package/src/opinion/opinionResults.ts +2 -2
- package/src/opinion/runOpinion.ts +6 -5
- package/src/planning/planMode.ts +5 -3
- package/src/planning/tools/enterPlanMode.ts +3 -3
- package/src/planning/tools/mfPlanSubagent.ts +2 -2
- package/src/runtime/activityTracking.ts +1 -91
- package/src/runtime/piCapabilities.ts +8 -19
- package/src/runtime/processPool.ts +14 -52
- package/src/runtime/results.ts +13 -3
- package/src/runtime/runner.ts +7 -20
- package/src/shared/modelRefs.ts +10 -71
- package/src/ui/agentStatus.ts +3 -9
- package/src/ui/cancelOverlay.ts +1 -1
- package/src/ui/chrome.ts +1 -2
- package/src/ui/debateModelPicker.ts +3 -3
- package/src/ui/moaModelPicker.ts +7 -8
- package/src/ui/moaProgressWidget.ts +14 -33
- package/src/ui/moaSetupOverlay.ts +13 -9
- package/src/ui/opinionModelPicker.ts +3 -3
- package/src/ui/twoPaneModelThinking.ts +31 -268
- package/src/ui/workingOverlay.ts +2 -2
- package/src/activityMeter.ts +0 -193
- package/src/config/modelCatalogue.ts +0 -54
- package/src/ui/modelLabel.ts +0 -61
- package/src/ui/shimmer.ts +0 -77
- package/src/ui/toolActivity.ts +0 -35
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.5.0]
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Depend on `@underactive/pi-topping-primitives` `^0.10.0` (was `^0.8.0`). Nothing it imports changed in between: 0.9.0 added `./env` and `./abort`, which it does not use, and 0.10.0 removed only the `pi-topping-compact` entry names from `./contracts`, which it never referenced. Every topping now asks for the same range, so an install of several shares one copy of the library. No visible change.
|
|
10
|
+
- Model references, registry catalogues, label truncation and the unframed model/thinking picker now use `@underactive/pi-topping-primitives` 0.5.0. MoA framing, saved/current thinking preferences and the registry's `off` fallback remain local.
|
|
11
|
+
- Saved model references with an empty provider or id are now treated as unset rather than accepted as valid references.
|
|
12
|
+
- Enter on Select with no matching model is now inert instead of returning to the overview; Cancel and Escape still go back.
|
|
13
|
+
- Model filters now accept printable pasted text and kitty-encoded characters, including while the action buttons are focused. Backspace edits one Unicode code point and returns focus to models.
|
|
14
|
+
- Picker list colours now follow the injected host theme (accent selection and muted descriptions, scroll indicators and no-match text) rather than pi's global select-list theme. Footer hints start at the body edge instead of a two-column inset and can use that extra wrapping space; action buttons keep their alignment.
|
|
15
|
+
- Use `PI_VIRTUAL_MODEL_API` from the shared primitives contracts for virtual-model detection.
|
|
16
|
+
- MoA settings now go through the shared `@underactive/pi-topping-primitives` 0.2.0 settings store. Every read-modify-write (debate, opinion, implementer, planning, roster changes and the setup overlay) is a locked update, which closes the lost-update race between concurrent sessions. The file keeps its path and tab format; writes are atomic, and the file is created 0600 (and its directory 0700 when new).
|
|
17
|
+
- A settings file that is not valid JSON is no longer silently overwritten: it reads as defaults without console output, and saves are refused with `Could not save MoA settings: …` until the file is fixed or removed.
|
|
18
|
+
- A failed settings save now shows that warning and the run continues; only the setup overlay still aborts, returning without reporting success.
|
|
19
|
+
- The vendored activity meter, shimmer, tool-activity labels and output-token tracker now come from `@underactive/pi-topping-primitives` 0.3.0 (`./activity` and `./activity-tui`).
|
|
20
|
+
- The MoA progress table's activity meters now saturate at an inclusive 80 tokens/s instead of a strict 40 (the shared `rateToLevel` default).
|
|
21
|
+
- The shimmer now matches a colour carried inside a compound SGR.
|
|
22
|
+
- Cost formatting (`formatCost`) and the braille and quadrant spinner frames now come from `@underactive/pi-topping-primitives` 0.6.0 (`./text`, `./glyphs`); rendered output is unchanged. The progress table's joined box glyphs stay local.
|
|
23
|
+
- Finding the pi to spawn, the SIGTERM-to-SIGKILL escalation, the tracked-process registry, the concurrency map, the pi version helpers and the agent `.md` loader now come from `@underactive/pi-topping-primitives` 0.8.0 (`./process`). The registry is still this extension's own (`trackedProcesses`), made by the shared `createProcessTracker()`, so `/reload` kills only what moa-fusion started. The `--no-mcp` probe and its cache stay local.
|
|
24
|
+
- An agent file that is malformed (bad YAML, or a non-string `name`, `description`, `tools`, `model` or `thinking`) is now skipped, and the rest of the directory still loads; before, one bad file aborted discovery. A YAML `null` for `tools`, `model` or `thinking` now counts as absent.
|
|
25
|
+
- A Debian-style `nodejs` binary is treated as a generic runtime, like `node` and `bun`, when moa-fusion looks for the pi to spawn.
|
|
26
|
+
- Verified compatibility with pi 1.4.0: no pi export this extension imports was removed or changed, no package entry point moved, and typecheck and the full test suite pass against 1.4.0.
|
|
27
|
+
- Updated all four pi development dependencies from 1.3.0 to 1.4.0.
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- Tool-activity labels are cleaned (terminal sequences and stray control characters removed) before they are truncated, so an escape sequence is never cut in half.
|
|
32
|
+
- A subagent reply cut off at the model's output-token limit was passed on as if complete. `mf_plan_subagent` output (single and parallel) and `/mf-opinion` answers now start with `[Response was truncated: the model reached its output token limit.]`. The reply still counts as a success: children run in pi's JSON mode, which exits 0 on a truncated response.
|
|
33
|
+
|
|
5
34
|
## [0.4.5]
|
|
6
35
|
|
|
7
36
|
### Changed
|
package/README.md
CHANGED
|
@@ -367,7 +367,6 @@ In non-interactive modes (`pi -p`, `--mode json`), `exit_plan_mode` is rejected
|
|
|
367
367
|
index.ts # Package entry point (re-exports src/index.ts)
|
|
368
368
|
src/
|
|
369
369
|
├── index.ts # Thin composition entry
|
|
370
|
-
├── activityMeter.ts # Dependency-free output-rate meter
|
|
371
370
|
├── shared/
|
|
372
371
|
│ ├── modelRefs.ts # Model refs, thinking levels, display labels
|
|
373
372
|
│ ├── controlChars.ts # Control-character stripping for argv and display
|
|
@@ -457,8 +456,6 @@ src/
|
|
|
457
456
|
├── observeOverlay.ts
|
|
458
457
|
├── cancelOverlay.ts
|
|
459
458
|
├── promptEditor.ts # File-path/@name completion editor
|
|
460
|
-
├── shimmer.ts
|
|
461
|
-
├── toolActivity.ts
|
|
462
459
|
├── verificationFindingsOverlay.ts
|
|
463
460
|
└── workingOverlay.ts # Blocking-work overlay (plan/opinion/debate naming)
|
|
464
461
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@underactive/pi-topping-moa-fusion",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Pi extension implementing /mf-plan — Claude Code-style 5-phase plan mode with a Mixture-of-Agents proposer/synthesizer pipeline.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"scripts": {
|
|
41
41
|
"check": "tsc --noEmit",
|
|
42
42
|
"typecheck": "tsc --noEmit",
|
|
43
|
-
"test": "node --test 'test/*.test.mjs'"
|
|
43
|
+
"test": "node -C compiled --experimental-transform-types --test 'test/*.test.mjs'"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
46
|
"@earendil-works/pi-agent-core": "*",
|
|
@@ -50,12 +50,15 @@
|
|
|
50
50
|
"typebox": "*"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
|
-
"@earendil-works/pi-agent-core": "1.
|
|
54
|
-
"@earendil-works/pi-ai": "1.
|
|
55
|
-
"@earendil-works/pi-coding-agent": "1.
|
|
56
|
-
"@earendil-works/pi-tui": "1.
|
|
53
|
+
"@earendil-works/pi-agent-core": "1.4.0",
|
|
54
|
+
"@earendil-works/pi-ai": "1.4.0",
|
|
55
|
+
"@earendil-works/pi-coding-agent": "1.4.0",
|
|
56
|
+
"@earendil-works/pi-tui": "1.4.0",
|
|
57
57
|
"@types/node": "^24.0.0",
|
|
58
58
|
"typebox": "^1.3.3",
|
|
59
59
|
"typescript": "^5.8.0"
|
|
60
|
+
},
|
|
61
|
+
"dependencies": {
|
|
62
|
+
"@underactive/pi-topping-primitives": "^0.10.0"
|
|
60
63
|
}
|
|
61
64
|
}
|
package/src/agents/discovery.ts
CHANGED
|
@@ -1,114 +1,50 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Agent discovery and configuration.
|
|
3
|
-
*
|
|
3
|
+
* The per-file and per-directory loading and the upward `.pi/agents` walk live in
|
|
4
|
+
* `@underactive/pi-topping-primitives/process` (ported from pi's subagent example); this module
|
|
5
|
+
* keeps moa-fusion's user-then-project layering and its thinking-level check.
|
|
4
6
|
*/
|
|
5
7
|
|
|
6
|
-
import * as fs from "node:fs";
|
|
7
8
|
import * as path from "node:path";
|
|
8
|
-
import {
|
|
9
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import {
|
|
11
|
+
type AgentDefinition,
|
|
12
|
+
type AgentFileOptions,
|
|
13
|
+
findNearestProjectAgentsDir,
|
|
14
|
+
loadAgentsFromDir,
|
|
15
|
+
parseAgentFile as parsePackageAgentFile,
|
|
16
|
+
} from "@underactive/pi-topping-primitives/process";
|
|
9
17
|
import { isThinkingLevel, type ThinkingLevel } from "../shared/modelRefs.ts";
|
|
10
18
|
|
|
11
19
|
export type AgentScope = "user" | "project" | "both";
|
|
12
20
|
|
|
13
|
-
export
|
|
14
|
-
name: string;
|
|
15
|
-
description: string;
|
|
16
|
-
tools?: string[];
|
|
17
|
-
model?: string;
|
|
18
|
-
thinking?: ThinkingLevel;
|
|
19
|
-
systemPrompt: string;
|
|
20
|
-
source: "user" | "project";
|
|
21
|
-
filePath: string;
|
|
22
|
-
}
|
|
21
|
+
export type AgentConfig = AgentDefinition<"user" | "project", ThinkingLevel>;
|
|
23
22
|
|
|
24
23
|
export interface AgentDiscoveryResult {
|
|
25
24
|
agents: AgentConfig[];
|
|
26
25
|
projectAgentsDir: string | null;
|
|
27
26
|
}
|
|
28
27
|
|
|
28
|
+
/**
|
|
29
|
+
* An unrecognized level is dropped rather than forwarded: `--thinking` on the child process
|
|
30
|
+
* would reject it and fail the whole agent run.
|
|
31
|
+
*/
|
|
32
|
+
const AGENT_FILE_OPTIONS: AgentFileOptions<ThinkingLevel> = Object.freeze({
|
|
33
|
+
parseThinking: (raw: string) => (isThinkingLevel(raw) ? raw : undefined),
|
|
34
|
+
});
|
|
35
|
+
|
|
29
36
|
/** Parse a single agent markdown file into an `AgentConfig`, or `undefined` if malformed/unreadable. */
|
|
30
37
|
export function parseAgentFile(filePath: string, source: "user" | "project"): AgentConfig | undefined {
|
|
31
|
-
|
|
32
|
-
try {
|
|
33
|
-
content = fs.readFileSync(filePath, "utf-8");
|
|
34
|
-
} catch {
|
|
35
|
-
return undefined;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const { frontmatter, body } = parseFrontmatter<Record<string, string>>(content);
|
|
39
|
-
|
|
40
|
-
if (!frontmatter.name || !frontmatter.description) return undefined;
|
|
41
|
-
|
|
42
|
-
const tools = frontmatter.tools
|
|
43
|
-
?.split(",")
|
|
44
|
-
.map((t: string) => t.trim())
|
|
45
|
-
.filter(Boolean);
|
|
46
|
-
|
|
47
|
-
// An unrecognized level is dropped rather than forwarded: `--thinking` on the
|
|
48
|
-
// child process would reject it and fail the whole agent run.
|
|
49
|
-
const thinking = frontmatter.thinking?.trim();
|
|
50
|
-
|
|
51
|
-
return {
|
|
52
|
-
name: frontmatter.name,
|
|
53
|
-
description: frontmatter.description,
|
|
54
|
-
tools: tools && tools.length > 0 ? tools : undefined,
|
|
55
|
-
model: frontmatter.model,
|
|
56
|
-
thinking: thinking && isThinkingLevel(thinking) ? thinking : undefined,
|
|
57
|
-
systemPrompt: body,
|
|
58
|
-
source,
|
|
59
|
-
filePath,
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function loadAgentsFromDir(dir: string, source: "user" | "project"): AgentConfig[] {
|
|
64
|
-
const agents: AgentConfig[] = [];
|
|
65
|
-
|
|
66
|
-
if (!fs.existsSync(dir)) return agents;
|
|
67
|
-
|
|
68
|
-
let entries: fs.Dirent[];
|
|
69
|
-
try {
|
|
70
|
-
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
71
|
-
} catch {
|
|
72
|
-
return agents;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
for (const entry of entries) {
|
|
76
|
-
if (!entry.name.endsWith(".md")) continue;
|
|
77
|
-
if (!entry.isFile() && !entry.isSymbolicLink()) continue;
|
|
78
|
-
|
|
79
|
-
const agent = parseAgentFile(path.join(dir, entry.name), source);
|
|
80
|
-
if (agent) agents.push(agent);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return agents;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function isDirectory(p: string): boolean {
|
|
87
|
-
try {
|
|
88
|
-
return fs.statSync(p).isDirectory();
|
|
89
|
-
} catch {
|
|
90
|
-
return false;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function findNearestProjectAgentsDir(cwd: string): string | null {
|
|
95
|
-
let currentDir = cwd;
|
|
96
|
-
while (true) {
|
|
97
|
-
const candidate = path.join(currentDir, CONFIG_DIR_NAME, "agents");
|
|
98
|
-
if (isDirectory(candidate)) return candidate;
|
|
99
|
-
|
|
100
|
-
const parentDir = path.dirname(currentDir);
|
|
101
|
-
if (parentDir === currentDir) return null;
|
|
102
|
-
currentDir = parentDir;
|
|
103
|
-
}
|
|
38
|
+
return parsePackageAgentFile(filePath, source, AGENT_FILE_OPTIONS);
|
|
104
39
|
}
|
|
105
40
|
|
|
106
41
|
export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryResult {
|
|
107
42
|
const userDir = path.join(getAgentDir(), "agents");
|
|
108
43
|
const projectAgentsDir = findNearestProjectAgentsDir(cwd);
|
|
109
44
|
|
|
110
|
-
const userAgents = scope === "project" ? [] : loadAgentsFromDir(userDir, "user");
|
|
111
|
-
const projectAgents =
|
|
45
|
+
const userAgents = scope === "project" ? [] : loadAgentsFromDir(userDir, "user", AGENT_FILE_OPTIONS);
|
|
46
|
+
const projectAgents =
|
|
47
|
+
scope === "user" || !projectAgentsDir ? [] : loadAgentsFromDir(projectAgentsDir, "project", AGENT_FILE_OPTIONS);
|
|
112
48
|
|
|
113
49
|
const agentMap = new Map<string, AgentConfig>();
|
|
114
50
|
|
package/src/config/settings.ts
CHANGED
|
@@ -7,11 +7,18 @@
|
|
|
7
7
|
* causes it to be skipped.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { existsSync
|
|
11
|
-
import {
|
|
10
|
+
import { existsSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
12
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
13
|
+
import { defaultThinkingForModel as sharedDefaultThinkingForModel } from "@underactive/pi-topping-primitives/models";
|
|
14
|
+
import {
|
|
15
|
+
createSettingsStore,
|
|
16
|
+
sanitize,
|
|
17
|
+
type SettingsChange,
|
|
18
|
+
type SettingsWriteFailure,
|
|
19
|
+
type SettingsWriteResult,
|
|
20
|
+
} from "@underactive/pi-topping-primitives/settings";
|
|
13
21
|
import {
|
|
14
|
-
THINKING_LEVELS,
|
|
15
22
|
isModelRef,
|
|
16
23
|
isThinkingLevel,
|
|
17
24
|
type ModelRef,
|
|
@@ -19,6 +26,8 @@ import {
|
|
|
19
26
|
} from "../shared/modelRefs.ts";
|
|
20
27
|
import { parseRosters, type PlanRoster } from "./rosters.ts";
|
|
21
28
|
|
|
29
|
+
export { thinkingOptionsForModel } from "@underactive/pi-topping-primitives/models";
|
|
30
|
+
|
|
22
31
|
export type MoaMode = "single" | "moa";
|
|
23
32
|
|
|
24
33
|
export const MIN_CONCURRENT_AGENTS = 1;
|
|
@@ -111,80 +120,86 @@ export function moaSettingsPath(): string {
|
|
|
111
120
|
return join(getAgentDir(), "mf-plan", "settings.json");
|
|
112
121
|
}
|
|
113
122
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
123
|
+
/** Field-by-field validation; a missing or non-object file reads as the defaults. */
|
|
124
|
+
function sanitizeMoaConfig(raw: unknown): MoaConfig {
|
|
125
|
+
if (raw === undefined || !sanitize.isRecord(raw)) return emptyConfig();
|
|
126
|
+
const parsed = raw as Partial<MoaConfig>;
|
|
127
|
+
const proposers = Array.isArray(parsed.proposers)
|
|
128
|
+
? parsed.proposers.filter(isModelRef)
|
|
129
|
+
: [];
|
|
130
|
+
const opinionModels = Array.isArray(parsed.opinionModels)
|
|
131
|
+
? parsed.opinionModels.filter(isModelRef)
|
|
132
|
+
: [];
|
|
133
|
+
const debateModels = Array.isArray(parsed.debateModels)
|
|
134
|
+
? parsed.debateModels.filter(isModelRef)
|
|
135
|
+
: [];
|
|
136
|
+
const debateRounds = typeof parsed.debateRounds === "number" && Number.isFinite(parsed.debateRounds)
|
|
137
|
+
? Math.min(5, Math.max(2, Math.round(parsed.debateRounds)))
|
|
138
|
+
: 3;
|
|
139
|
+
const maxConcurrentAgents = normalizeMaxConcurrentAgents(parsed.maxConcurrentAgents);
|
|
140
|
+
const maxVerificationRepairs = normalizeMaxVerificationRepairs(parsed.maxVerificationRepairs);
|
|
141
|
+
const synthesizer = isModelRef(parsed.synthesizer) ? parsed.synthesizer : undefined;
|
|
142
|
+
const implementer = isModelRef(parsed.implementer) ? parsed.implementer : undefined;
|
|
143
|
+
const verifier = isModelRef(parsed.verifier) ? parsed.verifier : undefined;
|
|
144
|
+
const cheap = isModelRef(parsed.cheap) ? parsed.cheap : undefined;
|
|
145
|
+
const mode: MoaMode = parsed.mode === "moa" ? "moa" : "single";
|
|
146
|
+
|
|
147
|
+
const thinkingOverrides: Record<string, ThinkingLevel> = {};
|
|
148
|
+
if (parsed.thinkingOverrides && typeof parsed.thinkingOverrides === "object") {
|
|
149
|
+
for (const [key, value] of Object.entries(parsed.thinkingOverrides)) {
|
|
150
|
+
if (typeof value === "string" && isThinkingLevel(value)) thinkingOverrides[key] = value;
|
|
143
151
|
}
|
|
152
|
+
}
|
|
144
153
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
154
|
+
const autoResolveConflicts = typeof parsed.autoResolveConflicts === "boolean"
|
|
155
|
+
? parsed.autoResolveConflicts
|
|
156
|
+
: false;
|
|
148
157
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
158
|
+
const useSummaryName = typeof parsed.useSummaryName === "boolean"
|
|
159
|
+
? parsed.useSummaryName
|
|
160
|
+
: true;
|
|
152
161
|
|
|
153
|
-
|
|
162
|
+
const minimalFanoutHeader = parsed.minimalFanoutHeader === true;
|
|
154
163
|
|
|
155
|
-
|
|
156
|
-
|
|
164
|
+
const agentDefaultsConfigured = parsed.agentDefaultsConfigured === true;
|
|
165
|
+
const rosters = parseRosters(parsed.rosters);
|
|
157
166
|
|
|
158
|
-
|
|
159
|
-
} catch (err) {
|
|
160
|
-
console.error("mf-plan: failed to parse settings, using defaults:", err);
|
|
161
|
-
return emptyConfig();
|
|
162
|
-
}
|
|
167
|
+
return { mode, proposers, opinionModels, debateModels, debateRounds, maxConcurrentAgents, maxVerificationRepairs, synthesizer, implementer, verifier, cheap, thinkingOverrides, rosters, autoResolveConflicts, useSummaryName, minimalFanoutHeader, agentDefaultsConfigured };
|
|
163
168
|
}
|
|
164
169
|
|
|
170
|
+
const moaStore = createSettingsStore<MoaConfig>({
|
|
171
|
+
path: moaSettingsPath,
|
|
172
|
+
codec: {
|
|
173
|
+
sanitize: sanitizeMoaConfig,
|
|
174
|
+
serialize: (config) => `${JSON.stringify(config, null, "\t")}\n`,
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
/** A missing, unreadable or corrupt file reads as the defaults. Never throws, never prints. */
|
|
165
179
|
export function loadMoaConfig(): MoaConfig {
|
|
166
|
-
|
|
167
|
-
if (existsSync(settingsPath)) return parseSettingsFile(settingsPath);
|
|
168
|
-
return emptyConfig();
|
|
180
|
+
return moaStore.read();
|
|
169
181
|
}
|
|
170
182
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
writeFileSync(path, `${JSON.stringify(config, null, "\t")}\n`, "utf-8");
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
export function moaSettingsExist(): boolean {
|
|
178
|
-
return existsSync(moaSettingsPath());
|
|
183
|
+
/** Whole-config write. Prefer `updateMoaConfig` for anything that starts from a loaded config. */
|
|
184
|
+
export function saveMoaConfig(config: MoaConfig): SettingsWriteResult<MoaConfig> {
|
|
185
|
+
return moaStore.write(config);
|
|
179
186
|
}
|
|
180
187
|
|
|
181
188
|
/**
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
* is one the backend will refuse.
|
|
189
|
+
* Locked read-modify-write: `change` sees the file as it is now, so two sessions saving
|
|
190
|
+
* different fields both land. Refuses to overwrite a file that is not valid JSON.
|
|
185
191
|
*/
|
|
186
|
-
export function
|
|
187
|
-
return
|
|
192
|
+
export function updateMoaConfig(change: SettingsChange<MoaConfig>): SettingsWriteResult<MoaConfig> {
|
|
193
|
+
return moaStore.update(change);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Notification text for a failed save. */
|
|
197
|
+
export function moaSaveFailureMessage(result: SettingsWriteFailure): string {
|
|
198
|
+
return `Could not save MoA settings: ${result.message}`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function moaSettingsExist(): boolean {
|
|
202
|
+
return existsSync(moaSettingsPath());
|
|
188
203
|
}
|
|
189
204
|
|
|
190
205
|
/** Selects a model's saved level, the current level, or a sensible default in that order. */
|
|
@@ -192,12 +207,7 @@ export function defaultThinkingForModel(
|
|
|
192
207
|
key: string,
|
|
193
208
|
config: MoaConfig,
|
|
194
209
|
currentLevel: ThinkingLevel,
|
|
195
|
-
registryLevels: ThinkingLevel[],
|
|
210
|
+
registryLevels: readonly ThinkingLevel[],
|
|
196
211
|
): ThinkingLevel {
|
|
197
|
-
|
|
198
|
-
const saved = config.thinkingOverrides[key];
|
|
199
|
-
if (saved && options.includes(saved)) return saved;
|
|
200
|
-
if (options.includes(currentLevel)) return currentLevel;
|
|
201
|
-
if (options.includes("medium")) return "medium";
|
|
202
|
-
return options[0] ?? "medium";
|
|
212
|
+
return sharedDefaultThinkingForModel(key, config.thinkingOverrides, currentLevel, registryLevels);
|
|
203
213
|
}
|
package/src/debate/runDebate.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { installShippedAgents, shippedAgentsDir, withAuthoritativeMoaAgents } fr
|
|
|
4
4
|
import { discoverAgents } from "../agents/discovery.ts";
|
|
5
5
|
import { summarizePlanPromptName } from "../config/planName.ts";
|
|
6
6
|
import { slugifyPlanName } from "../planning/planFile.ts";
|
|
7
|
-
import { loadMoaConfig,
|
|
7
|
+
import { loadMoaConfig, moaSaveFailureMessage, updateMoaConfig } from "../config/settings.ts";
|
|
8
8
|
import type { OpinionHost } from "../opinion/runOpinion.ts";
|
|
9
9
|
import { resolveContextWindow } from "../moa/modelRuntime.ts";
|
|
10
10
|
import { type CancelSession, subscribeCancelOverlayOnEsc } from "../runtime/cancelRun.ts";
|
|
@@ -63,12 +63,13 @@ export async function runInteractiveDebate(
|
|
|
63
63
|
if (!selection) return;
|
|
64
64
|
const { models, thinking, thinkingSelections, rounds } = selection;
|
|
65
65
|
const settings = loadMoaConfig();
|
|
66
|
-
|
|
67
|
-
...
|
|
66
|
+
const persisted = updateMoaConfig((current) => ({
|
|
67
|
+
...current,
|
|
68
68
|
debateModels: models,
|
|
69
69
|
debateRounds: rounds,
|
|
70
|
-
thinkingOverrides: { ...
|
|
71
|
-
});
|
|
70
|
+
thinkingOverrides: { ...current.thinkingOverrides, ...thinkingSelections },
|
|
71
|
+
}));
|
|
72
|
+
if (!persisted.ok) ctx.ui.notify(moaSaveFailureMessage(persisted), "warning");
|
|
72
73
|
|
|
73
74
|
const slug = await withWorkingOverlay(
|
|
74
75
|
ctx,
|
package/src/moa/modelRuntime.ts
CHANGED
|
@@ -1,34 +1,18 @@
|
|
|
1
1
|
import { calculateCost, type Api, type Model } from "@earendil-works/pi-ai";
|
|
2
2
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { PI_VIRTUAL_MODEL_API } from "@underactive/pi-topping-primitives/contracts";
|
|
4
5
|
import * as fs from "node:fs";
|
|
5
6
|
import * as path from "node:path";
|
|
6
7
|
|
|
7
|
-
import {
|
|
8
|
+
import { moaSaveFailureMessage, updateMoaConfig } from "../config/settings.ts";
|
|
8
9
|
import type { UsageStats } from "../runtime/results.ts";
|
|
9
10
|
import { modelRefLabel, type ModelRef, type ThinkingLevel } from "../shared/modelRefs.ts";
|
|
10
11
|
|
|
11
|
-
/**
|
|
12
|
-
* pi's internal `api` id for extension-registered virtual models (the autorouter's
|
|
13
|
-
* `litellm-auto/*`). pi does not export the constant, so mirror the value the
|
|
14
|
-
* autorouter extension pins in its own catalog.ts.
|
|
15
|
-
*/
|
|
16
|
-
const VIRTUAL_MODEL_API = "pi-virtual";
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Whether routing this model in a child subprocess requires loading the extension that
|
|
20
|
-
* registered its provider. True for native extension providers (claude-bridge) — surfaced by
|
|
21
|
-
* getRegisteredProviderIds() — and for virtual models registered via registerVirtualModel
|
|
22
|
-
* (litellm-auto/*). A virtual model's provider never reaches getRegisteredProviderIds():
|
|
23
|
-
* registerVirtualModel keeps it in a separate virtual-model map, so a child spawned with a plain
|
|
24
|
-
* --no-extensions cannot resolve `litellm-auto/auto` and dies with "Model not found". External
|
|
25
|
-
* extensions can register virtual models too, so this keys off pi's virtual-model api id rather
|
|
26
|
-
* than a hardcoded provider name.
|
|
27
|
-
*/
|
|
28
12
|
export function modelUsesExtensionRegisteredProvider(ctx: ExtensionContext, ref: ModelRef): boolean {
|
|
29
13
|
if (ctx.modelRegistry.getRegisteredProviderIds().includes(ref.provider)) return true;
|
|
30
|
-
// Only an extension can register a virtual
|
|
31
|
-
return ctx.modelRegistry.find(ref.provider, ref.id)?.api ===
|
|
14
|
+
// Only an extension can register a virtual model, so one mandates loadExtensions in the child.
|
|
15
|
+
return ctx.modelRegistry.find(ref.provider, ref.id)?.api === PI_VIRTUAL_MODEL_API;
|
|
32
16
|
}
|
|
33
17
|
|
|
34
18
|
export function cursorBridgeExtensionPath(): string | undefined {
|
|
@@ -130,12 +114,12 @@ export async function applyImplementingSelection(
|
|
|
130
114
|
ctx.ui.notify(`No API key for ${modelRefLabel(selection.ref)}; keeping current model.`, "warning");
|
|
131
115
|
return;
|
|
132
116
|
}
|
|
133
|
-
const
|
|
134
|
-
saveMoaConfig({
|
|
117
|
+
const saved = updateMoaConfig((settings) => ({
|
|
135
118
|
...settings,
|
|
136
119
|
implementer: selection.ref,
|
|
137
120
|
thinkingOverrides: { ...settings.thinkingOverrides, [modelRefLabel(selection.ref)]: selection.thinking },
|
|
138
|
-
});
|
|
121
|
+
}));
|
|
122
|
+
if (!saved.ok) ctx.ui.notify(moaSaveFailureMessage(saved), "warning");
|
|
139
123
|
pi.setThinkingLevel(selection.thinking);
|
|
140
124
|
ctx.ui.notify(successNotice);
|
|
141
125
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ModelRef, ThinkingLevel } from "../shared/modelRefs.ts";
|
|
2
2
|
import { modelRefLabel } from "../shared/modelRefs.ts";
|
|
3
3
|
import type { SingleResult } from "../runtime/results.ts";
|
|
4
|
-
import {
|
|
4
|
+
import { getResultOutput, isFailedResult } from "../runtime/results.ts";
|
|
5
5
|
|
|
6
6
|
export interface OpinionOutcome {
|
|
7
7
|
ref: ModelRef;
|
|
@@ -30,7 +30,7 @@ export function collectOpinionOutcomes(
|
|
|
30
30
|
ref,
|
|
31
31
|
thinking: thinking[index],
|
|
32
32
|
status: "done",
|
|
33
|
-
text:
|
|
33
|
+
text: getResultOutput(result).trim() || "(no output)",
|
|
34
34
|
};
|
|
35
35
|
});
|
|
36
36
|
}
|
|
@@ -4,7 +4,7 @@ import { installShippedAgents, shippedAgentsDir, withAuthoritativeMoaAgents } fr
|
|
|
4
4
|
import { discoverAgents } from "../agents/discovery.ts";
|
|
5
5
|
import { summarizePlanPromptName } from "../config/planName.ts";
|
|
6
6
|
import { slugifyPlanName } from "../planning/planFile.ts";
|
|
7
|
-
import { loadMoaConfig,
|
|
7
|
+
import { loadMoaConfig, moaSaveFailureMessage, updateMoaConfig } from "../config/settings.ts";
|
|
8
8
|
import { resolveContextWindow } from "../moa/modelRuntime.ts";
|
|
9
9
|
import { type CancelSession, subscribeCancelOverlayOnEsc } from "../runtime/cancelRun.ts";
|
|
10
10
|
import { formatMutationWarning, MutationTripwire } from "../runtime/mutationTripwire.ts";
|
|
@@ -70,11 +70,12 @@ export async function runInteractiveOpinion(
|
|
|
70
70
|
if (!selection) return;
|
|
71
71
|
const { models, thinking, thinkingSelections } = selection;
|
|
72
72
|
const settings = loadMoaConfig();
|
|
73
|
-
|
|
74
|
-
...
|
|
73
|
+
const persisted = updateMoaConfig((current) => ({
|
|
74
|
+
...current,
|
|
75
75
|
opinionModels: models,
|
|
76
|
-
thinkingOverrides: { ...
|
|
77
|
-
});
|
|
76
|
+
thinkingOverrides: { ...current.thinkingOverrides, ...thinkingSelections },
|
|
77
|
+
}));
|
|
78
|
+
if (!persisted.ok) ctx.ui.notify(moaSaveFailureMessage(persisted), "warning");
|
|
78
79
|
|
|
79
80
|
const slug = await withWorkingOverlay(
|
|
80
81
|
ctx,
|
package/src/planning/planMode.ts
CHANGED
|
@@ -15,6 +15,8 @@ import type {
|
|
|
15
15
|
ThemeColor,
|
|
16
16
|
ToolExecutionStartEvent,
|
|
17
17
|
} from "@earendil-works/pi-coding-agent";
|
|
18
|
+
import { OutputTokenAccount } from "@underactive/pi-topping-primitives/activity";
|
|
19
|
+
import { formatToolActivity } from "@underactive/pi-topping-primitives/activity-tui";
|
|
18
20
|
|
|
19
21
|
import { installShippedAgents } from "../agents/authoritative.ts";
|
|
20
22
|
import { summarizePlanPromptName } from "../config/planName.ts";
|
|
@@ -29,7 +31,7 @@ import { runImplementationVerification } from "../moa/verification.ts";
|
|
|
29
31
|
import { applyImplementingSelection, resolveContextWindow, resolveModelCost } from "../moa/modelRuntime.ts";
|
|
30
32
|
import { validateMfPlanInfo, type MfPlanInfo } from "../moa/planInfo.ts";
|
|
31
33
|
import type { MoaRunHost } from "../moa/runContext.ts";
|
|
32
|
-
import {
|
|
34
|
+
import { PartialAssistantAssembler } from "../runtime/activityTracking.ts";
|
|
33
35
|
import type { CancelSession } from "../runtime/cancelRun.ts";
|
|
34
36
|
import { cleanupTrackedProcesses } from "../runtime/processPool.ts";
|
|
35
37
|
import type { UsageStats } from "../runtime/results.ts";
|
|
@@ -141,7 +143,7 @@ export function createPlanModeController(pi: ExtensionAPI) {
|
|
|
141
143
|
// synthesis and review still own the table.
|
|
142
144
|
let runningProgressWidget: MoaProgressWidget | undefined;
|
|
143
145
|
// In-session implementer telemetry, reset each time implementation starts.
|
|
144
|
-
let implementationOutputTracker:
|
|
146
|
+
let implementationOutputTracker: OutputTokenAccount | undefined;
|
|
145
147
|
let implementationMessages: Message[] = [];
|
|
146
148
|
const implementationPartial = new PartialAssistantAssembler();
|
|
147
149
|
let implementationUsage: UsageStats | undefined;
|
|
@@ -470,7 +472,7 @@ export function createPlanModeController(pi: ExtensionAPI) {
|
|
|
470
472
|
markImplementationPending: (ctx) => {
|
|
471
473
|
footerError = undefined;
|
|
472
474
|
implementationPending = true;
|
|
473
|
-
implementationOutputTracker = new
|
|
475
|
+
implementationOutputTracker = new OutputTokenAccount();
|
|
474
476
|
resetImplementationCounters();
|
|
475
477
|
// Reconstruct the table for a resumed run that never adopted one (e.g.
|
|
476
478
|
// /mf-plan-implement after a restart). In-run paths already hold the
|
|
@@ -3,7 +3,7 @@ import { isKeyRelease, Key, matchesKey } from "@earendil-works/pi-tui";
|
|
|
3
3
|
import { Type } from "typebox";
|
|
4
4
|
|
|
5
5
|
import { installShippedAgents } from "../../agents/authoritative.ts";
|
|
6
|
-
import { loadMoaConfig,
|
|
6
|
+
import { loadMoaConfig, moaSaveFailureMessage, updateMoaConfig } from "../../config/settings.ts";
|
|
7
7
|
import { runMoaOrchestration } from "../../moa/orchestration.ts";
|
|
8
8
|
import type { MfPlanInfo } from "../../moa/planInfo.ts";
|
|
9
9
|
import type { MoaRunHost } from "../../moa/runContext.ts";
|
|
@@ -102,9 +102,9 @@ export async function runInteractivePlanMode(
|
|
|
102
102
|
pi.sendUserMessage(trimmedPrompt, TRIGGER_TURN);
|
|
103
103
|
return;
|
|
104
104
|
}
|
|
105
|
-
const settings = loadMoaConfig();
|
|
106
105
|
const { proposers, synthesizer, implementer, verifier, proposerThinking, synthesizerThinking, implementerThinking, verifierThinking, thinkingSelections } = pickerResult;
|
|
107
|
-
|
|
106
|
+
const saved = updateMoaConfig((settings) => ({ ...settings, mode: "moa", proposers, synthesizer, implementer, verifier, thinkingOverrides: { ...settings.thinkingOverrides, ...thinkingSelections } }));
|
|
107
|
+
if (!saved.ok) ctx.ui.notify(moaSaveFailureMessage(saved), "warning");
|
|
108
108
|
host.setActiveRunMoaInfo({ proposers, synthesizer });
|
|
109
109
|
const outcome = await runMoaOrchestration(host.moaRunHost, ctx, trimmedPrompt, proposers, synthesizer, proposerThinking, synthesizerThinking, session, { implementer, implementerThinking, verifier, verifierThinking });
|
|
110
110
|
if (outcome === "cancelled") { session.closeOverlay?.(); continue; }
|
|
@@ -11,7 +11,7 @@ import { discoverAgents, parseAgentFile } from "../../agents/discovery.ts";
|
|
|
11
11
|
import { modelExtensionOptions, qualifyBareModelId } from "../../moa/modelRuntime.ts";
|
|
12
12
|
import { CancelRun, type CancelRowExtras, type CancelSession } from "../../runtime/cancelRun.ts";
|
|
13
13
|
import { MutationTripwire, formatMutationWarning } from "../../runtime/mutationTripwire.ts";
|
|
14
|
-
import {
|
|
14
|
+
import { getResultOutput, isFailedResult, truncateOutput } from "../../runtime/results.ts";
|
|
15
15
|
import { runParallelAgents, runSingleAgent } from "../../runtime/runner.ts";
|
|
16
16
|
import { parseRef as parseModelRefLabel, type ThinkingLevel } from "../../shared/modelRefs.ts";
|
|
17
17
|
import { activityLoopCount } from "../../ui/agentStatus.ts";
|
|
@@ -328,7 +328,7 @@ export function registerMfPlanSubagentTool(pi: ExtensionAPI, host: MfPlanSubagen
|
|
|
328
328
|
}
|
|
329
329
|
return {
|
|
330
330
|
details: undefined,
|
|
331
|
-
content: [{ type: "text", text:
|
|
331
|
+
content: [{ type: "text", text: getResultOutput(result) }],
|
|
332
332
|
structuredContent,
|
|
333
333
|
};
|
|
334
334
|
} finally {
|