@herbertgao/sol-pi 0.1.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/LICENSE +19 -0
- package/README.md +159 -0
- package/SECURITY.md +26 -0
- package/THIRD_PARTY_NOTICES.md +19 -0
- package/agents-install.md +150 -0
- package/assets/sol-pi-hero.png +0 -0
- package/docs/compatibility.md +67 -0
- package/docs/configuration.md +75 -0
- package/package.json +76 -0
- package/scripts/check-pi-compat.mjs +32 -0
- package/scripts/check-sol-pi-config.mjs +120 -0
- package/sol-pi.example.json +10 -0
- package/src/sol-pi/config.ts +135 -0
- package/src/sol-pi/extensions/action-fusion/file-queue.ts +71 -0
- package/src/sol-pi/extensions/action-fusion/index.ts +185 -0
- package/src/sol-pi/extensions/action-fusion/then-run.ts +128 -0
- package/src/sol-pi/extensions/evidence-preserving-reducer/archive.ts +53 -0
- package/src/sol-pi/extensions/evidence-preserving-reducer/candidate.ts +101 -0
- package/src/sol-pi/extensions/evidence-preserving-reducer/config.ts +71 -0
- package/src/sol-pi/extensions/evidence-preserving-reducer/index.ts +220 -0
- package/src/sol-pi/extensions/evidence-preserving-reducer/journal.ts +25 -0
- package/src/sol-pi/extensions/evidence-preserving-reducer/provider.ts +164 -0
- package/src/sol-pi/extensions/evidence-preserving-reducer/receipt.ts +177 -0
- package/src/sol-pi/extensions/observation-pack/index.ts +227 -0
- package/src/sol-pi/extensions/observation-pack/ledger.ts +20 -0
- package/src/sol-pi/extensions/observation-pack/observation.ts +252 -0
- package/src/sol-pi/extensions/online-context-compact/economics.ts +237 -0
- package/src/sol-pi/extensions/online-context-compact/extension.ts +455 -0
- package/src/sol-pi/extensions/online-context-compact/index.ts +49 -0
- package/src/sol-pi/extensions/online-context-compact/plan.ts +79 -0
- package/src/sol-pi/extensions/online-context-compact/state.ts +208 -0
- package/src/sol-pi/extensions/online-context-compact/tools.ts +100 -0
- package/src/sol-pi/index.ts +42 -0
- package/src/sol-pi/runtime-paths.ts +17 -0
- package/src/sol-pi/tui.ts +71 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { DEFAULT_CACHE_WRITE_READ_RATIO } from "../../config.ts";
|
|
7
|
+
import { createOnlineContextCompactExtension } from "./extension.ts";
|
|
8
|
+
|
|
9
|
+
export {
|
|
10
|
+
DEFAULT_COMPACTION_ECONOMICS,
|
|
11
|
+
decideCompaction,
|
|
12
|
+
estimateRemainingRequests,
|
|
13
|
+
type CompactionDecision,
|
|
14
|
+
type CompactionEconomics,
|
|
15
|
+
type CompactionReason,
|
|
16
|
+
} from "./economics.ts";
|
|
17
|
+
export {
|
|
18
|
+
BOUNDARY_COMPACTION_INSTRUCTIONS,
|
|
19
|
+
createOnlineContextCompactExtension,
|
|
20
|
+
DEFAULT_KEEP_RECENT_TOKENS,
|
|
21
|
+
DEFAULT_NATIVE_SUMMARY_TOKEN_ESTIMATE,
|
|
22
|
+
POST_COMPACTION_PLAN_REMINDER,
|
|
23
|
+
type OnlineContextCompactOptions,
|
|
24
|
+
resolveKeepRecentTokens,
|
|
25
|
+
} from "./extension.ts";
|
|
26
|
+
export {
|
|
27
|
+
analyzePlanTransition,
|
|
28
|
+
formatPlanSnapshot,
|
|
29
|
+
parsePlanSteps,
|
|
30
|
+
type PlanStatus,
|
|
31
|
+
type PlanStep,
|
|
32
|
+
} from "./plan.ts";
|
|
33
|
+
export {
|
|
34
|
+
initialOnlineState,
|
|
35
|
+
ONLINE_STATE_ENTRY,
|
|
36
|
+
restoreOnlineState,
|
|
37
|
+
type OnlineState,
|
|
38
|
+
type ProgressSummary,
|
|
39
|
+
} from "./state.ts";
|
|
40
|
+
export type { PlanProgress, PlanUpdateInput } from "./tools.ts";
|
|
41
|
+
|
|
42
|
+
export function registerOnlineContextCompact(
|
|
43
|
+
pi: ExtensionAPI,
|
|
44
|
+
cacheWriteReadRatio = DEFAULT_CACHE_WRITE_READ_RATIO,
|
|
45
|
+
): void {
|
|
46
|
+
createOnlineContextCompactExtension({ cacheWriteReadRatio })(pi);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export default registerOnlineContextCompact;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const PLAN_STATUSES = ["pending", "in_progress", "completed"] as const;
|
|
7
|
+
|
|
8
|
+
export type PlanStatus = (typeof PLAN_STATUSES)[number];
|
|
9
|
+
|
|
10
|
+
export type PlanStep = {
|
|
11
|
+
readonly id: string;
|
|
12
|
+
readonly goal: string;
|
|
13
|
+
readonly status: PlanStatus;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type PlanTransition = {
|
|
17
|
+
readonly completedSteps: readonly PlanStep[];
|
|
18
|
+
readonly advice: readonly string[];
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const MAX_PLAN_STEPS = 128;
|
|
22
|
+
const MAX_PLAN_STRING_BYTES = 16_384;
|
|
23
|
+
|
|
24
|
+
function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {
|
|
25
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isBoundedString(value: unknown): value is string {
|
|
29
|
+
return typeof value === "string" && value.length > 0 && Buffer.byteLength(value) <= MAX_PLAN_STRING_BYTES;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isPlanStatus(value: unknown): value is PlanStatus {
|
|
33
|
+
return PLAN_STATUSES.some((status) => status === value);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function parsePlanSteps(value: unknown): readonly PlanStep[] | undefined {
|
|
37
|
+
if (!Array.isArray(value) || value.length > MAX_PLAN_STEPS) return;
|
|
38
|
+
const steps: PlanStep[] = [];
|
|
39
|
+
for (const item of value) {
|
|
40
|
+
if (
|
|
41
|
+
!isRecord(item) ||
|
|
42
|
+
Object.keys(item).length !== 3 ||
|
|
43
|
+
!isBoundedString(item.id) ||
|
|
44
|
+
!isBoundedString(item.goal) ||
|
|
45
|
+
!isPlanStatus(item.status)
|
|
46
|
+
) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
steps.push({ id: item.id, goal: item.goal, status: item.status });
|
|
50
|
+
}
|
|
51
|
+
if (new Set(steps.map((step) => step.id)).size !== steps.length) return;
|
|
52
|
+
return steps;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function analyzePlanTransition(previous: readonly PlanStep[], next: readonly PlanStep[]): PlanTransition {
|
|
56
|
+
const previousById = new Map(previous.map((step) => [step.id, step]));
|
|
57
|
+
const completedSteps: PlanStep[] = [];
|
|
58
|
+
const advice: string[] = [];
|
|
59
|
+
|
|
60
|
+
for (const step of next) {
|
|
61
|
+
const prior = previousById.get(step.id);
|
|
62
|
+
if ((!prior || prior.status !== "completed") && step.status === "completed") completedSteps.push(step);
|
|
63
|
+
if (prior && prior.goal !== step.goal) {
|
|
64
|
+
advice.push(`Plan step ${JSON.stringify(step.id)} changed goal; reuse an id only for the same goal.`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const inProgress = next.filter((step) => step.status === "in_progress").length;
|
|
69
|
+
if (inProgress > 1) advice.push("Keep at most one plan step in_progress.");
|
|
70
|
+
if (inProgress === 0 && next.some((step) => step.status === "pending")) {
|
|
71
|
+
advice.push("Mark one pending plan step in_progress before starting it.");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return { completedSteps, advice };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function formatPlanSnapshot(steps: readonly PlanStep[]): string {
|
|
78
|
+
return `<sol-pi-plan task_status="active">${JSON.stringify({ steps })}</sol-pi-plan>`;
|
|
79
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
import type { ExtensionAPI, SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { parsePlanSteps, type PlanStep } from "./plan.ts";
|
|
7
|
+
|
|
8
|
+
export const ONLINE_STATE_ENTRY = "sol-pi-online-context-state-v1";
|
|
9
|
+
|
|
10
|
+
export type ProgressSummary = {
|
|
11
|
+
readonly stepId: string;
|
|
12
|
+
readonly goal: string;
|
|
13
|
+
readonly filesChanged: readonly string[];
|
|
14
|
+
readonly verification: readonly string[];
|
|
15
|
+
readonly decisions: readonly string[];
|
|
16
|
+
readonly nextWork: readonly string[];
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type OnlineState = {
|
|
20
|
+
readonly version: 1;
|
|
21
|
+
readonly epoch: number;
|
|
22
|
+
readonly plan: readonly PlanStep[];
|
|
23
|
+
readonly pendingProgress: readonly ProgressSummary[];
|
|
24
|
+
readonly requestCount: number;
|
|
25
|
+
readonly lastBoundaryRequestCount: number;
|
|
26
|
+
readonly completedBoundaryRequestCounts: readonly number[];
|
|
27
|
+
readonly lastContextTokens: number | null;
|
|
28
|
+
readonly positiveContextDeltaTotal: number;
|
|
29
|
+
readonly positiveContextDeltaCount: number;
|
|
30
|
+
readonly nativeCompactionCount: number;
|
|
31
|
+
readonly cacheDebtTokens: number;
|
|
32
|
+
readonly cacheDebtRepaymentTokens: number;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export function initialOnlineState(): OnlineState {
|
|
36
|
+
return {
|
|
37
|
+
version: 1,
|
|
38
|
+
epoch: 0,
|
|
39
|
+
plan: [],
|
|
40
|
+
pendingProgress: [],
|
|
41
|
+
requestCount: 0,
|
|
42
|
+
lastBoundaryRequestCount: 0,
|
|
43
|
+
completedBoundaryRequestCounts: [],
|
|
44
|
+
lastContextTokens: null,
|
|
45
|
+
positiveContextDeltaTotal: 0,
|
|
46
|
+
positiveContextDeltaCount: 0,
|
|
47
|
+
nativeCompactionCount: 0,
|
|
48
|
+
cacheDebtTokens: 0,
|
|
49
|
+
cacheDebtRepaymentTokens: 0,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function nonNegativeInteger(value: unknown): value is number {
|
|
54
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function finiteNonNegative(value: unknown): value is number {
|
|
58
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function stringArray(value: unknown): readonly string[] | undefined {
|
|
62
|
+
if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) return;
|
|
63
|
+
return [...value];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function progressSummary(value: unknown): ProgressSummary | undefined {
|
|
67
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return;
|
|
68
|
+
const record = value as Record<string, unknown>;
|
|
69
|
+
const filesChanged = stringArray(record.filesChanged);
|
|
70
|
+
const verification = stringArray(record.verification);
|
|
71
|
+
const decisions = stringArray(record.decisions);
|
|
72
|
+
const nextWork = stringArray(record.nextWork);
|
|
73
|
+
if (
|
|
74
|
+
typeof record.stepId !== "string" ||
|
|
75
|
+
typeof record.goal !== "string" ||
|
|
76
|
+
!filesChanged ||
|
|
77
|
+
!verification ||
|
|
78
|
+
!decisions ||
|
|
79
|
+
!nextWork
|
|
80
|
+
) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
return { stepId: record.stepId, goal: record.goal, filesChanged, verification, decisions, nextWork };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function parseOnlineState(value: unknown): OnlineState | undefined {
|
|
87
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return;
|
|
88
|
+
const record = value as Record<string, unknown>;
|
|
89
|
+
const plan = parsePlanSteps(record.plan);
|
|
90
|
+
const pendingProgress = Array.isArray(record.pendingProgress)
|
|
91
|
+
? record.pendingProgress.map(progressSummary)
|
|
92
|
+
: undefined;
|
|
93
|
+
const completedBoundaryRequestCounts = Array.isArray(record.completedBoundaryRequestCounts)
|
|
94
|
+
? record.completedBoundaryRequestCounts
|
|
95
|
+
: undefined;
|
|
96
|
+
if (
|
|
97
|
+
record.version !== 1 ||
|
|
98
|
+
!plan ||
|
|
99
|
+
!pendingProgress ||
|
|
100
|
+
pendingProgress.some((item) => item === undefined) ||
|
|
101
|
+
!completedBoundaryRequestCounts ||
|
|
102
|
+
!completedBoundaryRequestCounts.every(nonNegativeInteger) ||
|
|
103
|
+
!nonNegativeInteger(record.epoch) ||
|
|
104
|
+
!nonNegativeInteger(record.requestCount) ||
|
|
105
|
+
!nonNegativeInteger(record.lastBoundaryRequestCount) ||
|
|
106
|
+
record.lastBoundaryRequestCount > record.requestCount ||
|
|
107
|
+
!(record.lastContextTokens === null || nonNegativeInteger(record.lastContextTokens)) ||
|
|
108
|
+
!finiteNonNegative(record.positiveContextDeltaTotal) ||
|
|
109
|
+
!nonNegativeInteger(record.positiveContextDeltaCount) ||
|
|
110
|
+
!nonNegativeInteger(record.nativeCompactionCount) ||
|
|
111
|
+
!finiteNonNegative(record.cacheDebtTokens) ||
|
|
112
|
+
!finiteNonNegative(record.cacheDebtRepaymentTokens)
|
|
113
|
+
) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
version: 1,
|
|
118
|
+
epoch: record.epoch,
|
|
119
|
+
plan,
|
|
120
|
+
pendingProgress: pendingProgress as readonly ProgressSummary[],
|
|
121
|
+
requestCount: record.requestCount,
|
|
122
|
+
lastBoundaryRequestCount: record.lastBoundaryRequestCount,
|
|
123
|
+
completedBoundaryRequestCounts: completedBoundaryRequestCounts as readonly number[],
|
|
124
|
+
lastContextTokens: record.lastContextTokens,
|
|
125
|
+
positiveContextDeltaTotal: record.positiveContextDeltaTotal,
|
|
126
|
+
positiveContextDeltaCount: record.positiveContextDeltaCount,
|
|
127
|
+
nativeCompactionCount: record.nativeCompactionCount,
|
|
128
|
+
cacheDebtTokens: record.cacheDebtTokens,
|
|
129
|
+
cacheDebtRepaymentTokens: record.cacheDebtRepaymentTokens,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function restoreOnlineState(entries: readonly SessionEntry[]): OnlineState {
|
|
134
|
+
for (let index = entries.length - 1; index >= 0; index--) {
|
|
135
|
+
const entry = entries[index];
|
|
136
|
+
if (entry?.type !== "custom" || entry.customType !== ONLINE_STATE_ENTRY) continue;
|
|
137
|
+
const state = parseOnlineState(entry.data);
|
|
138
|
+
if (state) return state;
|
|
139
|
+
}
|
|
140
|
+
return initialOnlineState();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function appendOnlineState(pi: ExtensionAPI, state: OnlineState): void {
|
|
144
|
+
pi.appendEntry(ONLINE_STATE_ENTRY, state);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function recordProviderRequest(state: OnlineState, contextTokens: number): OnlineState {
|
|
148
|
+
const delta = state.lastContextTokens === null ? 0 : contextTokens - state.lastContextTokens;
|
|
149
|
+
const cacheDebtTokens = Math.max(0, state.cacheDebtTokens - state.cacheDebtRepaymentTokens);
|
|
150
|
+
return {
|
|
151
|
+
...state,
|
|
152
|
+
requestCount: state.requestCount + 1,
|
|
153
|
+
lastContextTokens: contextTokens,
|
|
154
|
+
positiveContextDeltaTotal: state.positiveContextDeltaTotal + Math.max(0, delta),
|
|
155
|
+
positiveContextDeltaCount: state.positiveContextDeltaCount + (delta > 0 ? 1 : 0),
|
|
156
|
+
cacheDebtTokens,
|
|
157
|
+
cacheDebtRepaymentTokens: cacheDebtTokens === 0 ? 0 : state.cacheDebtRepaymentTokens,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function recordBoundary(
|
|
162
|
+
state: OnlineState,
|
|
163
|
+
plan: readonly PlanStep[],
|
|
164
|
+
progress: ProgressSummary | undefined,
|
|
165
|
+
): OnlineState {
|
|
166
|
+
const interval = Math.max(0, state.requestCount - state.lastBoundaryRequestCount);
|
|
167
|
+
return {
|
|
168
|
+
...state,
|
|
169
|
+
plan: [...plan],
|
|
170
|
+
pendingProgress: progress ? [...state.pendingProgress, progress] : state.pendingProgress,
|
|
171
|
+
lastBoundaryRequestCount: state.requestCount,
|
|
172
|
+
completedBoundaryRequestCounts: [...state.completedBoundaryRequestCounts, interval],
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function recordCompaction(
|
|
177
|
+
state: OnlineState,
|
|
178
|
+
debt: { readonly debtTokens: number; readonly repaymentTokens: number },
|
|
179
|
+
): OnlineState {
|
|
180
|
+
return {
|
|
181
|
+
...state,
|
|
182
|
+
epoch: state.epoch + 1,
|
|
183
|
+
plan: [],
|
|
184
|
+
pendingProgress: [],
|
|
185
|
+
lastContextTokens: null,
|
|
186
|
+
positiveContextDeltaTotal: 0,
|
|
187
|
+
positiveContextDeltaCount: 0,
|
|
188
|
+
nativeCompactionCount: state.nativeCompactionCount + 1,
|
|
189
|
+
cacheDebtTokens: Math.max(0, debt.debtTokens),
|
|
190
|
+
cacheDebtRepaymentTokens: Math.max(0, debt.repaymentTokens),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function recordCorrection(state: OnlineState): OnlineState {
|
|
195
|
+
return {
|
|
196
|
+
...state,
|
|
197
|
+
epoch: state.epoch + 1,
|
|
198
|
+
plan: [],
|
|
199
|
+
pendingProgress: [],
|
|
200
|
+
lastBoundaryRequestCount: state.requestCount,
|
|
201
|
+
completedBoundaryRequestCounts: [],
|
|
202
|
+
lastContextTokens: null,
|
|
203
|
+
positiveContextDeltaTotal: 0,
|
|
204
|
+
positiveContextDeltaCount: 0,
|
|
205
|
+
cacheDebtTokens: 0,
|
|
206
|
+
cacheDebtRepaymentTokens: 0,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
6
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
8
|
+
import { Type } from "typebox";
|
|
9
|
+
import { renderSolPiTool } from "../../tui.ts";
|
|
10
|
+
import { PLAN_STATUSES, type PlanStep } from "./plan.ts";
|
|
11
|
+
|
|
12
|
+
export type PlanProgress = {
|
|
13
|
+
readonly files_changed: readonly string[];
|
|
14
|
+
readonly verification: readonly string[];
|
|
15
|
+
readonly decisions: readonly string[];
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type PlanUpdateInput = {
|
|
19
|
+
readonly toolCallId: string;
|
|
20
|
+
readonly steps: readonly PlanStep[];
|
|
21
|
+
readonly progress: PlanProgress | undefined;
|
|
22
|
+
readonly signal: AbortSignal | undefined;
|
|
23
|
+
readonly context: ExtensionContext;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type OnlineToolHandlers = {
|
|
27
|
+
readonly updatePlan: (input: PlanUpdateInput) => Promise<AgentToolResult<Readonly<Record<string, unknown>>>>;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const progressSchema = Type.Object(
|
|
31
|
+
{
|
|
32
|
+
files_changed: Type.Array(Type.String({ maxLength: 1000 }), { maxItems: 128 }),
|
|
33
|
+
verification: Type.Array(Type.String({ maxLength: 1000 }), { maxItems: 64 }),
|
|
34
|
+
decisions: Type.Array(Type.String({ maxLength: 1000 }), { maxItems: 64 }),
|
|
35
|
+
},
|
|
36
|
+
{ additionalProperties: false },
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const planStepSchema = Type.Object(
|
|
40
|
+
{
|
|
41
|
+
id: Type.String({ minLength: 1, maxLength: 16_384 }),
|
|
42
|
+
goal: Type.String({ minLength: 1, maxLength: 16_384 }),
|
|
43
|
+
status: Type.Union(PLAN_STATUSES.map((status) => Type.Literal(status))),
|
|
44
|
+
},
|
|
45
|
+
{ additionalProperties: false },
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
export function registerOnlineTools(pi: ExtensionAPI, handlers: OnlineToolHandlers): void {
|
|
49
|
+
pi.registerTool({
|
|
50
|
+
name: "update_plan",
|
|
51
|
+
label: "Update plan",
|
|
52
|
+
description:
|
|
53
|
+
"Replace the complete working plan. A newly completed step becomes a safe point where SoL-Pi may compact context if doing so is economical.",
|
|
54
|
+
promptSnippet: "Keep the working plan current",
|
|
55
|
+
promptGuidelines: [
|
|
56
|
+
"Send the complete plan on every update_plan call.",
|
|
57
|
+
"Keep at most one step in_progress and mark finished steps completed.",
|
|
58
|
+
"When completing a step, include concise progress evidence when available.",
|
|
59
|
+
],
|
|
60
|
+
renderShell: "self",
|
|
61
|
+
parameters: Type.Object(
|
|
62
|
+
{
|
|
63
|
+
steps: Type.Array(planStepSchema, { minItems: 1, maxItems: 128 }),
|
|
64
|
+
progress: Type.Optional(progressSchema),
|
|
65
|
+
},
|
|
66
|
+
{ additionalProperties: false },
|
|
67
|
+
),
|
|
68
|
+
executionMode: "sequential",
|
|
69
|
+
execute: async (toolCallId, params, signal, _onUpdate, context) =>
|
|
70
|
+
await handlers.updatePlan({
|
|
71
|
+
toolCallId,
|
|
72
|
+
steps: params.steps,
|
|
73
|
+
progress: params.progress,
|
|
74
|
+
signal,
|
|
75
|
+
context,
|
|
76
|
+
}),
|
|
77
|
+
renderCall(params, theme) {
|
|
78
|
+
const completed = params.steps.filter((step) => step.status === "completed").length;
|
|
79
|
+
return renderSolPiTool(
|
|
80
|
+
theme,
|
|
81
|
+
"Online Context Compact",
|
|
82
|
+
"compacts only when projected savings are positive",
|
|
83
|
+
new Text(theme.fg("dim", `Plan: ${params.steps.length} steps, ${completed} completed`), 0, 0),
|
|
84
|
+
);
|
|
85
|
+
},
|
|
86
|
+
renderResult(result, { isPartial }, theme) {
|
|
87
|
+
const boundary = (result.details as { boundary?: boolean } | undefined)?.boundary === true;
|
|
88
|
+
return renderSolPiTool(
|
|
89
|
+
theme,
|
|
90
|
+
"Online Context Compact",
|
|
91
|
+
"compacts only when projected savings are positive",
|
|
92
|
+
new Text(
|
|
93
|
+
theme.fg(isPartial ? "warning" : "dim", isPartial ? "Updating plan..." : boundary ? "Progress boundary recorded" : "Plan recorded"),
|
|
94
|
+
0,
|
|
95
|
+
0,
|
|
96
|
+
),
|
|
97
|
+
);
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { getAgentDir, type ExtensionAPI, type ExtensionContext, type ExtensionFactory } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import { loadSolPiConfig, type SolPiConfig } from "./config.ts";
|
|
8
|
+
import { registerActionFusion } from "./extensions/action-fusion/index.ts";
|
|
9
|
+
import { registerEvidencePreservingReducer } from "./extensions/evidence-preserving-reducer/index.ts";
|
|
10
|
+
import { registerObservationPack } from "./extensions/observation-pack/index.ts";
|
|
11
|
+
import { registerOnlineContextCompact } from "./extensions/online-context-compact/index.ts";
|
|
12
|
+
|
|
13
|
+
export function registerConfiguredFeatures(pi: ExtensionAPI, config: SolPiConfig): void {
|
|
14
|
+
if (config.actionFusion) registerActionFusion(pi);
|
|
15
|
+
if (config.observationPack) registerObservationPack(pi);
|
|
16
|
+
if (config.evidencePreservingReducer) {
|
|
17
|
+
registerEvidencePreservingReducer(pi, {
|
|
18
|
+
reducerModel: config.evidencePreservingReducerModel,
|
|
19
|
+
reducerProvider: config.evidencePreservingReducerProvider,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
if (config.onlineContextCompact) registerOnlineContextCompact(pi, config.cacheWriteReadRatio);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type SolPiConfigLoader = (ctx: ExtensionContext) => SolPiConfig;
|
|
26
|
+
|
|
27
|
+
export function createSolPiExtension(
|
|
28
|
+
loadConfig: SolPiConfigLoader = (ctx) => loadSolPiConfig(ctx.cwd, getAgentDir(), ctx.isProjectTrusted()),
|
|
29
|
+
): ExtensionFactory {
|
|
30
|
+
return (pi) => {
|
|
31
|
+
let initialized = false;
|
|
32
|
+
pi.on("session_start", (_event, ctx) => {
|
|
33
|
+
if (initialized) return;
|
|
34
|
+
initialized = true;
|
|
35
|
+
registerConfiguredFeatures(pi, loadConfig(ctx));
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export default function solPiExtension(pi: ExtensionAPI): void {
|
|
41
|
+
createSolPiExtension()(pi);
|
|
42
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
|
|
9
|
+
export function runtimeRoot(ctx: ExtensionContext): string {
|
|
10
|
+
const sessionDir = ctx.sessionManager.getSessionDir();
|
|
11
|
+
if (!sessionDir) throw new Error("SoL-Pi requires a persistent Pi session directory");
|
|
12
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
13
|
+
if (!/^[a-z0-9][a-z0-9._-]*$/iu.test(sessionId)) {
|
|
14
|
+
throw new Error("SoL-Pi requires a safe Pi session id");
|
|
15
|
+
}
|
|
16
|
+
return join(sessionDir, "sol-pi", sessionId);
|
|
17
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import { type Component, Container, Text } from "@earendil-works/pi-tui";
|
|
8
|
+
|
|
9
|
+
export type SolPiTuiMechanism =
|
|
10
|
+
| "Action Fusion"
|
|
11
|
+
| "Observation Pack"
|
|
12
|
+
| "Luna Delegating"
|
|
13
|
+
| "Online Context Compact";
|
|
14
|
+
|
|
15
|
+
const STATUS_KEY = "sol-pi-savings";
|
|
16
|
+
const STATUS_DURATION_MS = 4_000;
|
|
17
|
+
const INTEGER_FORMAT = new Intl.NumberFormat("en-US", { maximumFractionDigits: 0 });
|
|
18
|
+
const statusTimers = new WeakMap<ExtensionContext["ui"], ReturnType<typeof setTimeout>>();
|
|
19
|
+
|
|
20
|
+
export function formatSavingsCount(value: number, unit: string): string {
|
|
21
|
+
const count = Number.isFinite(value) ? Math.max(0, Math.round(value)) : 0;
|
|
22
|
+
return `${INTEGER_FORMAT.format(count)} ${unit}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function compactDecimal(value: number): string {
|
|
26
|
+
return value.toFixed(1).replace(/\.0$/u, "");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function formatSavingsBytes(value: number): string {
|
|
30
|
+
const bytes = Number.isFinite(value) ? Math.max(0, Math.round(value)) : 0;
|
|
31
|
+
if (bytes >= 1024 * 1024) {
|
|
32
|
+
return `${compactDecimal(bytes / (1024 * 1024))} MiB removed from future prompts`;
|
|
33
|
+
}
|
|
34
|
+
if (bytes >= 1024) return `${compactDecimal(bytes / 1024)} KiB removed from future prompts`;
|
|
35
|
+
return `${INTEGER_FORMAT.format(bytes)} B removed from future prompts`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function renderSolPiTool(
|
|
39
|
+
theme: Theme,
|
|
40
|
+
mechanism: SolPiTuiMechanism,
|
|
41
|
+
saving: string,
|
|
42
|
+
base?: Component,
|
|
43
|
+
): Component {
|
|
44
|
+
const container = new Container();
|
|
45
|
+
const title = `${theme.fg("warning", "⚡")} ${theme.fg("accent", theme.bold(`SoL-Pi · ${mechanism}`))}`;
|
|
46
|
+
container.addChild(new Text(title, 0, 0));
|
|
47
|
+
container.addChild(new Text(theme.fg("success", `Money saved · ${saving}`), 0, 0));
|
|
48
|
+
if (base) container.addChild(base);
|
|
49
|
+
return container;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function showSolPiSavings(
|
|
53
|
+
context: ExtensionContext,
|
|
54
|
+
mechanism: SolPiTuiMechanism,
|
|
55
|
+
saving: string,
|
|
56
|
+
): void {
|
|
57
|
+
if (context.mode !== "tui") return;
|
|
58
|
+
const message = `⚡ SoL-Pi · ${mechanism}\nMoney saved · ${saving}`;
|
|
59
|
+
context.ui.notify(message, "info");
|
|
60
|
+
context.ui.setStatus(STATUS_KEY, `⚡ ${mechanism} · ${saving}`);
|
|
61
|
+
|
|
62
|
+
const previous = statusTimers.get(context.ui);
|
|
63
|
+
if (previous) clearTimeout(previous);
|
|
64
|
+
const timer = setTimeout(() => {
|
|
65
|
+
if (statusTimers.get(context.ui) !== timer) return;
|
|
66
|
+
statusTimers.delete(context.ui);
|
|
67
|
+
context.ui.setStatus(STATUS_KEY, undefined);
|
|
68
|
+
}, STATUS_DURATION_MS);
|
|
69
|
+
if (typeof timer === "object" && "unref" in timer) timer.unref();
|
|
70
|
+
statusTimers.set(context.ui, timer);
|
|
71
|
+
}
|