pi-spark 0.24.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/docs/background-model-calls-and-openai-codex-sessions.md +2 -0
- package/package.json +9 -9
- package/src/features/editor/index.ts +0 -5
- package/src/features/recap/idle.ts +56 -79
- package/src/features/recap/index.ts +13 -31
- package/src/features/recap/manager.ts +11 -15
- package/src/features/title/index.ts +2 -2
- package/src/features/title/manager.ts +7 -12
- package/src/utils/model.ts +5 -6
- package/src/utils/usage.ts +2 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.26.0](https://github.com/zlliang/pi-spark/compare/v0.25.0...v0.26.0) (2026-09-20)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### ⚠ BREAKING CHANGES
|
|
7
|
+
|
|
8
|
+
* Require Pi 0.86.0 or later.
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
|
|
12
|
+
* adopt Pi 0.86 model and usage APIs ([5d5868d](https://github.com/zlliang/pi-spark/commit/5d5868defdad303a0b8a9761b013886a198c268d))
|
|
13
|
+
|
|
14
|
+
## [0.25.0](https://github.com/zlliang/pi-spark/compare/v0.24.0...v0.25.0) (2026-09-14)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Bug Fixes
|
|
18
|
+
|
|
19
|
+
* refine recap lifecycle and feature event handling ([4bb7474](https://github.com/zlliang/pi-spark/commit/4bb7474b3d04a6d92179b1873afdd6155e5e0368))
|
|
20
|
+
|
|
3
21
|
## [0.24.0](https://github.com/zlliang/pi-spark/compare/v0.23.1...v0.24.0) (2026-09-14)
|
|
4
22
|
|
|
5
23
|
|
|
@@ -73,3 +73,5 @@ The independent-session design preserves the main thread's WebSocket reuse, `pre
|
|
|
73
73
|
Cache admission remains provider-controlled and noisy. In repeated GPT-5.6 Luna title A/B probes, isolated background calls caused no continuation errors and showed no negative main-thread cache trend. Main second-turn latency remained around the normal control range instead of paying the reconnect penalty seen when the shared main socket was closed.
|
|
74
74
|
|
|
75
75
|
Use the [`measuring-model-cache`](../.agents/skills/measuring-model-cache/SKILL.md) skill to repeat the comparison after changes to Pi, providers, session handling, title, recap, or other background model features.
|
|
76
|
+
|
|
77
|
+
**Update Sep 19, 2026:** Background calls now use Pi 0.86's `ctx.modelRegistry.streamSimple(...).result()` to support extension-registered providers and resolve authentication automatically. Independent Codex UUIDv7 sessions and cleanup in `finally` remain unchanged.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-spark",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
4
4
|
"description": "Pi package that polishes your daily experience",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-coding-agent",
|
|
@@ -44,17 +44,17 @@
|
|
|
44
44
|
"zod": "^4.4.3"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
|
-
"@earendil-works/pi-agent-core": ">=0.
|
|
48
|
-
"@earendil-works/pi-ai": ">=0.
|
|
49
|
-
"@earendil-works/pi-coding-agent": ">=0.
|
|
50
|
-
"@earendil-works/pi-tui": ">=0.
|
|
47
|
+
"@earendil-works/pi-agent-core": ">=0.86.0",
|
|
48
|
+
"@earendil-works/pi-ai": ">=0.86.0",
|
|
49
|
+
"@earendil-works/pi-coding-agent": ">=0.86.0",
|
|
50
|
+
"@earendil-works/pi-tui": ">=0.86.0",
|
|
51
51
|
"typebox": "*"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
|
-
"@earendil-works/pi-agent-core": ">=0.
|
|
55
|
-
"@earendil-works/pi-ai": ">=0.
|
|
56
|
-
"@earendil-works/pi-coding-agent": ">=0.
|
|
57
|
-
"@earendil-works/pi-tui": ">=0.
|
|
54
|
+
"@earendil-works/pi-agent-core": ">=0.86.0",
|
|
55
|
+
"@earendil-works/pi-ai": ">=0.86.0",
|
|
56
|
+
"@earendil-works/pi-coding-agent": ">=0.86.0",
|
|
57
|
+
"@earendil-works/pi-tui": ">=0.86.0",
|
|
58
58
|
"@types/node": "*",
|
|
59
59
|
"typebox": "*",
|
|
60
60
|
"typescript": "^7.0.2"
|
|
@@ -168,11 +168,6 @@ export function registerEditor(pi: ExtensionAPI, events: EventCollector): void {
|
|
|
168
168
|
editor?.setWorkingMessage("Running tools");
|
|
169
169
|
});
|
|
170
170
|
|
|
171
|
-
pi.on("tool_call", (event) => {
|
|
172
|
-
runningToolCallIds.add(event.toolCallId);
|
|
173
|
-
editor?.setWorkingMessage("Running tools");
|
|
174
|
-
});
|
|
175
|
-
|
|
176
171
|
pi.on("tool_execution_end", (event) => {
|
|
177
172
|
runningToolCallIds.delete(event.toolCallId);
|
|
178
173
|
editor?.setWorkingMessage(runningToolCallIds.size > 0 ? "Running tools" : undefined);
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import parseDuration from "parse-duration";
|
|
2
2
|
import * as z from "zod";
|
|
3
|
+
import { convertToLlm, sessionEntryToContextMessages } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
|
|
5
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
6
|
|
|
4
7
|
const MIN_IDLE_MS = 5_000;
|
|
5
8
|
|
|
@@ -17,105 +20,79 @@ export const idleTimeoutSchema = z
|
|
|
17
20
|
})
|
|
18
21
|
.pipe(z.number().min(MIN_IDLE_MS));
|
|
19
22
|
|
|
20
|
-
type IdleTimeout = z.infer<typeof idleTimeoutSchema>;
|
|
21
|
-
type IdleHash = string | number | boolean;
|
|
22
|
-
|
|
23
23
|
const DEFAULT_IDLE_MS = 5 * 60 * 1000;
|
|
24
24
|
const POLL_MS = 1_000;
|
|
25
25
|
|
|
26
|
-
export class IdleListener
|
|
27
|
-
private
|
|
28
|
-
private
|
|
29
|
-
private
|
|
30
|
-
|
|
26
|
+
export class IdleListener {
|
|
27
|
+
private entered = false;
|
|
28
|
+
private lastEditorText: string | undefined;
|
|
29
|
+
private stableSince = 0;
|
|
31
30
|
private idleMs: number;
|
|
32
|
-
private pollMs: number;
|
|
33
|
-
private idleTimer: ReturnType<typeof setTimeout> | undefined;
|
|
34
31
|
private pollTimer: ReturnType<typeof setInterval> | undefined;
|
|
35
32
|
|
|
36
|
-
private enterCallbacks: Set<(ctx:
|
|
37
|
-
private
|
|
33
|
+
private enterCallbacks: Set<(ctx: ExtensionContext) => void> = new Set();
|
|
34
|
+
private resetCallbacks: Set<(ctx: ExtensionContext) => void> = new Set();
|
|
38
35
|
|
|
39
|
-
constructor(
|
|
40
|
-
this.computeStateHash = computeStateHash;
|
|
36
|
+
constructor(idleMs: number = DEFAULT_IDLE_MS) {
|
|
41
37
|
this.idleMs = idleMs;
|
|
42
|
-
this.pollMs = pollMs;
|
|
43
38
|
}
|
|
44
39
|
|
|
45
|
-
on(event: "enter" | "
|
|
46
|
-
const
|
|
47
|
-
|
|
40
|
+
on(event: "enter" | "reset", callback: (ctx: ExtensionContext) => void): () => void {
|
|
41
|
+
const callbacks = event === "enter" ? this.enterCallbacks : this.resetCallbacks;
|
|
42
|
+
callbacks.add(callback);
|
|
48
43
|
|
|
49
|
-
return () =>
|
|
44
|
+
return () => callbacks.delete(callback);
|
|
50
45
|
}
|
|
51
46
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
this.
|
|
56
|
-
this.
|
|
57
|
-
|
|
58
|
-
this.lastStateHash = this.computeStateHash(ctx);
|
|
59
|
-
this.reset(ctx);
|
|
60
|
-
|
|
61
|
-
this.pollTimer = setInterval(() => {
|
|
62
|
-
const current = this.computeStateHash(ctx);
|
|
63
|
-
if (current === this.lastStateHash) return;
|
|
47
|
+
/** Clears any recap and starts a fresh idle period, keeping observation active while Pi is busy. */
|
|
48
|
+
reset(ctx: ExtensionContext): void {
|
|
49
|
+
this.entered = false;
|
|
50
|
+
this.lastEditorText = undefined;
|
|
51
|
+
this.resetCallbacks.forEach((callback) => callback(ctx));
|
|
64
52
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
enter(ctx: T): void {
|
|
71
|
-
if (this.state === "idle") return;
|
|
72
|
-
|
|
73
|
-
this.stop();
|
|
74
|
-
this.state = "idle";
|
|
75
|
-
|
|
76
|
-
this.enterCallbacks.forEach((callback) => callback(ctx));
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** Emits on every wake signal, even when the listener is already active. */
|
|
80
|
-
wake(ctx: T): void {
|
|
81
|
-
this.stop();
|
|
82
|
-
this.state = "active";
|
|
83
|
-
|
|
84
|
-
this.wakeCallbacks.forEach((callback) => callback(ctx));
|
|
53
|
+
this.check(ctx);
|
|
54
|
+
if (!this.pollTimer) {
|
|
55
|
+
this.pollTimer = setInterval(() => this.check(ctx), POLL_MS);
|
|
56
|
+
}
|
|
85
57
|
}
|
|
86
58
|
|
|
87
59
|
dispose(): void {
|
|
88
|
-
this.
|
|
89
|
-
|
|
60
|
+
if (this.pollTimer) {
|
|
61
|
+
clearInterval(this.pollTimer);
|
|
62
|
+
this.pollTimer = undefined;
|
|
63
|
+
}
|
|
90
64
|
|
|
65
|
+
this.entered = false;
|
|
66
|
+
this.lastEditorText = undefined;
|
|
91
67
|
this.enterCallbacks.clear();
|
|
92
|
-
this.
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
private reset(ctx: T): void {
|
|
96
|
-
this.clearIdleTimer();
|
|
97
|
-
this.idleTimer = setTimeout(() => {
|
|
98
|
-
this.idleTimer = undefined;
|
|
99
|
-
this.enter(ctx);
|
|
100
|
-
}, this.idleMs);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
private stop(): void {
|
|
104
|
-
this.clearIdleTimer();
|
|
105
|
-
this.clearPollTimer();
|
|
68
|
+
this.resetCallbacks.clear();
|
|
106
69
|
}
|
|
107
70
|
|
|
108
|
-
private
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
71
|
+
private check(ctx: ExtensionContext): void {
|
|
72
|
+
const canEnter = ctx.isIdle() && ctx.sessionManager.buildContextEntries().some((entry) => convertToLlm(sessionEntryToContextMessages(entry)).length > 0);
|
|
73
|
+
if (!canEnter) {
|
|
74
|
+
this.lastEditorText = undefined;
|
|
75
|
+
if (this.entered) {
|
|
76
|
+
this.entered = false;
|
|
77
|
+
this.resetCallbacks.forEach((callback) => callback(ctx));
|
|
78
|
+
}
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Keep the recap while the user edits; only a reset or Pi becoming busy clears it.
|
|
83
|
+
if (this.entered) return;
|
|
84
|
+
|
|
85
|
+
const editorText = ctx.ui.getEditorText();
|
|
86
|
+
const now = Date.now();
|
|
87
|
+
if (editorText !== this.lastEditorText) {
|
|
88
|
+
this.lastEditorText = editorText;
|
|
89
|
+
this.stableSince = now;
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (now - this.stableSince >= this.idleMs) {
|
|
94
|
+
this.entered = true;
|
|
95
|
+
this.enterCallbacks.forEach((callback) => callback(ctx));
|
|
96
|
+
}
|
|
120
97
|
}
|
|
121
98
|
}
|
|
@@ -2,13 +2,13 @@ import { IdleListener } from "./idle";
|
|
|
2
2
|
import { RecapManager } from "./manager";
|
|
3
3
|
import { loadConfig } from "../../config";
|
|
4
4
|
|
|
5
|
-
import type { ExtensionAPI
|
|
5
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
|
|
7
7
|
export function registerRecap(pi: ExtensionAPI): void {
|
|
8
|
-
let idleListener: IdleListener
|
|
8
|
+
let idleListener: IdleListener | undefined = undefined;
|
|
9
9
|
let recapManager: RecapManager | undefined = undefined;
|
|
10
10
|
|
|
11
|
-
pi.on("session_start", (
|
|
11
|
+
pi.on("session_start", (_event, ctx) => {
|
|
12
12
|
const config = loadConfig(ctx).recap;
|
|
13
13
|
if (ctx.mode !== "tui" || !config) return;
|
|
14
14
|
|
|
@@ -16,52 +16,34 @@ export function registerRecap(pi: ExtensionAPI): void {
|
|
|
16
16
|
|
|
17
17
|
pi.registerCommand("recap", {
|
|
18
18
|
description: "Generate a short recap of the current session",
|
|
19
|
-
handler: async () => await recapManager?.run(ctx, { force: true }),
|
|
19
|
+
handler: async (_args, ctx) => await recapManager?.run(ctx, { force: true }),
|
|
20
20
|
});
|
|
21
21
|
|
|
22
|
-
idleListener = new IdleListener(
|
|
23
|
-
idleListener.on("enter", (
|
|
24
|
-
idleListener.on("
|
|
22
|
+
idleListener = new IdleListener(config.idle);
|
|
23
|
+
idleListener.on("enter", (ctx) => recapManager?.run(ctx));
|
|
24
|
+
idleListener.on("reset", (ctx) => recapManager?.clear(ctx));
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
idleListener.watch(ctx);
|
|
28
|
-
}
|
|
26
|
+
idleListener.reset(ctx);
|
|
29
27
|
});
|
|
30
28
|
|
|
31
29
|
pi.on("input", (_event, ctx) => {
|
|
32
|
-
idleListener?.
|
|
30
|
+
idleListener?.reset(ctx);
|
|
33
31
|
});
|
|
34
32
|
|
|
35
33
|
pi.on("user_bash", (_event, ctx) => {
|
|
36
|
-
idleListener?.
|
|
34
|
+
idleListener?.reset(ctx);
|
|
37
35
|
});
|
|
38
36
|
|
|
39
37
|
pi.on("agent_start", (_event, ctx) => {
|
|
40
|
-
idleListener?.
|
|
38
|
+
idleListener?.reset(ctx);
|
|
41
39
|
});
|
|
42
40
|
|
|
43
41
|
pi.on("session_before_compact", (_event, ctx) => {
|
|
44
|
-
idleListener?.
|
|
42
|
+
idleListener?.reset(ctx);
|
|
45
43
|
});
|
|
46
44
|
|
|
47
45
|
pi.on("session_before_tree", (_event, ctx) => {
|
|
48
|
-
idleListener?.
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
pi.on("agent_settled", (_event, ctx) => {
|
|
52
|
-
idleListener?.watch(ctx);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
pi.on("session_compact", (_event, ctx) => {
|
|
56
|
-
idleListener?.watch(ctx);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
pi.on("session_compact_failed", (_event, ctx) => {
|
|
60
|
-
idleListener?.watch(ctx);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
pi.on("session_tree", (_event, ctx) => {
|
|
64
|
-
idleListener?.watch(ctx);
|
|
46
|
+
idleListener?.reset(ctx);
|
|
65
47
|
});
|
|
66
48
|
|
|
67
49
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { convertToLlm, serializeConversation } from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import { convertToLlm, serializeConversation, sessionEntryToContextMessages } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
|
|
3
3
|
import { clearRecapWidget, setRecapLoadingWidget, setRecapTextWidget } from "./widget";
|
|
4
4
|
import { sanitizeText } from "../../utils/format";
|
|
5
5
|
import { completeBackground, resolveModelSettings } from "../../utils/model";
|
|
6
6
|
|
|
7
|
-
import type { Api, Model, ModelThinkingLevel,
|
|
7
|
+
import type { Api, Model, ModelThinkingLevel, Usage } from "@earendil-works/pi-ai";
|
|
8
8
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
9
9
|
import type { RecapConfig } from "./config";
|
|
10
10
|
|
|
@@ -32,7 +32,7 @@ export class RecapManager {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
async run(ctx: ExtensionContext, options: { force?: boolean } = {}): Promise<void> {
|
|
35
|
-
if (this.active
|
|
35
|
+
if (!options.force && (this.active || this.inflight || !ctx.isIdle())) return;
|
|
36
36
|
|
|
37
37
|
this.cancelInflight();
|
|
38
38
|
const controller = new AbortController();
|
|
@@ -41,6 +41,7 @@ export class RecapManager {
|
|
|
41
41
|
try {
|
|
42
42
|
const modelSettings = await resolveModelSettings(ctx, this.config, "recap");
|
|
43
43
|
if (controller.signal.aborted || this.inflight !== controller || !modelSettings) return;
|
|
44
|
+
if (!options.force && !ctx.isIdle()) return;
|
|
44
45
|
|
|
45
46
|
const { model, thinkingLevel, warning } = modelSettings;
|
|
46
47
|
|
|
@@ -81,23 +82,18 @@ export class RecapManager {
|
|
|
81
82
|
}
|
|
82
83
|
|
|
83
84
|
private async generate(ctx: ExtensionContext, model: Model<Api>, thinkingLevel: ModelThinkingLevel, signal: AbortSignal): Promise<{ content: string; usage: Usage }> {
|
|
84
|
-
const
|
|
85
|
-
if (!auth.ok) throw new Error(auth.error);
|
|
86
|
-
|
|
87
|
-
const options: SimpleStreamOptions = { maxTokens: MAX_TOKENS, signal };
|
|
88
|
-
if (auth.apiKey) options.apiKey = auth.apiKey;
|
|
89
|
-
if (auth.headers) options.headers = auth.headers;
|
|
90
|
-
if (auth.env) options.env = auth.env;
|
|
91
|
-
if (thinkingLevel !== "off") options.reasoning = thinkingLevel;
|
|
92
|
-
|
|
93
|
-
const response = await completeBackground(model, {
|
|
85
|
+
const response = await completeBackground(ctx, model, {
|
|
94
86
|
systemPrompt: SYSTEM_PROMPT,
|
|
95
87
|
messages: [{
|
|
96
88
|
role: "user",
|
|
97
89
|
content: [{ type: "text", text: this.buildPrompt(ctx) }],
|
|
98
90
|
timestamp: Date.now(),
|
|
99
91
|
}],
|
|
100
|
-
},
|
|
92
|
+
}, {
|
|
93
|
+
maxTokens: MAX_TOKENS,
|
|
94
|
+
signal,
|
|
95
|
+
reasoning: thinkingLevel === "off" ? undefined : thinkingLevel,
|
|
96
|
+
});
|
|
101
97
|
|
|
102
98
|
if (response.stopReason === "error") {
|
|
103
99
|
throw new Error(response.errorMessage ?? "Recap generation failed");
|
|
@@ -113,7 +109,7 @@ export class RecapManager {
|
|
|
113
109
|
}
|
|
114
110
|
|
|
115
111
|
private buildPrompt(ctx: ExtensionContext): string {
|
|
116
|
-
const messages = ctx.sessionManager.
|
|
112
|
+
const messages = ctx.sessionManager.buildContextEntries().flatMap(sessionEntryToContextMessages);
|
|
117
113
|
const text = serializeConversation(convertToLlm(messages));
|
|
118
114
|
const conversation = text.length > MAX_CONVERSATION_CHARS ? text.slice(-MAX_CONVERSATION_CHARS) : text;
|
|
119
115
|
|
|
@@ -16,12 +16,12 @@ export function registerTitle(pi: ExtensionAPI): void {
|
|
|
16
16
|
// Generate the title after the first completed turn, without waiting for the full agent run.
|
|
17
17
|
// The manager skips sessions that already have a name and prevents concurrent attempts.
|
|
18
18
|
pi.on("turn_end", (_event, ctx) => {
|
|
19
|
-
|
|
19
|
+
titleManager?.run(ctx);
|
|
20
20
|
});
|
|
21
21
|
|
|
22
22
|
// Fall back to settlement if the run ends without a completed turn or an early attempt fails.
|
|
23
23
|
pi.on("agent_settled", (_event, ctx) => {
|
|
24
|
-
|
|
24
|
+
titleManager?.run(ctx);
|
|
25
25
|
});
|
|
26
26
|
|
|
27
27
|
pi.on("session_shutdown", () => {
|
|
@@ -3,7 +3,7 @@ import { convertToLlm, serializeConversation } from "@earendil-works/pi-coding-a
|
|
|
3
3
|
import { sanitizeText } from "../../utils/format";
|
|
4
4
|
import { completeBackground, resolveModelSettings } from "../../utils/model";
|
|
5
5
|
|
|
6
|
-
import type { Api, Model, ModelThinkingLevel,
|
|
6
|
+
import type { Api, Model, ModelThinkingLevel, Usage } from "@earendil-works/pi-ai";
|
|
7
7
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import type { TitleConfig } from "./config";
|
|
9
9
|
|
|
@@ -64,23 +64,18 @@ export class TitleManager {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
private async generate(ctx: ExtensionContext, model: Model<Api>, thinkingLevel: ModelThinkingLevel, signal: AbortSignal): Promise<{ content: string; usage: Usage }> {
|
|
67
|
-
const
|
|
68
|
-
if (!auth.ok) throw new Error(auth.error);
|
|
69
|
-
|
|
70
|
-
const options: SimpleStreamOptions = { maxTokens: MAX_TOKENS, signal };
|
|
71
|
-
if (auth.apiKey) options.apiKey = auth.apiKey;
|
|
72
|
-
if (auth.headers) options.headers = auth.headers;
|
|
73
|
-
if (auth.env) options.env = auth.env;
|
|
74
|
-
if (thinkingLevel !== "off") options.reasoning = thinkingLevel;
|
|
75
|
-
|
|
76
|
-
const response = await completeBackground(model, {
|
|
67
|
+
const response = await completeBackground(ctx, model, {
|
|
77
68
|
systemPrompt: SYSTEM_PROMPT,
|
|
78
69
|
messages: [{
|
|
79
70
|
role: "user",
|
|
80
71
|
content: [{ type: "text", text: this.buildPrompt(ctx) }],
|
|
81
72
|
timestamp: Date.now(),
|
|
82
73
|
}],
|
|
83
|
-
},
|
|
74
|
+
}, {
|
|
75
|
+
maxTokens: MAX_TOKENS,
|
|
76
|
+
signal,
|
|
77
|
+
reasoning: thinkingLevel === "off" ? undefined : thinkingLevel,
|
|
78
|
+
});
|
|
84
79
|
|
|
85
80
|
if (response.stopReason === "error") {
|
|
86
81
|
throw new Error(response.errorMessage ?? "Title generation failed");
|
package/src/utils/model.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { uuidv7 } from "@earendil-works/pi-agent-core";
|
|
2
2
|
import { clampThinkingLevel, cleanupSessionResources } from "@earendil-works/pi-ai";
|
|
3
|
-
import { completeSimple } from "@earendil-works/pi-ai/compat";
|
|
4
3
|
|
|
5
4
|
import { formatModel } from "./format";
|
|
6
5
|
|
|
7
|
-
import type { Api, Model, ModelThinkingLevel } from "@earendil-works/pi-ai";
|
|
6
|
+
import type { Api, AssistantMessage, Context, Model, ModelsSimpleStreamOptions, ModelThinkingLevel } from "@earendil-works/pi-ai";
|
|
8
7
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
9
8
|
import type { OptionalModelConfig } from "../config/model";
|
|
10
9
|
|
|
@@ -30,17 +29,17 @@ type ThinkingLevelSelection = {
|
|
|
30
29
|
* Completes a one-shot background request, using an isolated session for OpenAI Codex models.
|
|
31
30
|
* See [the investigation](../../docs/background-model-calls-and-openai-codex-sessions.md).
|
|
32
31
|
*/
|
|
33
|
-
export
|
|
34
|
-
if (model.api !== "openai-codex-responses") return
|
|
32
|
+
export async function completeBackground(ctx: ExtensionContext, model: Model<Api>, context: Context, options?: ModelsSimpleStreamOptions): Promise<AssistantMessage> {
|
|
33
|
+
if (model.api !== "openai-codex-responses") return ctx.modelRegistry.streamSimple(model, context, options).result();
|
|
35
34
|
|
|
36
35
|
const sessionId = uuidv7();
|
|
37
36
|
|
|
38
37
|
try {
|
|
39
|
-
return await
|
|
38
|
+
return await ctx.modelRegistry.streamSimple(model, context, { ...options, sessionId }).result();
|
|
40
39
|
} finally {
|
|
41
40
|
cleanupSessionResources(sessionId);
|
|
42
41
|
}
|
|
43
|
-
}
|
|
42
|
+
}
|
|
44
43
|
|
|
45
44
|
/**
|
|
46
45
|
* Resolves the model and thinking level for a background feature (recap, title, ...).
|
package/src/utils/usage.ts
CHANGED
|
@@ -33,7 +33,7 @@ export function isUsage(value: unknown): value is Usage {
|
|
|
33
33
|
* - `message` carries them on `message`.
|
|
34
34
|
* - `custom` on `data`.
|
|
35
35
|
* - `custom_message` on `details`.
|
|
36
|
-
* - `compaction
|
|
36
|
+
* - `usage`, `compaction`, and `branch_summary` directly on the entry.
|
|
37
37
|
*
|
|
38
38
|
* Other entry types carry no usage. Returns `undefined` when the resolved field has no `usage`.
|
|
39
39
|
*/
|
|
@@ -42,8 +42,7 @@ export function getEntryUsage(entry: SessionEntry): Usage | undefined {
|
|
|
42
42
|
if (entry.type === "message") source = entry.message;
|
|
43
43
|
else if (entry.type === "custom") source = entry.data;
|
|
44
44
|
else if (entry.type === "custom_message") source = entry.details;
|
|
45
|
-
else
|
|
46
|
-
else return;
|
|
45
|
+
else source = entry;
|
|
47
46
|
|
|
48
47
|
if (typeof source !== "object" || source === null) return;
|
|
49
48
|
const data = source as { usage?: unknown };
|