pi-openai-toolkit 0.14.7 → 0.14.8
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/README.md +36 -7
- package/README.zh.md +36 -7
- package/docs/internals.md +9 -1
- package/package.json +5 -2
- package/src/compaction-projection.ts +31 -0
- package/src/config.ts +100 -1
- package/src/context-management/messages.ts +1 -1
- package/src/context-management/tools.ts +17 -5
- package/src/context-management/window-manager.ts +110 -28
- package/src/extension-runtime.ts +263 -34
- package/src/payload-rewrite.ts +55 -10
- package/src/pi-context-hook.ts +116 -0
- package/src/request-context-cache.ts +16 -1
- package/src/runtime.ts +52 -0
- package/src/types.ts +46 -0
- package/src/web-search/alpha-client.ts +875 -0
- package/src/web-search/extension.ts +469 -26
- package/src/web-search/payload.ts +209 -10
- package/src/web-search/prompt.ts +44 -5
- package/src/web-search/types.ts +107 -8
- package/tsconfig.check.json +2 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import { ExtensionRunner, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
|
|
4
|
+
/** Context property added by the toolkit's host patch. */
|
|
5
|
+
export const PI_CONTEXT_PROJECTOR_FIELD = "projectContextForCompaction" as const;
|
|
6
|
+
const PI_CONTEXT_PROJECTION_REPORTER_FIELD = "__piOpenAiToolkitReportContextProjectionFailure" as const;
|
|
7
|
+
const PATCH_MARKER_FIELD = "__piOpenAiToolkitContextHookPatch" as const;
|
|
8
|
+
|
|
9
|
+
type ProjectionState = {
|
|
10
|
+
failure?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const activeProjectionByRunner = new WeakMap<ExtensionRunner, ProjectionState>();
|
|
14
|
+
|
|
15
|
+
type PatchedExtensionContext = ExtensionContext & {
|
|
16
|
+
[PI_CONTEXT_PROJECTOR_FIELD]?: PiContextHookProjector;
|
|
17
|
+
[PI_CONTEXT_PROJECTION_REPORTER_FIELD]?: (reason: string) => void;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type MarkedCreateContext = typeof ExtensionRunner.prototype.createContext & {
|
|
21
|
+
[PATCH_MARKER_FIELD]?: true;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** The ordered context-hook chain, applied to a cloned compaction context. */
|
|
25
|
+
export type PiContextHookProjector = (
|
|
26
|
+
messages: readonly AgentMessage[],
|
|
27
|
+
) => Promise<readonly AgentMessage[]>;
|
|
28
|
+
|
|
29
|
+
export type PiContextHookPatchResult =
|
|
30
|
+
| { ok: true; installed: boolean; reason?: "already-installed" }
|
|
31
|
+
| { ok: false; reason: "emit-context-unavailable" | "create-context-unavailable" };
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Add a narrow projection method to every ExtensionContext created by Pi.
|
|
35
|
+
*
|
|
36
|
+
* Pi 0.85.1 already has the ordered `ExtensionRunner.emitContext()` method,
|
|
37
|
+
* but the event-level ExtensionContext does not expose the runner. The patch
|
|
38
|
+
* bridges only the method needed by Remote V2; it does not expose the runner
|
|
39
|
+
* or alter Pi's scheduler.
|
|
40
|
+
*/
|
|
41
|
+
export function installPiContextHookPatch(): PiContextHookPatchResult {
|
|
42
|
+
const prototype = ExtensionRunner.prototype;
|
|
43
|
+
if (typeof prototype.emitContext !== "function") {
|
|
44
|
+
return { ok: false, reason: "emit-context-unavailable" };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const original = prototype.createContext;
|
|
48
|
+
if (typeof original !== "function") {
|
|
49
|
+
return { ok: false, reason: "create-context-unavailable" };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const markedOriginal = original as MarkedCreateContext;
|
|
53
|
+
if (markedOriginal[PATCH_MARKER_FIELD] === true) {
|
|
54
|
+
return { ok: true, installed: false, reason: "already-installed" };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const patched = function patchedCreateContext(this: ExtensionRunner): ExtensionContext {
|
|
58
|
+
const context = original.call(this) as PatchedExtensionContext;
|
|
59
|
+
if (!(PI_CONTEXT_PROJECTOR_FIELD in context)) {
|
|
60
|
+
Object.defineProperty(context, PI_CONTEXT_PROJECTOR_FIELD, {
|
|
61
|
+
configurable: false,
|
|
62
|
+
enumerable: false,
|
|
63
|
+
writable: false,
|
|
64
|
+
value: async (messages: readonly AgentMessage[]) => {
|
|
65
|
+
const state: ProjectionState = {};
|
|
66
|
+
activeProjectionByRunner.set(this, state);
|
|
67
|
+
try {
|
|
68
|
+
const projected = await this.emitContext([...messages]);
|
|
69
|
+
if (state.failure) {
|
|
70
|
+
throw new Error(`context projection failed: ${state.failure}`);
|
|
71
|
+
}
|
|
72
|
+
return projected;
|
|
73
|
+
} finally {
|
|
74
|
+
if (activeProjectionByRunner.get(this) === state) {
|
|
75
|
+
activeProjectionByRunner.delete(this);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
if (!(PI_CONTEXT_PROJECTION_REPORTER_FIELD in context)) {
|
|
82
|
+
Object.defineProperty(context, PI_CONTEXT_PROJECTION_REPORTER_FIELD, {
|
|
83
|
+
configurable: false,
|
|
84
|
+
enumerable: false,
|
|
85
|
+
writable: false,
|
|
86
|
+
value: (reason: string) => {
|
|
87
|
+
const state = activeProjectionByRunner.get(this);
|
|
88
|
+
if (state) state.failure = reason;
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
return context;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
Object.defineProperty(patched, PATCH_MARKER_FIELD, {
|
|
96
|
+
configurable: false,
|
|
97
|
+
enumerable: false,
|
|
98
|
+
writable: false,
|
|
99
|
+
value: true,
|
|
100
|
+
});
|
|
101
|
+
prototype.createContext = patched;
|
|
102
|
+
return { ok: true, installed: true };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Return the patched projection method, if this Pi process supports it. */
|
|
106
|
+
export function getPiContextHookProjector(
|
|
107
|
+
ctx: ExtensionContext,
|
|
108
|
+
): PiContextHookProjector | undefined {
|
|
109
|
+
const candidate = (ctx as PatchedExtensionContext)[PI_CONTEXT_PROJECTOR_FIELD];
|
|
110
|
+
return typeof candidate === "function" ? candidate : undefined;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Preserve fail-closed context-hook errors across Pi's emitContext boundary. */
|
|
114
|
+
export function reportPiContextHookFailure(ctx: ExtensionContext, reason: string): void {
|
|
115
|
+
(ctx as PatchedExtensionContext)[PI_CONTEXT_PROJECTION_REPORTER_FIELD]?.(reason);
|
|
116
|
+
}
|
|
@@ -30,12 +30,23 @@ type CachedRequestContext = {
|
|
|
30
30
|
extras: CompactionRequestExtras;
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
+
export type RequestContextCaptureOptions = {
|
|
34
|
+
/** Standalone Web Search is a local tool and must not enter synthetic compact requests. */
|
|
35
|
+
excludeWebSearchTools?: boolean;
|
|
36
|
+
};
|
|
37
|
+
|
|
33
38
|
let cached: CachedRequestContext | undefined;
|
|
34
39
|
|
|
35
40
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
36
41
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
37
42
|
}
|
|
38
43
|
|
|
44
|
+
function isWebSearchTool(value: unknown): boolean {
|
|
45
|
+
if (!isRecord(value)) return false;
|
|
46
|
+
if (value.type === "web_search" || value.type === "web_search_preview") return true;
|
|
47
|
+
return value.type === "function" && (value.name === "web_search" || value.name === "web.run");
|
|
48
|
+
}
|
|
49
|
+
|
|
39
50
|
function sameIdentity(cachedIdentity: RequestContextIdentity, current: RequestContextIdentity): boolean {
|
|
40
51
|
return (
|
|
41
52
|
cachedIdentity.provider === current.provider &&
|
|
@@ -54,6 +65,7 @@ function sameIdentity(cachedIdentity: RequestContextIdentity, current: RequestCo
|
|
|
54
65
|
export function rememberRequestContext(
|
|
55
66
|
payload: ResponsesCompatibleRequestPayload,
|
|
56
67
|
identity: RequestContextIdentity,
|
|
68
|
+
options: RequestContextCaptureOptions = {},
|
|
57
69
|
): void {
|
|
58
70
|
try {
|
|
59
71
|
if (payload.model !== identity.model) {
|
|
@@ -63,7 +75,10 @@ export function rememberRequestContext(
|
|
|
63
75
|
|
|
64
76
|
const extras: CompactionRequestExtras = {};
|
|
65
77
|
if (Array.isArray(payload.tools)) {
|
|
66
|
-
|
|
78
|
+
const tools = options.excludeWebSearchTools
|
|
79
|
+
? payload.tools.filter((tool) => !isWebSearchTool(tool))
|
|
80
|
+
: payload.tools;
|
|
81
|
+
extras.tools = structuredClone(tools);
|
|
67
82
|
}
|
|
68
83
|
if (typeof payload.parallel_tool_calls === "boolean") {
|
|
69
84
|
extras.parallel_tool_calls = payload.parallel_tool_calls;
|
package/src/runtime.ts
CHANGED
|
@@ -11,6 +11,7 @@ const OPENAI_RESPONSES_PATH = "responses";
|
|
|
11
11
|
const CODEX_RESPONSES_PATH = "codex/responses";
|
|
12
12
|
const OPENAI_COMPACT_PATH = "responses/compact";
|
|
13
13
|
const CODEX_COMPACT_PATH = "codex/responses/compact";
|
|
14
|
+
const ALPHA_SEARCH_PATH = "alpha/search";
|
|
14
15
|
|
|
15
16
|
export type ResponsesApi = (typeof RESPONSES_COMPACT_CAPABLE_APIS)[number];
|
|
16
17
|
|
|
@@ -215,6 +216,57 @@ export function buildCompactPath(api: ResponsesCompactApi): string {
|
|
|
215
216
|
return api === "openai-codex-responses" ? CODEX_COMPACT_PATH : OPENAI_COMPACT_PATH;
|
|
216
217
|
}
|
|
217
218
|
|
|
219
|
+
/**
|
|
220
|
+
* Build the provider-relative standalone-search endpoint. This deliberately
|
|
221
|
+
* rejects a full Responses endpoint so an already-resolved URL cannot become
|
|
222
|
+
* `/responses/alpha/search` or `/codex/responses/alpha/search` by accident.
|
|
223
|
+
*/
|
|
224
|
+
export function buildAlphaSearchUrl(baseUrl: string): string | undefined {
|
|
225
|
+
const normalized = normalizeBaseUrl(baseUrl);
|
|
226
|
+
if (!normalized) return undefined;
|
|
227
|
+
|
|
228
|
+
let parsed: URL;
|
|
229
|
+
try {
|
|
230
|
+
parsed = new URL(normalized);
|
|
231
|
+
} catch {
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
if (
|
|
235
|
+
(parsed.protocol !== "http:" && parsed.protocol !== "https:") ||
|
|
236
|
+
parsed.username ||
|
|
237
|
+
parsed.password ||
|
|
238
|
+
parsed.search ||
|
|
239
|
+
parsed.hash
|
|
240
|
+
) {
|
|
241
|
+
return undefined;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const pathname = parsed.pathname.replace(/\/+$/, "");
|
|
245
|
+
if (
|
|
246
|
+
pathname.endsWith("/responses/alpha/search") ||
|
|
247
|
+
pathname.endsWith("/codex/responses/alpha/search") ||
|
|
248
|
+
pathname.endsWith("/responses/compact/alpha/search") ||
|
|
249
|
+
pathname.endsWith("/codex/responses/compact/alpha/search")
|
|
250
|
+
) {
|
|
251
|
+
return undefined;
|
|
252
|
+
}
|
|
253
|
+
if (pathname.endsWith("/alpha/search")) {
|
|
254
|
+
parsed.pathname = pathname;
|
|
255
|
+
return parsed.toString();
|
|
256
|
+
}
|
|
257
|
+
if (
|
|
258
|
+
pathname.endsWith("/responses") ||
|
|
259
|
+
pathname.endsWith("/codex/responses") ||
|
|
260
|
+
pathname.endsWith("/responses/compact") ||
|
|
261
|
+
pathname.endsWith("/codex/responses/compact")
|
|
262
|
+
) {
|
|
263
|
+
return undefined;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
parsed.pathname = `${pathname}/${ALPHA_SEARCH_PATH}`.replace(/^\/\//, "/");
|
|
267
|
+
return parsed.toString();
|
|
268
|
+
}
|
|
269
|
+
|
|
218
270
|
async function resolveRequestAuth(ctx: ExtensionContext, model: RuntimeModel): Promise<ResolvedRequestAuth> {
|
|
219
271
|
const modelRegistry = ctx.modelRegistry as {
|
|
220
272
|
getApiKeyAndHeaders?: (currentModel: RuntimeModel) => Promise<ResolvedRequestAuth>;
|
package/src/types.ts
CHANGED
|
@@ -22,6 +22,17 @@ export const ASTRA_MODEL_ID = "gpt-6-astra";
|
|
|
22
22
|
export const LEGACY_NATIVE_COMPACTION_STRATEGY = "openai-native-compact-v1";
|
|
23
23
|
export const REMOTE_V2_COMPACTION_STRATEGY = "openai-remote-compaction-v2";
|
|
24
24
|
export const NATIVE_COMPACTION_STRATEGY = REMOTE_V2_COMPACTION_STRATEGY;
|
|
25
|
+
/**
|
|
26
|
+
* Provenance recorded when the checkpoint input came from Pi's ordered context
|
|
27
|
+
* hook projection. Older persisted entries may omit it and must not be
|
|
28
|
+
* replayed by Remote V2.
|
|
29
|
+
*/
|
|
30
|
+
export const NATIVE_COMPACTION_INPUT_PROVENANCE = "pi-context-hook-v1" as const;
|
|
31
|
+
/** Explicit opt-in marker for the pre-projection raw session/branch chain. */
|
|
32
|
+
export const LEGACY_REMOTE_V2_INPUT_PROVENANCE = "legacy-raw-context-v1" as const;
|
|
33
|
+
export type NativeCompactionInputProvenance =
|
|
34
|
+
| typeof NATIVE_COMPACTION_INPUT_PROVENANCE
|
|
35
|
+
| typeof LEGACY_REMOTE_V2_INPUT_PROVENANCE;
|
|
25
36
|
/**
|
|
26
37
|
* Pi currently requires CompactionResult.summary to be text. This marker is only a
|
|
27
38
|
* replay shim; the provider receives the opaque item from details.compactedWindow.
|
|
@@ -46,6 +57,9 @@ export type DebugArtifactKind =
|
|
|
46
57
|
|
|
47
58
|
export type ContextManagementMode = "off" | "remote";
|
|
48
59
|
|
|
60
|
+
/** How Remote V2 obtains the history it sends to the compaction endpoint. */
|
|
61
|
+
export type RemoteV2ContextSource = "pi-context-hook" | "legacy";
|
|
62
|
+
|
|
49
63
|
/** Native-method fallback compaction: which model runs pi's compact() and how deeply. */
|
|
50
64
|
export type NativeFallbackConfig = {
|
|
51
65
|
enabled: boolean;
|
|
@@ -74,6 +88,11 @@ export type CompactionConfig = {
|
|
|
74
88
|
* remains the checkpoint consumer and continues handling normal requests.
|
|
75
89
|
*/
|
|
76
90
|
remoteCompactModel?: string;
|
|
91
|
+
/**
|
|
92
|
+
* Remote V2 input source. Pi's patched context hook preserves the normal
|
|
93
|
+
* provider-visible projection; legacy keeps the pre-patch raw session/branch chain.
|
|
94
|
+
*/
|
|
95
|
+
remoteV2ContextSource: RemoteV2ContextSource;
|
|
77
96
|
/** Native-method fallback compaction policy (non-Responses APIs, or when the compact endpoint fails). */
|
|
78
97
|
nativeFallback: NativeFallbackConfig;
|
|
79
98
|
/**
|
|
@@ -99,10 +118,16 @@ export type CompactionConfig = {
|
|
|
99
118
|
artifactRoot: string;
|
|
100
119
|
};
|
|
101
120
|
|
|
121
|
+
export type WebSearchRoute = "local" | "hosted" | "standalone-alpha";
|
|
122
|
+
|
|
102
123
|
export type WebSearchConfig = {
|
|
103
124
|
enabled: boolean;
|
|
104
125
|
/** Exact provider/model keys allowed to use toolkit-native Web Search. */
|
|
105
126
|
models: string[];
|
|
127
|
+
/** Explicit route for every model not matched by `routes`. */
|
|
128
|
+
defaultRoute?: WebSearchRoute;
|
|
129
|
+
/** Exact `provider/model-id` overrides; no globs or fuzzy matching. */
|
|
130
|
+
routes?: Record<string, WebSearchRoute>;
|
|
106
131
|
};
|
|
107
132
|
|
|
108
133
|
export type ImageGenerationConfig = {
|
|
@@ -252,6 +277,12 @@ export type DeferredToolCarryoverV1 = {
|
|
|
252
277
|
|
|
253
278
|
export type NativeCompactionDetails = NativeCompactionIdentity & {
|
|
254
279
|
strategy: NativeCompactionStrategy;
|
|
280
|
+
/**
|
|
281
|
+
* Input source recorded for checkpoints created by this extension. Older
|
|
282
|
+
* persisted entries may omit it and must not be treated as verified for
|
|
283
|
+
* Remote V2 replay.
|
|
284
|
+
*/
|
|
285
|
+
inputProvenance?: NativeCompactionInputProvenance;
|
|
255
286
|
/** Actual producer of the opaque checkpoint; absent on legacy same-model entries. */
|
|
256
287
|
compactionModel?: NativeCompactionIdentity;
|
|
257
288
|
/** Cache-stack activation state captured at this opaque checkpoint. */
|
|
@@ -265,6 +296,7 @@ export type NativeCompactionDetails = NativeCompactionIdentity & {
|
|
|
265
296
|
export type NativeCompactionEntry = CompactionEntry<NativeCompactionDetails>;
|
|
266
297
|
|
|
267
298
|
export type CreateNativeCompactionDetailsInput = NativeCompactionIdentity & {
|
|
299
|
+
inputProvenance: NativeCompactionInputProvenance;
|
|
268
300
|
compactionModel?: NativeCompactionIdentity;
|
|
269
301
|
deferredToolCarryover?: DeferredToolCarryoverV1;
|
|
270
302
|
compactedWindow: unknown[];
|
|
@@ -380,6 +412,16 @@ export function isNativeCompactionIdentity(value: unknown): value is NativeCompa
|
|
|
380
412
|
);
|
|
381
413
|
}
|
|
382
414
|
|
|
415
|
+
export function isNativeCompactionInputProvenance(value: unknown): value is NativeCompactionInputProvenance {
|
|
416
|
+
return value === NATIVE_COMPACTION_INPUT_PROVENANCE || value === LEGACY_REMOTE_V2_INPUT_PROVENANCE;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
export function getRemoteV2InputProvenance(source: RemoteV2ContextSource): NativeCompactionInputProvenance {
|
|
420
|
+
return source === "pi-context-hook"
|
|
421
|
+
? NATIVE_COMPACTION_INPUT_PROVENANCE
|
|
422
|
+
: LEGACY_REMOTE_V2_INPUT_PROVENANCE;
|
|
423
|
+
}
|
|
424
|
+
|
|
383
425
|
export function isDeferredToolCarryover(value: unknown): value is DeferredToolCarryoverV1 {
|
|
384
426
|
if (!isRecord(value) || value.version !== 1 || value.source !== CACHE_STACK_ACTIVATION_ENTRY_TYPE) {
|
|
385
427
|
return false;
|
|
@@ -447,6 +489,7 @@ export function isNativeCompactionDetails(value: unknown): value is NativeCompac
|
|
|
447
489
|
return (
|
|
448
490
|
(candidate.strategy === LEGACY_NATIVE_COMPACTION_STRATEGY ||
|
|
449
491
|
candidate.strategy === REMOTE_V2_COMPACTION_STRATEGY) &&
|
|
492
|
+
(candidate.inputProvenance === undefined || isNativeCompactionInputProvenance(candidate.inputProvenance)) &&
|
|
450
493
|
(candidate.compactionModel === undefined || isNativeCompactionIdentity(candidate.compactionModel)) &&
|
|
451
494
|
(candidate.deferredToolCarryover === undefined || isDeferredToolCarryover(candidate.deferredToolCarryover)) &&
|
|
452
495
|
Array.isArray(candidate.compactedWindow) &&
|
|
@@ -464,6 +507,7 @@ export function isNativeCompactionEntry(value: unknown): value is NativeCompacti
|
|
|
464
507
|
export function createNativeCompactionDetails(input: CreateNativeCompactionDetailsInput): NativeCompactionDetails {
|
|
465
508
|
return {
|
|
466
509
|
strategy: NATIVE_COMPACTION_STRATEGY,
|
|
510
|
+
inputProvenance: input.inputProvenance,
|
|
467
511
|
provider: normalizeString(input.provider),
|
|
468
512
|
api: normalizeString(input.api),
|
|
469
513
|
model: normalizeString(input.model),
|
|
@@ -516,6 +560,8 @@ export const DEFAULT_COMPACTION_CONFIG: CompactionConfig = {
|
|
|
516
560
|
contextManagement: "off",
|
|
517
561
|
allowCompactionContinuityBreak: false,
|
|
518
562
|
remoteCompactModel: undefined,
|
|
563
|
+
remoteV2ContextSource: "legacy",
|
|
564
|
+
|
|
519
565
|
nativeFallback: { ...DEFAULT_NATIVE_FALLBACK_CONFIG },
|
|
520
566
|
responsesApis: [...RESPONSES_COMPACT_CAPABLE_APIS],
|
|
521
567
|
gatewayContextModels: [],
|