@lll9p/pi-better-compaction 0.2.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 +21 -0
- package/README.md +183 -0
- package/index.ts +1 -0
- package/package.json +59 -0
- package/src/compact-client.ts +428 -0
- package/src/config.ts +157 -0
- package/src/debug.ts +165 -0
- package/src/details-store.ts +151 -0
- package/src/extension-runtime.ts +499 -0
- package/src/native-fallback.ts +149 -0
- package/src/payload-rewrite.ts +548 -0
- package/src/request-context-cache.ts +84 -0
- package/src/runtime.ts +250 -0
- package/src/serializer.ts +555 -0
- package/src/supported-environment.ts +16 -0
- package/src/types.ts +296 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 lll9p
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# pi-better-compaction
|
|
2
|
+
|
|
3
|
+
A Pi extension that upgrades compaction with two coordinated strategies:
|
|
4
|
+
|
|
5
|
+
1. **OpenAI Responses APIs** (`openai-responses`, `openai-codex-responses`) use the provider's
|
|
6
|
+
native `/responses/compact` endpoint, then replay the opaque compacted window on later
|
|
7
|
+
requests without patching Pi core.
|
|
8
|
+
2. **Every other API** (Anthropic, Gemini, etc.) runs Pi's own native compaction method,
|
|
9
|
+
optionally driven by a **dedicated compaction model** so you can summarize with a cheaper/faster
|
|
10
|
+
model than the one you are chatting with.
|
|
11
|
+
|
|
12
|
+
Everything fails open: if any step cannot proceed, the extension returns control to Pi's default
|
|
13
|
+
compaction so a compaction never breaks because of this extension.
|
|
14
|
+
|
|
15
|
+
## Requirements
|
|
16
|
+
|
|
17
|
+
- **Minimum Pi version:** `@earendil-works/pi-coding-agent >= 0.80.0`
|
|
18
|
+
|
|
19
|
+
This extension relies on `modelRegistry.getApiKeyAndHeaders(model)` and the exported native
|
|
20
|
+
`compact()` function.
|
|
21
|
+
|
|
22
|
+
## Behavior
|
|
23
|
+
|
|
24
|
+
The `session_before_compact` decision tree:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
session_before_compact
|
|
28
|
+
│
|
|
29
|
+
├─ config.enabled == false ───────────────────────► Pi default compaction
|
|
30
|
+
│
|
|
31
|
+
├─ current model API is a Responses API
|
|
32
|
+
│ │ (openai-responses / openai-codex-responses, narrowable via config)
|
|
33
|
+
│ ├─ POST /responses/compact
|
|
34
|
+
│ │ ├─ success ─────────────────────────────────► store opaque window + real summary
|
|
35
|
+
│ │ ├─ user aborted ─────────────────────────────► cancel
|
|
36
|
+
│ │ └─ failure (404 / network / malformed) ──────► fall through ▼
|
|
37
|
+
│ └─ (missing base URL / API key) ─────────────────► fall through ▼
|
|
38
|
+
│
|
|
39
|
+
├─ config.compactionModel is set and resolvable and ≠ current model
|
|
40
|
+
│ └─ run Pi's native compact() with that model ────► use its result
|
|
41
|
+
│
|
|
42
|
+
└─ otherwise ─────────────────────────────────────► Pi default compaction
|
|
43
|
+
(no model configured, or it equals the current model — Pi runs the
|
|
44
|
+
same native method itself, keeping its streaming progress UI)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
On the next supported Responses request after a native `/responses/compact`, the
|
|
48
|
+
`before_provider_request` hook rewrites Pi's summary-oriented replay into:
|
|
49
|
+
|
|
50
|
+
- fresh current prompt envelope
|
|
51
|
+
- stored opaque compacted window
|
|
52
|
+
- live post-compaction tail
|
|
53
|
+
|
|
54
|
+
Requests produced by the native-method fallback carry Pi's standard `{readFiles, modifiedFiles}`
|
|
55
|
+
details, so they replay through Pi's default path — no rewrite, no special handling.
|
|
56
|
+
|
|
57
|
+
### Selection is by API, not provider
|
|
58
|
+
|
|
59
|
+
Any provider speaking a Responses API gets a native compact attempt, including OpenAI-compatible
|
|
60
|
+
proxies with a custom `baseUrl`. If such an endpoint does not implement `/responses/compact`, the
|
|
61
|
+
request 404s and the extension fails through to the configured fallback model (or Pi default). To
|
|
62
|
+
avoid the probe entirely for one API, narrow `responsesCompactApis`.
|
|
63
|
+
|
|
64
|
+
## Install
|
|
65
|
+
|
|
66
|
+
From npm (recommended):
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
pi install npm:@lll9p/pi-better-compaction
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Try it for a single run without installing:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
pi -e npm:@lll9p/pi-better-compaction
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
From a checkout (development):
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
git clone https://github.com/lll9p/pi-better-compaction.git
|
|
82
|
+
cd pi-better-compaction
|
|
83
|
+
pi install .
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
After installation, run `/reload`.
|
|
87
|
+
|
|
88
|
+
## Configuration
|
|
89
|
+
|
|
90
|
+
Single source, merged over built-in defaults:
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
~/.pi/agent/extensions/pi-better-compaction/config.json
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
A missing file silently uses the defaults below. The extension never writes this file for you.
|
|
97
|
+
|
|
98
|
+
```json
|
|
99
|
+
{
|
|
100
|
+
"enabled": true,
|
|
101
|
+
|
|
102
|
+
"compactionModel": "openai/gpt-5.1-mini",
|
|
103
|
+
"compactionThinkingLevel": "off",
|
|
104
|
+
|
|
105
|
+
"responsesCompactApis": ["openai-responses", "openai-codex-responses"],
|
|
106
|
+
|
|
107
|
+
"notifyOnLoad": false,
|
|
108
|
+
"debug": false,
|
|
109
|
+
"logProviderPayloads": false,
|
|
110
|
+
"logCompactResponses": false,
|
|
111
|
+
"redactSensitiveData": true,
|
|
112
|
+
"artifactRoot": "~/.pi/agent/artifacts/pi-better-compaction"
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
| Key | Default | Description |
|
|
117
|
+
|-----|---------|-------------|
|
|
118
|
+
| `enabled` | `true` | Master switch. `false` → Pi default compaction everywhere. |
|
|
119
|
+
| `compactionModel` | *(unset)* | `"provider/model-id"` used for native-method fallback (non-Responses APIs, or when the compact endpoint fails). `null`/unset → use the current model via Pi's default path. The provider is split on the first `/`, so model ids may contain slashes (e.g. `"openrouter/deepseek/deepseek-chat"`). |
|
|
120
|
+
| `compactionThinkingLevel` | `"off"` | Thinking level passed to the native `compact()` fallback. One of `off, minimal, low, medium, high, xhigh, max`. |
|
|
121
|
+
| `responsesCompactApis` | both Responses APIs | Which Responses APIs use the compact endpoint. May only narrow the built-in set; unknown entries are ignored with a warning. |
|
|
122
|
+
| `notifyOnLoad` | `false` | Show a load notification in the TUI. |
|
|
123
|
+
| `debug` | `false` | Write lifecycle + compaction-event artifacts. |
|
|
124
|
+
| `logProviderPayloads` | `false` | Write `before_provider_request` payload artifacts. |
|
|
125
|
+
| `logCompactResponses` | `false` | Write compact endpoint request/response artifacts. |
|
|
126
|
+
| `redactSensitiveData` | `true` | Redact secrets in artifacts. Keep on. |
|
|
127
|
+
| `artifactRoot` | `~/.pi/agent/artifacts/pi-better-compaction` | Debug artifact root. `~/` and relative paths (resolved against the config dir) are supported. |
|
|
128
|
+
|
|
129
|
+
### Codex-aligned compact request
|
|
130
|
+
|
|
131
|
+
For Responses compaction, the extension mirrors the latest codex_rs `CompactionInput` fields
|
|
132
|
+
(`tools`, `parallel_tool_calls`, `reasoning`, `service_tier`, `prompt_cache_key`, `text`) by
|
|
133
|
+
capturing them from the most recent live provider request for the same model/session and attaching
|
|
134
|
+
them to the compact request body. When no such request has been seen yet, the compact request falls
|
|
135
|
+
back to the minimal `model` / `input` / `instructions` body.
|
|
136
|
+
|
|
137
|
+
## Debug artifacts
|
|
138
|
+
|
|
139
|
+
Written per session under:
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
<artifactRoot>/sessions/<session-id>/
|
|
143
|
+
├── provider-requests/
|
|
144
|
+
├── compact-responses/
|
|
145
|
+
├── compaction-events/
|
|
146
|
+
└── lifecycle/
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Troubleshooting flow:
|
|
150
|
+
|
|
151
|
+
1. set `debug: true` and `logCompactResponses: true` (keep `redactSensitiveData: true`)
|
|
152
|
+
2. `/reload`
|
|
153
|
+
3. run `/compact`, then send a follow-up message
|
|
154
|
+
4. inspect the newest artifact in the session directory
|
|
155
|
+
|
|
156
|
+
## Package structure
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
package-root/
|
|
160
|
+
├── index.ts # entrypoint declared in package.json
|
|
161
|
+
├── src/
|
|
162
|
+
│ ├── extension-runtime.ts # hook registration + compaction decision tree
|
|
163
|
+
│ ├── config.ts # config.json loader (single source + defaults)
|
|
164
|
+
│ ├── runtime.ts # API-based environment resolution + auth
|
|
165
|
+
│ ├── compact-client.ts # /responses/compact client + summary extraction
|
|
166
|
+
│ ├── native-fallback.ts # configured-model native compact() driver
|
|
167
|
+
│ ├── request-context-cache.ts # captures codex-aligned fields from live requests
|
|
168
|
+
│ ├── serializer.ts # Responses input serialization
|
|
169
|
+
│ ├── payload-rewrite.ts # native opaque-window replay rewrite
|
|
170
|
+
│ ├── details-store.ts # latest-valid native compaction lookup
|
|
171
|
+
│ ├── debug.ts # artifact writing + redaction
|
|
172
|
+
│ ├── supported-environment.ts # re-exports
|
|
173
|
+
│ └── types.ts # config + persisted native-compaction types
|
|
174
|
+
└── test/
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Tests
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
bun test
|
|
181
|
+
bun test --coverage --coverage-reporter=text --coverage-reporter=lcov
|
|
182
|
+
bun test ./test/pi-smoke.test.ts
|
|
183
|
+
```
|
package/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from "./src/extension-runtime.ts";
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lll9p/pi-better-compaction",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Better compaction for pi: native /responses/compact replay for OpenAI Responses APIs, plus a configurable compaction model driving pi's native summarization everywhere else.",
|
|
6
|
+
"author": "Lilin Lao",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/lll9p/pi-better-compaction.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/lll9p/pi-better-compaction/issues"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://github.com/lll9p/pi-better-compaction#readme",
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"pi-package",
|
|
21
|
+
"pi-extension",
|
|
22
|
+
"openai",
|
|
23
|
+
"compaction",
|
|
24
|
+
"codex",
|
|
25
|
+
"responses"
|
|
26
|
+
],
|
|
27
|
+
"pi": {
|
|
28
|
+
"extensions": [
|
|
29
|
+
"./index.ts"
|
|
30
|
+
]
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"index.ts",
|
|
34
|
+
"src/compact-client.ts",
|
|
35
|
+
"src/config.ts",
|
|
36
|
+
"src/debug.ts",
|
|
37
|
+
"src/details-store.ts",
|
|
38
|
+
"src/extension-runtime.ts",
|
|
39
|
+
"src/native-fallback.ts",
|
|
40
|
+
"src/payload-rewrite.ts",
|
|
41
|
+
"src/request-context-cache.ts",
|
|
42
|
+
"src/runtime.ts",
|
|
43
|
+
"src/serializer.ts",
|
|
44
|
+
"src/supported-environment.ts",
|
|
45
|
+
"src/types.ts",
|
|
46
|
+
"README.md",
|
|
47
|
+
"LICENSE"
|
|
48
|
+
],
|
|
49
|
+
"scripts": {
|
|
50
|
+
"test": "bun test",
|
|
51
|
+
"test:coverage": "bun test --coverage --coverage-reporter=text --coverage-reporter=lcov && node ./scripts/check-lcov.mjs ./coverage/lcov.info",
|
|
52
|
+
"test:pi": "bun test ./test/pi-smoke.test.ts"
|
|
53
|
+
},
|
|
54
|
+
"peerDependencies": {
|
|
55
|
+
"@earendil-works/pi-agent-core": "*",
|
|
56
|
+
"@earendil-works/pi-ai": "*",
|
|
57
|
+
"@earendil-works/pi-coding-agent": ">=0.80.0"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
import { writeDebugArtifact } from "./debug";
|
|
2
|
+
import type { NativeCompactionRuntime } from "./runtime";
|
|
3
|
+
import type { NativeCompactionRequestBody } from "./serializer";
|
|
4
|
+
import type { ArtifactContext, ExtensionConfig } from "./types";
|
|
5
|
+
|
|
6
|
+
const JSON_CONTENT_TYPE = "application/json";
|
|
7
|
+
|
|
8
|
+
type CompactResponseEnvelope = {
|
|
9
|
+
id?: string;
|
|
10
|
+
created_at?: number | string;
|
|
11
|
+
output: unknown[];
|
|
12
|
+
[key: string]: unknown;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type NativeCompactionClientFailureReason =
|
|
16
|
+
| "aborted"
|
|
17
|
+
| "network-error"
|
|
18
|
+
| "non-2xx"
|
|
19
|
+
| "empty-body"
|
|
20
|
+
| "invalid-json"
|
|
21
|
+
| "malformed-response"
|
|
22
|
+
| "empty-output";
|
|
23
|
+
|
|
24
|
+
export type NativeCompactionClientSuccess = {
|
|
25
|
+
ok: true;
|
|
26
|
+
status: number;
|
|
27
|
+
compactedWindow: unknown[];
|
|
28
|
+
compactResponseId?: string;
|
|
29
|
+
createdAt?: string;
|
|
30
|
+
/** Assistant summary text extracted from the compact output, for CompactionEntry.summary. */
|
|
31
|
+
summaryText?: string;
|
|
32
|
+
response: CompactResponseEnvelope;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type NativeCompactionClientFailure = {
|
|
36
|
+
ok: false;
|
|
37
|
+
reason: NativeCompactionClientFailureReason;
|
|
38
|
+
status?: number;
|
|
39
|
+
errorMessage?: string;
|
|
40
|
+
responseText?: string;
|
|
41
|
+
responseJson?: unknown;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type NativeCompactionClientResult = NativeCompactionClientSuccess | NativeCompactionClientFailure;
|
|
45
|
+
|
|
46
|
+
export type ExecuteNativeCompactionOptions = {
|
|
47
|
+
runtime: NativeCompactionRuntime;
|
|
48
|
+
request: NativeCompactionRequestBody;
|
|
49
|
+
signal?: AbortSignal;
|
|
50
|
+
settings?: ExtensionConfig;
|
|
51
|
+
context?: ArtifactContext;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
55
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isAbortError(error: unknown): boolean {
|
|
59
|
+
return (
|
|
60
|
+
(error instanceof DOMException && error.name === "AbortError") ||
|
|
61
|
+
(error instanceof Error && (error.name === "AbortError" || error.name === "ABORT_ERR"))
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function normalizeResponseTimestamp(value: unknown): string | undefined {
|
|
66
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
67
|
+
const milliseconds = value > 1_000_000_000_000 ? value : value * 1000;
|
|
68
|
+
return new Date(milliseconds).toISOString();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (typeof value !== "string") {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const trimmed = value.trim();
|
|
76
|
+
if (!trimmed) {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const parsed = Date.parse(trimmed);
|
|
81
|
+
return Number.isNaN(parsed) ? trimmed : new Date(parsed).toISOString();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function isCompactOutputItem(value: unknown): value is Record<string, unknown> {
|
|
85
|
+
return isRecord(value);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function isCompactResponseEnvelope(value: unknown): value is CompactResponseEnvelope {
|
|
89
|
+
return isRecord(value) && Array.isArray(value.output) && value.output.every(isCompactOutputItem);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Extract the assistant-authored summary text from the compacted window so the
|
|
94
|
+
* persisted CompactionEntry.summary carries real context. Without this, switching
|
|
95
|
+
* to a non-Responses model later would replay a meaningless placeholder.
|
|
96
|
+
*/
|
|
97
|
+
export function extractCompactedSummaryText(output: readonly unknown[]): string | undefined {
|
|
98
|
+
const texts: string[] = [];
|
|
99
|
+
for (const item of output) {
|
|
100
|
+
if (!isRecord(item) || item.type !== "message" || item.role !== "assistant" || !Array.isArray(item.content)) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
for (const block of item.content) {
|
|
104
|
+
if (isRecord(block) && block.type === "output_text" && typeof block.text === "string" && block.text.trim()) {
|
|
105
|
+
texts.push(block.text.trim());
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const joined = texts.join("\n\n").trim();
|
|
111
|
+
return joined.length > 0 ? joined : undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function decodeJwtPayload(token: string): Record<string, unknown> | undefined {
|
|
115
|
+
const parts = token.split(".");
|
|
116
|
+
if (parts.length !== 3) {
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const payloadText = Buffer.from(parts[1]!, "base64url").toString("utf8");
|
|
122
|
+
const payload = JSON.parse(payloadText);
|
|
123
|
+
return isRecord(payload) ? payload : undefined;
|
|
124
|
+
} catch {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function extractCodexAccountId(token: string): string | undefined {
|
|
130
|
+
const payload = decodeJwtPayload(token);
|
|
131
|
+
const authClaims = payload?.["https://api.openai.com/auth"];
|
|
132
|
+
if (!isRecord(authClaims)) {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const accountId = authClaims.chatgpt_account_id;
|
|
137
|
+
return typeof accountId === "string" && accountId.trim().length > 0 ? accountId.trim() : undefined;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function buildCodexUserAgent(): string {
|
|
141
|
+
const platform = typeof process !== "undefined" ? process.platform : "browser";
|
|
142
|
+
const arch = typeof process !== "undefined" ? process.arch : "unknown";
|
|
143
|
+
return `pi (${platform}; ${arch})`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function toHeaders(runtime: NativeCompactionRuntime): Record<string, string> {
|
|
147
|
+
const headers = new Headers(runtime.currentModel.headers ?? {});
|
|
148
|
+
for (const [key, value] of Object.entries(runtime.headers ?? {})) {
|
|
149
|
+
headers.set(key, value);
|
|
150
|
+
}
|
|
151
|
+
headers.set("accept", JSON_CONTENT_TYPE);
|
|
152
|
+
headers.set("content-type", JSON_CONTENT_TYPE);
|
|
153
|
+
if (!headers.has("authorization")) {
|
|
154
|
+
headers.set("authorization", `Bearer ${runtime.apiKey}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (runtime.api === "openai-codex-responses") {
|
|
158
|
+
const accountId = extractCodexAccountId(runtime.apiKey);
|
|
159
|
+
if (accountId) {
|
|
160
|
+
headers.set("chatgpt-account-id", accountId);
|
|
161
|
+
}
|
|
162
|
+
headers.set("originator", "pi");
|
|
163
|
+
headers.set("user-agent", buildCodexUserAgent());
|
|
164
|
+
headers.set("openai-beta", "responses=experimental");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return Object.fromEntries(headers.entries());
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function writeCompactArtifact(
|
|
171
|
+
data: unknown,
|
|
172
|
+
settings: ExtensionConfig | undefined,
|
|
173
|
+
context: ArtifactContext | undefined,
|
|
174
|
+
): void {
|
|
175
|
+
if (!settings || !context) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
writeDebugArtifact("compact-response", data, settings, context);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export async function executeNativeCompaction(
|
|
183
|
+
options: ExecuteNativeCompactionOptions,
|
|
184
|
+
): Promise<NativeCompactionClientResult> {
|
|
185
|
+
const { runtime, request, signal, settings, context } = options;
|
|
186
|
+
const headers = toHeaders(runtime);
|
|
187
|
+
|
|
188
|
+
if (signal?.aborted) {
|
|
189
|
+
const aborted: NativeCompactionClientFailure = {
|
|
190
|
+
ok: false,
|
|
191
|
+
reason: "aborted",
|
|
192
|
+
};
|
|
193
|
+
writeCompactArtifact(
|
|
194
|
+
{
|
|
195
|
+
request: {
|
|
196
|
+
url: runtime.compactUrl,
|
|
197
|
+
headers,
|
|
198
|
+
body: request,
|
|
199
|
+
},
|
|
200
|
+
outcome: aborted,
|
|
201
|
+
},
|
|
202
|
+
settings,
|
|
203
|
+
context,
|
|
204
|
+
);
|
|
205
|
+
return aborted;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
const response = await fetch(runtime.compactUrl, {
|
|
210
|
+
method: "POST",
|
|
211
|
+
headers,
|
|
212
|
+
body: JSON.stringify(request),
|
|
213
|
+
signal,
|
|
214
|
+
});
|
|
215
|
+
const responseText = await response.text();
|
|
216
|
+
const responseHeaders: Record<string, string> = {};
|
|
217
|
+
response.headers.forEach((value, key) => {
|
|
218
|
+
responseHeaders[key] = value;
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
if (!response.ok) {
|
|
222
|
+
let responseJson: unknown;
|
|
223
|
+
if (responseText.trim().length > 0) {
|
|
224
|
+
try {
|
|
225
|
+
responseJson = JSON.parse(responseText);
|
|
226
|
+
} catch {
|
|
227
|
+
responseJson = undefined;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const failure: NativeCompactionClientFailure = {
|
|
232
|
+
ok: false,
|
|
233
|
+
reason: "non-2xx",
|
|
234
|
+
status: response.status,
|
|
235
|
+
responseText: responseText || undefined,
|
|
236
|
+
responseJson,
|
|
237
|
+
};
|
|
238
|
+
writeCompactArtifact(
|
|
239
|
+
{
|
|
240
|
+
request: {
|
|
241
|
+
url: runtime.compactUrl,
|
|
242
|
+
headers,
|
|
243
|
+
body: request,
|
|
244
|
+
},
|
|
245
|
+
response: {
|
|
246
|
+
status: response.status,
|
|
247
|
+
headers: responseHeaders,
|
|
248
|
+
body: responseJson ?? responseText,
|
|
249
|
+
},
|
|
250
|
+
outcome: failure,
|
|
251
|
+
},
|
|
252
|
+
settings,
|
|
253
|
+
context,
|
|
254
|
+
);
|
|
255
|
+
return failure;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (!responseText.trim()) {
|
|
259
|
+
const failure: NativeCompactionClientFailure = {
|
|
260
|
+
ok: false,
|
|
261
|
+
reason: "empty-body",
|
|
262
|
+
status: response.status,
|
|
263
|
+
};
|
|
264
|
+
writeCompactArtifact(
|
|
265
|
+
{
|
|
266
|
+
request: {
|
|
267
|
+
url: runtime.compactUrl,
|
|
268
|
+
headers,
|
|
269
|
+
body: request,
|
|
270
|
+
},
|
|
271
|
+
response: {
|
|
272
|
+
status: response.status,
|
|
273
|
+
headers: responseHeaders,
|
|
274
|
+
body: responseText,
|
|
275
|
+
},
|
|
276
|
+
outcome: failure,
|
|
277
|
+
},
|
|
278
|
+
settings,
|
|
279
|
+
context,
|
|
280
|
+
);
|
|
281
|
+
return failure;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
let parsed: unknown;
|
|
285
|
+
try {
|
|
286
|
+
parsed = JSON.parse(responseText);
|
|
287
|
+
} catch (error) {
|
|
288
|
+
const failure: NativeCompactionClientFailure = {
|
|
289
|
+
ok: false,
|
|
290
|
+
reason: "invalid-json",
|
|
291
|
+
status: response.status,
|
|
292
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
293
|
+
responseText,
|
|
294
|
+
};
|
|
295
|
+
writeCompactArtifact(
|
|
296
|
+
{
|
|
297
|
+
request: {
|
|
298
|
+
url: runtime.compactUrl,
|
|
299
|
+
headers,
|
|
300
|
+
body: request,
|
|
301
|
+
},
|
|
302
|
+
response: {
|
|
303
|
+
status: response.status,
|
|
304
|
+
headers: responseHeaders,
|
|
305
|
+
body: responseText,
|
|
306
|
+
},
|
|
307
|
+
outcome: failure,
|
|
308
|
+
},
|
|
309
|
+
settings,
|
|
310
|
+
context,
|
|
311
|
+
);
|
|
312
|
+
return failure;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (!isCompactResponseEnvelope(parsed)) {
|
|
316
|
+
const failure: NativeCompactionClientFailure = {
|
|
317
|
+
ok: false,
|
|
318
|
+
reason: "malformed-response",
|
|
319
|
+
status: response.status,
|
|
320
|
+
responseJson: parsed,
|
|
321
|
+
};
|
|
322
|
+
writeCompactArtifact(
|
|
323
|
+
{
|
|
324
|
+
request: {
|
|
325
|
+
url: runtime.compactUrl,
|
|
326
|
+
headers,
|
|
327
|
+
body: request,
|
|
328
|
+
},
|
|
329
|
+
response: {
|
|
330
|
+
status: response.status,
|
|
331
|
+
headers: responseHeaders,
|
|
332
|
+
body: parsed,
|
|
333
|
+
},
|
|
334
|
+
outcome: failure,
|
|
335
|
+
},
|
|
336
|
+
settings,
|
|
337
|
+
context,
|
|
338
|
+
);
|
|
339
|
+
return failure;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (parsed.output.length === 0) {
|
|
343
|
+
const failure: NativeCompactionClientFailure = {
|
|
344
|
+
ok: false,
|
|
345
|
+
reason: "empty-output",
|
|
346
|
+
status: response.status,
|
|
347
|
+
responseJson: parsed,
|
|
348
|
+
};
|
|
349
|
+
writeCompactArtifact(
|
|
350
|
+
{
|
|
351
|
+
request: {
|
|
352
|
+
url: runtime.compactUrl,
|
|
353
|
+
headers,
|
|
354
|
+
body: request,
|
|
355
|
+
},
|
|
356
|
+
response: {
|
|
357
|
+
status: response.status,
|
|
358
|
+
headers: responseHeaders,
|
|
359
|
+
body: parsed,
|
|
360
|
+
},
|
|
361
|
+
outcome: failure,
|
|
362
|
+
},
|
|
363
|
+
settings,
|
|
364
|
+
context,
|
|
365
|
+
);
|
|
366
|
+
return failure;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const success: NativeCompactionClientSuccess = {
|
|
370
|
+
ok: true,
|
|
371
|
+
status: response.status,
|
|
372
|
+
compactedWindow: [...parsed.output],
|
|
373
|
+
compactResponseId: typeof parsed.id === "string" && parsed.id.trim() ? parsed.id.trim() : undefined,
|
|
374
|
+
createdAt: normalizeResponseTimestamp(parsed.created_at),
|
|
375
|
+
summaryText: extractCompactedSummaryText(parsed.output),
|
|
376
|
+
response: parsed,
|
|
377
|
+
};
|
|
378
|
+
writeCompactArtifact(
|
|
379
|
+
{
|
|
380
|
+
request: {
|
|
381
|
+
url: runtime.compactUrl,
|
|
382
|
+
headers,
|
|
383
|
+
body: request,
|
|
384
|
+
},
|
|
385
|
+
response: {
|
|
386
|
+
status: response.status,
|
|
387
|
+
headers: responseHeaders,
|
|
388
|
+
body: parsed,
|
|
389
|
+
},
|
|
390
|
+
outcome: {
|
|
391
|
+
ok: true,
|
|
392
|
+
status: success.status,
|
|
393
|
+
compactResponseId: success.compactResponseId,
|
|
394
|
+
createdAt: success.createdAt,
|
|
395
|
+
compactedItems: success.compactedWindow.length,
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
settings,
|
|
399
|
+
context,
|
|
400
|
+
);
|
|
401
|
+
return success;
|
|
402
|
+
} catch (error) {
|
|
403
|
+
const failure: NativeCompactionClientFailure = isAbortError(error)
|
|
404
|
+
? {
|
|
405
|
+
ok: false,
|
|
406
|
+
reason: "aborted",
|
|
407
|
+
}
|
|
408
|
+
: {
|
|
409
|
+
ok: false,
|
|
410
|
+
reason: "network-error",
|
|
411
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
writeCompactArtifact(
|
|
415
|
+
{
|
|
416
|
+
request: {
|
|
417
|
+
url: runtime.compactUrl,
|
|
418
|
+
headers,
|
|
419
|
+
body: request,
|
|
420
|
+
},
|
|
421
|
+
outcome: failure,
|
|
422
|
+
},
|
|
423
|
+
settings,
|
|
424
|
+
context,
|
|
425
|
+
);
|
|
426
|
+
return failure;
|
|
427
|
+
}
|
|
428
|
+
}
|