pi-ui-extend 0.1.60 → 0.1.63
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/dist/app/commands/command-model-actions.js +8 -8
- package/dist/app/commands/command-session-actions.js +2 -2
- package/dist/app/input/autocomplete-controller.js +5 -11
- package/dist/app/input/prompt-enhancer-controller.js +4 -5
- package/dist/app/model/model-usage-status.js +16 -6
- package/dist/app/popup/menu-items-controller.js +5 -7
- package/dist/app/runtime.js +3 -3
- package/dist/bundled-extensions/session-title/index.d.ts +2 -1
- package/dist/bundled-extensions/session-title/index.js +4 -2
- package/dist/bundled-extensions/session-title/title-generation-compat.d.ts +17 -0
- package/dist/bundled-extensions/session-title/title-generation-compat.js +40 -0
- package/dist/bundled-extensions/session-title/title-generation.d.ts +16 -13
- package/dist/bundled-extensions/session-title/title-generation.js +10 -17
- package/external/pi-tools-suite/README.md +16 -0
- package/external/pi-tools-suite/package.json +9 -4
- package/external/pi-tools-suite/src/async-subagents/core/routing.ts +10 -6
- package/external/pi-tools-suite/src/async-subagents/core/ultrawork-auto.ts +9 -5
- package/external/pi-tools-suite/src/coding-discipline/index.ts +19 -4
- package/external/pi-tools-suite/src/dcp/auto-compress.ts +15 -6
- package/external/pi-tools-suite/src/dcp/commands.ts +4 -3
- package/external/pi-tools-suite/src/dcp/prompts.ts +3 -1
- package/external/pi-tools-suite/src/model-completion.ts +37 -0
- package/external/pi-tools-suite/src/tool-descriptions.ts +1 -0
- package/package.json +4 -4
|
@@ -63,8 +63,8 @@ export class ModelCommandActions {
|
|
|
63
63
|
const parsed = parseScopedModelRef(modelRef);
|
|
64
64
|
if (!parsed)
|
|
65
65
|
throw new Error("Model must use provider/model[:thinking] format");
|
|
66
|
-
runtime.services.
|
|
67
|
-
const model = runtime.services.
|
|
66
|
+
await runtime.services.modelRuntime.reloadConfig();
|
|
67
|
+
const model = runtime.services.modelRuntime.getModel(parsed.provider, parsed.modelId);
|
|
68
68
|
if (!model)
|
|
69
69
|
throw new Error(`Model not found: ${parsed.provider}/${parsed.modelId}`);
|
|
70
70
|
await this.runModelCommand(model);
|
|
@@ -97,8 +97,8 @@ export class ModelCommandActions {
|
|
|
97
97
|
const parsed = parseScopedModelRef(modelRef);
|
|
98
98
|
if (!parsed)
|
|
99
99
|
throw new Error("Model must use provider/model[:thinking] format");
|
|
100
|
-
runtime.services.
|
|
101
|
-
const model = runtime.services.
|
|
100
|
+
await runtime.services.modelRuntime.reloadConfig();
|
|
101
|
+
const model = runtime.services.modelRuntime.getModel(parsed.provider, parsed.modelId);
|
|
102
102
|
if (!model)
|
|
103
103
|
throw new Error(`Model not found: ${parsed.provider}/${parsed.modelId}`);
|
|
104
104
|
this.saveDefaultModel(modelRef);
|
|
@@ -118,8 +118,8 @@ export class ModelCommandActions {
|
|
|
118
118
|
const parsed = parseScopedModelRef(modelRef);
|
|
119
119
|
if (!parsed)
|
|
120
120
|
throw new Error("Model must use provider/model[:thinking] format, or run /autocomplete with no arguments to disable");
|
|
121
|
-
runtime.services.
|
|
122
|
-
const model = runtime.services.
|
|
121
|
+
await runtime.services.modelRuntime.reloadConfig();
|
|
122
|
+
const model = runtime.services.modelRuntime.getModel(parsed.provider, parsed.modelId);
|
|
123
123
|
if (!model)
|
|
124
124
|
throw new Error(`Model not found: ${parsed.provider}/${parsed.modelId}`);
|
|
125
125
|
const saved = savePixAutocompleteModel(modelRef);
|
|
@@ -199,10 +199,10 @@ export class ModelCommandActions {
|
|
|
199
199
|
const refs = value.split(/[,\s]+/).map((ref) => ref.trim()).filter(Boolean);
|
|
200
200
|
const scopedModels = [];
|
|
201
201
|
const invalidRefs = [];
|
|
202
|
-
runtime.services.
|
|
202
|
+
await runtime.services.modelRuntime.reloadConfig();
|
|
203
203
|
for (const ref of refs) {
|
|
204
204
|
const parsed = parseScopedModelRef(ref);
|
|
205
|
-
const model = parsed ? runtime.services.
|
|
205
|
+
const model = parsed ? runtime.services.modelRuntime.getModel(parsed.provider, parsed.modelId) : undefined;
|
|
206
206
|
if (!parsed || !model) {
|
|
207
207
|
invalidRefs.push(ref);
|
|
208
208
|
continue;
|
|
@@ -12,7 +12,7 @@ import { checkPixUpdate, formatPixUpdateCheck, parsePixUpdateArgs, pixUpdateUsag
|
|
|
12
12
|
import { createStartupInfoMessage } from "../cli/startup-info.js";
|
|
13
13
|
import { getCompleteSessionStats } from "../session/session-stats.js";
|
|
14
14
|
import { loadSessionTitleConfig } from "../../bundled-extensions/session-title/config.js";
|
|
15
|
-
import { fallbackSessionTitleFromInput, firstUserMessageText,
|
|
15
|
+
import { fallbackSessionTitleFromInput, firstUserMessageText, generateSessionTitleWithRuntime, sessionTitleModelRefs, } from "../../bundled-extensions/session-title/title-generation.js";
|
|
16
16
|
export class SessionCommandActions {
|
|
17
17
|
host;
|
|
18
18
|
constructor(host) {
|
|
@@ -116,7 +116,7 @@ export class SessionCommandActions {
|
|
|
116
116
|
if (config.enabled) {
|
|
117
117
|
for (const modelRef of modelRefs) {
|
|
118
118
|
for (let attempt = 0; attempt < config.generationAttempts; attempt++) {
|
|
119
|
-
generatedName = await
|
|
119
|
+
generatedName = await generateSessionTitleWithRuntime(firstPrompt.slice(0, config.maxInputChars).trim(), runtime.services.modelRuntime, config, modelRef, AbortSignal.timeout(config.timeoutMs));
|
|
120
120
|
if (generatedName)
|
|
121
121
|
break;
|
|
122
122
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { streamSimple } from "@earendil-works/pi-ai/compat";
|
|
2
1
|
import { isRecord } from "../guards.js";
|
|
3
2
|
import { parseModelRef } from "../model/model-ref.js";
|
|
4
3
|
const AUTOCOMPLETE_DEBOUNCE_MS = 350;
|
|
@@ -140,17 +139,14 @@ export class AppAutocompleteController {
|
|
|
140
139
|
}
|
|
141
140
|
export async function completeInputWithPi(runtime, draft, config, signal) {
|
|
142
141
|
const parsedModel = parseModelRef(config.modelRef);
|
|
143
|
-
const
|
|
144
|
-
let model =
|
|
142
|
+
const modelRuntime = runtime.services.modelRuntime;
|
|
143
|
+
let model = modelRuntime.getModel(parsedModel.provider, parsedModel.modelId);
|
|
145
144
|
if (!model) {
|
|
146
|
-
|
|
147
|
-
model =
|
|
145
|
+
await modelRuntime.reloadConfig();
|
|
146
|
+
model = modelRuntime.getModel(parsedModel.provider, parsedModel.modelId);
|
|
148
147
|
}
|
|
149
148
|
if (!model)
|
|
150
149
|
throw new Error(`Model not found: ${parsedModel.provider}/${parsedModel.modelId}`);
|
|
151
|
-
const auth = await registry.getApiKeyAndHeaders(model);
|
|
152
|
-
if (!auth.ok)
|
|
153
|
-
throw new Error(auth.error);
|
|
154
150
|
const timeoutMs = numberInRange(config.timeoutMs, AUTOCOMPLETE_TIMEOUT_MS, 250, 10_000);
|
|
155
151
|
const maxTokens = numberInRange(config.maxTokens, AUTOCOMPLETE_MAX_TOKENS, 8, 256);
|
|
156
152
|
const maxPromptTokens = numberInRange(config.maxPromptTokens, AUTOCOMPLETE_MAX_PROMPT_TOKENS, 256, 16_000);
|
|
@@ -165,12 +161,10 @@ export async function completeInputWithPi(runtime, draft, config, signal) {
|
|
|
165
161
|
let output = "";
|
|
166
162
|
let streamError;
|
|
167
163
|
try {
|
|
168
|
-
const stream = streamSimple(requestModel, {
|
|
164
|
+
const stream = modelRuntime.streamSimple(requestModel, {
|
|
169
165
|
systemPrompt: AUTOCOMPLETE_SYSTEM_PROMPT,
|
|
170
166
|
messages: [{ role: "user", content: prompt, timestamp: Date.now() }],
|
|
171
167
|
}, {
|
|
172
|
-
...(auth.apiKey === undefined ? {} : { apiKey: auth.apiKey }),
|
|
173
|
-
...(auth.headers === undefined ? {} : { headers: auth.headers }),
|
|
174
168
|
cacheRetention: "none",
|
|
175
169
|
maxRetryDelayMs: 0,
|
|
176
170
|
maxRetries: 0,
|
|
@@ -127,9 +127,8 @@ async function enhancePromptWithPi(runtime, draft, config) {
|
|
|
127
127
|
const services = await promptEnhancerPiDeps.createAgentSessionServices({
|
|
128
128
|
cwd: runtime.cwd,
|
|
129
129
|
agentDir: runtime.services.agentDir,
|
|
130
|
-
authStorage: runtime.services.authStorage,
|
|
131
130
|
settingsManager: runtime.services.settingsManager,
|
|
132
|
-
|
|
131
|
+
modelRuntime: runtime.services.modelRuntime,
|
|
133
132
|
resourceLoaderOptions: {
|
|
134
133
|
noExtensions: true,
|
|
135
134
|
noSkills: true,
|
|
@@ -139,10 +138,10 @@ async function enhancePromptWithPi(runtime, draft, config) {
|
|
|
139
138
|
systemPrompt: PROMPT_ENHANCER_SYSTEM_PROMPT,
|
|
140
139
|
},
|
|
141
140
|
});
|
|
142
|
-
services.
|
|
143
|
-
const model = services.
|
|
141
|
+
await services.modelRuntime.reloadConfig();
|
|
142
|
+
const model = services.modelRuntime.getModel(parsedModel.provider, parsedModel.modelId);
|
|
144
143
|
if (!model) {
|
|
145
|
-
throw new Error(modelNotFoundMessage(parsedModel.provider, parsedModel.modelId, services.
|
|
144
|
+
throw new Error(modelNotFoundMessage(parsedModel.provider, parsedModel.modelId, services.modelRuntime.getModels()));
|
|
146
145
|
}
|
|
147
146
|
const { session } = await promptEnhancerPiDeps.createAgentSessionFromServices({
|
|
148
147
|
services,
|
|
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import {
|
|
5
|
+
import { ModelRuntime, readStoredCredential } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import { formatCompactProgressBar } from "../../context-progress-bar.js";
|
|
7
7
|
import { APP_ICONS } from "../icons.js";
|
|
8
8
|
const OPENAI_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
@@ -222,17 +222,27 @@ async function queryOpenAIAccountUsage(now) {
|
|
|
222
222
|
async function refreshOpenAICodexAuth() {
|
|
223
223
|
// Delegate to pi core so refresh-token rotation is persisted under the same
|
|
224
224
|
// cross-process auth.json lock used by model requests.
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
225
|
+
const modelRuntime = await ModelRuntime.create({
|
|
226
|
+
authPath: getPiAuthPath(),
|
|
227
|
+
allowModelNetwork: false,
|
|
228
|
+
});
|
|
229
|
+
let resolvedAuth;
|
|
230
|
+
try {
|
|
231
|
+
resolvedAuth = await modelRuntime.getAuth("openai-codex");
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
throw new Error("OpenAI Codex OAuth token refresh failed", { cause: error });
|
|
235
|
+
}
|
|
236
|
+
const credential = readStoredCredential("openai-codex", getPiAuthPath());
|
|
237
|
+
const access = resolvedAuth?.auth.apiKey ?? credential?.access;
|
|
228
238
|
if (!access || credential?.type !== "oauth" || !credential.access || isExpired(credential)) {
|
|
229
239
|
throw new Error("OpenAI Codex OAuth token refresh failed");
|
|
230
240
|
}
|
|
231
241
|
return {
|
|
232
242
|
type: "oauth",
|
|
233
243
|
access: credential.access,
|
|
234
|
-
refresh: credential.refresh,
|
|
235
|
-
expires: credential.expires,
|
|
244
|
+
...(credential.refresh === undefined ? {} : { refresh: credential.refresh }),
|
|
245
|
+
...(credential.expires === undefined ? {} : { expires: credential.expires }),
|
|
236
246
|
};
|
|
237
247
|
}
|
|
238
248
|
async function readOpenAIAuth() {
|
|
@@ -173,16 +173,15 @@ export class AppMenuItemsController {
|
|
|
173
173
|
return current?.provider === model.provider && current.id === model.id;
|
|
174
174
|
}
|
|
175
175
|
resolveScopedModelRefs(modelRefs) {
|
|
176
|
-
const
|
|
177
|
-
if (!
|
|
176
|
+
const modelRuntime = this.host.runtime()?.services.modelRuntime;
|
|
177
|
+
if (!modelRuntime)
|
|
178
178
|
return [];
|
|
179
|
-
registry.refresh();
|
|
180
179
|
const scopedModels = [];
|
|
181
180
|
for (const modelRef of modelRefs) {
|
|
182
181
|
const parsed = parseScopedModelRef(modelRef);
|
|
183
182
|
if (!parsed)
|
|
184
183
|
continue;
|
|
185
|
-
const model =
|
|
184
|
+
const model = modelRuntime.getModel(parsed.provider, parsed.modelId);
|
|
186
185
|
if (!model)
|
|
187
186
|
continue;
|
|
188
187
|
scopedModels.push({
|
|
@@ -197,10 +196,9 @@ export class AppMenuItemsController {
|
|
|
197
196
|
const scopedModels = session?.scopedModels.length ? session.scopedModels : this.getFavoriteScopedModels();
|
|
198
197
|
if (!scopedModels.length)
|
|
199
198
|
return [];
|
|
200
|
-
const
|
|
201
|
-
registry?.refresh();
|
|
199
|
+
const modelRuntime = this.host.runtime()?.services.modelRuntime;
|
|
202
200
|
return scopedModels.map((scoped) => {
|
|
203
|
-
const refreshed =
|
|
201
|
+
const refreshed = modelRuntime?.getModel(scoped.model.provider, scoped.model.id);
|
|
204
202
|
return (refreshed ?? scoped.model);
|
|
205
203
|
});
|
|
206
204
|
}
|
package/dist/app/runtime.js
CHANGED
|
@@ -239,8 +239,8 @@ export async function createPixRuntime(options, runtimeOptions = {}) {
|
|
|
239
239
|
config,
|
|
240
240
|
...(runtimeOptions.eventBus === undefined ? {} : { eventBus: runtimeOptions.eventBus }),
|
|
241
241
|
});
|
|
242
|
-
services.
|
|
243
|
-
const model = parsedModel ? services.
|
|
242
|
+
await services.modelRuntime.reloadConfig();
|
|
243
|
+
const model = parsedModel ? services.modelRuntime.getModel(parsedModel.provider, parsedModel.modelId) : undefined;
|
|
244
244
|
if (parsedModel && !model) {
|
|
245
245
|
throw new Error(`Model not found: ${parsedModel.provider}/${parsedModel.modelId}`);
|
|
246
246
|
}
|
|
@@ -250,7 +250,7 @@ export async function createPixRuntime(options, runtimeOptions = {}) {
|
|
|
250
250
|
const scoped = parseScopedModelRef(modelRef);
|
|
251
251
|
if (!scoped)
|
|
252
252
|
return [];
|
|
253
|
-
const scopedModel = services.
|
|
253
|
+
const scopedModel = services.modelRuntime.getModel(scoped.provider, scoped.modelId);
|
|
254
254
|
if (!scopedModel)
|
|
255
255
|
return [];
|
|
256
256
|
return [
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
export {
|
|
2
|
+
export { generateSessionTitle } from "./title-generation-compat.js";
|
|
3
|
+
export { fallbackSessionTitleFromInput, generateSessionTitleWithRuntime, sessionTitleModelRefs, sanitizeSessionTitle } from "./title-generation.js";
|
|
3
4
|
export declare function firstUserMessageText(ctx: ExtensionContext): string | undefined;
|
|
4
5
|
export declare function buildForkTitleInput(parentTitle: string | undefined, forkPrompt: string): string;
|
|
5
6
|
export default function sessionTitle(pi: ExtensionAPI): void;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { SessionManager } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { loadSessionTitleConfig } from "./config.js";
|
|
4
|
-
import { fallbackSessionTitleFromInput, firstUserMessageText as firstUserMessageTextFromEntries,
|
|
5
|
-
|
|
4
|
+
import { fallbackSessionTitleFromInput, firstUserMessageText as firstUserMessageTextFromEntries, sessionTitleModelRefs, } from "./title-generation.js";
|
|
5
|
+
import { generateSessionTitle } from "./title-generation-compat.js";
|
|
6
|
+
export { generateSessionTitle } from "./title-generation-compat.js";
|
|
7
|
+
export { fallbackSessionTitleFromInput, generateSessionTitleWithRuntime, sessionTitleModelRefs, sanitizeSessionTitle } from "./title-generation.js";
|
|
6
8
|
const DEFAULT_TERMINAL_TITLE = "pi";
|
|
7
9
|
function isStaleExtensionContextError(error) {
|
|
8
10
|
if (!(error instanceof Error))
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { SessionTitleConfig } from "./config.js";
|
|
3
|
+
type TitleModelRegistry = {
|
|
4
|
+
find(provider: string, modelId: string): Model<Api> | undefined;
|
|
5
|
+
getApiKeyAndHeaders(model: Model<Api>): Promise<{
|
|
6
|
+
ok: true;
|
|
7
|
+
apiKey?: string;
|
|
8
|
+
headers?: Record<string, string>;
|
|
9
|
+
env?: Record<string, string>;
|
|
10
|
+
} | {
|
|
11
|
+
ok: false;
|
|
12
|
+
error: string;
|
|
13
|
+
}>;
|
|
14
|
+
};
|
|
15
|
+
/** Extension-side title generation through Pi's public ModelRegistry facade. */
|
|
16
|
+
export declare function generateSessionTitle(input: string, modelRegistry: TitleModelRegistry, config: SessionTitleConfig, modelRef: string, signal: AbortSignal, onWarning?: (message: string) => void): Promise<string | undefined>;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { complete } from "@earendil-works/pi-ai/compat";
|
|
2
|
+
import { buildTitlePrompt, parseTitleModelRef, sanitizeSessionTitle, TITLE_SYSTEM_PROMPT, titleResponseText, } from "./title-generation.js";
|
|
3
|
+
/** Extension-side title generation through Pi's public ModelRegistry facade. */
|
|
4
|
+
export async function generateSessionTitle(input, modelRegistry, config, modelRef, signal, onWarning) {
|
|
5
|
+
const parsedModel = parseTitleModelRef(modelRef);
|
|
6
|
+
if (!parsedModel) {
|
|
7
|
+
onWarning?.(`Invalid session-title model: ${modelRef}`);
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
const model = modelRegistry.find(parsedModel.provider, parsedModel.modelId);
|
|
11
|
+
if (!model) {
|
|
12
|
+
onWarning?.(`Session-title model not found: ${modelRef}`);
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
const auth = await modelRegistry.getApiKeyAndHeaders(model);
|
|
16
|
+
if (auth.ok === false) {
|
|
17
|
+
onWarning?.(auth.error);
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
const response = await complete(model, {
|
|
21
|
+
systemPrompt: TITLE_SYSTEM_PROMPT,
|
|
22
|
+
messages: [
|
|
23
|
+
{
|
|
24
|
+
role: "user",
|
|
25
|
+
content: [{ type: "text", text: buildTitlePrompt(input, config.maxTitleChars) }],
|
|
26
|
+
timestamp: Date.now(),
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
}, {
|
|
30
|
+
...(auth.apiKey === undefined ? {} : { apiKey: auth.apiKey }),
|
|
31
|
+
...(auth.headers === undefined ? {} : { headers: auth.headers }),
|
|
32
|
+
...(auth.env === undefined ? {} : { env: auth.env }),
|
|
33
|
+
cacheRetention: "none",
|
|
34
|
+
maxRetries: config.maxRetries,
|
|
35
|
+
maxTokens: config.maxTokens,
|
|
36
|
+
signal,
|
|
37
|
+
timeoutMs: config.timeoutMs,
|
|
38
|
+
});
|
|
39
|
+
return sanitizeSessionTitle(titleResponseText(response), config.maxTitleChars);
|
|
40
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ModelRuntime } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { SessionTitleConfig } from "./config.js";
|
|
3
3
|
type SessionEntryLike = {
|
|
4
4
|
type?: string;
|
|
@@ -7,20 +7,23 @@ type SessionEntryLike = {
|
|
|
7
7
|
content?: unknown;
|
|
8
8
|
};
|
|
9
9
|
};
|
|
10
|
-
type
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
} | {
|
|
17
|
-
ok: false;
|
|
18
|
-
error: string;
|
|
19
|
-
}>;
|
|
20
|
-
};
|
|
10
|
+
type TitleModelRuntime = Pick<ModelRuntime, "getModel" | "completeSimple">;
|
|
11
|
+
export declare const TITLE_SYSTEM_PROMPT: string;
|
|
12
|
+
export declare function parseTitleModelRef(modelRef: string): {
|
|
13
|
+
provider: string;
|
|
14
|
+
modelId: string;
|
|
15
|
+
} | undefined;
|
|
21
16
|
export declare function firstUserMessageText(entries: readonly SessionEntryLike[]): string | undefined;
|
|
22
17
|
export declare function fallbackSessionTitleFromInput(input: string, maxTitleChars: number): string | undefined;
|
|
18
|
+
export declare function buildTitlePrompt(input: string, maxTitleChars: number): string;
|
|
19
|
+
export declare function titleResponseText(response: {
|
|
20
|
+
content: Array<{
|
|
21
|
+
type: string;
|
|
22
|
+
text?: string;
|
|
23
|
+
}>;
|
|
24
|
+
}): string;
|
|
23
25
|
export declare function sanitizeSessionTitle(raw: string, maxTitleChars: number): string | undefined;
|
|
24
26
|
export declare function sessionTitleModelRefs(config: SessionTitleConfig): string[];
|
|
25
|
-
|
|
27
|
+
/** Host-side title generation through the canonical SDK ModelRuntime. */
|
|
28
|
+
export declare function generateSessionTitleWithRuntime(input: string, modelRuntime: TitleModelRuntime, config: SessionTitleConfig, modelRef: string, signal: AbortSignal, onWarning?: (message: string) => void): Promise<string | undefined>;
|
|
26
29
|
export {};
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
const TITLE_SYSTEM_PROMPT = [
|
|
1
|
+
export const TITLE_SYSTEM_PROMPT = [
|
|
3
2
|
"You name Pi coding-agent sessions from the user's first request.",
|
|
4
3
|
"Return only one concise title, without quotes, markdown, emoji, or explanations.",
|
|
5
4
|
"Use the same language as the task when possible.",
|
|
6
5
|
"Keep it specific, 3-7 words, and under the requested character limit.",
|
|
7
6
|
].join("\n");
|
|
8
|
-
function
|
|
7
|
+
export function parseTitleModelRef(modelRef) {
|
|
9
8
|
const trimmed = modelRef.trim();
|
|
10
9
|
const slash = trimmed.indexOf("/");
|
|
11
10
|
if (slash <= 0 || slash === trimmed.length - 1)
|
|
@@ -58,7 +57,7 @@ export function fallbackSessionTitleFromInput(input, maxTitleChars) {
|
|
|
58
57
|
const candidate = selected.join(" ");
|
|
59
58
|
return sanitizeSessionTitle(candidate || normalized, maxTitleChars);
|
|
60
59
|
}
|
|
61
|
-
function buildTitlePrompt(input, maxTitleChars) {
|
|
60
|
+
export function buildTitlePrompt(input, maxTitleChars) {
|
|
62
61
|
return [
|
|
63
62
|
`Generate a session title under ${maxTitleChars} characters for this session.`,
|
|
64
63
|
"If a parent session title is provided, use it only as context and focus on the new request.",
|
|
@@ -69,7 +68,7 @@ function buildTitlePrompt(input, maxTitleChars) {
|
|
|
69
68
|
"</session_context>",
|
|
70
69
|
].join("\n");
|
|
71
70
|
}
|
|
72
|
-
function
|
|
71
|
+
export function titleResponseText(response) {
|
|
73
72
|
return response.content
|
|
74
73
|
.filter((block) => block.type === "text" && typeof block.text === "string")
|
|
75
74
|
.map((block) => block.text)
|
|
@@ -103,23 +102,19 @@ export function sessionTitleModelRefs(config) {
|
|
|
103
102
|
.filter(Boolean)
|
|
104
103
|
.filter((modelRef, index, refs) => refs.indexOf(modelRef) === index);
|
|
105
104
|
}
|
|
106
|
-
|
|
107
|
-
|
|
105
|
+
/** Host-side title generation through the canonical SDK ModelRuntime. */
|
|
106
|
+
export async function generateSessionTitleWithRuntime(input, modelRuntime, config, modelRef, signal, onWarning) {
|
|
107
|
+
const parsedModel = parseTitleModelRef(modelRef);
|
|
108
108
|
if (!parsedModel) {
|
|
109
109
|
onWarning?.(`Invalid session-title model: ${modelRef}`);
|
|
110
110
|
return undefined;
|
|
111
111
|
}
|
|
112
|
-
const model =
|
|
112
|
+
const model = modelRuntime.getModel(parsedModel.provider, parsedModel.modelId);
|
|
113
113
|
if (!model) {
|
|
114
114
|
onWarning?.(`Session-title model not found: ${modelRef}`);
|
|
115
115
|
return undefined;
|
|
116
116
|
}
|
|
117
|
-
const
|
|
118
|
-
if (auth.ok === false) {
|
|
119
|
-
onWarning?.(auth.error);
|
|
120
|
-
return undefined;
|
|
121
|
-
}
|
|
122
|
-
const response = await complete(model, {
|
|
117
|
+
const response = await modelRuntime.completeSimple(model, {
|
|
123
118
|
systemPrompt: TITLE_SYSTEM_PROMPT,
|
|
124
119
|
messages: [
|
|
125
120
|
{
|
|
@@ -129,13 +124,11 @@ export async function generateSessionTitle(input, modelRegistry, config, modelRe
|
|
|
129
124
|
},
|
|
130
125
|
],
|
|
131
126
|
}, {
|
|
132
|
-
...(auth.apiKey === undefined ? {} : { apiKey: auth.apiKey }),
|
|
133
|
-
...(auth.headers === undefined ? {} : { headers: auth.headers }),
|
|
134
127
|
cacheRetention: "none",
|
|
135
128
|
maxRetries: config.maxRetries,
|
|
136
129
|
maxTokens: config.maxTokens,
|
|
137
130
|
signal,
|
|
138
131
|
timeoutMs: config.timeoutMs,
|
|
139
132
|
});
|
|
140
|
-
return sanitizeSessionTitle(
|
|
133
|
+
return sanitizeSessionTitle(titleResponseText(response), config.maxTitleChars);
|
|
141
134
|
}
|
|
@@ -362,6 +362,22 @@ npm run test:async-subagents-selection-e2e
|
|
|
362
362
|
npm run test:e2e
|
|
363
363
|
```
|
|
364
364
|
|
|
365
|
+
### Prompt evaluations
|
|
366
|
+
|
|
367
|
+
Prompt evaluations are opt-in because they call a real model. They cover model-facing behavior that deterministic tests cannot prove: tool selection for `todo` and `compress`, async-subagent delegation/lifecycle boundaries, default internal role routing, ultrawork classification, and DCP summary retention. They are intentionally excluded from `npm test`.
|
|
368
|
+
|
|
369
|
+
```bash
|
|
370
|
+
# Full prompt-eval suite
|
|
371
|
+
npm run test:prompt-evals
|
|
372
|
+
|
|
373
|
+
# Focused suites
|
|
374
|
+
npm run test:prompt-evals:tool-selection
|
|
375
|
+
npm run test:prompt-evals:async
|
|
376
|
+
npm run test:prompt-evals:dcp
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
The default live model is `zai/glm-5-turbo`. Override it for the whole suite with `PI_TOOLS_SUITE_E2E_MODEL=provider/model`, or use the existing component variables such as `TOOL_SELECTION_E2E_MODEL`, `ASYNC_SUBAGENTS_MODEL`, `ASYNC_SUBAGENTS_ROUTING_E2E_MODEL`, and `DCP_SUMMARY_E2E_MODEL`. The normal deterministic coverage remains `npm test`; run prompt evals after changing tool descriptions, routing/classifier prompts, DCP summary prompts, or the default evaluation model.
|
|
380
|
+
|
|
365
381
|
Supporting docs and historical standalone README content are kept in `docs/`; third-party license texts are kept in `licenses/`.
|
|
366
382
|
|
|
367
383
|
## SDK pin
|
|
@@ -24,13 +24,18 @@
|
|
|
24
24
|
"test": "bun test test",
|
|
25
25
|
"test:async-subagents-e2e": "ASYNC_SUBAGENTS_E2E=1 ASYNC_SUBAGENTS_DEBUG_LOGS=1 ASYNC_SUBAGENTS_MODEL=zai/glm-5-turbo bun test --concurrent --max-concurrency=30 test/async-subagents",
|
|
26
26
|
"test:async-subagents-selection-e2e": "ASYNC_SUBAGENTS_SELECTION_E2E=1 ASYNC_SUBAGENTS_MODEL=zai/glm-5-turbo bun test --concurrent --max-concurrency=30 test/async-subagents/selection-e2e.test.ts",
|
|
27
|
+
"test:prompt-evals:tool-selection": "PROMPT_EVAL_E2E=1 bun test --concurrent --max-concurrency=10 test/tool-selection-e2e.test.ts",
|
|
28
|
+
"test:prompt-evals:async": "PROMPT_EVAL_E2E=1 bun test --concurrent --max-concurrency=5 test/async-subagents/selection-e2e.test.ts test/prompt-evals/async-routing-e2e.test.ts",
|
|
29
|
+
"test:prompt-evals:dcp": "PROMPT_EVAL_E2E=1 bun test --concurrent --max-concurrency=5 test/prompt-evals/dcp-summary-e2e.test.ts",
|
|
30
|
+
"test:prompt-evals": "PROMPT_EVAL_E2E=1 bun test --concurrent --max-concurrency=5 test/tool-selection-e2e.test.ts test/async-subagents/selection-e2e.test.ts test/prompt-evals",
|
|
27
31
|
"bench:locate": "PI_LOCATE_BENCH_ITERATIONS=5 PI_LOCATE_BENCH_FAKE_IDX=0 PI_LOCATE_BENCH_MODEL=zai/glm-5-turbo PI_LOCATE_BENCH_MODES=direct-read-grep,ast-structural,repo-search-hybrid,repo-discovery,subagent-search,unrestricted-suite node test/fixtures/hard-to-find-project/benchmark/run-locate-benchmark.mjs",
|
|
28
32
|
"bench:locate:analyze": "node test/fixtures/hard-to-find-project/benchmark/analyze-locate-benchmark.mjs",
|
|
29
33
|
"test:locate-benchmark-e2e": "PI_LOCATE_BENCH_E2E=1 PI_LOCATE_BENCH_MODEL=zai/glm-5-turbo bun test test/locate-benchmark-e2e.test.ts",
|
|
30
34
|
"test:tool-selection-e2e": "TOOL_SELECTION_E2E=1 TOOL_SELECTION_E2E_MODEL=zai/glm-5-turbo bun test --concurrent --max-concurrency=30 test/tool-selection-e2e.test.ts",
|
|
31
35
|
"test:e2e": "TOOL_SELECTION_E2E=1 TOOL_SELECTION_E2E_MODEL=zai/glm-5-turbo ASYNC_SUBAGENTS_E2E=1 ASYNC_SUBAGENTS_DEBUG_LOGS=1 ASYNC_SUBAGENTS_MODEL=zai/glm-5-turbo bun test --concurrent --max-concurrency=5 test/tool-selection-e2e.test.ts test/async-subagents/e2e.test.ts test/async-subagents/selection-e2e.test.ts test/todo-persistence-e2e.test.ts",
|
|
36
|
+
"typecheck": "tsc -p tsconfig.source.json",
|
|
32
37
|
"typecheck:async-subagents": "bash scripts/typecheck-source.sh",
|
|
33
|
-
"check": "npm run smoke"
|
|
38
|
+
"check": "npm run typecheck && npm test && npm run smoke"
|
|
34
39
|
},
|
|
35
40
|
"dependencies": {
|
|
36
41
|
"jsonc-parser": "^3.3.1",
|
|
@@ -38,9 +43,9 @@
|
|
|
38
43
|
"vscode-languageserver-protocol": "^3.17.5"
|
|
39
44
|
},
|
|
40
45
|
"peerDependencies": {
|
|
41
|
-
"@earendil-works/pi-ai": "0.80.
|
|
42
|
-
"@earendil-works/pi-coding-agent": "0.80.
|
|
43
|
-
"@earendil-works/pi-tui": "0.80.
|
|
46
|
+
"@earendil-works/pi-ai": "0.80.9",
|
|
47
|
+
"@earendil-works/pi-coding-agent": "0.80.9",
|
|
48
|
+
"@earendil-works/pi-tui": "0.80.9",
|
|
44
49
|
"typebox": "*"
|
|
45
50
|
},
|
|
46
51
|
"devDependencies": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { complete } from "@earendil-works/pi-ai/compat";
|
|
2
1
|
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
2
|
+
import { completeWithModelRegistry, type ModelCompletionRegistry } from "../../model-completion.js";
|
|
3
3
|
import type { AgentTask } from "./types.js";
|
|
4
4
|
import {
|
|
5
5
|
currentModelRef,
|
|
@@ -11,10 +11,10 @@ import {
|
|
|
11
11
|
|
|
12
12
|
export interface SubagentRoutingContext {
|
|
13
13
|
model?: unknown;
|
|
14
|
-
modelRegistry?: {
|
|
14
|
+
modelRegistry?: ModelCompletionRegistry & {
|
|
15
15
|
find(provider: string, modelId: string): Model<Api> | undefined;
|
|
16
16
|
getApiKeyAndHeaders(model: Model<Api>): Promise<
|
|
17
|
-
| { ok?: true; apiKey?: string; headers?: Record<string, string> }
|
|
17
|
+
| { ok?: true; apiKey?: string; headers?: Record<string, string>; env?: Record<string, string> }
|
|
18
18
|
| { ok: false; error: string }
|
|
19
19
|
>;
|
|
20
20
|
};
|
|
@@ -67,7 +67,8 @@ export async function routeSubagentTasks(
|
|
|
67
67
|
for (const candidate of candidates) {
|
|
68
68
|
if (signal?.aborted) throw new Error("Aborted");
|
|
69
69
|
try {
|
|
70
|
-
response = await
|
|
70
|
+
response = await completeWithModelRegistry(
|
|
71
|
+
ctx.modelRegistry,
|
|
71
72
|
candidate.model,
|
|
72
73
|
{
|
|
73
74
|
systemPrompt: ROUTER_SYSTEM_PROMPT,
|
|
@@ -82,6 +83,7 @@ export async function routeSubagentTasks(
|
|
|
82
83
|
{
|
|
83
84
|
apiKey: candidate.apiKey,
|
|
84
85
|
headers: candidate.headers,
|
|
86
|
+
env: candidate.env,
|
|
85
87
|
cacheRetention: "none",
|
|
86
88
|
maxRetries: routing.maxRetries,
|
|
87
89
|
maxTokens: routing.maxTokens,
|
|
@@ -179,14 +181,16 @@ interface RoutingCandidate {
|
|
|
179
181
|
model: Model<Api>;
|
|
180
182
|
apiKey?: string;
|
|
181
183
|
headers?: Record<string, string>;
|
|
184
|
+
env?: Record<string, string>;
|
|
182
185
|
}
|
|
183
186
|
|
|
184
|
-
type RoutingResponse = Awaited<ReturnType<typeof
|
|
187
|
+
type RoutingResponse = Awaited<ReturnType<typeof completeWithModelRegistry>>;
|
|
185
188
|
|
|
186
189
|
async function resolveModelRef(ctx: SubagentRoutingContext, modelRef: string): Promise<{
|
|
187
190
|
model: Model<Api>;
|
|
188
191
|
apiKey?: string;
|
|
189
192
|
headers?: Record<string, string>;
|
|
193
|
+
env?: Record<string, string>;
|
|
190
194
|
} | undefined> {
|
|
191
195
|
const parsed = parseModelRef(modelRef);
|
|
192
196
|
if (!parsed || !ctx.modelRegistry) return undefined;
|
|
@@ -194,7 +198,7 @@ async function resolveModelRef(ctx: SubagentRoutingContext, modelRef: string): P
|
|
|
194
198
|
if (!model) return undefined;
|
|
195
199
|
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
196
200
|
if (auth.ok === false) return undefined;
|
|
197
|
-
return { model, apiKey: auth.apiKey, headers: auth.headers };
|
|
201
|
+
return { model, apiKey: auth.apiKey, headers: auth.headers, env: auth.env };
|
|
198
202
|
}
|
|
199
203
|
|
|
200
204
|
function parseModelRef(modelRef: string): { provider: string; modelId: string } | undefined {
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { complete } from "@earendil-works/pi-ai/compat";
|
|
2
1
|
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
2
|
+
import { completeWithModelRegistry, type ModelCompletionRegistry } from "../../model-completion.js";
|
|
3
3
|
import { currentModelRef, resolveSubagentRoutingConfig, type SubagentConfig } from "./config.js";
|
|
4
4
|
|
|
5
5
|
export type UltraworkAutoDecision = "ultrawork" | "hint" | "none";
|
|
6
6
|
|
|
7
7
|
export interface UltraworkAutoContext {
|
|
8
8
|
model?: unknown;
|
|
9
|
-
modelRegistry?: {
|
|
9
|
+
modelRegistry?: ModelCompletionRegistry & {
|
|
10
10
|
find(provider: string, modelId: string): Model<Api> | undefined;
|
|
11
11
|
getApiKeyAndHeaders(model: Model<Api>): Promise<
|
|
12
|
-
| { ok?: true; apiKey?: string; headers?: Record<string, string> }
|
|
12
|
+
| { ok?: true; apiKey?: string; headers?: Record<string, string>; env?: Record<string, string> }
|
|
13
13
|
| { ok: false; error: string }
|
|
14
14
|
>;
|
|
15
15
|
};
|
|
@@ -63,7 +63,8 @@ export async function decideUltraworkAuto(
|
|
|
63
63
|
const resolved = await resolveClassifierModel(ctx, routing.model);
|
|
64
64
|
if (!resolved) return "none";
|
|
65
65
|
|
|
66
|
-
const response = await
|
|
66
|
+
const response = await completeWithModelRegistry(
|
|
67
|
+
ctx.modelRegistry,
|
|
67
68
|
resolved.model,
|
|
68
69
|
{
|
|
69
70
|
systemPrompt: CLASSIFIER_SYSTEM_PROMPT,
|
|
@@ -78,6 +79,7 @@ export async function decideUltraworkAuto(
|
|
|
78
79
|
{
|
|
79
80
|
apiKey: resolved.apiKey,
|
|
80
81
|
headers: resolved.headers,
|
|
82
|
+
env: resolved.env,
|
|
81
83
|
cacheRetention: "none",
|
|
82
84
|
maxRetries: routing.maxRetries,
|
|
83
85
|
maxTokens: Math.min(routing.maxTokens, 32),
|
|
@@ -130,6 +132,7 @@ async function resolveClassifierModel(ctx: UltraworkAutoContext, modelRef: strin
|
|
|
130
132
|
model: Model<Api>;
|
|
131
133
|
apiKey?: string;
|
|
132
134
|
headers?: Record<string, string>;
|
|
135
|
+
env?: Record<string, string>;
|
|
133
136
|
} | undefined> {
|
|
134
137
|
const configured = await resolveModelRef(ctx, modelRef);
|
|
135
138
|
if (configured) return configured;
|
|
@@ -141,6 +144,7 @@ async function resolveModelRef(ctx: UltraworkAutoContext, modelRef: string): Pro
|
|
|
141
144
|
model: Model<Api>;
|
|
142
145
|
apiKey?: string;
|
|
143
146
|
headers?: Record<string, string>;
|
|
147
|
+
env?: Record<string, string>;
|
|
144
148
|
} | undefined> {
|
|
145
149
|
const parsed = parseModelRef(modelRef);
|
|
146
150
|
if (!parsed || !ctx.modelRegistry) return undefined;
|
|
@@ -148,7 +152,7 @@ async function resolveModelRef(ctx: UltraworkAutoContext, modelRef: string): Pro
|
|
|
148
152
|
if (!model) return undefined;
|
|
149
153
|
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
150
154
|
if (auth.ok === false) return undefined;
|
|
151
|
-
return { model, apiKey: auth.apiKey, headers: auth.headers };
|
|
155
|
+
return { model, apiKey: auth.apiKey, headers: auth.headers, env: auth.env };
|
|
152
156
|
}
|
|
153
157
|
|
|
154
158
|
function parseModelRef(modelRef: string): { provider: string; modelId: string } | undefined {
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { complete } from "@earendil-works/pi-ai/compat";
|
|
4
3
|
import type { Api, AssistantMessage, ImageContent, Model, TextContent } from "@earendil-works/pi-ai";
|
|
5
4
|
import { Type } from "typebox";
|
|
6
5
|
|
|
7
6
|
import { loadPiToolsSuiteConfig } from "../config.js";
|
|
8
7
|
import { ignoreStaleExtensionContextError } from "../context-usage.js";
|
|
8
|
+
import { completeWithModelRegistry, type ModelCompletionRegistry } from "../model-completion.js";
|
|
9
9
|
|
|
10
10
|
type ExtensionAPI = any;
|
|
11
11
|
|
|
@@ -25,9 +25,11 @@ type LookupDetails = {
|
|
|
25
25
|
};
|
|
26
26
|
|
|
27
27
|
type ResolvedLookupModel = {
|
|
28
|
+
modelRegistry: ModelCompletionRegistry;
|
|
28
29
|
model: Model<Api>;
|
|
29
30
|
apiKey?: string;
|
|
30
31
|
headers?: Record<string, string>;
|
|
32
|
+
env?: Record<string, string>;
|
|
31
33
|
};
|
|
32
34
|
|
|
33
35
|
const SILENT_PROMPT_MARKER_START = "<glm_silent_mode>";
|
|
@@ -504,7 +506,8 @@ function createLookupTool() {
|
|
|
504
506
|
const promptText = buildLookupPrompt(params, recentContext, images.length, pathImages.warnings);
|
|
505
507
|
|
|
506
508
|
try {
|
|
507
|
-
const response = await
|
|
509
|
+
const response = await completeWithModelRegistry(
|
|
510
|
+
resolved.modelRegistry,
|
|
508
511
|
resolved.model,
|
|
509
512
|
{
|
|
510
513
|
systemPrompt: LOOKUP_SYSTEM_PROMPT,
|
|
@@ -519,6 +522,7 @@ function createLookupTool() {
|
|
|
519
522
|
{
|
|
520
523
|
apiKey: resolved.apiKey,
|
|
521
524
|
headers: resolved.headers,
|
|
525
|
+
env: resolved.env,
|
|
522
526
|
cacheRetention: "none",
|
|
523
527
|
maxRetries: 1,
|
|
524
528
|
maxTokens: DEFAULT_LOOKUP_MAX_TOKENS,
|
|
@@ -765,9 +769,20 @@ async function resolveLookupModel(ctx: unknown, modelRef: string): Promise<Resol
|
|
|
765
769
|
if (!isRecord(registry) || typeof registry.find !== "function" || typeof registry.getApiKeyAndHeaders !== "function") return undefined;
|
|
766
770
|
const model = registry.find(parsed.provider, parsed.modelId) as Model<Api> | undefined;
|
|
767
771
|
if (!model) return undefined;
|
|
768
|
-
const auth = await registry.getApiKeyAndHeaders(model) as {
|
|
772
|
+
const auth = await registry.getApiKeyAndHeaders(model) as {
|
|
773
|
+
ok?: true;
|
|
774
|
+
apiKey?: string;
|
|
775
|
+
headers?: Record<string, string>;
|
|
776
|
+
env?: Record<string, string>;
|
|
777
|
+
} | { ok: false; error: string };
|
|
769
778
|
if (auth.ok === false) return undefined;
|
|
770
|
-
return {
|
|
779
|
+
return {
|
|
780
|
+
modelRegistry: registry as ModelCompletionRegistry,
|
|
781
|
+
model,
|
|
782
|
+
apiKey: auth.apiKey,
|
|
783
|
+
headers: auth.headers,
|
|
784
|
+
env: auth.env,
|
|
785
|
+
};
|
|
771
786
|
}
|
|
772
787
|
|
|
773
788
|
function parseModelRef(modelRef: string): { provider: string; modelId: string } | undefined {
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
// automatic fallback to the programmatic digest on any failure/timeout.
|
|
14
14
|
// ---------------------------------------------------------------------------
|
|
15
15
|
|
|
16
|
-
import { complete } from "@earendil-works/pi-ai/compat"
|
|
17
16
|
import type { Model, Api } from "@earendil-works/pi-ai"
|
|
17
|
+
import { completeWithModelRegistry, type ModelCompletionRegistry } from "../model-completion.js"
|
|
18
18
|
import type { DcpState } from "./state.js"
|
|
19
19
|
import type { DcpConfig } from "./config.js"
|
|
20
20
|
import type { CompressionCandidate } from "./pruner-types.js"
|
|
@@ -116,7 +116,7 @@ export function buildProgrammaticSummary(
|
|
|
116
116
|
return lines.join("\n")
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
const SUMMARIZER_SYSTEM_PROMPT = `You summarize a slice of a coding agent's conversation so it can replace the raw messages in context. Produce a dense, continuation-focused summary: preserve user intent, decisions made, files/symbols changed or inspected, exact errors still actionable, verification status, and next steps. Drop full logs, repeated output, and incidental detail. Be concise (roughly 4-10 bullets). Output ONLY the summary text, no preamble.`
|
|
119
|
+
const SUMMARIZER_SYSTEM_PROMPT = `You summarize a slice of a coding agent's conversation so it can replace the raw messages in context. Produce a dense, continuation-focused summary: preserve user intent, decisions made, files/symbols changed or inspected, exact errors still actionable, verification status, and next steps. Do not infer, invent, or add facts absent from the source; preserve uncertainty instead of filling gaps. Drop full logs, repeated output, and incidental detail. Be concise (roughly 4-10 bullets). Output ONLY the summary text, no preamble.`
|
|
120
120
|
|
|
121
121
|
/** Outcome of one summarizer-model attempt, surfaced in DCP debug logs. */
|
|
122
122
|
export interface ModelSummaryAttempt {
|
|
@@ -134,6 +134,14 @@ export interface ModelSummaryResult {
|
|
|
134
134
|
attempts: ModelSummaryAttempt[]
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
type ModelSummaryRegistry = ModelCompletionRegistry & {
|
|
138
|
+
find(provider: string, modelId: string): Model<Api> | undefined
|
|
139
|
+
getApiKeyAndHeaders(model: Model<Api>): Promise<
|
|
140
|
+
| { ok: true; apiKey?: string; headers?: Record<string, string>; env?: Record<string, string> }
|
|
141
|
+
| { ok: false; error: string }
|
|
142
|
+
>
|
|
143
|
+
}
|
|
144
|
+
|
|
137
145
|
/**
|
|
138
146
|
* Try to produce a model-generated summary by calling each model in
|
|
139
147
|
* `modelRefs` in order. On success returns `{ text, usedModelRef, attempts }`;
|
|
@@ -146,7 +154,7 @@ export interface ModelSummaryResult {
|
|
|
146
154
|
*/
|
|
147
155
|
export async function generateModelSummary(
|
|
148
156
|
modelRefs: string[],
|
|
149
|
-
modelRegistry:
|
|
157
|
+
modelRegistry: ModelSummaryRegistry | undefined,
|
|
150
158
|
signal: AbortSignal | undefined,
|
|
151
159
|
topic: string,
|
|
152
160
|
messagesInRange: any[],
|
|
@@ -178,7 +186,7 @@ export async function generateModelSummary(
|
|
|
178
186
|
continue
|
|
179
187
|
}
|
|
180
188
|
|
|
181
|
-
let auth
|
|
189
|
+
let auth: Awaited<ReturnType<ModelSummaryRegistry["getApiKeyAndHeaders"]>>
|
|
182
190
|
try {
|
|
183
191
|
auth = await modelRegistry.getApiKeyAndHeaders(model)
|
|
184
192
|
} catch (error) {
|
|
@@ -186,7 +194,7 @@ export async function generateModelSummary(
|
|
|
186
194
|
attempts.push({ ref, outcome: "no-auth", error: error instanceof Error ? error.message : String(error) })
|
|
187
195
|
continue
|
|
188
196
|
}
|
|
189
|
-
if (
|
|
197
|
+
if (auth.ok === false) {
|
|
190
198
|
attempts.push({ ref, outcome: "no-auth" })
|
|
191
199
|
continue
|
|
192
200
|
}
|
|
@@ -202,7 +210,8 @@ export async function generateModelSummary(
|
|
|
202
210
|
}
|
|
203
211
|
|
|
204
212
|
try {
|
|
205
|
-
const result = await
|
|
213
|
+
const result = await completeWithModelRegistry(
|
|
214
|
+
modelRegistry,
|
|
206
215
|
model,
|
|
207
216
|
{ systemPrompt: SUMMARIZER_SYSTEM_PROMPT, messages: [{ role: "user", content: userPrompt, timestamp: Date.now() }] },
|
|
208
217
|
{
|
|
@@ -109,9 +109,10 @@ function collectNudgeStats(ctx: ExtensionCommandContext, state: DcpState): DcpNu
|
|
|
109
109
|
else stats.upgraded++
|
|
110
110
|
stats.byType[data.type]++
|
|
111
111
|
const createdAt = typeof data.createdAt === "number" ? data.createdAt : undefined
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
112
|
+
const rawContextPercent = data.contextPercent
|
|
113
|
+
let contextPercent: number | null | undefined
|
|
114
|
+
if (typeof rawContextPercent === "number") contextPercent = rawContextPercent
|
|
115
|
+
else if (rawContextPercent === null) contextPercent = null
|
|
115
116
|
if (!stats.last || (createdAt ?? 0) >= (stats.last.createdAt ?? 0)) {
|
|
116
117
|
stats.last = { type: data.type, event, createdAt, contextPercent }
|
|
117
118
|
}
|
|
@@ -25,7 +25,7 @@ Do not compress active work, still-needed raw context, or material whose exact c
|
|
|
25
25
|
|
|
26
26
|
DCP reminders: handle critical/high-context reminders promptly and compress any safe high-yield closed slice before more exploration; routine reminders mean compress only if a safe, closed, useful slice exists, otherwise continue the next atomic step and re-check later.
|
|
27
27
|
|
|
28
|
-
Summaries must preserve only what is needed to continue: user intent and constraints, accepted decisions, files/symbols changed or inspected, actionable errors, verification status, and next steps. Drop incidental transcript detail, duplicate outputs, full logs, long code/JSON/diffs, and prose not needed later; include short literals only when required.
|
|
28
|
+
Summaries must preserve only what is needed to continue: user intent and constraints, accepted decisions, files/symbols changed or inspected, actionable errors, verification status, and next steps. Do not infer, invent, or add facts that are not present in the selected messages. Drop incidental transcript detail, duplicate outputs, full logs, long code/JSON/diffs, and prose not needed later; include short literals only when required.
|
|
29
29
|
`.trim()
|
|
30
30
|
|
|
31
31
|
/**
|
|
@@ -55,6 +55,8 @@ If a \`<dcp-system-reminder>\` is present in context, treat it as a signal to ev
|
|
|
55
55
|
THE SUMMARY
|
|
56
56
|
Your summary must be COMPLETE FOR CONTINUATION, not a transcript rewrite. Preserve only information that will plausibly matter later: user intent, accepted constraints, decisions, files/symbols changed or inspected, exact errors that are still actionable, verification status, and next steps.
|
|
57
57
|
|
|
58
|
+
Do not infer, invent, or add facts that are not present in the selected range. If the source is uncertain or incomplete, preserve that uncertainty instead of filling gaps.
|
|
59
|
+
|
|
58
60
|
If active unfinished work exists, start with \`Active objective\` and \`Next step\`.
|
|
59
61
|
|
|
60
62
|
Default to a compact structured summary (roughly 4-10 bullets for a normal completed work slice). Grow beyond that only when the compressed range contains multiple independent decisions, unresolved blockers, or precise state that is genuinely required to continue.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Api,
|
|
3
|
+
AssistantMessage,
|
|
4
|
+
AssistantMessageEventStream,
|
|
5
|
+
Context,
|
|
6
|
+
Model,
|
|
7
|
+
SimpleStreamOptions,
|
|
8
|
+
} from "@earendil-works/pi-ai";
|
|
9
|
+
import { completeSimple } from "@earendil-works/pi-ai/compat";
|
|
10
|
+
|
|
11
|
+
type RegisteredProviderConfig = {
|
|
12
|
+
streamSimple?: (
|
|
13
|
+
model: Model<Api>,
|
|
14
|
+
context: Context,
|
|
15
|
+
options?: SimpleStreamOptions,
|
|
16
|
+
) => AssistantMessageEventStream;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type ModelCompletionRegistry = {
|
|
20
|
+
getRegisteredProviderConfig?(providerId: string): RegisteredProviderConfig | undefined;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Complete through an extension provider's registered stream when available.
|
|
25
|
+
* Pi 0.80.8+ no longer copies extension streams into pi-ai's global compat
|
|
26
|
+
* registry, so falling back to compat is valid only for built-in APIs.
|
|
27
|
+
*/
|
|
28
|
+
export async function completeWithModelRegistry(
|
|
29
|
+
modelRegistry: ModelCompletionRegistry | undefined,
|
|
30
|
+
model: Model<Api>,
|
|
31
|
+
context: Context,
|
|
32
|
+
options?: SimpleStreamOptions,
|
|
33
|
+
): Promise<AssistantMessage> {
|
|
34
|
+
const providerConfig = modelRegistry?.getRegisteredProviderConfig?.(model.provider);
|
|
35
|
+
if (providerConfig?.streamSimple) return providerConfig.streamSimple(model, context, options).result();
|
|
36
|
+
return completeSimple(model, context, options);
|
|
37
|
+
}
|
|
@@ -220,6 +220,7 @@ export const TODO_TOOL_DESCRIPTION: ToolDescription = {
|
|
|
220
220
|
promptGuidelines: [
|
|
221
221
|
"Use `todo` for complex work with 3+ steps, explicit user task lists, or new non-trivial requirements. Skip single trivial tasks and purely conversational requests.",
|
|
222
222
|
"For multi-step implementation/debugging plans, include a final user-facing report todo in the initial plan with acceptance criteria for changed files/behavior, verification results, and remaining manual actions; close it immediately before the final response, never via compression.",
|
|
223
|
+
"When create or batch_create already sets the intended status, do not issue a redundant update with the same status; continue the work instead.",
|
|
223
224
|
"Resync before continuing when user/new findings change scope, requirements, safety, feasibility, approach, dependencies, or order; update tasks/blockers and defer obsolete work.",
|
|
224
225
|
"Update todos when starting, finishing, blocking, splitting, abandoning, or materially changing a step; before planned work mark exactly one in_progress with activeForm and complete it only after verification.",
|
|
225
226
|
"If partial, tests fail, or blocked, keep the task in_progress and add/update a blocker. Never use `clear`, `delete`, or batch deletion to hide unfinished/stale/forgotten todos; delete only on explicit request or creation mistake.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-ui-extend",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.63",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -65,9 +65,9 @@
|
|
|
65
65
|
"prepublishOnly": "npm run check && npm run build:pix && npm run generate-schemas"
|
|
66
66
|
},
|
|
67
67
|
"dependencies": {
|
|
68
|
-
"@earendil-works/pi-ai": "0.80.
|
|
69
|
-
"@earendil-works/pi-coding-agent": "0.80.
|
|
70
|
-
"@earendil-works/pi-tui": "0.80.
|
|
68
|
+
"@earendil-works/pi-ai": "0.80.9",
|
|
69
|
+
"@earendil-works/pi-coding-agent": "0.80.9",
|
|
70
|
+
"@earendil-works/pi-tui": "0.80.9",
|
|
71
71
|
"@mariozechner/clipboard": "^0.3.9",
|
|
72
72
|
"jsonc-parser": "3.3.1",
|
|
73
73
|
"typebox": "1.1.38",
|