pi-openai-toolkit 0.14.10 → 0.14.12
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 +10 -1
- package/src/context-management/tools.ts +78 -40
- package/src/extension-runtime.ts +66 -29
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-openai-toolkit",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.12",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -32,6 +32,15 @@
|
|
|
32
32
|
"codex",
|
|
33
33
|
"responses"
|
|
34
34
|
],
|
|
35
|
+
"pi": {
|
|
36
|
+
"extensions": [
|
|
37
|
+
"./src/extension-runtime.ts",
|
|
38
|
+
"./src/web-search/extension.ts",
|
|
39
|
+
"./src/image-generation/extension.ts",
|
|
40
|
+
"./src/auto-mode/extension.ts",
|
|
41
|
+
"./src/codex-astra/extension.ts"
|
|
42
|
+
]
|
|
43
|
+
},
|
|
35
44
|
"files": [
|
|
36
45
|
"src/compact-client.ts",
|
|
37
46
|
"src/compaction-projection.ts",
|
|
@@ -12,13 +12,25 @@ import type { HistoryAction, NotesAction } from "./types";
|
|
|
12
12
|
|
|
13
13
|
const EMPTY_PARAMETERS = Type.Object({}, { additionalProperties: false });
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
15
|
+
type PublishedTool = {
|
|
16
|
+
name: string;
|
|
17
|
+
description: string;
|
|
18
|
+
parameters: unknown;
|
|
19
|
+
promptGuidelines?: string[];
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Pi's ToolInfo wrapper is new, but its definition fields retain the objects
|
|
24
|
+
* registered by the extension. Keep identity checks for those fields: an exact
|
|
25
|
+
* foreign clone under the same name is still not ownership of our tool.
|
|
26
|
+
*/
|
|
27
|
+
function isOurTool(
|
|
28
|
+
actual: PublishedTool,
|
|
29
|
+
expected: { description: string; parameters: unknown; promptGuidelines?: string[] },
|
|
30
|
+
): boolean {
|
|
31
|
+
return actual.description === expected.description
|
|
32
|
+
&& actual.parameters === expected.parameters
|
|
33
|
+
&& actual.promptGuidelines === expected.promptGuidelines;
|
|
22
34
|
}
|
|
23
35
|
const HISTORY_ACTIONS = Object.keys(HISTORY_ENDPOINTS) as [HistoryAction, ...HistoryAction[]];
|
|
24
36
|
const NOTES_ACTIONS = Object.keys(NOTES_ENDPOINTS) as [NotesAction, ...NotesAction[]];
|
|
@@ -182,6 +194,13 @@ export function createContextManagementTools(
|
|
|
182
194
|
return { newContext, getContextRemaining, history, notes };
|
|
183
195
|
}
|
|
184
196
|
|
|
197
|
+
export type ContextToolRegistrationState = "verified" | "conflict" | "unverified";
|
|
198
|
+
|
|
199
|
+
export type ContextToolSyncResult = {
|
|
200
|
+
synced: boolean;
|
|
201
|
+
registrationState: ContextToolRegistrationState;
|
|
202
|
+
};
|
|
203
|
+
|
|
185
204
|
export class ContextManagementToolController {
|
|
186
205
|
private readonly registeredNames = new Set<string>();
|
|
187
206
|
private readonly definitions = new Map<string, {
|
|
@@ -190,8 +209,7 @@ export class ContextManagementToolController {
|
|
|
190
209
|
promptGuidelines?: string[];
|
|
191
210
|
}>();
|
|
192
211
|
private registered = false;
|
|
193
|
-
private
|
|
194
|
-
private registrationValid = false;
|
|
212
|
+
private verifiedOnce = false;
|
|
195
213
|
|
|
196
214
|
constructor(private readonly pi: ExtensionAPI) {}
|
|
197
215
|
|
|
@@ -205,12 +223,16 @@ export class ContextManagementToolController {
|
|
|
205
223
|
// those are bound only after extension loading completes.
|
|
206
224
|
try {
|
|
207
225
|
for (const definition of definitions) api.registerTool(definition);
|
|
226
|
+
this.registered = true;
|
|
208
227
|
} catch {
|
|
228
|
+
// Pi rejects a registration whose name is already owned elsewhere.
|
|
229
|
+
this.registered = false;
|
|
209
230
|
return false;
|
|
210
231
|
}
|
|
211
232
|
|
|
212
233
|
this.registeredNames.clear();
|
|
213
234
|
this.definitions.clear();
|
|
235
|
+
this.verifiedOnce = false;
|
|
214
236
|
for (const definition of definitions) {
|
|
215
237
|
this.registeredNames.add(definition.name);
|
|
216
238
|
this.definitions.set(definition.name, {
|
|
@@ -219,43 +241,58 @@ export class ContextManagementToolController {
|
|
|
219
241
|
promptGuidelines: definition.promptGuidelines,
|
|
220
242
|
});
|
|
221
243
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
this.registrationValid = false;
|
|
244
|
+
// A successful call only proves the hand-off; it does not prove the runtime
|
|
245
|
+
// published the definitions. `checkRegistration()` reads that from Pi.
|
|
225
246
|
return true;
|
|
226
247
|
}
|
|
227
248
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
249
|
+
/**
|
|
250
|
+
* Read registration state until the runtime confirms publication.
|
|
251
|
+
*
|
|
252
|
+
* Pi 0.86 can reject action methods while a session replacement is still
|
|
253
|
+
* binding, and can publish the registry after `session_start` ran. Negative
|
|
254
|
+
* reads remain retryable; only a successful verification is cached for this
|
|
255
|
+
* controller instance. A Pi reload creates a new controller.
|
|
256
|
+
*/
|
|
257
|
+
checkRegistration(): ContextToolRegistrationState {
|
|
258
|
+
if (!this.registered) return "conflict";
|
|
259
|
+
// A confirmed publication is kept: the expensive part was never the read, it
|
|
260
|
+
// was trusting a failed one. Only negative verdicts are recomputed each time.
|
|
261
|
+
if (this.verifiedOnce) return "verified";
|
|
262
|
+
let available: PublishedTool[];
|
|
232
263
|
try {
|
|
233
|
-
const api = this.pi as ExtensionAPI & { getAllTools?: () =>
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
parameters: unknown;
|
|
237
|
-
promptGuidelines?: string[];
|
|
238
|
-
}> };
|
|
239
|
-
if (typeof api.getAllTools !== "function") return false;
|
|
240
|
-
const available = new Map(api.getAllTools().map((tool) => [tool.name, tool]));
|
|
241
|
-
this.registrationValid = [...this.registeredNames].every((name) => {
|
|
242
|
-
const actual = available.get(name);
|
|
243
|
-
const expected = this.definitions.get(name);
|
|
244
|
-
return actual !== undefined && expected !== undefined &&
|
|
245
|
-
actual.description === expected.description &&
|
|
246
|
-
actual.parameters === expected.parameters &&
|
|
247
|
-
actual.promptGuidelines === expected.promptGuidelines;
|
|
248
|
-
});
|
|
264
|
+
const api = this.pi as ExtensionAPI & { getAllTools?: () => PublishedTool[] };
|
|
265
|
+
if (typeof api.getAllTools !== "function") return "conflict";
|
|
266
|
+
available = api.getAllTools();
|
|
249
267
|
} catch {
|
|
250
|
-
|
|
268
|
+
// Stale or unbound runtime: retry on the next sync.
|
|
269
|
+
return "unverified";
|
|
270
|
+
}
|
|
271
|
+
const byName = new Map(available.map((tool) => [tool.name, tool]));
|
|
272
|
+
let missing = false;
|
|
273
|
+
let conflict = false;
|
|
274
|
+
for (const name of this.registeredNames) {
|
|
275
|
+
const actual = byName.get(name);
|
|
276
|
+
const expected = this.definitions.get(name);
|
|
277
|
+
if (actual === undefined || expected === undefined) {
|
|
278
|
+
missing = true;
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
if (!isOurTool(actual, expected)) conflict = true;
|
|
251
282
|
}
|
|
252
|
-
return
|
|
283
|
+
if (conflict) return "conflict";
|
|
284
|
+
if (missing) return "unverified";
|
|
285
|
+
this.verifiedOnce = true;
|
|
286
|
+
return "verified";
|
|
253
287
|
}
|
|
254
288
|
|
|
255
|
-
sync(active: boolean):
|
|
256
|
-
|
|
289
|
+
sync(active: boolean): ContextToolSyncResult {
|
|
290
|
+
const registrationState = this.checkRegistration();
|
|
291
|
+
if (registrationState !== "verified") return { synced: false, registrationState };
|
|
257
292
|
const api = this.pi as ExtensionAPI & { getActiveTools?: () => string[]; setActiveTools?: (names: string[]) => void };
|
|
258
|
-
if (typeof api.getActiveTools !== "function" || typeof api.setActiveTools !== "function")
|
|
293
|
+
if (typeof api.getActiveTools !== "function" || typeof api.setActiveTools !== "function") {
|
|
294
|
+
return { synced: false, registrationState };
|
|
295
|
+
}
|
|
259
296
|
try {
|
|
260
297
|
const current = api.getActiveTools();
|
|
261
298
|
// Pi may activate a newly registered tool before the first sync. Once
|
|
@@ -265,16 +302,17 @@ export class ContextManagementToolController {
|
|
|
265
302
|
? [...current, ...[...this.registeredNames].filter((name) => !current.includes(name))]
|
|
266
303
|
: current.filter((name) => !this.registeredNames.has(name));
|
|
267
304
|
if (next.length !== current.length) api.setActiveTools(next);
|
|
268
|
-
return true;
|
|
305
|
+
return { synced: true, registrationState };
|
|
269
306
|
} catch {
|
|
270
|
-
return false;
|
|
307
|
+
return { synced: false, registrationState };
|
|
271
308
|
}
|
|
272
309
|
}
|
|
273
310
|
|
|
274
311
|
reset(): void {
|
|
275
312
|
this.sync(false);
|
|
276
313
|
}
|
|
277
|
-
get isRegistered(): boolean { return this.
|
|
314
|
+
get isRegistered(): boolean { return this.checkRegistration() === "verified"; }
|
|
315
|
+
get registrationState(): ContextToolRegistrationState { return this.checkRegistration(); }
|
|
278
316
|
}
|
|
279
317
|
|
|
280
318
|
export function registerContextManagementTools(
|
package/src/extension-runtime.ts
CHANGED
|
@@ -17,7 +17,10 @@ import {
|
|
|
17
17
|
import { routeContextNamespaceToolMessage } from "./context-management/namespace-tools";
|
|
18
18
|
import { loadHistoryNotesThreadHint } from "./context-management/history-notes";
|
|
19
19
|
import { CodexContextWindowManager } from "./context-management/window-manager";
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
registerContextManagementTools,
|
|
22
|
+
type ContextToolRegistrationState,
|
|
23
|
+
} from "./context-management/tools";
|
|
21
24
|
import { writeDebugArtifact, writeReplayFailureArtifact } from "./debug";
|
|
22
25
|
import {
|
|
23
26
|
COMPACTION_CHECKPOINT_PROVENANCE_UNAVAILABLE,
|
|
@@ -88,6 +91,13 @@ type RemoteContextActive = (
|
|
|
88
91
|
model?: ExtensionContext["model"],
|
|
89
92
|
) => Promise<boolean>;
|
|
90
93
|
|
|
94
|
+
type ContextToolSyncOutcome = {
|
|
95
|
+
eligible: boolean;
|
|
96
|
+
toolsSynced: boolean;
|
|
97
|
+
registrationState: ContextToolRegistrationState;
|
|
98
|
+
windowInitialized: boolean;
|
|
99
|
+
};
|
|
100
|
+
|
|
91
101
|
type ResponsesCompactOutcome =
|
|
92
102
|
| { outcome: "success"; compaction: CompactionResult<NativeCompactionDetails> }
|
|
93
103
|
| { outcome: "aborted" }
|
|
@@ -1015,28 +1025,60 @@ export default function registerCompactionExtension(
|
|
|
1015
1025
|
piContextHookPatch: overrides.piContextHookPatch ?? piContextHookPatch,
|
|
1016
1026
|
};
|
|
1017
1027
|
let tools!: ReturnType<typeof registerContextManagementTools>;
|
|
1028
|
+
let contextWindowReady = false;
|
|
1029
|
+
const isContextRuntimeActive = async (
|
|
1030
|
+
ctx: ExtensionContext,
|
|
1031
|
+
config: CompactionConfig,
|
|
1032
|
+
model = ctx.model,
|
|
1033
|
+
): Promise<boolean> =>
|
|
1034
|
+
contextWindowReady && tools.isRegistered && await isRemoteContextActive(ctx, config, model);
|
|
1018
1035
|
tools = registerContextManagementTools(
|
|
1019
1036
|
pi,
|
|
1020
1037
|
contextWindows,
|
|
1021
1038
|
async (ctx) => {
|
|
1022
1039
|
const config = dependencies.loadConfig().config.compaction;
|
|
1023
|
-
return
|
|
1040
|
+
return isContextRuntimeActive(ctx, config);
|
|
1024
1041
|
},
|
|
1025
1042
|
() => dependencies.loadConfig().config.compaction.gatewayContextModels,
|
|
1026
1043
|
);
|
|
1027
|
-
const remoteContextActive: RemoteContextActive =
|
|
1028
|
-
|
|
1029
|
-
|
|
1044
|
+
const remoteContextActive: RemoteContextActive = isContextRuntimeActive;
|
|
1045
|
+
const syncTools = async (
|
|
1046
|
+
ctx: ExtensionContext,
|
|
1047
|
+
model = ctx.model,
|
|
1048
|
+
options: { notifyWindowFailure?: boolean } = {},
|
|
1049
|
+
): Promise<ContextToolSyncOutcome> => {
|
|
1050
|
+
// Until this call proves otherwise, do not let a previous session/window
|
|
1051
|
+
// identity make a failed activation look usable.
|
|
1052
|
+
contextWindowReady = false;
|
|
1030
1053
|
const config = dependencies.loadConfig().config.compaction;
|
|
1031
|
-
const
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1054
|
+
const eligible = await isRemoteContextActive(ctx, config, model);
|
|
1055
|
+
const toolSync = tools.sync(eligible);
|
|
1056
|
+
const outcome: ContextToolSyncOutcome = {
|
|
1057
|
+
eligible,
|
|
1058
|
+
toolsSynced: toolSync.synced,
|
|
1059
|
+
registrationState: toolSync.registrationState,
|
|
1060
|
+
windowInitialized: false,
|
|
1061
|
+
};
|
|
1062
|
+
if (!eligible || !toolSync.synced) return outcome;
|
|
1063
|
+
// Activation can succeed after `session_start` missed it, because Pi 0.86 may
|
|
1064
|
+
// reject the registration read while a session replacement is still binding.
|
|
1065
|
+
// The window lifecycle has to open on that later activation too, or requests
|
|
1066
|
+
// carry no window metadata and the backend never ingests the turns.
|
|
1067
|
+
try {
|
|
1068
|
+
contextWindows.ensureInitialized(pi, ctx, true);
|
|
1069
|
+
contextWindowReady = true;
|
|
1070
|
+
return { ...outcome, windowInitialized: true };
|
|
1071
|
+
} catch {
|
|
1072
|
+
// Do not leave tools exposed while the request path has no valid window
|
|
1073
|
+
// identity. The next lifecycle hook may retry once Pi is usable again.
|
|
1074
|
+
tools.sync(false);
|
|
1075
|
+
if (options.notifyWindowFailure) notifyRemoteContextFailure(ctx, "malformed-window-state");
|
|
1076
|
+
return outcome;
|
|
1077
|
+
}
|
|
1037
1078
|
};
|
|
1038
1079
|
pi.on("session_start", async (_event, ctx) => {
|
|
1039
|
-
const
|
|
1080
|
+
const syncOutcome = await syncTools(ctx, ctx.model, { notifyWindowFailure: true });
|
|
1081
|
+
const active = syncOutcome.eligible && syncOutcome.toolsSynced && syncOutcome.windowInitialized;
|
|
1040
1082
|
const { config: toolkitConfig, source, warnings } = dependencies.loadConfig();
|
|
1041
1083
|
const config = toolkitConfig.compaction;
|
|
1042
1084
|
if (!config.enabled) return;
|
|
@@ -1044,16 +1086,16 @@ export default function registerCompactionExtension(
|
|
|
1044
1086
|
let activationReason: string | undefined;
|
|
1045
1087
|
// Only models the built-in Remote Context coverage targets may activate or
|
|
1046
1088
|
// warn; everything else silently runs Pi's normal compaction path.
|
|
1047
|
-
if (config.contextManagement === "remote" && isCodexContextModel(ctx.model, config)) {
|
|
1048
|
-
if (
|
|
1049
|
-
try {
|
|
1050
|
-
contextWindows.ensureInitialized(pi, ctx, true);
|
|
1051
|
-
} catch {
|
|
1052
|
-
notifyRemoteContextFailure(ctx, "malformed-window-state");
|
|
1053
|
-
}
|
|
1054
|
-
} else if (!tools.isRegistered) {
|
|
1089
|
+
if (config.contextManagement === "remote" && isCodexContextModel(ctx.model, config) && !active) {
|
|
1090
|
+
if (syncOutcome.registrationState === "conflict") {
|
|
1055
1091
|
activationReason = "tool-name-conflict";
|
|
1056
1092
|
notifyRemoteContextFailure(ctx, activationReason);
|
|
1093
|
+
} else if (syncOutcome.registrationState === "unverified" || !syncOutcome.toolsSynced) {
|
|
1094
|
+
// Transient: the runtime has not published or accepted our tools yet. A
|
|
1095
|
+
// later before_agent_start re-verifies, so do not report provider failure.
|
|
1096
|
+
activationReason = "tool-registration-pending";
|
|
1097
|
+
} else if (syncOutcome.eligible && !syncOutcome.windowInitialized) {
|
|
1098
|
+
activationReason = "malformed-window-state";
|
|
1057
1099
|
} else {
|
|
1058
1100
|
const remoteResolution = await resolveCodexContextProvider(ctx, ctx.model, config.gatewayContextModels);
|
|
1059
1101
|
activationReason = remoteResolution.ok ? "codex-context-unavailable" : remoteResolution.reason;
|
|
@@ -1099,25 +1141,20 @@ export default function registerCompactionExtension(
|
|
|
1099
1141
|
pi.on("session_before_compact", (event, ctx) => handleSessionBeforeCompact(event, ctx, dependencies, remoteContextActive));
|
|
1100
1142
|
pi.on("session_compact", (event, _ctx) => contextWindows.recordCompaction(event.compactionEntry.details));
|
|
1101
1143
|
pi.on("session_shutdown", () => {
|
|
1144
|
+
contextWindowReady = false;
|
|
1102
1145
|
contextWindows.reset();
|
|
1103
1146
|
tools.reset();
|
|
1104
1147
|
});
|
|
1105
1148
|
pi.on("model_select", async (event, ctx) => {
|
|
1106
|
-
const active = await syncTools(ctx, event.model);
|
|
1107
1149
|
// Switching into a covered model mid-session must open the window
|
|
1108
1150
|
// lifecycle immediately: without an identity the request rewrite skips
|
|
1109
1151
|
// window metadata, the backend never ingests those turns, and the first
|
|
1110
1152
|
// new_context would trim pre-switch history that no history can recover.
|
|
1111
|
-
//
|
|
1112
|
-
|
|
1113
|
-
try {
|
|
1114
|
-
contextWindows.ensureInitialized(pi, ctx, true);
|
|
1115
|
-
} catch {
|
|
1116
|
-
notifyRemoteContextFailure(ctx, "malformed-window-state");
|
|
1117
|
-
}
|
|
1153
|
+
// syncTools() activates and initializes the window when the model is covered.
|
|
1154
|
+
await syncTools(ctx, event.model, { notifyWindowFailure: true });
|
|
1118
1155
|
});
|
|
1119
1156
|
pi.on("before_agent_start", async (_event, ctx) => {
|
|
1120
|
-
await syncTools(ctx);
|
|
1157
|
+
await syncTools(ctx, ctx.model, { notifyWindowFailure: true });
|
|
1121
1158
|
});
|
|
1122
1159
|
pi.on("before_provider_request", (event, ctx) => handleBeforeProviderRequest(event, ctx, dependencies.loadConfig, contextWindows, remoteContextActive));
|
|
1123
1160
|
pi.on("before_provider_headers", async (event, ctx) => {
|