pi-codebuddy-sdk 0.4.24 → 0.4.26
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/package.json +2 -2
- package/src/index.ts +51 -62
- package/src/models.ts +23 -4
- package/src/provider-registration.ts +40 -0
- package/src/served-window.ts +37 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-codebuddy-sdk",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.26",
|
|
4
4
|
"description": "Pi extension that uses CodeBuddy (via Agent SDK) as a model provider and adds an AskCodebuddy tool.",
|
|
5
5
|
"author": "RealAlexandreAI",
|
|
6
6
|
"license": "MIT",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"@earendil-works/pi-ai": "^1.1.0",
|
|
41
41
|
"@earendil-works/pi-coding-agent": "^1.1.0",
|
|
42
42
|
"@earendil-works/pi-tui": "^1.1.0",
|
|
43
|
-
"typebox": "1.3.
|
|
43
|
+
"typebox": "1.3.27"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@earendil-works/pi-ai": "^1.1.0",
|
package/src/index.ts
CHANGED
|
@@ -11,6 +11,8 @@ import { dirname, join } from "path";
|
|
|
11
11
|
import { PROVIDER_ID, messageContentToText, convertPiMessages } from "./convert.js";
|
|
12
12
|
import { buildModels, codebuddyModelId, FALLBACK_MODELS, rawModelsFromSdk, resolveModel as _resolveModel, type PiModel } from "./models.js";
|
|
13
13
|
import { readModelsCache, writeModelsCache } from "./models-cache.js";
|
|
14
|
+
import { clearOwnedStream, providerRegistration } from "./provider-registration.js";
|
|
15
|
+
import { syncServedWindow } from "./served-window.js";
|
|
14
16
|
import { readTranscript } from "./transcript-compat.js";
|
|
15
17
|
import { MCP_SERVER_NAME, MCP_TOOL_PREFIX, buildCodebuddySystemPrompt } from "./skills.js";
|
|
16
18
|
import { verifyWrittenSession as _verifyWrittenSession } from "./session-verify.js";
|
|
@@ -174,7 +176,7 @@ function startQuery(args: Parameters<typeof query>[0]): ReturnType<typeof query>
|
|
|
174
176
|
|
|
175
177
|
// --- Constants ---
|
|
176
178
|
|
|
177
|
-
// Global key
|
|
179
|
+
// Global key for the first streamSimple that owns the shared ModelRegistry.
|
|
178
180
|
//
|
|
179
181
|
// Extensions like pi-subagents spawn a subagent and it loads this module
|
|
180
182
|
// again. Without this guard, the subagent's call to registerProvider() would
|
|
@@ -182,13 +184,14 @@ function startQuery(args: Parameters<typeof query>[0]): ReturnType<typeof query>
|
|
|
182
184
|
// ModelRegistry. When the parent later delivers a tool result, it would call
|
|
183
185
|
// the subagent's `streamSimple` (which has empty state) instead of its own.
|
|
184
186
|
//
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
//
|
|
187
|
+
// The first instance stores its streamSimple in a Symbol.for() global, keyed
|
|
188
|
+
// by the ModelRegistry seen at session start. The same registry reuses that
|
|
189
|
+
// stream. A different registry still registers, with its own stream, because
|
|
190
|
+
// a host such as pi-web gives each session its own ModelRuntime.
|
|
188
191
|
//
|
|
189
192
|
// On session_shutdown (including /reload), clearSession() resets this so a fresh
|
|
190
193
|
// registration can occur for the next session.
|
|
191
|
-
const
|
|
194
|
+
const PROVIDER_NAME = "CodeBuddy";
|
|
192
195
|
|
|
193
196
|
const SDK_TO_PI_TOOL_NAME: Record<string, string> = {
|
|
194
197
|
read: "read", write: "write", edit: "edit", bash: "bash",
|
|
@@ -206,6 +209,22 @@ function resolveModel(input: string) {
|
|
|
206
209
|
return _resolveModel(MODELS, input);
|
|
207
210
|
}
|
|
208
211
|
|
|
212
|
+
function registerCodebuddyProvider(pi: ExtensionAPI, globals: Record<symbol, any> = globalThis as Record<symbol, any>): void {
|
|
213
|
+
const { streamFn, claimStream } = providerRegistration(globals, streamCodebuddySdk, boundRuntime);
|
|
214
|
+
pi.registerProvider(PROVIDER_ID, {
|
|
215
|
+
name: PROVIDER_NAME,
|
|
216
|
+
baseUrl: PROVIDER_ID,
|
|
217
|
+
apiKey: "not-used",
|
|
218
|
+
api: "codebuddy-sdk",
|
|
219
|
+
models: MODELS as any,
|
|
220
|
+
// Cast: pi-ai AssistantMessageEventStream diamond dep between pi-coding-agent and pi-agent-core
|
|
221
|
+
streamSimple: streamFn as any,
|
|
222
|
+
});
|
|
223
|
+
if (!claimStream) {
|
|
224
|
+
debug(`provider: reusing parent streamSimple (module=${moduleInstanceId})`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
209
228
|
// --- Error handling ---
|
|
210
229
|
|
|
211
230
|
function errorMessage(err: unknown): string {
|
|
@@ -461,7 +480,7 @@ async function runIsolatedSummary(
|
|
|
461
480
|
if (block.type === "text" && typeof block.text === "string") assistantText += block.text;
|
|
462
481
|
}
|
|
463
482
|
} else if (message.type === "result") {
|
|
464
|
-
|
|
483
|
+
syncServedContextWindow("compact summary", message, model);
|
|
465
484
|
if (message.subtype === "success") {
|
|
466
485
|
finalText = message.result || assistantText;
|
|
467
486
|
} else {
|
|
@@ -761,6 +780,7 @@ function mapToolArgs(
|
|
|
761
780
|
// Global (not query state):
|
|
762
781
|
let piUI: ExtensionUIContext | null = null;
|
|
763
782
|
const activeQueryContexts = new Set<QueryContext>();
|
|
783
|
+
let boundRuntime: object | undefined;
|
|
764
784
|
|
|
765
785
|
function contextForToolResults(results: McpResult[]): QueryContext | undefined {
|
|
766
786
|
for (const result of results) {
|
|
@@ -848,18 +868,26 @@ function updateUsage(output: AssistantMessage, usage: Record<string, number | un
|
|
|
848
868
|
debug(`usage: in=${output.usage.input} out=${output.usage.output} cacheRead=${output.usage.cacheRead} cacheWrite=${output.usage.cacheWrite} total=${output.usage.totalTokens}${reasoningText} cachePct=${cachePct}% model=${model.id}`);
|
|
849
869
|
}
|
|
850
870
|
|
|
851
|
-
//
|
|
852
|
-
//
|
|
853
|
-
//
|
|
854
|
-
//
|
|
855
|
-
|
|
856
|
-
// gap observable. See issue #18.
|
|
857
|
-
function logServedContextWindow(label: string, message: CbMessage, model: Model<any>): void {
|
|
871
|
+
// Apply the served window from an SDK result (modelUsage[id]) onto the
|
|
872
|
+
// registered catalog and the live model pi reads for context usage. The
|
|
873
|
+
// served value wins over the registration estimate when the entitlement
|
|
874
|
+
// differs, e.g. bare Opus served 200K on Pro.
|
|
875
|
+
function syncServedContextWindow(label: string, message: CbMessage, model: Model<any>): void {
|
|
858
876
|
const modelUsage = (message as any).modelUsage as Record<string, { contextWindow?: number; maxOutputTokens?: number }> | undefined;
|
|
859
877
|
if (!modelUsage) return;
|
|
860
878
|
for (const [k, v] of Object.entries(modelUsage)) {
|
|
861
879
|
debug(`${label}: served contextWindow=${v.contextWindow ?? "?"} maxOutputTokens=${v.maxOutputTokens ?? "?"} servedModel=${k} registered=${model.contextWindow}`);
|
|
862
880
|
}
|
|
881
|
+
const changes = syncServedWindow(modelUsage, (id) => {
|
|
882
|
+
const targets: Array<{ contextWindow?: number; maxTokens?: number }> = [];
|
|
883
|
+
const registered = MODELS.find((m) => m.id === id);
|
|
884
|
+
if (registered) targets.push(registered);
|
|
885
|
+
if (model.id === id) targets.push(model);
|
|
886
|
+
return targets;
|
|
887
|
+
});
|
|
888
|
+
for (const change of changes) {
|
|
889
|
+
debug(`${label}: ${change.id} ${change.field} ${change.from} -> ${change.to}`);
|
|
890
|
+
}
|
|
863
891
|
}
|
|
864
892
|
|
|
865
893
|
// --- Effort level mapping ---
|
|
@@ -1142,7 +1170,7 @@ async function consumeQuery(
|
|
|
1142
1170
|
break;
|
|
1143
1171
|
case "result": {
|
|
1144
1172
|
const resultMsg = message as Extract<CbMessage, { type: "result" }>;
|
|
1145
|
-
|
|
1173
|
+
syncServedContextWindow("result", message, model);
|
|
1146
1174
|
if (!queryCtx.turnSawStreamEvent && resultMsg.subtype === "success") {
|
|
1147
1175
|
ensureTurnStarted(queryCtx);
|
|
1148
1176
|
const text = resultMsg.result || "";
|
|
@@ -1706,16 +1734,7 @@ async function discoverModels(pi: ExtensionAPI): Promise<void> {
|
|
|
1706
1734
|
// recover it synchronously at module load — the in-process global
|
|
1707
1735
|
// alone dies with the old process.
|
|
1708
1736
|
writeModelsCache(MODELS);
|
|
1709
|
-
|
|
1710
|
-
const streamFn = g[ACTIVE_STREAM_SIMPLE_KEY] ?? streamCodebuddySdk;
|
|
1711
|
-
pi.registerProvider(PROVIDER_ID, {
|
|
1712
|
-
name: "CodeBuddy",
|
|
1713
|
-
baseUrl: PROVIDER_ID,
|
|
1714
|
-
apiKey: "not-used",
|
|
1715
|
-
api: "codebuddy-sdk",
|
|
1716
|
-
models: MODELS as any,
|
|
1717
|
-
streamSimple: streamFn as any,
|
|
1718
|
-
});
|
|
1737
|
+
registerCodebuddyProvider(pi);
|
|
1719
1738
|
modelsDiscovered = true;
|
|
1720
1739
|
debug(`discoverModels: registered ${MODELS.length} models from SDK`);
|
|
1721
1740
|
} catch (err) {
|
|
@@ -1728,16 +1747,7 @@ async function discoverModels(pi: ExtensionAPI): Promise<void> {
|
|
|
1728
1747
|
if (cached && cached.length) {
|
|
1729
1748
|
MODELS = cached;
|
|
1730
1749
|
debug(`discoverModels: failed, recovered ${MODELS.length} cached models`, err);
|
|
1731
|
-
|
|
1732
|
-
const streamFn = g[ACTIVE_STREAM_SIMPLE_KEY] ?? streamCodebuddySdk;
|
|
1733
|
-
pi.registerProvider(PROVIDER_ID, {
|
|
1734
|
-
name: "CodeBuddy",
|
|
1735
|
-
baseUrl: PROVIDER_ID,
|
|
1736
|
-
apiKey: "not-used",
|
|
1737
|
-
api: "codebuddy-sdk",
|
|
1738
|
-
models: MODELS as any,
|
|
1739
|
-
streamSimple: streamFn as any,
|
|
1740
|
-
});
|
|
1750
|
+
registerCodebuddyProvider(pi);
|
|
1741
1751
|
modelsDiscovered = true;
|
|
1742
1752
|
} else {
|
|
1743
1753
|
debug("discoverModels: failed, using fallback models", err);
|
|
@@ -1787,14 +1797,14 @@ export default async function (pi: ExtensionAPI) {
|
|
|
1787
1797
|
// Clear the global streamSimple if this instance registered it.
|
|
1788
1798
|
// This allows /reload to work — the old instance clears the flag so
|
|
1789
1799
|
// the new instance can register fresh without wrapping stale state.
|
|
1790
|
-
|
|
1791
|
-
if (g[ACTIVE_STREAM_SIMPLE_KEY] === streamCodebuddySdk) {
|
|
1800
|
+
if (clearOwnedStream(globalThis as Record<symbol, any>, streamCodebuddySdk)) {
|
|
1792
1801
|
debug(`${event}: clearing ACTIVE_STREAM_SIMPLE_KEY`);
|
|
1793
|
-
g[ACTIVE_STREAM_SIMPLE_KEY] = undefined;
|
|
1794
1802
|
}
|
|
1795
1803
|
};
|
|
1796
1804
|
pi.on("session_start", (event, ctx) => {
|
|
1797
1805
|
piUI = ctx.ui;
|
|
1806
|
+
boundRuntime = ctx.modelRegistry;
|
|
1807
|
+
registerCodebuddyProvider(pi);
|
|
1798
1808
|
if (event.reason === "new" || event.reason === "resume" || event.reason === "fork") {
|
|
1799
1809
|
clearSession(`session_start:${event.reason}`);
|
|
1800
1810
|
}
|
|
@@ -1852,31 +1862,10 @@ export default async function (pi: ExtensionAPI) {
|
|
|
1852
1862
|
|
|
1853
1863
|
// --- Provider ---
|
|
1854
1864
|
//
|
|
1855
|
-
//
|
|
1856
|
-
//
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
const g = globalThis as Record<symbol, any>;
|
|
1861
|
-
if (!g[ACTIVE_STREAM_SIMPLE_KEY]) {
|
|
1862
|
-
// First instance: store our streamSimple and register.
|
|
1863
|
-
g[ACTIVE_STREAM_SIMPLE_KEY] = streamCodebuddySdk;
|
|
1864
|
-
pi.registerProvider(PROVIDER_ID, {
|
|
1865
|
-
name: "CodeBuddy",
|
|
1866
|
-
baseUrl: PROVIDER_ID,
|
|
1867
|
-
apiKey: "not-used",
|
|
1868
|
-
api: "codebuddy-sdk",
|
|
1869
|
-
models: MODELS as any,
|
|
1870
|
-
// Cast: pi-ai AssistantMessageEventStream diamond dep between pi-coding-agent and pi-agent-core
|
|
1871
|
-
streamSimple: streamCodebuddySdk as any,
|
|
1872
|
-
});
|
|
1873
|
-
} else {
|
|
1874
|
-
// Subsequent instance (subagent session): skip registration entirely.
|
|
1875
|
-
// The subagent already has access to codebuddy-sdk models via the shared
|
|
1876
|
-
// ModelRegistry from the parent's registration. Calls to those models
|
|
1877
|
-
// route through the parent's streamSimple via reentrant QueryContexts.
|
|
1878
|
-
debug(`provider: skipping re-registration, parent instance active (module=${moduleInstanceId})`);
|
|
1879
|
-
}
|
|
1865
|
+
// Register on every runtime. The same ModelRegistry keeps the first
|
|
1866
|
+
// streamSimple; a different runtime gets its own.
|
|
1867
|
+
|
|
1868
|
+
registerCodebuddyProvider(pi);
|
|
1880
1869
|
|
|
1881
1870
|
// --- AskCodebuddy tool ---
|
|
1882
1871
|
|
package/src/models.ts
CHANGED
|
@@ -16,6 +16,14 @@ export type PiModel = {
|
|
|
16
16
|
const DEFAULT_CONTEXT = 131_072;
|
|
17
17
|
const DEFAULT_MAX_TOKENS = 8192;
|
|
18
18
|
|
|
19
|
+
type SdkModelMeta = {
|
|
20
|
+
id?: string;
|
|
21
|
+
name?: string;
|
|
22
|
+
maxInputTokens?: number;
|
|
23
|
+
maxOutputTokens?: number;
|
|
24
|
+
contextWindow?: { supportedLengths?: ReadonlyArray<number>; defaultLength?: number };
|
|
25
|
+
};
|
|
26
|
+
|
|
19
27
|
function detectThinking(id: string): boolean {
|
|
20
28
|
return /claude|gemini|gpt-5|hy3|deepseek|glm/i.test(id);
|
|
21
29
|
}
|
|
@@ -36,17 +44,28 @@ function estimateMaxTokens(id: string): number {
|
|
|
36
44
|
return DEFAULT_MAX_TOKENS;
|
|
37
45
|
}
|
|
38
46
|
|
|
39
|
-
|
|
47
|
+
function positive(value: number | undefined): number | undefined {
|
|
48
|
+
return value && value > 0 ? value : undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function contextFromMetadata(model: SdkModelMeta): number | undefined {
|
|
52
|
+
const lengths = model.contextWindow?.supportedLengths?.filter((n) => n > 0) ?? [];
|
|
53
|
+
return positive(model.maxInputTokens)
|
|
54
|
+
?? positive(model.contextWindow?.defaultLength)
|
|
55
|
+
?? (lengths.length ? Math.max(...lengths) : undefined);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function rawModelsFromSdk(supported: Array<ModelInfo & SdkModelMeta>): PiModel[] {
|
|
40
59
|
return supported
|
|
41
|
-
.map((m) => ({ id: m.id ?? m.value, name: m.name ?? m.displayName ?? m.id ?? m.value }))
|
|
60
|
+
.map((m) => ({ ...m, id: m.id ?? m.value, name: m.name ?? m.displayName ?? m.id ?? m.value }))
|
|
42
61
|
.filter((m) => m.id)
|
|
43
62
|
.map((m) => ({
|
|
44
63
|
id: m.id!,
|
|
45
64
|
name: m.name || m.id!,
|
|
46
65
|
reasoning: detectThinking(m.id!),
|
|
47
66
|
input: detectImages(m.id!) ? ["text", "image"] as const : ["text"] as const,
|
|
48
|
-
contextWindow: estimateContext(m.id!),
|
|
49
|
-
maxTokens: estimateMaxTokens(m.id!),
|
|
67
|
+
contextWindow: contextFromMetadata(m) ?? estimateContext(m.id!),
|
|
68
|
+
maxTokens: positive(m.maxOutputTokens) ?? estimateMaxTokens(m.id!),
|
|
50
69
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
51
70
|
}));
|
|
52
71
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Process-wide stream owner for the shared-registry case (CLI subagents).
|
|
2
|
+
// A fresh ModelRuntime (pi-web per chat) does not see the first registration,
|
|
3
|
+
// so each runtime still needs its own provider entry.
|
|
4
|
+
export const ACTIVE_STREAM_SIMPLE_KEY = Symbol.for("codebuddy-sdk:activeStreamSimple");
|
|
5
|
+
|
|
6
|
+
export interface ProviderRegistration {
|
|
7
|
+
streamFn: unknown;
|
|
8
|
+
claimStream: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface ActiveStream {
|
|
12
|
+
runtime: object | undefined;
|
|
13
|
+
streamFn: unknown;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function providerRegistration(
|
|
17
|
+
globals: Record<symbol, unknown>,
|
|
18
|
+
ownStream: unknown,
|
|
19
|
+
runtime: object | undefined,
|
|
20
|
+
): ProviderRegistration {
|
|
21
|
+
const active = globals[ACTIVE_STREAM_SIMPLE_KEY] as ActiveStream | undefined;
|
|
22
|
+
if (!active) {
|
|
23
|
+
globals[ACTIVE_STREAM_SIMPLE_KEY] = { runtime, streamFn: ownStream };
|
|
24
|
+
return { streamFn: ownStream, claimStream: true };
|
|
25
|
+
}
|
|
26
|
+
if (runtime !== undefined && active.runtime === runtime) {
|
|
27
|
+
return { streamFn: active.streamFn, claimStream: false };
|
|
28
|
+
}
|
|
29
|
+
if (runtime === undefined || active.runtime === undefined) {
|
|
30
|
+
return { streamFn: active.streamFn, claimStream: false };
|
|
31
|
+
}
|
|
32
|
+
return { streamFn: ownStream, claimStream: false };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function clearOwnedStream(globals: Record<symbol, unknown>, ownStream: unknown): boolean {
|
|
36
|
+
const active = globals[ACTIVE_STREAM_SIMPLE_KEY] as ActiveStream | undefined;
|
|
37
|
+
if (!active || active.streamFn !== ownStream) return false;
|
|
38
|
+
globals[ACTIVE_STREAM_SIMPLE_KEY] = undefined;
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export interface ServedWindow {
|
|
2
|
+
contextWindow?: number;
|
|
3
|
+
maxOutputTokens?: number;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface WindowTarget {
|
|
7
|
+
contextWindow?: number;
|
|
8
|
+
maxTokens?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface WindowSyncChange {
|
|
12
|
+
id: string;
|
|
13
|
+
field: "contextWindow" | "maxTokens";
|
|
14
|
+
from: number | undefined;
|
|
15
|
+
to: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function syncServedWindow(
|
|
19
|
+
usage: Record<string, ServedWindow> | undefined,
|
|
20
|
+
targetsFor: (id: string) => WindowTarget[],
|
|
21
|
+
): WindowSyncChange[] {
|
|
22
|
+
if (!usage) return [];
|
|
23
|
+
const changes: WindowSyncChange[] = [];
|
|
24
|
+
for (const [id, served] of Object.entries(usage)) {
|
|
25
|
+
for (const target of targetsFor(id)) {
|
|
26
|
+
if (served.contextWindow && served.contextWindow > 0 && target.contextWindow !== served.contextWindow) {
|
|
27
|
+
changes.push({ id, field: "contextWindow", from: target.contextWindow, to: served.contextWindow });
|
|
28
|
+
target.contextWindow = served.contextWindow;
|
|
29
|
+
}
|
|
30
|
+
if (served.maxOutputTokens && served.maxOutputTokens > 0 && target.maxTokens !== served.maxOutputTokens) {
|
|
31
|
+
changes.push({ id, field: "maxTokens", from: target.maxTokens, to: served.maxOutputTokens });
|
|
32
|
+
target.maxTokens = served.maxOutputTokens;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return changes;
|
|
37
|
+
}
|