@underactive/pi-topping-moa-fusion 0.4.4 → 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 +41 -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 +36 -24
- package/src/opinion/opinionResults.ts +2 -2
- package/src/opinion/runOpinion.ts +6 -5
- package/src/planning/planMode.ts +15 -4
- package/src/planning/tools/enterPlanMode.ts +3 -3
- package/src/planning/tools/mfPlanSubagent.ts +13 -4
- 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/src/planning/planMode.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
AgentBeforeSettleEvent,
|
|
6
6
|
AgentSettledEvent,
|
|
7
7
|
AgentStartEvent,
|
|
8
|
+
BeforeAgentStartEvent,
|
|
8
9
|
ExtensionAPI,
|
|
9
10
|
ExtensionContext,
|
|
10
11
|
MessageEndEvent,
|
|
@@ -14,6 +15,8 @@ import type {
|
|
|
14
15
|
ThemeColor,
|
|
15
16
|
ToolExecutionStartEvent,
|
|
16
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";
|
|
17
20
|
|
|
18
21
|
import { installShippedAgents } from "../agents/authoritative.ts";
|
|
19
22
|
import { summarizePlanPromptName } from "../config/planName.ts";
|
|
@@ -28,7 +31,7 @@ import { runImplementationVerification } from "../moa/verification.ts";
|
|
|
28
31
|
import { applyImplementingSelection, resolveContextWindow, resolveModelCost } from "../moa/modelRuntime.ts";
|
|
29
32
|
import { validateMfPlanInfo, type MfPlanInfo } from "../moa/planInfo.ts";
|
|
30
33
|
import type { MoaRunHost } from "../moa/runContext.ts";
|
|
31
|
-
import {
|
|
34
|
+
import { PartialAssistantAssembler } from "../runtime/activityTracking.ts";
|
|
32
35
|
import type { CancelSession } from "../runtime/cancelRun.ts";
|
|
33
36
|
import { cleanupTrackedProcesses } from "../runtime/processPool.ts";
|
|
34
37
|
import type { UsageStats } from "../runtime/results.ts";
|
|
@@ -140,7 +143,7 @@ export function createPlanModeController(pi: ExtensionAPI) {
|
|
|
140
143
|
// synthesis and review still own the table.
|
|
141
144
|
let runningProgressWidget: MoaProgressWidget | undefined;
|
|
142
145
|
// In-session implementer telemetry, reset each time implementation starts.
|
|
143
|
-
let implementationOutputTracker:
|
|
146
|
+
let implementationOutputTracker: OutputTokenAccount | undefined;
|
|
144
147
|
let implementationMessages: Message[] = [];
|
|
145
148
|
const implementationPartial = new PartialAssistantAssembler();
|
|
146
149
|
let implementationUsage: UsageStats | undefined;
|
|
@@ -469,7 +472,7 @@ export function createPlanModeController(pi: ExtensionAPI) {
|
|
|
469
472
|
markImplementationPending: (ctx) => {
|
|
470
473
|
footerError = undefined;
|
|
471
474
|
implementationPending = true;
|
|
472
|
-
implementationOutputTracker = new
|
|
475
|
+
implementationOutputTracker = new OutputTokenAccount();
|
|
473
476
|
resetImplementationCounters();
|
|
474
477
|
// Reconstruct the table for a resumed run that never adopted one (e.g.
|
|
475
478
|
// /mf-plan-implement after a restart). In-run paths already hold the
|
|
@@ -739,7 +742,15 @@ export function createPlanModeController(pi: ExtensionAPI) {
|
|
|
739
742
|
return !isPlanModeInstructionText(msg.content);
|
|
740
743
|
}) };
|
|
741
744
|
},
|
|
742
|
-
onBeforeAgentStart: async () => {
|
|
745
|
+
onBeforeAgentStart: async (event?: Partial<Pick<BeforeAgentStartEvent, "messageType">>) => {
|
|
746
|
+
// pi >= 1.2 also fires this for runs another extension starts with
|
|
747
|
+
// sendMessage(..., { triggerTurn: true }). Those must neither receive plan
|
|
748
|
+
// instructions nor consume the one-shot exit reminder meant for the user's
|
|
749
|
+
// next prompt. Older hosts omit messageType and only fire for user runs.
|
|
750
|
+
if (event?.messageType === "custom") {
|
|
751
|
+
if (!planModeEnabled) deactivatePlanOnlyTools();
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
743
754
|
if (needsExitReminder) {
|
|
744
755
|
needsExitReminder = false;
|
|
745
756
|
// The reminder turn is also the first normal-mode turn: repair a
|
|
@@ -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; }
|
|
@@ -8,10 +8,10 @@ import * as path from "node:path";
|
|
|
8
8
|
import { installShippedAgents, shippedAgentsDir, withAuthoritativeMoaAgents } from "../../agents/authoritative.ts";
|
|
9
9
|
import { loadMoaConfig } from "../../config/settings.ts";
|
|
10
10
|
import { discoverAgents, parseAgentFile } from "../../agents/discovery.ts";
|
|
11
|
-
import { modelExtensionOptions } from "../../moa/modelRuntime.ts";
|
|
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";
|
|
@@ -141,7 +141,16 @@ export function registerMfPlanSubagentTool(pi: ExtensionAPI, host: MfPlanSubagen
|
|
|
141
141
|
const planOverride = sessionModel
|
|
142
142
|
? { model: `${sessionModel.provider}/${sessionModel.id}`, thinking: host.currentThinkingLevel() }
|
|
143
143
|
: undefined;
|
|
144
|
-
|
|
144
|
+
// Shipped and hand-edited frontmatter can carry a bare id, which pi >= 1.3.0
|
|
145
|
+
// rejects as ambiguous when several authenticated providers offer it. Qualify
|
|
146
|
+
// it here; thinking stays undefined so the frontmatter level still applies.
|
|
147
|
+
const frontmatterOverrideFor = (agentName: string): { model: string; thinking?: ThinkingLevel } | undefined => {
|
|
148
|
+
const model = agents.find((agent) => agent.name === agentName)?.model;
|
|
149
|
+
if (!model) return undefined;
|
|
150
|
+
const qualified = qualifyBareModelId(ctx.modelRegistry, model);
|
|
151
|
+
return qualified === model ? undefined : { model: qualified };
|
|
152
|
+
};
|
|
153
|
+
const overrideFor = (agentName: string) => (agentName === "mf-plan" && planOverride ? planOverride : frontmatterOverrideFor(agentName));
|
|
145
154
|
|
|
146
155
|
// An agent whose effective model belongs to an extension-registered
|
|
147
156
|
// provider (claude-bridge, cursor-bridge) or is an extension-registered virtual model
|
|
@@ -319,7 +328,7 @@ export function registerMfPlanSubagentTool(pi: ExtensionAPI, host: MfPlanSubagen
|
|
|
319
328
|
}
|
|
320
329
|
return {
|
|
321
330
|
details: undefined,
|
|
322
|
-
content: [{ type: "text", text:
|
|
331
|
+
content: [{ type: "text", text: getResultOutput(result) }],
|
|
323
332
|
structuredContent,
|
|
324
333
|
};
|
|
325
334
|
} finally {
|
|
@@ -1,96 +1,6 @@
|
|
|
1
1
|
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
2
2
|
import type { AssistantMessage, Message, ToolCall } from "@earendil-works/pi-ai";
|
|
3
|
-
import {
|
|
4
|
-
import { StreamingWordCounter } from "../activityMeter.ts";
|
|
5
|
-
import { isNonnegativeFiniteNumber, isValidContentIndex } from "./wire.ts";
|
|
6
|
-
|
|
7
|
-
/** Max length of a one-line tool-activity description surfaced for progress UIs. */
|
|
8
|
-
const ACTIVITY_MAX = 80;
|
|
9
|
-
|
|
10
|
-
/** Collapse all whitespace/newlines to single spaces and trim. */
|
|
11
|
-
function collapseWhitespace(text: string): string {
|
|
12
|
-
return text.replace(/\s+/g, " ").trim();
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Pick the most informative string argument for a one-line activity label.
|
|
17
|
-
* Prefers well-known tool arg names (command, pattern, path, …); falls back to
|
|
18
|
-
* the first non-empty string value.
|
|
19
|
-
*/
|
|
20
|
-
function summarizeToolArgs(args: unknown): string {
|
|
21
|
-
if (!args || typeof args !== "object") return "";
|
|
22
|
-
const record = args as Record<string, unknown>;
|
|
23
|
-
const preferred = ["command", "pattern", "query", "path", "file", "glob", "dir", "url", "name"];
|
|
24
|
-
for (const key of preferred) {
|
|
25
|
-
const value = record[key];
|
|
26
|
-
if (typeof value === "string" && value.trim()) return collapseWhitespace(value);
|
|
27
|
-
}
|
|
28
|
-
for (const value of Object.values(record)) {
|
|
29
|
-
if (typeof value === "string" && value.trim()) return collapseWhitespace(value);
|
|
30
|
-
}
|
|
31
|
-
return "";
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** Format a `tool_execution_start` event into a single-line "toolName arg" label. */
|
|
35
|
-
export function formatToolActivity(toolName: string, args: unknown): string {
|
|
36
|
-
const detail = summarizeToolArgs(args);
|
|
37
|
-
const label = detail ? `${toolName} ${detail}` : String(toolName);
|
|
38
|
-
return stripTerminalSequences(collapseWhitespace(label).slice(0, ACTIVITY_MAX));
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** Cumulative generated-output reading for the fan-out activity monitor. */
|
|
42
|
-
export interface OutputActivity {
|
|
43
|
-
/** Tokens generated so far this run: confirmed turns plus the in-flight turn. */
|
|
44
|
-
tokens: number;
|
|
45
|
-
/**
|
|
46
|
-
* Bumped whenever exact provider usage replaces a differing estimate.
|
|
47
|
-
* Consumers reset their rate tracker on a change so the correction doesn't
|
|
48
|
-
* register as a burst of generation that never happened.
|
|
49
|
-
*/
|
|
50
|
-
revision: number;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export class OutputActivityTracker {
|
|
54
|
-
#counter = new StreamingWordCounter();
|
|
55
|
-
#confirmed = 0;
|
|
56
|
-
#liveWords = 0;
|
|
57
|
-
#revision = 0;
|
|
58
|
-
|
|
59
|
-
/** Begin a new assistant turn, discarding only the previous turn's live estimate. */
|
|
60
|
-
messageStart(message: { role?: string } | undefined): void {
|
|
61
|
-
if (message?.role !== "assistant") return;
|
|
62
|
-
this.#resetTurn();
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
messageUpdate(assistantEvent: { type?: string; delta?: string } | undefined): void {
|
|
66
|
-
if (assistantEvent?.type !== "text_delta" && assistantEvent?.type !== "thinking_delta") return;
|
|
67
|
-
if (typeof assistantEvent.delta !== "string") return;
|
|
68
|
-
// Counted per stream kind so a word split across deltas isn't double
|
|
69
|
-
// counted, and interleaved thinking/text streams don't corrupt each other.
|
|
70
|
-
this.#liveWords += this.#counter.count(assistantEvent.delta, assistantEvent.type);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
messageEnd(message: { role?: string; usage?: { output?: unknown } } | undefined): void {
|
|
74
|
-
if (message?.role !== "assistant") return;
|
|
75
|
-
const exact = message.usage?.output;
|
|
76
|
-
if (isNonnegativeFiniteNumber(exact)) {
|
|
77
|
-
this.#confirmed += exact;
|
|
78
|
-
if (exact !== this.#liveWords) this.#revision++;
|
|
79
|
-
} else {
|
|
80
|
-
this.#confirmed += this.#liveWords;
|
|
81
|
-
}
|
|
82
|
-
this.#resetTurn();
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
snapshot(): OutputActivity {
|
|
86
|
-
return { tokens: this.#confirmed + this.#liveWords, revision: this.#revision };
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
#resetTurn(): void {
|
|
90
|
-
this.#liveWords = 0;
|
|
91
|
-
this.#counter.reset();
|
|
92
|
-
}
|
|
93
|
-
}
|
|
3
|
+
import { isValidContentIndex } from "./wire.ts";
|
|
94
4
|
|
|
95
5
|
type AssistantContentPart = AssistantMessage["content"][number];
|
|
96
6
|
|
|
@@ -2,34 +2,23 @@
|
|
|
2
2
|
* Capability probing for the pi executable a child process will actually run.
|
|
3
3
|
* The local `@earendil-works/pi-coding-agent` VERSION export describes the
|
|
4
4
|
* package this extension was built against, not the host (or `pi` on PATH)
|
|
5
|
-
* that `
|
|
5
|
+
* that `resolvePiInvocation()` spawns, so flag support is probed on the executable.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { execFile } from "node:child_process";
|
|
9
|
+
import {
|
|
10
|
+
type PiInvocation,
|
|
11
|
+
parsePiVersion,
|
|
12
|
+
versionAtLeast,
|
|
13
|
+
} from "@underactive/pi-topping-primitives/process";
|
|
9
14
|
|
|
10
|
-
export
|
|
11
|
-
|
|
12
|
-
args: string[];
|
|
13
|
-
}
|
|
15
|
+
export { parsePiVersion, versionAtLeast };
|
|
16
|
+
export type { PiInvocation };
|
|
14
17
|
|
|
15
18
|
/** `--no-mcp` first ships in pi 1.0.4; 1.0.3 rejects it as an unknown option. */
|
|
16
19
|
const NO_MCP_MIN_VERSION = [1, 0, 4] as const;
|
|
17
20
|
const VERSION_PROBE_TIMEOUT_MS = 10_000;
|
|
18
21
|
|
|
19
|
-
export function parsePiVersion(output: string): [number, number, number] | undefined {
|
|
20
|
-
const match = /(\d+)\.(\d+)\.(\d+)/.exec(output);
|
|
21
|
-
if (!match) return undefined;
|
|
22
|
-
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function versionAtLeast(version: readonly number[], minimum: readonly number[]): boolean {
|
|
26
|
-
for (let i = 0; i < minimum.length; i++) {
|
|
27
|
-
const diff = (version[i] ?? 0) - minimum[i];
|
|
28
|
-
if (diff !== 0) return diff > 0;
|
|
29
|
-
}
|
|
30
|
-
return true;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
22
|
export function versionSupportsNoMcp(output: string): boolean {
|
|
34
23
|
const version = parsePiVersion(output);
|
|
35
24
|
return version !== undefined && versionAtLeast(version, NO_MCP_MIN_VERSION);
|
|
@@ -1,57 +1,19 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
createProcessTracker,
|
|
3
|
+
escalateKill,
|
|
4
|
+
mapWithConcurrencyLimit,
|
|
5
|
+
} from "@underactive/pi-topping-primitives/process";
|
|
2
6
|
|
|
3
|
-
|
|
4
|
-
export const trackedProcesses: Set<ChildProcess> = new Set();
|
|
7
|
+
export { escalateKill, mapWithConcurrencyLimit };
|
|
5
8
|
|
|
6
|
-
/**
|
|
7
|
-
export
|
|
8
|
-
if (proc.exitCode !== null || proc.signalCode !== null) return;
|
|
9
|
-
try { proc.kill("SIGTERM"); } catch { return; }
|
|
10
|
-
setTimeout(() => {
|
|
11
|
-
if (proc.exitCode !== null || proc.signalCode !== null) return;
|
|
12
|
-
try { proc.kill("SIGKILL"); } catch { /* ignore */ }
|
|
13
|
-
}, termTimeoutMs).unref();
|
|
14
|
-
}
|
|
9
|
+
/** This extension's spawned processes, so /reload kills only what moa-fusion started. */
|
|
10
|
+
export const trackedProcesses = createProcessTracker();
|
|
15
11
|
|
|
16
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* Kill every tracked process. Safe to call multiple times. The snapshot-then-clear ordering
|
|
14
|
+
* (the delayed SIGKILL pass still reaches the processes that were just signalled) lives in the
|
|
15
|
+
* package's tracker.
|
|
16
|
+
*/
|
|
17
17
|
export function cleanupTrackedProcesses(termTimeoutMs = 5000): void {
|
|
18
|
-
|
|
19
|
-
// reaches the same processes that were just signalled.
|
|
20
|
-
const processes = [...trackedProcesses];
|
|
21
|
-
for (const proc of processes) {
|
|
22
|
-
if (proc.exitCode !== null || proc.signalCode !== null) continue;
|
|
23
|
-
try { proc.kill("SIGTERM"); } catch { /* ignore */ }
|
|
24
|
-
}
|
|
25
|
-
trackedProcesses.clear();
|
|
26
|
-
|
|
27
|
-
// Give SIGTERM a moment, then SIGKILL any process that has not exited.
|
|
28
|
-
setTimeout(() => {
|
|
29
|
-
for (const proc of processes) {
|
|
30
|
-
if (proc.exitCode !== null || proc.signalCode !== null) continue;
|
|
31
|
-
try { proc.kill("SIGKILL"); } catch { /* ignore */ }
|
|
32
|
-
}
|
|
33
|
-
}, termTimeoutMs).unref();
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
export async function mapWithConcurrencyLimit<TIn, TOut>(
|
|
38
|
-
items: TIn[],
|
|
39
|
-
concurrency: number,
|
|
40
|
-
fn: (item: TIn, index: number) => Promise<TOut>,
|
|
41
|
-
onStart?: (index: number) => void,
|
|
42
|
-
): Promise<TOut[]> {
|
|
43
|
-
if (items.length === 0) return [];
|
|
44
|
-
const limit = Math.max(1, Math.min(concurrency, items.length));
|
|
45
|
-
const results: TOut[] = new Array(items.length);
|
|
46
|
-
let nextIndex = 0;
|
|
47
|
-
const workers = new Array(limit).fill(null).map(async () => {
|
|
48
|
-
while (true) {
|
|
49
|
-
const current = nextIndex++;
|
|
50
|
-
if (current >= items.length) return;
|
|
51
|
-
onStart?.(current);
|
|
52
|
-
results[current] = await fn(items[current], current);
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
await Promise.all(workers);
|
|
56
|
-
return results;
|
|
18
|
+
trackedProcesses.cleanup(termTimeoutMs);
|
|
57
19
|
}
|
package/src/runtime/results.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Message } from "@earendil-works/pi-ai";
|
|
2
|
-
import type {
|
|
2
|
+
import type { OutputTokenSnapshot } from "@underactive/pi-topping-primitives/activity";
|
|
3
3
|
|
|
4
4
|
export interface UsageStats {
|
|
5
5
|
input: number;
|
|
@@ -41,7 +41,7 @@ export interface SingleResult {
|
|
|
41
41
|
* Distinct from `usage.output`, which only advances at turn boundaries and
|
|
42
42
|
* stays at 0 for providers that never report usage.
|
|
43
43
|
*/
|
|
44
|
-
outputActivity?:
|
|
44
|
+
outputActivity?: OutputTokenSnapshot;
|
|
45
45
|
/**
|
|
46
46
|
* True when this agent was aborted (per-agent/user cancellation) and the
|
|
47
47
|
* caller opted into `resolveOnAbort`. Cancelled results still count as
|
|
@@ -85,11 +85,21 @@ export function isFailedResult(result: { exitCode: number; stopReason?: string;
|
|
|
85
85
|
);
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Prefixed to a child's output when its final response stopped at the model's
|
|
90
|
+
* output-token limit. Children run in `--mode json`, where pi exits 0 on a
|
|
91
|
+
* `length` stop (only text mode exits 1), so the reply counts as a success and
|
|
92
|
+
* this is the only sign that it was cut off. It leads rather than trails the
|
|
93
|
+
* output so `truncateOutput` cannot cut it away.
|
|
94
|
+
*/
|
|
95
|
+
export const RESPONSE_TRUNCATED_WARNING = "[Response was truncated: the model reached its output token limit.]";
|
|
96
|
+
|
|
88
97
|
export function getResultOutput(result: { exitCode: number; stopReason?: string; errorMessage?: string; stderr?: string; messages: Message[] }): string {
|
|
89
98
|
if (isFailedResult(result)) {
|
|
90
99
|
return result.errorMessage || result.stderr || getFinalOutput(result.messages) || "No agent messages were emitted.";
|
|
91
100
|
}
|
|
92
|
-
|
|
101
|
+
const output = getFinalOutput(result.messages) || "(no output)";
|
|
102
|
+
return result.stopReason === "length" ? `${RESPONSE_TRUNCATED_WARNING}\n\n${output}` : output;
|
|
93
103
|
}
|
|
94
104
|
|
|
95
105
|
/**
|
package/src/runtime/runner.ts
CHANGED
|
@@ -10,10 +10,13 @@ import * as os from "node:os";
|
|
|
10
10
|
import * as path from "node:path";
|
|
11
11
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
12
12
|
import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
13
|
+
import { OutputTokenAccount } from "@underactive/pi-topping-primitives/activity";
|
|
14
|
+
import { formatToolActivity } from "@underactive/pi-topping-primitives/activity-tui";
|
|
15
|
+
import { resolvePiInvocation } from "@underactive/pi-topping-primitives/process";
|
|
13
16
|
import type { AgentConfig } from "../agents/discovery.ts";
|
|
14
17
|
import { normalizeMaxConcurrentAgents } from "../config/settings.ts";
|
|
15
18
|
import type { ThinkingLevel } from "../shared/modelRefs.ts";
|
|
16
|
-
import {
|
|
19
|
+
import { PartialAssistantAssembler } from "./activityTracking.ts";
|
|
17
20
|
import { piSupportsNoMcp, type PiInvocation } from "./piCapabilities.ts";
|
|
18
21
|
import { escalateKill, mapWithConcurrencyLimit, trackedProcesses } from "./processPool.ts";
|
|
19
22
|
import { getFinalOutput, type SingleResult } from "./results.ts";
|
|
@@ -71,22 +74,6 @@ export function getReadOnlyAgentTools(configuredTools?: string[]): string[] {
|
|
|
71
74
|
return configuredTools.filter((tool) => READ_ONLY_AGENT_TOOL_SET.has(tool));
|
|
72
75
|
}
|
|
73
76
|
|
|
74
|
-
function getPiInvocation(args: string[]): PiInvocation {
|
|
75
|
-
const currentScript = process.argv[1];
|
|
76
|
-
const isBunVirtualScript = currentScript?.startsWith("/$bunfs/root/");
|
|
77
|
-
if (currentScript && !isBunVirtualScript && fs.existsSync(currentScript)) {
|
|
78
|
-
return { command: process.execPath, args: [currentScript, ...args] };
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const execName = path.basename(process.execPath).toLowerCase();
|
|
82
|
-
const isGenericRuntime = /^(node|bun)(\.exe)?$/.test(execName);
|
|
83
|
-
if (!isGenericRuntime) {
|
|
84
|
-
return { command: process.execPath, args };
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return { command: "pi", args };
|
|
88
|
-
}
|
|
89
|
-
|
|
90
77
|
export interface RunSingleAgentOptions {
|
|
91
78
|
/**
|
|
92
79
|
* Load all configured pi extensions in the child process. Retained as a
|
|
@@ -201,7 +188,7 @@ export async function runSingleAgent(
|
|
|
201
188
|
return currentResult;
|
|
202
189
|
}
|
|
203
190
|
|
|
204
|
-
const outputActivity = new
|
|
191
|
+
const outputActivity = new OutputTokenAccount();
|
|
205
192
|
const partialMessage = new PartialAssistantAssembler();
|
|
206
193
|
|
|
207
194
|
const emitUpdate = () => {
|
|
@@ -230,7 +217,7 @@ export async function runSingleAgent(
|
|
|
230
217
|
// will run knows the flag. The default and isolated paths already pass
|
|
231
218
|
// --no-extensions, which excludes built-in MCP.
|
|
232
219
|
if (options.loadExtensions && !options.extensionPath) {
|
|
233
|
-
const probe = options.piInvocation ??
|
|
220
|
+
const probe = options.piInvocation ?? resolvePiInvocation([]);
|
|
234
221
|
if (await piSupportsNoMcp(probe)) args.push("--no-mcp");
|
|
235
222
|
}
|
|
236
223
|
|
|
@@ -240,7 +227,7 @@ export async function runSingleAgent(
|
|
|
240
227
|
const exitCode = await new Promise<number>((resolve) => {
|
|
241
228
|
const invocation = options.piInvocation
|
|
242
229
|
? { command: options.piInvocation.command, args: [...options.piInvocation.args, ...args] }
|
|
243
|
-
:
|
|
230
|
+
: resolvePiInvocation(args);
|
|
244
231
|
|
|
245
232
|
const appendDiagnostics = (diagnostics: string) => {
|
|
246
233
|
currentResult.stderr = (currentResult.stderr + diagnostics).slice(-MAX_DIAGNOSTIC_CHARS);
|
package/src/shared/modelRefs.ts
CHANGED
|
@@ -1,42 +1,18 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
export {
|
|
2
|
+
isModelRef,
|
|
3
|
+
isThinkingLevel,
|
|
4
|
+
modelRefLabel,
|
|
5
|
+
parseRef,
|
|
6
|
+
shortModelName,
|
|
7
|
+
THINKING_LEVELS,
|
|
8
|
+
type ModelRef,
|
|
9
|
+
type ThinkingLevel,
|
|
10
|
+
} from "@underactive/pi-topping-primitives/models";
|
|
5
11
|
|
|
6
12
|
export type SendUserMessageOptions = { deliverAs?: "steer" | "followUp"; triggerTurn?: boolean };
|
|
7
13
|
export const TRIGGER_TURN: SendUserMessageOptions = { triggerTurn: true };
|
|
8
14
|
export const FOLLOW_UP: SendUserMessageOptions = { deliverAs: "followUp" };
|
|
9
15
|
|
|
10
|
-
/**
|
|
11
|
-
* pi's thinking-level vocabulary, in pi's canonical order (see
|
|
12
|
-
* `pi.getThinkingLevel()` / `--thinking <level>`). `xhigh` and `max` are
|
|
13
|
-
* distinct levels a model may expose independently, so neither is folded
|
|
14
|
-
* into the other.
|
|
15
|
-
*/
|
|
16
|
-
export const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
|
|
17
|
-
export type ThinkingLevel = (typeof THINKING_LEVELS)[number];
|
|
18
|
-
|
|
19
|
-
export function isThinkingLevel(value: string): value is ThinkingLevel {
|
|
20
|
-
return (THINKING_LEVELS as readonly string[]).includes(value);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function isModelRef(value: unknown): value is ModelRef {
|
|
24
|
-
return !!value
|
|
25
|
-
&& typeof value === "object"
|
|
26
|
-
&& typeof (value as ModelRef).provider === "string"
|
|
27
|
-
&& typeof (value as ModelRef).id === "string";
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function modelRefLabel(ref: ModelRef): string {
|
|
31
|
-
return `${ref.provider}/${ref.id}`;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function parseRef(value: string): ModelRef {
|
|
35
|
-
const idx = value.indexOf("/");
|
|
36
|
-
if (idx === -1) return { provider: value, id: "" };
|
|
37
|
-
return { provider: value.slice(0, idx), id: value.slice(idx + 1) };
|
|
38
|
-
}
|
|
39
|
-
|
|
40
16
|
/**
|
|
41
17
|
* Anonymous slot label substituted for the real `modelRefLabel(ref)` in the
|
|
42
18
|
* synthesizer's input ONLY. Blinding prevents the synthesizer from favoring a
|
|
@@ -52,40 +28,3 @@ export function parseRef(value: string): ModelRef {
|
|
|
52
28
|
export function proposerBlindedLabel(slotIndex: number): string {
|
|
53
29
|
return `Proposer ${slotIndex + 1}`;
|
|
54
30
|
}
|
|
55
|
-
|
|
56
|
-
const MODEL_ATTR_SEGMENTS = new Set([
|
|
57
|
-
"pro", "plus", "max", "ultra", "mini", "flash", "turbo", "coder", "instruct",
|
|
58
|
-
"preview", "lite", "base", "sonnet", "opus", "haiku", "thinking", "reasoning",
|
|
59
|
-
]);
|
|
60
|
-
const CLAUDE_FAMILY_SEGMENTS = new Set(["sonnet", "opus", "haiku"]);
|
|
61
|
-
|
|
62
|
-
function isVersionOrAttrSegment(segment: string): boolean {
|
|
63
|
-
const seg = segment.toLowerCase();
|
|
64
|
-
if (MODEL_ATTR_SEGMENTS.has(seg)) return true;
|
|
65
|
-
if (/^v\d/.test(seg)) return true;
|
|
66
|
-
if (/^k\d/.test(seg)) return true;
|
|
67
|
-
if (/^[a-z]\d+(\.\d+)*$/i.test(seg)) return true;
|
|
68
|
-
if (/^\d+(\.\d+)*$/.test(seg)) return true;
|
|
69
|
-
return false;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function modelIdBase(ref: ModelRef): string {
|
|
73
|
-
const slashIdx = ref.id.lastIndexOf("/");
|
|
74
|
-
return slashIdx >= 0 ? ref.id.slice(slashIdx + 1) : ref.id;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** Short display name for MoA overlay (e.g. "claude-opus-4-6" → "opus", "deepseek-v4-pro" → "deepseek"). */
|
|
78
|
-
export function shortModelName(ref: ModelRef): string {
|
|
79
|
-
const base = modelIdBase(ref);
|
|
80
|
-
const segments = base.split("-").filter(Boolean);
|
|
81
|
-
// Claude's family is its useful compact identifier; "claude" only names
|
|
82
|
-
// the product and does not distinguish Opus, Sonnet, and Haiku.
|
|
83
|
-
if (segments[0]?.toLowerCase() === "claude") {
|
|
84
|
-
const family = segments.find((segment) => CLAUDE_FAMILY_SEGMENTS.has(segment.toLowerCase()));
|
|
85
|
-
if (family) return family.toLowerCase();
|
|
86
|
-
}
|
|
87
|
-
const kept = segments.filter((s) => !isVersionOrAttrSegment(s));
|
|
88
|
-
let name = kept.length > 0 ? kept.join("-") : base;
|
|
89
|
-
name = name.replace(/\d+(\.\d+)*$/, "");
|
|
90
|
-
return name.toLowerCase() || base;
|
|
91
|
-
}
|
package/src/ui/agentStatus.ts
CHANGED
|
@@ -71,14 +71,8 @@ export function contextPercent(contextTokens: number | undefined, contextWindow:
|
|
|
71
71
|
return `${(ratio * 100).toFixed(1)}%/${formatWindowLabel(contextWindow)}`.padStart(CTX_COL_WIDTH);
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
/**
|
|
75
|
-
export
|
|
76
|
-
if (costUsd === undefined || !Number.isFinite(costUsd)) return "—";
|
|
77
|
-
return `$${Math.max(0, costUsd).toFixed(3)}`;
|
|
78
|
-
}
|
|
74
|
+
/** Cumulative model cost in the same precision as pi's usage footer; `—` when unknown. Shared from the primitives package. */
|
|
75
|
+
export { formatCostUsd as formatCost } from "@underactive/pi-topping-primitives/text";
|
|
79
76
|
|
|
80
77
|
/** Elapsed time as `M:SS`. Minutes keep counting past 60 rather than rolling into an hours field. */
|
|
81
|
-
export
|
|
82
|
-
const totalSeconds = Math.max(0, Math.floor(ms / 1000));
|
|
83
|
-
return `${Math.floor(totalSeconds / 60)}:${String(totalSeconds % 60).padStart(2, "0")}`;
|
|
84
|
-
}
|
|
78
|
+
export { formatElapsedClock as formatElapsed } from "@underactive/pi-topping-primitives/activity";
|
package/src/ui/cancelOverlay.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import type { ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
17
17
|
import { Key, matchesKey, type Component, type TUI } from "@earendil-works/pi-tui";
|
|
18
|
+
import { highlightActivity } from "@underactive/pi-topping-primitives/activity-tui";
|
|
18
19
|
import type { CancelSession } from "../runtime/cancelRun.ts";
|
|
19
20
|
import { LOOP_THRESHOLD, usageBar } from "./agentStatus.ts";
|
|
20
21
|
import {
|
|
@@ -24,7 +25,6 @@ import {
|
|
|
24
25
|
UI_TICK_MS,
|
|
25
26
|
createFrame,
|
|
26
27
|
} from "./chrome.ts";
|
|
27
|
-
import { highlightActivity } from "./toolActivity.ts";
|
|
28
28
|
|
|
29
29
|
const OVERLAY_MARGIN = 1;
|
|
30
30
|
|
package/src/ui/chrome.ts
CHANGED
|
@@ -3,8 +3,7 @@ import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
|
3
3
|
|
|
4
4
|
export const UI_TICK_MS = 100;
|
|
5
5
|
|
|
6
|
-
export
|
|
7
|
-
export const QUADRANT_SPINNER_FRAMES = ["◐", "◓", "◑", "◒"] as const;
|
|
6
|
+
export { SPINNER_FRAMES as BRAILLE_SPINNER_FRAMES, QUADRANT_SPINNER_FRAMES } from "@underactive/pi-topping-primitives/glyphs";
|
|
8
7
|
|
|
9
8
|
export interface BoxGlyphs {
|
|
10
9
|
topLeft: string;
|
|
@@ -10,8 +10,8 @@ import {
|
|
|
10
10
|
ratioViewport,
|
|
11
11
|
} from "./chrome.ts";
|
|
12
12
|
import { getAvailableModelRefs } from "./moaModelPicker.ts";
|
|
13
|
-
import { smartTruncateModelLabel } from "
|
|
14
|
-
import { TwoPaneModelThinking } from "./twoPaneModelThinking.ts";
|
|
13
|
+
import { smartTruncateModelLabel } from "@underactive/pi-topping-primitives/models";
|
|
14
|
+
import { createMoaModelPicker, type TwoPaneModelThinking } from "./twoPaneModelThinking.ts";
|
|
15
15
|
|
|
16
16
|
const HINT_BASE = "type filters models • ↑↓ navigate pane • tab panes/buttons • ←→ switch/select • enter select";
|
|
17
17
|
|
|
@@ -60,7 +60,7 @@ export class DebateModelPickerComponent implements Component {
|
|
|
60
60
|
this.defaults = defaults;
|
|
61
61
|
this.done = done;
|
|
62
62
|
this.rounds = Math.min(MAX_DEBATE_ROUNDS, Math.max(MIN_DEBATE_ROUNDS, config.debateRounds));
|
|
63
|
-
this.twoPane =
|
|
63
|
+
this.twoPane = createMoaModelPicker(tui, theme, availableRefs, config, currentThinking, ctx);
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
private assignedCount(): number {
|
package/src/ui/moaModelPicker.ts
CHANGED
|
@@ -26,9 +26,9 @@
|
|
|
26
26
|
* while that slot is still empty — they never count as assignments, so
|
|
27
27
|
* readiness always reflects explicit selections.
|
|
28
28
|
*
|
|
29
|
-
* Models and thinking levels both come from the
|
|
30
|
-
*
|
|
31
|
-
*
|
|
29
|
+
* Models and thinking levels both come from the shared registry catalogue,
|
|
30
|
+
* so a confirmed selection is usable by construction and nothing verifies
|
|
31
|
+
* it after the picker closes. Whether a model actually answers
|
|
32
32
|
* is decided when it runs.
|
|
33
33
|
*/
|
|
34
34
|
|
|
@@ -43,10 +43,9 @@ import {
|
|
|
43
43
|
wrapWords,
|
|
44
44
|
} from "./chrome.ts";
|
|
45
45
|
import { PLAN_OVERLAY_OPTIONS } from "./menu.ts";
|
|
46
|
-
import { TwoPaneModelThinking } from "./twoPaneModelThinking.ts";
|
|
47
|
-
import { smartTruncateModelLabel } from "
|
|
46
|
+
import { createMoaModelPicker, type TwoPaneModelThinking } from "./twoPaneModelThinking.ts";
|
|
47
|
+
import { getModelCatalogue, smartTruncateModelLabel } from "@underactive/pi-topping-primitives/models";
|
|
48
48
|
import { proposerDiversityWarning } from "../moa/panelDependence.ts";
|
|
49
|
-
import { getModelCatalogue } from "../config/modelCatalogue.ts";
|
|
50
49
|
import {
|
|
51
50
|
loadMoaConfig,
|
|
52
51
|
moaSettingsExist,
|
|
@@ -171,7 +170,7 @@ class MoaModelPickerComponent implements Component {
|
|
|
171
170
|
].filter((slot) => this.availableLabels.has(modelRefLabel(slot.ref))).length,
|
|
172
171
|
}))
|
|
173
172
|
.sort((a, b) => a.roster.name.localeCompare(b.roster.name));
|
|
174
|
-
this.twoPane =
|
|
173
|
+
this.twoPane = createMoaModelPicker(tui, theme, availableRefs, config, currentThinking, ctx);
|
|
175
174
|
}
|
|
176
175
|
|
|
177
176
|
/** The committed model/thinking pair for a slot picker screen, if assigned. */
|
|
@@ -811,7 +810,7 @@ class ImplementingModelPickerComponent implements Component {
|
|
|
811
810
|
private readonly title: string,
|
|
812
811
|
private readonly done: (result: { ref: ModelRef; thinking: ThinkingLevel } | undefined) => void,
|
|
813
812
|
) {
|
|
814
|
-
this.twoPane =
|
|
813
|
+
this.twoPane = createMoaModelPicker(tui, theme, availableRefs, config, currentThinking, ctx);
|
|
815
814
|
this.twoPane.reset(defaultRef, defaultThinking);
|
|
816
815
|
}
|
|
817
816
|
|