@nijaru/pi-provider-compaction 0.0.6 → 0.0.7
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/README.md +2 -2
- package/dist/index.js +28 -15
- package/package.json +4 -4
- package/replay.ts +22 -5
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# pi-provider-compaction
|
|
2
2
|
|
|
3
|
-
Experimental provider-native compaction for [Pi](https://github.com/earendil-works/pi), with a meaningful portable summary saved alongside each native checkpoint. Minimum runtime: Pi 0.87.0. Development checks target Pi 1.
|
|
3
|
+
Experimental provider-native compaction for [Pi](https://github.com/earendil-works/pi), with a meaningful portable summary saved alongside each native checkpoint. Minimum runtime: Pi 0.87.0. Development checks target Pi 1.3.0.
|
|
4
4
|
|
|
5
5
|
**This experimental design is not production-validated. Do not enable it globally yet.** Real runner/adapter fixtures and a Codex live smoke test pass; broader backend and deployment validation remains incomplete.
|
|
6
6
|
|
|
@@ -73,7 +73,7 @@ pi --extension ./dist/index.js
|
|
|
73
73
|
|
|
74
74
|
The manifest loads the built `dist/index.js`, including a bundled stateless Responses serializer validator; source or host dependency changes require a rebuild. `bun run check` rebuilds first and tests the artifact against Pi's actual host serializer, including function-call history replayed as grammar tools. Keep global installation disabled while testing. Publishing and global activation are separate approval steps.
|
|
75
75
|
|
|
76
|
-
Tests include real Pi 1.
|
|
76
|
+
Tests include real Pi 1.3.0 runner/adapter fixtures for all three protocols, repeated prefix compaction, transformed suffix preservation, unchanged fallback, grammar tools, strict defaults, tool IDs, images, prompt updates, cross-model thinking and mid-conversation effort changes. Set `PI_COMPACTOR_FIXTURE_PATH` to a `pi-compactor` entrypoint to check SDK flag precedence in both extension orders. The optional installed-fast-mode fixture (`PI_FAST_MODE_FIXTURE_PATH`) checks actual `pi-fast-mode` composition across `/reload`; it creates an isolated fast-mode configuration.
|
|
77
77
|
|
|
78
78
|
Authenticated protocol smoke tests are separate and send three small requests:
|
|
79
79
|
|
package/dist/index.js
CHANGED
|
@@ -637,19 +637,13 @@ function transformMessages(messages, model, normalizeToolCallId) {
|
|
|
637
637
|
const isSameModel = assistantMsg.provider === model.provider && assistantMsg.api === model.api && assistantMsg.model === model.id;
|
|
638
638
|
const transformedContent = assistantMsg.content.flatMap((block) => {
|
|
639
639
|
if (block.type === "thinking") {
|
|
640
|
-
if (
|
|
641
|
-
return
|
|
642
|
-
|
|
643
|
-
if (isSameModel && block.thinkingSignature)
|
|
640
|
+
if (!isSameModel)
|
|
641
|
+
return [];
|
|
642
|
+
if (block.redacted || block.thinkingSignature)
|
|
644
643
|
return block;
|
|
645
644
|
if (!block.thinking || block.thinking.trim() === "")
|
|
646
645
|
return [];
|
|
647
|
-
|
|
648
|
-
return block;
|
|
649
|
-
return {
|
|
650
|
-
type: "text",
|
|
651
|
-
text: block.thinking
|
|
652
|
-
};
|
|
646
|
+
return block;
|
|
653
647
|
}
|
|
654
648
|
if (block.type === "text") {
|
|
655
649
|
if (isSameModel)
|
|
@@ -840,6 +834,12 @@ function convertResponsesMessages(model, context, allowedToolCallProviders, opti
|
|
|
840
834
|
});
|
|
841
835
|
};
|
|
842
836
|
const includeInitialSystemMessage = options?.includeSystemPrompt ?? true;
|
|
837
|
+
const pushConfigurationUpdate = (effort) => {
|
|
838
|
+
messages.push({
|
|
839
|
+
type: "configuration_update",
|
|
840
|
+
reasoning: { effort }
|
|
841
|
+
});
|
|
842
|
+
};
|
|
843
843
|
const compat = model.compat;
|
|
844
844
|
const instructionRole = model.reasoning && compat?.supportsDeveloperRole !== false ? "developer" : "system";
|
|
845
845
|
let msgIndex = 0;
|
|
@@ -946,6 +946,9 @@ function convertResponsesMessages(model, context, allowedToolCallProviders, opti
|
|
|
946
946
|
}
|
|
947
947
|
if (output.length === 0)
|
|
948
948
|
continue;
|
|
949
|
+
if (options?.configurationUpdateEffort !== undefined && isSameModel && msg.providerThinkingLevel !== undefined) {
|
|
950
|
+
pushConfigurationUpdate(msg.providerThinkingLevel);
|
|
951
|
+
}
|
|
949
952
|
messages.push(...output);
|
|
950
953
|
} else if (msg.role === "toolResult") {
|
|
951
954
|
const [callId] = msg.toolCallId.split("|");
|
|
@@ -967,6 +970,8 @@ function convertResponsesMessages(model, context, allowedToolCallProviders, opti
|
|
|
967
970
|
if (!isLeadingSystemMessage)
|
|
968
971
|
msgIndex++;
|
|
969
972
|
}
|
|
973
|
+
if (options?.configurationUpdateEffort !== undefined)
|
|
974
|
+
pushConfigurationUpdate(options.configurationUpdateEffort);
|
|
970
975
|
return messages;
|
|
971
976
|
}
|
|
972
977
|
function convertResponsesTools(tools, options) {
|
|
@@ -1463,7 +1468,7 @@ var SNAPSHOT_TTL_MS = 5 * 60000;
|
|
|
1463
1468
|
function equal(left, right) {
|
|
1464
1469
|
return JSON.stringify(left) === JSON.stringify(right);
|
|
1465
1470
|
}
|
|
1466
|
-
function serializeInput(model, context, declarations = context) {
|
|
1471
|
+
function serializeInput(model, context, declarations = context, configurationUpdateEffort) {
|
|
1467
1472
|
const compat = model.compat;
|
|
1468
1473
|
const flag = (name) => compat?.[name] === true;
|
|
1469
1474
|
const codex = model.api === "openai-codex-responses";
|
|
@@ -1471,6 +1476,7 @@ function serializeInput(model, context, declarations = context) {
|
|
|
1471
1476
|
const tools = resolveTranscript2(declarations, flag("supportsMidConvoSystemMessages"));
|
|
1472
1477
|
return convertResponsesMessages(model, normalized, model.api === "azure-openai-responses" ? AZURE_TOOL_CALL_PROVIDERS : TOOL_CALL_PROVIDERS, {
|
|
1473
1478
|
includeSystemPrompt: !codex,
|
|
1479
|
+
configurationUpdateEffort,
|
|
1474
1480
|
grammarToolInputProperties: createGrammarToolInputProperties(getDeclaredTools(tools.messages), flag("supportsOpenAIGrammarTools")),
|
|
1475
1481
|
supportsMidConvoSystemMessages: flag("supportsMidConvoSystemMessages"),
|
|
1476
1482
|
supportsAdditionalTools: flag("supportsAdditionalTools"),
|
|
@@ -1495,12 +1501,15 @@ function capturePreparedRequest(model, context, canonical, payload, reject = ()
|
|
|
1495
1501
|
reject("snapshot exceeds size limit");
|
|
1496
1502
|
return;
|
|
1497
1503
|
}
|
|
1498
|
-
const
|
|
1504
|
+
const last = payload.input.at(-1);
|
|
1505
|
+
const compat = model.compat;
|
|
1506
|
+
const configurationUpdateEffort = model.api === "openai-responses" && compat?.supportsMidConvoEffort === true && last?.type === "configuration_update" && isObject(last.reasoning) && typeof last.reasoning.effort === "string" ? last.reasoning.effort : undefined;
|
|
1507
|
+
const expected = serializeInput(model, context, context, configurationUpdateEffort);
|
|
1499
1508
|
if (!equal(expected, payload.input)) {
|
|
1500
1509
|
reject("prepared input differs from serializer");
|
|
1501
1510
|
return;
|
|
1502
1511
|
}
|
|
1503
|
-
return { context: structuredClone(context), canonical: structuredClone(canonical), payload: structuredClone(payload), portableInput: expected, createdAt: Date.now() };
|
|
1512
|
+
return { context: structuredClone(context), canonical: structuredClone(canonical), payload: structuredClone(payload), portableInput: expected, configurationUpdateEffort, createdAt: Date.now() };
|
|
1504
1513
|
}
|
|
1505
1514
|
function promptCount(input) {
|
|
1506
1515
|
let count = 0;
|
|
@@ -1508,6 +1517,10 @@ function promptCount(input) {
|
|
|
1508
1517
|
count++;
|
|
1509
1518
|
return count;
|
|
1510
1519
|
}
|
|
1520
|
+
function serializePrefix(model, snapshot, prefix) {
|
|
1521
|
+
const input = serializeInput(model, prefix, snapshot.context, snapshot.configurationUpdateEffort);
|
|
1522
|
+
return snapshot.configurationUpdateEffort === undefined ? input : input.slice(0, -1);
|
|
1523
|
+
}
|
|
1511
1524
|
function closedTools(context) {
|
|
1512
1525
|
const pending = new Set;
|
|
1513
1526
|
for (const message of context.messages) {
|
|
@@ -1551,7 +1564,7 @@ function selectCoveredPrefix(model, snapshot, prefix, reject = () => {}) {
|
|
|
1551
1564
|
reject("projected prefix differs from request context");
|
|
1552
1565
|
return;
|
|
1553
1566
|
}
|
|
1554
|
-
const expected =
|
|
1567
|
+
const expected = serializePrefix(model, snapshot, mapped);
|
|
1555
1568
|
if (!equal(snapshot.portableInput.slice(0, expected.length), expected)) {
|
|
1556
1569
|
reject("serialized prefix does not match prepared input");
|
|
1557
1570
|
return;
|
|
@@ -1584,7 +1597,7 @@ function summarySlot(model, snapshot, summaryPrefix) {
|
|
|
1584
1597
|
const mapped = mappedPrefix(snapshot, summaryPrefix);
|
|
1585
1598
|
if (!mapped)
|
|
1586
1599
|
return;
|
|
1587
|
-
const prefix =
|
|
1600
|
+
const prefix = serializePrefix(model, snapshot, mapped);
|
|
1588
1601
|
const index = promptCount(prefix);
|
|
1589
1602
|
if (prefix.length !== index + 1 || !equal(snapshot.portableInput.slice(0, prefix.length), prefix))
|
|
1590
1603
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nijaru/pi-provider-compaction",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Provider-native context compaction adapters for Pi",
|
|
6
6
|
"keywords": [
|
|
@@ -44,9 +44,9 @@
|
|
|
44
44
|
"LICENSE"
|
|
45
45
|
],
|
|
46
46
|
"devDependencies": {
|
|
47
|
-
"@earendil-works/pi-ai": "1.
|
|
48
|
-
"@earendil-works/pi-agent-core": "1.
|
|
49
|
-
"@earendil-works/pi-coding-agent": "1.
|
|
47
|
+
"@earendil-works/pi-ai": "1.3.0",
|
|
48
|
+
"@earendil-works/pi-agent-core": "1.3.0",
|
|
49
|
+
"@earendil-works/pi-coding-agent": "1.3.0",
|
|
50
50
|
"@types/bun": "latest",
|
|
51
51
|
"oxlint": "^1.81.0",
|
|
52
52
|
"typescript": "^6.0.3"
|
package/replay.ts
CHANGED
|
@@ -14,7 +14,7 @@ export function equal(left: unknown, right: unknown): boolean {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
/** Validator only. No serialized item from this function is transmitted. */
|
|
17
|
-
export function serializeInput(model: Model<any>, context: TranscriptContext, declarations = context): ResponseItem[] {
|
|
17
|
+
export function serializeInput(model: Model<any>, context: TranscriptContext, declarations = context, configurationUpdateEffort?: string): ResponseItem[] {
|
|
18
18
|
const compat = model.compat as Record<string, unknown> | undefined;
|
|
19
19
|
const flag = (name: string) => compat?.[name] === true;
|
|
20
20
|
const codex = model.api === "openai-codex-responses";
|
|
@@ -22,6 +22,7 @@ export function serializeInput(model: Model<any>, context: TranscriptContext, de
|
|
|
22
22
|
const tools = resolveTranscript(declarations, flag("supportsMidConvoSystemMessages"));
|
|
23
23
|
return convertResponsesMessages(model, normalized, model.api === "azure-openai-responses" ? AZURE_TOOL_CALL_PROVIDERS : TOOL_CALL_PROVIDERS, {
|
|
24
24
|
includeSystemPrompt: !codex,
|
|
25
|
+
configurationUpdateEffort,
|
|
25
26
|
grammarToolInputProperties: createGrammarToolInputProperties(getDeclaredTools(tools.messages), flag("supportsOpenAIGrammarTools")),
|
|
26
27
|
supportsMidConvoSystemMessages: flag("supportsMidConvoSystemMessages"),
|
|
27
28
|
supportsAdditionalTools: flag("supportsAdditionalTools"),
|
|
@@ -40,6 +41,8 @@ export interface PreparedRequest {
|
|
|
40
41
|
payload: JsonObject & { input: ResponseItem[] };
|
|
41
42
|
/** The portable wire input before our sole summary substitution. */
|
|
42
43
|
portableInput: ResponseItem[];
|
|
44
|
+
/** The final request-only effort update; historical efforts come from the transcript. */
|
|
45
|
+
configurationUpdateEffort?: string;
|
|
43
46
|
replacement?: { index: number; count: number };
|
|
44
47
|
createdAt: number;
|
|
45
48
|
}
|
|
@@ -52,9 +55,16 @@ export function capturePreparedRequest(
|
|
|
52
55
|
// Reject stateful provider requests: coverage must be the complete visible input.
|
|
53
56
|
if (payload.previous_response_id || payload.conversation) { reject("stateful request"); return; }
|
|
54
57
|
if (Buffer.byteLength(JSON.stringify({ context, payload })) > MAX_SNAPSHOT_BYTES) { reject("snapshot exceeds size limit"); return; }
|
|
55
|
-
|
|
58
|
+
// Read the actual prepared request's mode, not provider/auth guesses. Exact conversion
|
|
59
|
+
// below still validates every historical update, its position and its complete shape.
|
|
60
|
+
const last = payload.input.at(-1);
|
|
61
|
+
const compat = model.compat as Record<string, unknown> | undefined;
|
|
62
|
+
const configurationUpdateEffort = model.api === "openai-responses" && compat?.supportsMidConvoEffort === true &&
|
|
63
|
+
last?.type === "configuration_update" && isObject(last.reasoning) && typeof last.reasoning.effort === "string"
|
|
64
|
+
? last.reasoning.effort : undefined;
|
|
65
|
+
const expected = serializeInput(model, context, context, configurationUpdateEffort);
|
|
56
66
|
if (!equal(expected, payload.input)) { reject("prepared input differs from serializer"); return; }
|
|
57
|
-
return { context: structuredClone(context), canonical: structuredClone(canonical), payload: structuredClone(payload) as PreparedRequest["payload"], portableInput: expected, createdAt: Date.now() };
|
|
67
|
+
return { context: structuredClone(context), canonical: structuredClone(canonical), payload: structuredClone(payload) as PreparedRequest["payload"], portableInput: expected, configurationUpdateEffort, createdAt: Date.now() };
|
|
58
68
|
}
|
|
59
69
|
|
|
60
70
|
function promptCount(input: readonly ResponseItem[]): number {
|
|
@@ -63,6 +73,13 @@ function promptCount(input: readonly ResponseItem[]): number {
|
|
|
63
73
|
return count;
|
|
64
74
|
}
|
|
65
75
|
|
|
76
|
+
function serializePrefix(model: Model<any>, snapshot: PreparedRequest, prefix: TranscriptContext): ResponseItem[] {
|
|
77
|
+
const input = serializeInput(model, prefix, snapshot.context, snapshot.configurationUpdateEffort);
|
|
78
|
+
// The converter appends the next reply's effort even to a partial transcript.
|
|
79
|
+
// It is not historical coverage and must remain in the live suffix on replay.
|
|
80
|
+
return snapshot.configurationUpdateEffort === undefined ? input : input.slice(0, -1);
|
|
81
|
+
}
|
|
82
|
+
|
|
66
83
|
/** No open/synthetic tool exchange may cross the coverage boundary. */
|
|
67
84
|
function closedTools(context: TranscriptContext): boolean {
|
|
68
85
|
const pending = new Set<string>();
|
|
@@ -94,7 +111,7 @@ export function selectCoveredPrefix(model: Model<any>, snapshot: PreparedRequest
|
|
|
94
111
|
if (!closedTools(prefix)) { reject("prefix has an incomplete tool exchange or failed assistant"); return; }
|
|
95
112
|
const mapped = mappedPrefix(snapshot, prefix);
|
|
96
113
|
if (!mapped) { reject("projected prefix differs from request context"); return; }
|
|
97
|
-
const expected =
|
|
114
|
+
const expected = serializePrefix(model, snapshot, mapped);
|
|
98
115
|
if (!equal(snapshot.portableInput.slice(0, expected.length), expected)) { reject("serialized prefix does not match prepared input"); return; }
|
|
99
116
|
const start = promptCount(expected);
|
|
100
117
|
let end = expected.length;
|
|
@@ -121,7 +138,7 @@ export function substituteSummary(
|
|
|
121
138
|
export function summarySlot(model: Model<any>, snapshot: PreparedRequest, summaryPrefix: TranscriptContext): number | undefined {
|
|
122
139
|
const mapped = mappedPrefix(snapshot, summaryPrefix);
|
|
123
140
|
if (!mapped) return;
|
|
124
|
-
const prefix =
|
|
141
|
+
const prefix = serializePrefix(model, snapshot, mapped);
|
|
125
142
|
const index = promptCount(prefix);
|
|
126
143
|
if (prefix.length !== index + 1 || !equal(snapshot.portableInput.slice(0, prefix.length), prefix)) return;
|
|
127
144
|
return index;
|