@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,237 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type CompactionEconomics = {
|
|
7
|
+
readonly remainingRequestScale: number;
|
|
8
|
+
readonly remainingRequestStddevK: number;
|
|
9
|
+
readonly windowReserveTokens: number;
|
|
10
|
+
readonly firstCompactionRequestScale: number;
|
|
11
|
+
readonly subsequentCompactionMargin: number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const DEFAULT_COMPACTION_ECONOMICS: CompactionEconomics = Object.freeze({
|
|
15
|
+
remainingRequestScale: 1,
|
|
16
|
+
remainingRequestStddevK: 0,
|
|
17
|
+
windowReserveTokens: 16_384,
|
|
18
|
+
firstCompactionRequestScale: 2,
|
|
19
|
+
subsequentCompactionMargin: 1.5,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export type CompactionReason =
|
|
23
|
+
| "economic"
|
|
24
|
+
| "window_protection"
|
|
25
|
+
| "deferred_economic"
|
|
26
|
+
| "deferred_subsequent_margin"
|
|
27
|
+
| "deferred_carried_debt"
|
|
28
|
+
| "horizon_unavailable"
|
|
29
|
+
| "cache_ratio_unavailable"
|
|
30
|
+
| "native_not_compactable"
|
|
31
|
+
| "non_positive_saving";
|
|
32
|
+
|
|
33
|
+
export type RequestHorizonEstimate = {
|
|
34
|
+
readonly completedBoundaryRequestCounts: readonly number[];
|
|
35
|
+
readonly requestsPerBoundaryMean: number;
|
|
36
|
+
readonly requestsPerBoundaryLowerBound: number;
|
|
37
|
+
readonly unboundedExpectedRemainingRequests: number;
|
|
38
|
+
readonly averageContextTokenIncrement: number | null;
|
|
39
|
+
readonly windowRequestUpperBound: number | null;
|
|
40
|
+
readonly expectedRemainingRequests: number;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type CompactionDecision = {
|
|
44
|
+
readonly writeTokens: number;
|
|
45
|
+
readonly archiveTokens: number;
|
|
46
|
+
readonly memoTokens: number;
|
|
47
|
+
readonly contextTokens: number;
|
|
48
|
+
readonly completedBoundaryRequestCounts: readonly number[] | null;
|
|
49
|
+
readonly requestsPerBoundaryMean: number | null;
|
|
50
|
+
readonly requestsPerBoundaryLowerBound: number | null;
|
|
51
|
+
readonly unboundedExpectedRemainingRequests: number | null;
|
|
52
|
+
readonly averageContextTokenIncrement: number | null;
|
|
53
|
+
readonly windowRequestUpperBound: number | null;
|
|
54
|
+
readonly expectedRemainingRequests: number | null;
|
|
55
|
+
readonly breakevenRequests: number | null;
|
|
56
|
+
readonly combinedBreakevenRequests: number | null;
|
|
57
|
+
readonly effectiveHorizonRequests: number | null;
|
|
58
|
+
readonly cacheWriteReadRatio: number | null;
|
|
59
|
+
readonly incrementalCacheCostRatio: number | null;
|
|
60
|
+
readonly priorCompactionCount: number;
|
|
61
|
+
readonly carriedDebtTokens: number;
|
|
62
|
+
readonly cacheDebtRepaymentTokens: number;
|
|
63
|
+
readonly compact: boolean;
|
|
64
|
+
readonly reason: CompactionReason;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const MINIMUM_VARIANCE_SAMPLES = 3;
|
|
68
|
+
const SMALL_SAMPLE_SCALE = 0.5;
|
|
69
|
+
|
|
70
|
+
export function estimateRemainingRequests(input: {
|
|
71
|
+
readonly completedBoundaryRequestCounts: readonly number[];
|
|
72
|
+
readonly remainingBoundaries: number;
|
|
73
|
+
readonly scale: number;
|
|
74
|
+
readonly standardDeviationK: number;
|
|
75
|
+
readonly contextTokens: number;
|
|
76
|
+
readonly contextWindowTokens: number | null;
|
|
77
|
+
readonly averageContextTokenIncrement: number | null;
|
|
78
|
+
}): RequestHorizonEstimate {
|
|
79
|
+
const mean =
|
|
80
|
+
input.completedBoundaryRequestCounts.reduce((total, count) => total + count, 0) /
|
|
81
|
+
Math.max(1, input.completedBoundaryRequestCounts.length);
|
|
82
|
+
let lowerBound = mean;
|
|
83
|
+
if (input.standardDeviationK !== 0) {
|
|
84
|
+
if (input.completedBoundaryRequestCounts.length < MINIMUM_VARIANCE_SAMPLES) {
|
|
85
|
+
lowerBound *= SMALL_SAMPLE_SCALE;
|
|
86
|
+
} else {
|
|
87
|
+
const variance = input.completedBoundaryRequestCounts.reduce(
|
|
88
|
+
(total, count) => total + (count - mean) ** 2,
|
|
89
|
+
0,
|
|
90
|
+
);
|
|
91
|
+
const deviation = Math.sqrt(variance / (input.completedBoundaryRequestCounts.length - 1));
|
|
92
|
+
lowerBound = Math.max(0, mean - input.standardDeviationK * deviation);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const unboundedExpectedRemainingRequests =
|
|
97
|
+
1 + Math.floor(lowerBound * Math.max(0, input.remainingBoundaries) * input.scale);
|
|
98
|
+
const windowRequestUpperBound =
|
|
99
|
+
input.contextWindowTokens === null ||
|
|
100
|
+
input.averageContextTokenIncrement === null ||
|
|
101
|
+
input.averageContextTokenIncrement <= 0
|
|
102
|
+
? null
|
|
103
|
+
: Math.max(
|
|
104
|
+
0,
|
|
105
|
+
Math.floor((input.contextWindowTokens - input.contextTokens) / input.averageContextTokenIncrement),
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
completedBoundaryRequestCounts: [...input.completedBoundaryRequestCounts],
|
|
110
|
+
requestsPerBoundaryMean: mean,
|
|
111
|
+
requestsPerBoundaryLowerBound: lowerBound,
|
|
112
|
+
unboundedExpectedRemainingRequests,
|
|
113
|
+
averageContextTokenIncrement: input.averageContextTokenIncrement,
|
|
114
|
+
windowRequestUpperBound,
|
|
115
|
+
expectedRemainingRequests:
|
|
116
|
+
windowRequestUpperBound === null
|
|
117
|
+
? unboundedExpectedRemainingRequests
|
|
118
|
+
: Math.min(unboundedExpectedRemainingRequests, windowRequestUpperBound),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function decideCompaction(input: {
|
|
123
|
+
readonly writeTokens: number;
|
|
124
|
+
readonly archiveTokens: number;
|
|
125
|
+
readonly memoTokens: number;
|
|
126
|
+
readonly contextTokens: number;
|
|
127
|
+
readonly completedBoundaryRequestCounts: readonly number[] | null;
|
|
128
|
+
readonly remainingBoundaries: number;
|
|
129
|
+
readonly averageContextTokenIncrement: number | null;
|
|
130
|
+
readonly contextWindowTokens: number | null;
|
|
131
|
+
readonly priorCompactionCount: number;
|
|
132
|
+
readonly carriedDebtTokens: number;
|
|
133
|
+
readonly cacheDebtRepaymentTokens: number;
|
|
134
|
+
readonly cacheWriteReadRatio: number | null;
|
|
135
|
+
readonly economics: CompactionEconomics;
|
|
136
|
+
}): CompactionDecision {
|
|
137
|
+
const horizon =
|
|
138
|
+
input.completedBoundaryRequestCounts === null
|
|
139
|
+
? null
|
|
140
|
+
: estimateRemainingRequests({
|
|
141
|
+
completedBoundaryRequestCounts: input.completedBoundaryRequestCounts,
|
|
142
|
+
remainingBoundaries: input.remainingBoundaries,
|
|
143
|
+
scale: input.economics.remainingRequestScale,
|
|
144
|
+
standardDeviationK: input.economics.remainingRequestStddevK,
|
|
145
|
+
contextTokens: input.contextTokens,
|
|
146
|
+
contextWindowTokens: input.contextWindowTokens,
|
|
147
|
+
averageContextTokenIncrement: input.averageContextTokenIncrement,
|
|
148
|
+
});
|
|
149
|
+
const savingTokens = input.archiveTokens - input.memoTokens;
|
|
150
|
+
const incrementalCacheCostRatio =
|
|
151
|
+
input.cacheWriteReadRatio === null ? null : Math.max(0, input.cacheWriteReadRatio - 1);
|
|
152
|
+
const breakevenRequests =
|
|
153
|
+
savingTokens > 0 && incrementalCacheCostRatio !== null
|
|
154
|
+
? (input.writeTokens * incrementalCacheCostRatio) / savingTokens
|
|
155
|
+
: null;
|
|
156
|
+
const combinedBreakevenRequests =
|
|
157
|
+
savingTokens > 0 && incrementalCacheCostRatio !== null
|
|
158
|
+
? (input.carriedDebtTokens + input.writeTokens * incrementalCacheCostRatio) / savingTokens
|
|
159
|
+
: null;
|
|
160
|
+
const firstCompaction = input.priorCompactionCount === 0;
|
|
161
|
+
const effectiveHorizonRequests =
|
|
162
|
+
horizon === null
|
|
163
|
+
? null
|
|
164
|
+
: firstCompaction
|
|
165
|
+
? Math.min(
|
|
166
|
+
horizon.expectedRemainingRequests * input.economics.firstCompactionRequestScale,
|
|
167
|
+
horizon.windowRequestUpperBound ?? Number.POSITIVE_INFINITY,
|
|
168
|
+
)
|
|
169
|
+
: horizon.expectedRemainingRequests;
|
|
170
|
+
const windowProtection =
|
|
171
|
+
input.contextWindowTokens !== null &&
|
|
172
|
+
input.contextTokens >= input.contextWindowTokens - input.economics.windowReserveTokens;
|
|
173
|
+
const baseEconomic =
|
|
174
|
+
horizon !== null &&
|
|
175
|
+
horizon.expectedRemainingRequests > 0 &&
|
|
176
|
+
breakevenRequests !== null &&
|
|
177
|
+
breakevenRequests <= horizon.expectedRemainingRequests;
|
|
178
|
+
const firstEconomic =
|
|
179
|
+
firstCompaction &&
|
|
180
|
+
effectiveHorizonRequests !== null &&
|
|
181
|
+
effectiveHorizonRequests > 0 &&
|
|
182
|
+
breakevenRequests !== null &&
|
|
183
|
+
breakevenRequests <= effectiveHorizonRequests;
|
|
184
|
+
const subsequentMarginOpen =
|
|
185
|
+
!firstCompaction &&
|
|
186
|
+
horizon !== null &&
|
|
187
|
+
breakevenRequests !== null &&
|
|
188
|
+
breakevenRequests * input.economics.subsequentCompactionMargin <= horizon.expectedRemainingRequests;
|
|
189
|
+
const carriedDebtGateOpen =
|
|
190
|
+
!firstCompaction &&
|
|
191
|
+
horizon !== null &&
|
|
192
|
+
combinedBreakevenRequests !== null &&
|
|
193
|
+
combinedBreakevenRequests <= horizon.expectedRemainingRequests;
|
|
194
|
+
const economic = firstCompaction ? firstEconomic : baseEconomic && subsequentMarginOpen && carriedDebtGateOpen;
|
|
195
|
+
const compressible = savingTokens > 0;
|
|
196
|
+
const compact = compressible && (windowProtection || economic);
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
writeTokens: input.writeTokens,
|
|
200
|
+
archiveTokens: input.archiveTokens,
|
|
201
|
+
memoTokens: input.memoTokens,
|
|
202
|
+
contextTokens: input.contextTokens,
|
|
203
|
+
...(horizon ?? {
|
|
204
|
+
completedBoundaryRequestCounts: null,
|
|
205
|
+
requestsPerBoundaryMean: null,
|
|
206
|
+
requestsPerBoundaryLowerBound: null,
|
|
207
|
+
unboundedExpectedRemainingRequests: null,
|
|
208
|
+
averageContextTokenIncrement: input.averageContextTokenIncrement,
|
|
209
|
+
windowRequestUpperBound: null,
|
|
210
|
+
expectedRemainingRequests: null,
|
|
211
|
+
}),
|
|
212
|
+
breakevenRequests,
|
|
213
|
+
combinedBreakevenRequests,
|
|
214
|
+
effectiveHorizonRequests,
|
|
215
|
+
cacheWriteReadRatio: input.cacheWriteReadRatio,
|
|
216
|
+
incrementalCacheCostRatio,
|
|
217
|
+
priorCompactionCount: input.priorCompactionCount,
|
|
218
|
+
carriedDebtTokens: input.carriedDebtTokens,
|
|
219
|
+
cacheDebtRepaymentTokens: input.cacheDebtRepaymentTokens,
|
|
220
|
+
compact,
|
|
221
|
+
reason: !compressible
|
|
222
|
+
? "non_positive_saving"
|
|
223
|
+
: windowProtection
|
|
224
|
+
? "window_protection"
|
|
225
|
+
: economic
|
|
226
|
+
? "economic"
|
|
227
|
+
: horizon === null
|
|
228
|
+
? "horizon_unavailable"
|
|
229
|
+
: breakevenRequests === null
|
|
230
|
+
? "cache_ratio_unavailable"
|
|
231
|
+
: !firstCompaction && baseEconomic && !subsequentMarginOpen
|
|
232
|
+
? "deferred_subsequent_margin"
|
|
233
|
+
: !firstCompaction && baseEconomic && !carriedDebtGateOpen
|
|
234
|
+
? "deferred_carried_debt"
|
|
235
|
+
: "deferred_economic",
|
|
236
|
+
};
|
|
237
|
+
}
|
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
import type { AgentMessage, AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
6
|
+
import {
|
|
7
|
+
buildSessionContext,
|
|
8
|
+
estimateTokens,
|
|
9
|
+
findCutPoint,
|
|
10
|
+
sessionEntryToContextMessages,
|
|
11
|
+
type ExtensionContext,
|
|
12
|
+
type ExtensionFactory,
|
|
13
|
+
type SessionEntry,
|
|
14
|
+
} from "@earendil-works/pi-coding-agent";
|
|
15
|
+
import { formatSavingsCount, showSolPiSavings } from "../../tui.ts";
|
|
16
|
+
import {
|
|
17
|
+
DEFAULT_COMPACTION_ECONOMICS,
|
|
18
|
+
decideCompaction,
|
|
19
|
+
type CompactionDecision,
|
|
20
|
+
} from "./economics.ts";
|
|
21
|
+
import { analyzePlanTransition, formatPlanSnapshot, parsePlanSteps } from "./plan.ts";
|
|
22
|
+
import {
|
|
23
|
+
appendOnlineState,
|
|
24
|
+
initialOnlineState,
|
|
25
|
+
recordBoundary,
|
|
26
|
+
recordCompaction,
|
|
27
|
+
recordCorrection,
|
|
28
|
+
recordProviderRequest,
|
|
29
|
+
restoreOnlineState,
|
|
30
|
+
type OnlineState,
|
|
31
|
+
type ProgressSummary,
|
|
32
|
+
} from "./state.ts";
|
|
33
|
+
import { registerOnlineTools, type PlanUpdateInput } from "./tools.ts";
|
|
34
|
+
|
|
35
|
+
export const DEFAULT_KEEP_RECENT_TOKENS = 20_000;
|
|
36
|
+
export const DEFAULT_NATIVE_SUMMARY_TOKEN_ESTIMATE = 1_000;
|
|
37
|
+
export const BOUNDARY_COMPACTION_INSTRUCTIONS =
|
|
38
|
+
"Preserve completed work, verification results, important decisions, and remaining work.";
|
|
39
|
+
export const POST_COMPACTION_PLAN_REMINDER =
|
|
40
|
+
"Online context compaction finished. The parent task is still active. " +
|
|
41
|
+
"Before continuing work, call update_plan with a fresh plan for the remaining work.";
|
|
42
|
+
|
|
43
|
+
export type OnlineContextCompactOptions = {
|
|
44
|
+
readonly cacheWriteReadRatio?: number | null;
|
|
45
|
+
readonly keepRecentTokens?: number;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
type PendingBoundary = { readonly toolCallId: string };
|
|
49
|
+
type SelectedCompaction = { readonly decision: CompactionDecision };
|
|
50
|
+
type CacheDebt = { readonly debtTokens: number; readonly repaymentTokens: number };
|
|
51
|
+
type PendingContinuation = { readonly promise: Promise<void>; readonly resolve: () => void };
|
|
52
|
+
|
|
53
|
+
const SUBAGENTS_MANAGER_KEY = Symbol.for("pi-subagents:manager");
|
|
54
|
+
|
|
55
|
+
export function hasRunningSubagents(): boolean {
|
|
56
|
+
try {
|
|
57
|
+
const manager = (globalThis as Record<PropertyKey, unknown>)[SUBAGENTS_MANAGER_KEY];
|
|
58
|
+
if (!manager || typeof manager !== "object") return false;
|
|
59
|
+
const hasRunning = (manager as { hasRunning?: unknown }).hasRunning;
|
|
60
|
+
return typeof hasRunning === "function" && hasRunning.call(manager) === true;
|
|
61
|
+
} catch {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function resolveKeepRecentTokens(value: number | undefined): number {
|
|
67
|
+
const resolved = value ?? DEFAULT_KEEP_RECENT_TOKENS;
|
|
68
|
+
if (!Number.isSafeInteger(resolved) || resolved < 1) {
|
|
69
|
+
throw new Error("Online Context Compact keepRecentTokens must be a positive safe integer");
|
|
70
|
+
}
|
|
71
|
+
return resolved;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function resolveCacheWriteReadRatio(value: number | null | undefined): number | null {
|
|
75
|
+
if (value === undefined || value === null) return null;
|
|
76
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
77
|
+
throw new Error("Online Context Compact cacheWriteReadRatio must be finite and non-negative");
|
|
78
|
+
}
|
|
79
|
+
return value;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function tokenEstimate(text: string): number {
|
|
83
|
+
return Math.ceil(Buffer.byteLength(text) / 4);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function result(text: string, details: Readonly<Record<string, unknown>>): AgentToolResult<Readonly<Record<string, unknown>>> {
|
|
87
|
+
return { content: [{ type: "text", text }], details };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function progressSummary(input: PlanUpdateInput, completedStepId: string): ProgressSummary | undefined {
|
|
91
|
+
const step = input.steps.find((item) => item.id === completedStepId);
|
|
92
|
+
if (!step || !input.progress) return;
|
|
93
|
+
return {
|
|
94
|
+
stepId: step.id,
|
|
95
|
+
goal: step.goal,
|
|
96
|
+
filesChanged: [...input.progress.files_changed],
|
|
97
|
+
verification: [...input.progress.verification],
|
|
98
|
+
decisions: [...input.progress.decisions],
|
|
99
|
+
nextWork: input.steps.filter((item) => item.status !== "completed").map((item) => item.goal),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function compactionMessageCount(entries: readonly SessionEntry[], startIndex: number, endIndex: number): number {
|
|
104
|
+
let count = 0;
|
|
105
|
+
for (let index = startIndex; index < endIndex; index++) {
|
|
106
|
+
const entry = entries[index];
|
|
107
|
+
if (entry && entry.type !== "compaction" && sessionEntryToContextMessages(entry).length > 0) count++;
|
|
108
|
+
}
|
|
109
|
+
return count;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function branchAfterAbort(entries: readonly SessionEntry[]): SessionEntry[] {
|
|
113
|
+
const last = entries.at(-1);
|
|
114
|
+
const markerProvider = ["sol", "pi"].join("-");
|
|
115
|
+
return [
|
|
116
|
+
...entries,
|
|
117
|
+
{
|
|
118
|
+
type: "message",
|
|
119
|
+
id: "sol-pi-online-context-compact-abort-marker",
|
|
120
|
+
parentId: last?.id ?? null,
|
|
121
|
+
timestamp: new Date(0).toISOString(),
|
|
122
|
+
message: {
|
|
123
|
+
role: "assistant",
|
|
124
|
+
content: [],
|
|
125
|
+
api: markerProvider,
|
|
126
|
+
provider: markerProvider,
|
|
127
|
+
model: "aborted",
|
|
128
|
+
usage: {
|
|
129
|
+
input: 0,
|
|
130
|
+
output: 0,
|
|
131
|
+
cacheRead: 0,
|
|
132
|
+
cacheWrite: 0,
|
|
133
|
+
totalTokens: 0,
|
|
134
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
135
|
+
},
|
|
136
|
+
stopReason: "aborted",
|
|
137
|
+
timestamp: 0,
|
|
138
|
+
},
|
|
139
|
+
} as SessionEntry,
|
|
140
|
+
];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function nativeCompactionFeasible(entries: readonly SessionEntry[], keepRecentTokens: number): boolean {
|
|
144
|
+
const path = branchAfterAbort(entries);
|
|
145
|
+
let startIndex = 0;
|
|
146
|
+
for (let index = path.length - 1; index >= 0; index--) {
|
|
147
|
+
const entry = path[index];
|
|
148
|
+
if (entry?.type !== "compaction") continue;
|
|
149
|
+
const keptIndex = path.findIndex((item) => item.id === entry.firstKeptEntryId);
|
|
150
|
+
startIndex = keptIndex >= 0 ? keptIndex : index + 1;
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const cut = findCutPoint(path, startIndex, path.length, keepRecentTokens);
|
|
155
|
+
const historyEnd = cut.isSplitTurn ? cut.turnStartIndex : cut.firstKeptEntryIndex;
|
|
156
|
+
const historyMessages = historyEnd > startIndex ? compactionMessageCount(path, startIndex, historyEnd) : 0;
|
|
157
|
+
const prefixMessages =
|
|
158
|
+
cut.isSplitTurn && cut.turnStartIndex >= 0
|
|
159
|
+
? compactionMessageCount(path, cut.turnStartIndex, cut.firstKeptEntryIndex)
|
|
160
|
+
: 0;
|
|
161
|
+
return historyMessages > 0 || prefixMessages > 0;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function validPositiveInteger(value: unknown): value is number {
|
|
165
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function releaseParentContinuation(continuation: PendingContinuation | undefined): void {
|
|
169
|
+
if (continuation) setTimeout(continuation.resolve, 0);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function createOnlineContextCompactExtension(options: OnlineContextCompactOptions = {}): ExtensionFactory {
|
|
173
|
+
const keepRecentTokens = resolveKeepRecentTokens(options.keepRecentTokens);
|
|
174
|
+
const cacheWriteReadRatio = resolveCacheWriteReadRatio(options.cacheWriteReadRatio);
|
|
175
|
+
|
|
176
|
+
return (pi) => {
|
|
177
|
+
let state: OnlineState = initialOnlineState();
|
|
178
|
+
let restored = false;
|
|
179
|
+
let observedMessages: readonly AgentMessage[] = [];
|
|
180
|
+
let pendingBoundary: PendingBoundary | undefined;
|
|
181
|
+
let selected: SelectedCompaction | undefined;
|
|
182
|
+
let activeDebt: CacheDebt | undefined;
|
|
183
|
+
let nextContinuation: PendingContinuation | undefined;
|
|
184
|
+
let compactionInFlight = false;
|
|
185
|
+
|
|
186
|
+
const releaseContinuation = (): void => {
|
|
187
|
+
const continuation = nextContinuation;
|
|
188
|
+
nextContinuation = undefined;
|
|
189
|
+
continuation?.resolve();
|
|
190
|
+
};
|
|
191
|
+
const restore = (context: ExtensionContext): void => {
|
|
192
|
+
releaseContinuation();
|
|
193
|
+
state = restoreOnlineState(context.sessionManager.getBranch());
|
|
194
|
+
restored = true;
|
|
195
|
+
observedMessages = buildSessionContext(
|
|
196
|
+
context.sessionManager.getEntries(),
|
|
197
|
+
context.sessionManager.getLeafId(),
|
|
198
|
+
).messages;
|
|
199
|
+
pendingBoundary = undefined;
|
|
200
|
+
selected = undefined;
|
|
201
|
+
activeDebt = undefined;
|
|
202
|
+
compactionInFlight = false;
|
|
203
|
+
};
|
|
204
|
+
const ensureRestored = (context: ExtensionContext): void => {
|
|
205
|
+
if (!restored) restore(context);
|
|
206
|
+
};
|
|
207
|
+
const save = (): void => appendOnlineState(pi, state);
|
|
208
|
+
const contextTokens = (context: ExtensionContext): number => {
|
|
209
|
+
const visible = observedMessages.reduce((total, message) => total + estimateTokens(message), 0);
|
|
210
|
+
const estimated = visible + tokenEstimate(context.getSystemPrompt());
|
|
211
|
+
const reported = context.getContextUsage()?.tokens;
|
|
212
|
+
return validPositiveInteger(reported) ? Math.max(reported, estimated) : estimated;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
registerOnlineTools(pi, {
|
|
216
|
+
updatePlan: async (input) => {
|
|
217
|
+
ensureRestored(input.context);
|
|
218
|
+
if (input.signal?.aborted) throw new Error("Plan update was aborted");
|
|
219
|
+
const steps = parsePlanSteps(input.steps);
|
|
220
|
+
if (!steps || steps.length === 0) throw new Error("Plan must contain at least one valid step");
|
|
221
|
+
|
|
222
|
+
const transition = analyzePlanTransition(state.plan, steps);
|
|
223
|
+
const completedIds = transition.completedSteps.map((step) => step.id);
|
|
224
|
+
if (completedIds.length > 0) {
|
|
225
|
+
state = recordBoundary(state, steps, progressSummary(input, completedIds[0] ?? ""));
|
|
226
|
+
if (!pendingBoundary) pendingBoundary = { toolCallId: input.toolCallId };
|
|
227
|
+
} else if (JSON.stringify(state.plan) !== JSON.stringify(steps)) {
|
|
228
|
+
state = { ...state, plan: [...steps] };
|
|
229
|
+
}
|
|
230
|
+
save();
|
|
231
|
+
|
|
232
|
+
return result(
|
|
233
|
+
[formatPlanSnapshot(steps), ...transition.advice].join("\n"),
|
|
234
|
+
{
|
|
235
|
+
boundary: completedIds.length > 0,
|
|
236
|
+
completed_step_ids: completedIds,
|
|
237
|
+
progress_recorded: completedIds.length > 0 && input.progress !== undefined,
|
|
238
|
+
task_status: "active",
|
|
239
|
+
plan: steps,
|
|
240
|
+
},
|
|
241
|
+
);
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
pi.on("session_start", (_event, context) => restore(context));
|
|
246
|
+
pi.on("session_before_tree", () => (compactionInFlight ? { cancel: true } : undefined));
|
|
247
|
+
pi.on("session_tree", (_event, context) => restore(context));
|
|
248
|
+
|
|
249
|
+
pi.on("context", (event, context) => {
|
|
250
|
+
ensureRestored(context);
|
|
251
|
+
observedMessages = [...event.messages];
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
pi.on("before_provider_request", (_event, context) => {
|
|
255
|
+
ensureRestored(context);
|
|
256
|
+
state = recordProviderRequest(state, contextTokens(context));
|
|
257
|
+
save();
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
pi.on("input", (event, context) => {
|
|
261
|
+
if (event.streamingBehavior !== "steer" && !event.text.startsWith("CORRECTION:")) {
|
|
262
|
+
return { action: "continue" as const };
|
|
263
|
+
}
|
|
264
|
+
ensureRestored(context);
|
|
265
|
+
pendingBoundary = undefined;
|
|
266
|
+
selected = undefined;
|
|
267
|
+
activeDebt = undefined;
|
|
268
|
+
state = recordCorrection(state);
|
|
269
|
+
save();
|
|
270
|
+
return { action: "continue" as const };
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
pi.on("turn_end", (event, context) => {
|
|
274
|
+
const boundary = pendingBoundary;
|
|
275
|
+
pendingBoundary = undefined;
|
|
276
|
+
if (!boundary || selected || hasRunningSubagents()) return;
|
|
277
|
+
const toolResult = event.toolResults.find((item) => item.toolCallId === boundary.toolCallId);
|
|
278
|
+
if (
|
|
279
|
+
event.message.role !== "assistant" ||
|
|
280
|
+
event.message.stopReason === "error" ||
|
|
281
|
+
event.message.stopReason === "aborted" ||
|
|
282
|
+
context.signal?.aborted ||
|
|
283
|
+
!toolResult ||
|
|
284
|
+
toolResult.isError
|
|
285
|
+
) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const usage = context.getContextUsage();
|
|
290
|
+
const writeTokens = contextTokens(context);
|
|
291
|
+
const fixedTokens = tokenEstimate(context.getSystemPrompt());
|
|
292
|
+
const archiveTokens = Math.max(0, writeTokens - fixedTokens - keepRecentTokens);
|
|
293
|
+
const contextWindowTokens = validPositiveInteger(usage?.contextWindow)
|
|
294
|
+
? usage.contextWindow
|
|
295
|
+
: validPositiveInteger(context.model?.contextWindow)
|
|
296
|
+
? context.model.contextWindow
|
|
297
|
+
: null;
|
|
298
|
+
const averageContextTokenIncrement =
|
|
299
|
+
state.positiveContextDeltaCount === 0
|
|
300
|
+
? null
|
|
301
|
+
: state.positiveContextDeltaTotal / state.positiveContextDeltaCount;
|
|
302
|
+
const priced = decideCompaction({
|
|
303
|
+
writeTokens,
|
|
304
|
+
archiveTokens,
|
|
305
|
+
memoTokens: DEFAULT_NATIVE_SUMMARY_TOKEN_ESTIMATE,
|
|
306
|
+
contextTokens: writeTokens,
|
|
307
|
+
completedBoundaryRequestCounts: state.completedBoundaryRequestCounts,
|
|
308
|
+
remainingBoundaries: state.plan.filter((step) => step.status !== "completed").length,
|
|
309
|
+
averageContextTokenIncrement,
|
|
310
|
+
contextWindowTokens,
|
|
311
|
+
priorCompactionCount: state.nativeCompactionCount,
|
|
312
|
+
carriedDebtTokens: state.cacheDebtTokens,
|
|
313
|
+
cacheDebtRepaymentTokens: state.cacheDebtRepaymentTokens,
|
|
314
|
+
cacheWriteReadRatio,
|
|
315
|
+
economics: DEFAULT_COMPACTION_ECONOMICS,
|
|
316
|
+
});
|
|
317
|
+
const decision: CompactionDecision =
|
|
318
|
+
priced.compact && !nativeCompactionFeasible(context.sessionManager.getBranch(), keepRecentTokens)
|
|
319
|
+
? { ...priced, compact: false, reason: "native_not_compactable" }
|
|
320
|
+
: priced;
|
|
321
|
+
if (!decision.compact) return;
|
|
322
|
+
|
|
323
|
+
selected = { decision };
|
|
324
|
+
context.abort();
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
pi.on("agent_settled", async (_event, context) => {
|
|
328
|
+
// sendMessage() starts a turn without returning its promise. Capture the
|
|
329
|
+
// child settlement so print/JSON mode cannot dispose while it is running.
|
|
330
|
+
const parentContinuation = nextContinuation;
|
|
331
|
+
nextContinuation = undefined;
|
|
332
|
+
const pending = selected;
|
|
333
|
+
selected = undefined;
|
|
334
|
+
if (!context.isIdle()) {
|
|
335
|
+
selected = pending;
|
|
336
|
+
nextContinuation = parentContinuation;
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
if (!pending || hasRunningSubagents()) {
|
|
340
|
+
releaseParentContinuation(parentContinuation);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
activeDebt = {
|
|
345
|
+
debtTokens: pending.decision.writeTokens * (pending.decision.incrementalCacheCostRatio ?? 0),
|
|
346
|
+
repaymentTokens: Math.max(0, pending.decision.archiveTokens - pending.decision.memoTokens),
|
|
347
|
+
};
|
|
348
|
+
let compacted = false;
|
|
349
|
+
let compactionError: Error | undefined;
|
|
350
|
+
try {
|
|
351
|
+
compactionInFlight = true;
|
|
352
|
+
await new Promise<void>((resolve) => {
|
|
353
|
+
let finished = false;
|
|
354
|
+
const finish = (): void => {
|
|
355
|
+
if (finished) return;
|
|
356
|
+
finished = true;
|
|
357
|
+
resolve();
|
|
358
|
+
};
|
|
359
|
+
context.compact({
|
|
360
|
+
customInstructions: BOUNDARY_COMPACTION_INSTRUCTIONS,
|
|
361
|
+
onComplete: (compaction) => {
|
|
362
|
+
try {
|
|
363
|
+
compacted = true;
|
|
364
|
+
const removed = Math.max(
|
|
365
|
+
0,
|
|
366
|
+
pending.decision.archiveTokens - tokenEstimate(compaction.summary),
|
|
367
|
+
);
|
|
368
|
+
if (removed > 0) {
|
|
369
|
+
showSolPiSavings(
|
|
370
|
+
context,
|
|
371
|
+
"Online Context Compact",
|
|
372
|
+
formatSavingsCount(removed, "context tokens removed"),
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
} finally {
|
|
376
|
+
finish();
|
|
377
|
+
}
|
|
378
|
+
},
|
|
379
|
+
onError: (error) => {
|
|
380
|
+
compactionError = error;
|
|
381
|
+
finish();
|
|
382
|
+
},
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
compactionInFlight = false;
|
|
386
|
+
if (
|
|
387
|
+
compactionError &&
|
|
388
|
+
compactionError.name !== "AbortError" &&
|
|
389
|
+
compactionError.message !== "Compaction cancelled"
|
|
390
|
+
) {
|
|
391
|
+
throw compactionError;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (compacted) {
|
|
395
|
+
let resolveContinuation!: () => void;
|
|
396
|
+
const continuation: PendingContinuation = {
|
|
397
|
+
promise: new Promise<void>((resolve) => {
|
|
398
|
+
resolveContinuation = resolve;
|
|
399
|
+
}),
|
|
400
|
+
resolve: () => resolveContinuation(),
|
|
401
|
+
};
|
|
402
|
+
nextContinuation = continuation;
|
|
403
|
+
try {
|
|
404
|
+
pi.sendMessage(
|
|
405
|
+
{
|
|
406
|
+
customType: "sol-pi-online-context-compact",
|
|
407
|
+
content: POST_COMPACTION_PLAN_REMINDER,
|
|
408
|
+
display: false,
|
|
409
|
+
},
|
|
410
|
+
{ triggerTurn: true },
|
|
411
|
+
);
|
|
412
|
+
} catch (error) {
|
|
413
|
+
if (nextContinuation === continuation) nextContinuation = undefined;
|
|
414
|
+
continuation.resolve();
|
|
415
|
+
throw error;
|
|
416
|
+
}
|
|
417
|
+
if (context.isIdle() && nextContinuation === continuation) {
|
|
418
|
+
nextContinuation = undefined;
|
|
419
|
+
continuation.resolve();
|
|
420
|
+
throw new Error("Online context compact continuation did not start");
|
|
421
|
+
}
|
|
422
|
+
await continuation.promise;
|
|
423
|
+
}
|
|
424
|
+
} finally {
|
|
425
|
+
compactionInFlight = false;
|
|
426
|
+
activeDebt = undefined;
|
|
427
|
+
releaseParentContinuation(parentContinuation);
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
pi.on("session_compact", (event, context) => {
|
|
432
|
+
ensureRestored(context);
|
|
433
|
+
state = recordCompaction(
|
|
434
|
+
state,
|
|
435
|
+
event.fromExtension || !activeDebt ? { debtTokens: 0, repaymentTokens: 0 } : activeDebt,
|
|
436
|
+
);
|
|
437
|
+
save();
|
|
438
|
+
pendingBoundary = undefined;
|
|
439
|
+
selected = undefined;
|
|
440
|
+
activeDebt = undefined;
|
|
441
|
+
observedMessages = buildSessionContext(
|
|
442
|
+
context.sessionManager.getEntries(),
|
|
443
|
+
context.sessionManager.getLeafId(),
|
|
444
|
+
).messages;
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
pi.on("session_shutdown", () => {
|
|
448
|
+
releaseContinuation();
|
|
449
|
+
pendingBoundary = undefined;
|
|
450
|
+
selected = undefined;
|
|
451
|
+
activeDebt = undefined;
|
|
452
|
+
compactionInFlight = false;
|
|
453
|
+
});
|
|
454
|
+
};
|
|
455
|
+
}
|