pi-openai-toolkit 0.14.7 → 0.14.9
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 +37 -8
- package/README.zh.md +37 -8
- package/docs/internals.md +27 -1
- package/extensions/auto-mode.ts +1 -1
- package/extensions/codex-astra.ts +1 -1
- package/extensions/compaction.ts +1 -1
- package/extensions/image-generation.ts +1 -1
- package/extensions/web-search.ts +1 -1
- package/package.json +6 -2
- package/src/auto-mode/classifier.ts +165 -165
- package/src/auto-mode/extension.ts +129 -20
- package/src/auto-mode/policy.ts +146 -146
- package/src/auto-mode/prompt.ts +279 -279
- package/src/auto-mode/reviewer.ts +325 -325
- package/src/auto-mode/tool-review-tui.ts +211 -0
- package/src/auto-mode/transcript.ts +133 -133
- package/src/auto-mode/types.ts +137 -137
- package/src/codex-astra/effort-planner.ts +175 -175
- package/src/codex-astra/extension.ts +122 -122
- package/src/compaction-projection.ts +31 -0
- package/src/config.ts +100 -1
- package/src/context-management/messages.ts +112 -112
- package/src/context-management/namespace-tools.ts +426 -426
- package/src/context-management/tool-contract.ts +23 -23
- package/src/context-management/tools.ts +291 -279
- package/src/context-management/window-budget.ts +83 -83
- package/src/context-management/window-manager.ts +110 -28
- package/src/context-management/window-request.ts +125 -125
- package/src/extension-runtime.ts +263 -34
- package/src/image-generation/artifacts.ts +237 -237
- package/src/image-generation/client.ts +248 -248
- package/src/image-generation/eligibility.ts +18 -18
- package/src/image-generation/references.ts +194 -194
- package/src/image-generation/render.ts +252 -252
- package/src/model-scope.ts +17 -17
- package/src/payload-rewrite.ts +55 -10
- package/src/pi-context-hook.ts +116 -0
- package/src/provider-headers.ts +23 -23
- 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 +523 -80
- package/src/web-search/payload.ts +295 -96
- package/src/web-search/prompt.ts +59 -20
- package/src/web-search/types.ts +144 -45
- package/tsconfig.check.json +2 -0
|
@@ -1,122 +1,122 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { ASTRA_MODEL_ID } from "../types";
|
|
3
|
-
import { CODEX_CLIENT_VERSION } from "../responses-headers";
|
|
4
|
-
import {
|
|
5
|
-
getEffortControlState,
|
|
6
|
-
planStableEffort,
|
|
7
|
-
type EffortControlState,
|
|
8
|
-
type PlannableInputItem,
|
|
9
|
-
} from "./effort-planner";
|
|
10
|
-
|
|
11
|
-
type AstraPayload = {
|
|
12
|
-
model: string;
|
|
13
|
-
input: PlannableInputItem[];
|
|
14
|
-
reasoning: { effort: string; [key: string]: unknown };
|
|
15
|
-
[key: string]: unknown;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
19
|
-
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Recognize the live Codex Responses request body: model + wire input array
|
|
24
|
-
* + a request-level string effort. Anything else (titles, embeddings, or a
|
|
25
|
-
* shape we do not understand) is left untouched.
|
|
26
|
-
*/
|
|
27
|
-
function parseAstraPayload(payload: unknown): AstraPayload | undefined {
|
|
28
|
-
if (!isRecord(payload)) return undefined;
|
|
29
|
-
if (typeof payload.model !== "string") return undefined;
|
|
30
|
-
if (!Array.isArray(payload.input)) return undefined;
|
|
31
|
-
const reasoning = payload.reasoning;
|
|
32
|
-
if (!isRecord(reasoning) || typeof reasoning.effort !== "string") return undefined;
|
|
33
|
-
if (reasoning.effort.length === 0 || reasoning.effort === "none") return undefined;
|
|
34
|
-
if (!payload.input.every((item) => isRecord(item))) return undefined;
|
|
35
|
-
return payload as unknown as AstraPayload;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function isCompactionShaped(payload: AstraPayload): boolean {
|
|
39
|
-
// A `compaction_trigger` history belongs to a compact endpoint request,
|
|
40
|
-
// which rejects `configuration_update` items. Our compaction clients use
|
|
41
|
-
// direct fetch and never reach this hook; this guard covers any future
|
|
42
|
-
// provider-routed compaction.
|
|
43
|
-
const last = payload.input[payload.input.length - 1];
|
|
44
|
-
return !!last && isRecord(last) && last.type === "compaction_trigger";
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function getSessionId(ctx: { sessionManager: { getSessionId(): string } }): string | undefined {
|
|
48
|
-
try {
|
|
49
|
-
return ctx.sessionManager.getSessionId();
|
|
50
|
-
} catch {
|
|
51
|
-
return undefined;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function registerCodexAstraExtension(pi: ExtensionAPI): void {
|
|
56
|
-
const states = new Map<string, EffortControlState>();
|
|
57
|
-
|
|
58
|
-
// Switching or forking a session rebuilds the runtime and re-fires
|
|
59
|
-
// session_start; clearing there covers every replacement path.
|
|
60
|
-
pi.on("session_start", () => {
|
|
61
|
-
states.clear();
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
pi.on("before_provider_request", (event, ctx) => {
|
|
65
|
-
try {
|
|
66
|
-
// Silent activation by design: the Astra compatibility layer exists only
|
|
67
|
-
// to keep `gpt-6-astra` working through the Codex backend gate, so it
|
|
68
|
-
// keys off the bare model id instead of an operator allowlist.
|
|
69
|
-
const model = ctx.model;
|
|
70
|
-
// Both Responses wire families carry the same reasoning.effort prefix
|
|
71
|
-
// problem, and upstream OMP plans on both providers.
|
|
72
|
-
if (!model || (model.api !== "openai-codex-responses" && model.api !== "openai-responses")) {
|
|
73
|
-
return undefined;
|
|
74
|
-
}
|
|
75
|
-
if (model.id !== ASTRA_MODEL_ID) return undefined;
|
|
76
|
-
|
|
77
|
-
const payload = parseAstraPayload(event.payload);
|
|
78
|
-
// The payload must belong to the model the eligibility check just
|
|
79
|
-
// passed; a mismatch means this request is not ours to plan.
|
|
80
|
-
if (!payload || payload.model !== model.id) return undefined;
|
|
81
|
-
if (isCompactionShaped(payload)) return undefined;
|
|
82
|
-
|
|
83
|
-
const sessionId = getSessionId(ctx);
|
|
84
|
-
const state = getEffortControlState(
|
|
85
|
-
states,
|
|
86
|
-
`${model.baseUrl ?? ""}\u0000${model.id}\u0000${sessionId ?? "no-session"}`,
|
|
87
|
-
);
|
|
88
|
-
|
|
89
|
-
// Idempotence: never plan on top of items a previous pass injected.
|
|
90
|
-
const history = payload.input.filter((item) => item.type !== "configuration_update");
|
|
91
|
-
const { input, result } = planStableEffort(state, history, payload.reasoning.effort);
|
|
92
|
-
if (result.effort === payload.reasoning.effort && result.spliced === 0) return undefined;
|
|
93
|
-
|
|
94
|
-
return {
|
|
95
|
-
...payload,
|
|
96
|
-
input,
|
|
97
|
-
reasoning: { ...payload.reasoning, effort: result.effort },
|
|
98
|
-
};
|
|
99
|
-
} catch {
|
|
100
|
-
// A planner failure must never break the provider request path.
|
|
101
|
-
return undefined;
|
|
102
|
-
}
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
pi.on("before_provider_headers", (event, ctx) => {
|
|
106
|
-
try {
|
|
107
|
-
const model = ctx.model;
|
|
108
|
-
if (!model || model.api !== "openai-codex-responses") return;
|
|
109
|
-
// Live requests need the same backend version gate as our synthetic
|
|
110
|
-
// ones: an old or missing `version` never reaches gated SKUs.
|
|
111
|
-
if (!event.headers.version) {
|
|
112
|
-
event.headers.version = CODEX_CLIENT_VERSION;
|
|
113
|
-
}
|
|
114
|
-
} catch {
|
|
115
|
-
// Header injection is best-effort.
|
|
116
|
-
}
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export default function codexAstraExtension(pi: ExtensionAPI): void {
|
|
121
|
-
registerCodexAstraExtension(pi);
|
|
122
|
-
}
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { ASTRA_MODEL_ID } from "../types";
|
|
3
|
+
import { CODEX_CLIENT_VERSION } from "../responses-headers";
|
|
4
|
+
import {
|
|
5
|
+
getEffortControlState,
|
|
6
|
+
planStableEffort,
|
|
7
|
+
type EffortControlState,
|
|
8
|
+
type PlannableInputItem,
|
|
9
|
+
} from "./effort-planner";
|
|
10
|
+
|
|
11
|
+
type AstraPayload = {
|
|
12
|
+
model: string;
|
|
13
|
+
input: PlannableInputItem[];
|
|
14
|
+
reasoning: { effort: string; [key: string]: unknown };
|
|
15
|
+
[key: string]: unknown;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
19
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Recognize the live Codex Responses request body: model + wire input array
|
|
24
|
+
* + a request-level string effort. Anything else (titles, embeddings, or a
|
|
25
|
+
* shape we do not understand) is left untouched.
|
|
26
|
+
*/
|
|
27
|
+
function parseAstraPayload(payload: unknown): AstraPayload | undefined {
|
|
28
|
+
if (!isRecord(payload)) return undefined;
|
|
29
|
+
if (typeof payload.model !== "string") return undefined;
|
|
30
|
+
if (!Array.isArray(payload.input)) return undefined;
|
|
31
|
+
const reasoning = payload.reasoning;
|
|
32
|
+
if (!isRecord(reasoning) || typeof reasoning.effort !== "string") return undefined;
|
|
33
|
+
if (reasoning.effort.length === 0 || reasoning.effort === "none") return undefined;
|
|
34
|
+
if (!payload.input.every((item) => isRecord(item))) return undefined;
|
|
35
|
+
return payload as unknown as AstraPayload;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isCompactionShaped(payload: AstraPayload): boolean {
|
|
39
|
+
// A `compaction_trigger` history belongs to a compact endpoint request,
|
|
40
|
+
// which rejects `configuration_update` items. Our compaction clients use
|
|
41
|
+
// direct fetch and never reach this hook; this guard covers any future
|
|
42
|
+
// provider-routed compaction.
|
|
43
|
+
const last = payload.input[payload.input.length - 1];
|
|
44
|
+
return !!last && isRecord(last) && last.type === "compaction_trigger";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getSessionId(ctx: { sessionManager: { getSessionId(): string } }): string | undefined {
|
|
48
|
+
try {
|
|
49
|
+
return ctx.sessionManager.getSessionId();
|
|
50
|
+
} catch {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function registerCodexAstraExtension(pi: ExtensionAPI): void {
|
|
56
|
+
const states = new Map<string, EffortControlState>();
|
|
57
|
+
|
|
58
|
+
// Switching or forking a session rebuilds the runtime and re-fires
|
|
59
|
+
// session_start; clearing there covers every replacement path.
|
|
60
|
+
pi.on("session_start", () => {
|
|
61
|
+
states.clear();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
pi.on("before_provider_request", (event, ctx) => {
|
|
65
|
+
try {
|
|
66
|
+
// Silent activation by design: the Astra compatibility layer exists only
|
|
67
|
+
// to keep `gpt-6-astra` working through the Codex backend gate, so it
|
|
68
|
+
// keys off the bare model id instead of an operator allowlist.
|
|
69
|
+
const model = ctx.model;
|
|
70
|
+
// Both Responses wire families carry the same reasoning.effort prefix
|
|
71
|
+
// problem, and upstream OMP plans on both providers.
|
|
72
|
+
if (!model || (model.api !== "openai-codex-responses" && model.api !== "openai-responses")) {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
if (model.id !== ASTRA_MODEL_ID) return undefined;
|
|
76
|
+
|
|
77
|
+
const payload = parseAstraPayload(event.payload);
|
|
78
|
+
// The payload must belong to the model the eligibility check just
|
|
79
|
+
// passed; a mismatch means this request is not ours to plan.
|
|
80
|
+
if (!payload || payload.model !== model.id) return undefined;
|
|
81
|
+
if (isCompactionShaped(payload)) return undefined;
|
|
82
|
+
|
|
83
|
+
const sessionId = getSessionId(ctx);
|
|
84
|
+
const state = getEffortControlState(
|
|
85
|
+
states,
|
|
86
|
+
`${model.baseUrl ?? ""}\u0000${model.id}\u0000${sessionId ?? "no-session"}`,
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
// Idempotence: never plan on top of items a previous pass injected.
|
|
90
|
+
const history = payload.input.filter((item) => item.type !== "configuration_update");
|
|
91
|
+
const { input, result } = planStableEffort(state, history, payload.reasoning.effort);
|
|
92
|
+
if (result.effort === payload.reasoning.effort && result.spliced === 0) return undefined;
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
...payload,
|
|
96
|
+
input,
|
|
97
|
+
reasoning: { ...payload.reasoning, effort: result.effort },
|
|
98
|
+
};
|
|
99
|
+
} catch {
|
|
100
|
+
// A planner failure must never break the provider request path.
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
pi.on("before_provider_headers", (event, ctx) => {
|
|
106
|
+
try {
|
|
107
|
+
const model = ctx.model;
|
|
108
|
+
if (!model || model.api !== "openai-codex-responses") return;
|
|
109
|
+
// Live requests need the same backend version gate as our synthetic
|
|
110
|
+
// ones: an old or missing `version` never reaches gated SKUs.
|
|
111
|
+
if (!event.headers.version) {
|
|
112
|
+
event.headers.version = CODEX_CLIENT_VERSION;
|
|
113
|
+
}
|
|
114
|
+
} catch {
|
|
115
|
+
// Header injection is best-effort.
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export default function codexAstraExtension(pi: ExtensionAPI): void {
|
|
121
|
+
registerCodexAstraExtension(pi);
|
|
122
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
NativeCompactionDetails,
|
|
3
|
+
NativeCompactionInputProvenance,
|
|
4
|
+
} from "./types";
|
|
5
|
+
|
|
6
|
+
/** The configured Pi context-hook projector is unavailable or failed. */
|
|
7
|
+
export const COMPACTION_PROJECTION_UNAVAILABLE = "no-supported-projection-seam" as const;
|
|
8
|
+
/** The current session context could not be read before projection. */
|
|
9
|
+
export const COMPACTION_SESSION_CONTEXT_UNAVAILABLE = "session-context-unavailable" as const;
|
|
10
|
+
/** A projected recursive context did not contain the current compaction summary. */
|
|
11
|
+
export const COMPACTION_PROJECTED_CONTEXT_UNAVAILABLE = "projected-compaction-context-unavailable" as const;
|
|
12
|
+
/** The latest opaque checkpoint was created with no or a different input marker. */
|
|
13
|
+
export const COMPACTION_CHECKPOINT_PROVENANCE_UNAVAILABLE = "unverified-compaction-checkpoint" as const;
|
|
14
|
+
|
|
15
|
+
export type UnprojectedCompactionReason =
|
|
16
|
+
| typeof COMPACTION_PROJECTION_UNAVAILABLE
|
|
17
|
+
| typeof COMPACTION_SESSION_CONTEXT_UNAVAILABLE
|
|
18
|
+
| typeof COMPACTION_CHECKPOINT_PROVENANCE_UNAVAILABLE
|
|
19
|
+
| typeof COMPACTION_PROJECTED_CONTEXT_UNAVAILABLE;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A Remote V2 checkpoint is safe for the current replay chain only when its
|
|
23
|
+
* marker matches the configured input source. Structural persistence validation
|
|
24
|
+
* intentionally remains more permissive for old session files.
|
|
25
|
+
*/
|
|
26
|
+
export function hasVerifiedCompactionInputProvenance(
|
|
27
|
+
details: Pick<NativeCompactionDetails, "inputProvenance"> | undefined,
|
|
28
|
+
expected: NativeCompactionInputProvenance,
|
|
29
|
+
): boolean {
|
|
30
|
+
return details?.inputProvenance === expected;
|
|
31
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -31,10 +31,12 @@ import {
|
|
|
31
31
|
type CompactionConfig,
|
|
32
32
|
type ContextManagementMode,
|
|
33
33
|
type ImageGenerationConfig,
|
|
34
|
+
type RemoteV2ContextSource,
|
|
34
35
|
type LoadedToolkitConfig,
|
|
35
36
|
type NativeFallbackConfig,
|
|
36
37
|
type ToolkitConfig,
|
|
37
38
|
type WebSearchConfig,
|
|
39
|
+
type WebSearchRoute,
|
|
38
40
|
} from "./types";
|
|
39
41
|
import { MAX_IMAGE_MODEL_ID_CHARS } from "./image-generation/types";
|
|
40
42
|
|
|
@@ -47,6 +49,7 @@ const COMPACTION_FIELDS = new Set([
|
|
|
47
49
|
"contextManagement",
|
|
48
50
|
"allowCompactionContinuityBreak",
|
|
49
51
|
"remoteCompactModel",
|
|
52
|
+
"remoteV2ContextSource",
|
|
50
53
|
"nativeFallback",
|
|
51
54
|
"responsesApis",
|
|
52
55
|
"gatewayContextModels",
|
|
@@ -59,7 +62,7 @@ const COMPACTION_FIELDS = new Set([
|
|
|
59
62
|
"artifactRoot",
|
|
60
63
|
]);
|
|
61
64
|
const NATIVE_FALLBACK_FIELDS = new Set(["enabled", "model", "thinkingLevel"]);
|
|
62
|
-
const WEB_SEARCH_FIELDS = new Set(["enabled", "models"]);
|
|
65
|
+
const WEB_SEARCH_FIELDS = new Set(["enabled", "models", "defaultRoute", "routes"]);
|
|
63
66
|
const IMAGE_GENERATION_FIELDS = new Set(["enabled", "models"]);
|
|
64
67
|
const AUTO_MODE_FIELDS = new Set([
|
|
65
68
|
"enabled",
|
|
@@ -148,6 +151,20 @@ function toContextManagementMode(
|
|
|
148
151
|
return undefined;
|
|
149
152
|
}
|
|
150
153
|
|
|
154
|
+
function toRemoteV2ContextSource(
|
|
155
|
+
value: unknown,
|
|
156
|
+
fieldPath: string,
|
|
157
|
+
warnings: string[],
|
|
158
|
+
): RemoteV2ContextSource | undefined {
|
|
159
|
+
if (value === undefined) return undefined;
|
|
160
|
+
if (typeof value === "string") {
|
|
161
|
+
const normalized = value.trim();
|
|
162
|
+
if (normalized === "pi-context-hook" || normalized === "legacy") return normalized;
|
|
163
|
+
}
|
|
164
|
+
warnings.push(`Ignoring ${fieldPath}: expected one of pi-context-hook, legacy.`);
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
|
|
151
168
|
function toModelSpec(value: unknown, fieldPath: string, warnings: string[]): string | null | undefined {
|
|
152
169
|
if (value === undefined) return undefined;
|
|
153
170
|
if (value === null) return null;
|
|
@@ -231,6 +248,53 @@ function toImageGenerationModels(value: unknown, fieldPath: string, warnings: st
|
|
|
231
248
|
return models;
|
|
232
249
|
}
|
|
233
250
|
|
|
251
|
+
function toWebSearchRoute(
|
|
252
|
+
value: unknown,
|
|
253
|
+
fieldPath: string,
|
|
254
|
+
warnings: string[],
|
|
255
|
+
): WebSearchRoute | undefined {
|
|
256
|
+
if (value === undefined) return undefined;
|
|
257
|
+
if (value === "local" || value === "hosted" || value === "standalone-alpha") {
|
|
258
|
+
return value;
|
|
259
|
+
}
|
|
260
|
+
warnings.push(`Ignoring ${fieldPath}: expected one of local, hosted, standalone-alpha.`);
|
|
261
|
+
return undefined;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function isExactWebSearchModelKey(value: string): boolean {
|
|
265
|
+
const trimmed = value.trim();
|
|
266
|
+
const separatorIndex = trimmed.indexOf("/");
|
|
267
|
+
if (separatorIndex <= 0 || separatorIndex >= trimmed.length - 1) return false;
|
|
268
|
+
if (/[\s*?\[\]{}]/.test(trimmed)) return false;
|
|
269
|
+
const provider = trimmed.slice(0, separatorIndex);
|
|
270
|
+
const modelId = trimmed.slice(separatorIndex + 1);
|
|
271
|
+
return provider.length > 0 && modelId.length > 0;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function toWebSearchRoutes(
|
|
275
|
+
value: unknown,
|
|
276
|
+
fieldPath: string,
|
|
277
|
+
warnings: string[],
|
|
278
|
+
): Record<string, WebSearchRoute> | undefined {
|
|
279
|
+
if (value === undefined) return undefined;
|
|
280
|
+
if (!isRecord(value)) {
|
|
281
|
+
warnings.push(`Ignoring ${fieldPath}: expected a JSON object.`);
|
|
282
|
+
return undefined;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const routes: Record<string, WebSearchRoute> = {};
|
|
286
|
+
for (const [rawKey, rawRoute] of Object.entries(value)) {
|
|
287
|
+
const key = rawKey.trim();
|
|
288
|
+
if (!isExactWebSearchModelKey(key)) {
|
|
289
|
+
warnings.push(`Ignoring ${fieldPath}.${rawKey}: expected an exact "provider/model-id" key.`);
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
const route = toWebSearchRoute(rawRoute, `${fieldPath}.${rawKey}`, warnings);
|
|
293
|
+
if (route !== undefined) routes[key] = route;
|
|
294
|
+
}
|
|
295
|
+
return routes;
|
|
296
|
+
}
|
|
297
|
+
|
|
234
298
|
function toAutoModeGate(value: unknown, fieldPath: string, warnings: string[]): AutoModeGate | undefined {
|
|
235
299
|
if (value === undefined) return undefined;
|
|
236
300
|
if (value === "side-effect" || value === "all") return value;
|
|
@@ -264,6 +328,12 @@ function cloneDefaults(): ToolkitConfig {
|
|
|
264
328
|
webSearch: {
|
|
265
329
|
...DEFAULT_WEB_SEARCH_CONFIG,
|
|
266
330
|
models: [...DEFAULT_WEB_SEARCH_CONFIG.models],
|
|
331
|
+
...(DEFAULT_WEB_SEARCH_CONFIG.defaultRoute
|
|
332
|
+
? { defaultRoute: DEFAULT_WEB_SEARCH_CONFIG.defaultRoute }
|
|
333
|
+
: {}),
|
|
334
|
+
...(DEFAULT_WEB_SEARCH_CONFIG.routes
|
|
335
|
+
? { routes: { ...DEFAULT_WEB_SEARCH_CONFIG.routes } }
|
|
336
|
+
: {}),
|
|
267
337
|
},
|
|
268
338
|
imageGeneration: {
|
|
269
339
|
...DEFAULT_IMAGE_GENERATION_CONFIG,
|
|
@@ -347,6 +417,10 @@ function applyCompactionConfig(
|
|
|
347
417
|
resolved.remoteCompactModel = remoteCompactModelSpec === null ? undefined : remoteCompactModelSpec;
|
|
348
418
|
}
|
|
349
419
|
|
|
420
|
+
resolved.remoteV2ContextSource =
|
|
421
|
+
toRemoteV2ContextSource(raw.remoteV2ContextSource, "compaction.remoteV2ContextSource", warnings) ??
|
|
422
|
+
resolved.remoteV2ContextSource;
|
|
423
|
+
|
|
350
424
|
if (raw.nativeFallback !== undefined) {
|
|
351
425
|
if (isRecord(raw.nativeFallback)) {
|
|
352
426
|
applyNativeFallbackConfig(raw.nativeFallback, resolved.nativeFallback, warnings);
|
|
@@ -393,6 +467,31 @@ function applyWebSearchConfig(
|
|
|
393
467
|
if (models !== undefined) {
|
|
394
468
|
resolved.models = models;
|
|
395
469
|
}
|
|
470
|
+
|
|
471
|
+
const defaultRoute = toWebSearchRoute(raw.defaultRoute, "webSearch.defaultRoute", warnings);
|
|
472
|
+
if (defaultRoute !== undefined) {
|
|
473
|
+
resolved.defaultRoute = defaultRoute;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const routes = toWebSearchRoutes(raw.routes, "webSearch.routes", warnings);
|
|
477
|
+
if (routes !== undefined) {
|
|
478
|
+
resolved.routes = routes;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (defaultRoute !== undefined && resolved.models.length > 0) {
|
|
482
|
+
warnings.push(
|
|
483
|
+
"webSearch.defaultRoute overrides legacy webSearch.models for models without an exact webSearch.routes entry.",
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
if (routes !== undefined) {
|
|
487
|
+
for (const key of Object.keys(routes)) {
|
|
488
|
+
if (resolved.models.includes(key)) {
|
|
489
|
+
warnings.push(
|
|
490
|
+
`webSearch.routes.${key} overrides the legacy webSearch.models entry for the same exact model.`,
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
396
495
|
}
|
|
397
496
|
|
|
398
497
|
function applyImageGenerationConfig(
|
|
@@ -1,112 +1,112 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
-
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
3
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import {
|
|
5
|
-
CODEX_CONTEXT_WINDOW_MESSAGE_TYPE,
|
|
6
|
-
CONTEXT_MANAGEMENT_PROTOCOL,
|
|
7
|
-
CONTEXT_WINDOW_COMPACTION_STRATEGY,
|
|
8
|
-
CONTEXT_WINDOW_FALLBACK_BUFFER,
|
|
9
|
-
CONTEXT_WINDOW_REMINDER_THRESHOLD,
|
|
10
|
-
type CodexContextManagementMessageDetails,
|
|
11
|
-
type ContextManagementMessageKind,
|
|
12
|
-
type ContextWindowCompactionDetails,
|
|
13
|
-
type ContextWindowIdentity,
|
|
14
|
-
type NotesCheckpointReceipt,
|
|
15
|
-
isCodexContextManagementMessageDetails,
|
|
16
|
-
isContextWindowCompactionDetails,
|
|
17
|
-
} from "./types";
|
|
18
|
-
|
|
19
|
-
export {
|
|
20
|
-
CODEX_CONTEXT_WINDOW_MESSAGE_TYPE,
|
|
21
|
-
CONTEXT_WINDOW_COMPACTION_STRATEGY,
|
|
22
|
-
CONTEXT_WINDOW_FALLBACK_BUFFER,
|
|
23
|
-
CONTEXT_WINDOW_REMINDER_THRESHOLD,
|
|
24
|
-
isCodexContextManagementMessageDetails,
|
|
25
|
-
isContextWindowCompactionDetails,
|
|
26
|
-
};
|
|
27
|
-
export type {
|
|
28
|
-
CodexContextManagementMessageDetails,
|
|
29
|
-
ContextManagementMessageKind,
|
|
30
|
-
ContextWindowCompactionDetails,
|
|
31
|
-
ContextWindowIdentity,
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
export const CONTEXT_WINDOW_COMPACTION_SUMMARY =
|
|
35
|
-
"[Pi Codex context-window boundary; no conversation summary was generated.]";
|
|
36
|
-
|
|
37
|
-
const CONTEXT_WINDOW_GUIDANCE = `<context_window_guidance>
|
|
38
|
-
Checkpoint the active request, known history IDs, decisions, progress, learnings and next steps in notes before new_context; no summary carries over. After rollover, read the checkpoint receipt note first when present; use a thread hint only as supplemental guidance. Use history only for a missing detail.
|
|
39
|
-
</context_window_guidance>`;
|
|
40
|
-
|
|
41
|
-
export function renderContextWindowMessage(
|
|
42
|
-
identity: ContextWindowIdentity,
|
|
43
|
-
threadHint?: string,
|
|
44
|
-
checkpoint?: NotesCheckpointReceipt,
|
|
45
|
-
): string {
|
|
46
|
-
const lines = [
|
|
47
|
-
"<context_window>",
|
|
48
|
-
"Agent name: /root",
|
|
49
|
-
`First context window id: ${identity.firstWindowId}`,
|
|
50
|
-
`Current context window id: ${identity.currentWindowId}`,
|
|
51
|
-
];
|
|
52
|
-
if (identity.previousWindowId) lines.push(`Previous context window id: ${identity.previousWindowId}`);
|
|
53
|
-
if (checkpoint) {
|
|
54
|
-
lines.push("Checkpoint successfully written:");
|
|
55
|
-
lines.push(` Read this note before doing anything else with notes action "read_file": ${JSON.stringify(checkpoint.path)}`);
|
|
56
|
-
}
|
|
57
|
-
if (threadHint) lines.push(threadHint);
|
|
58
|
-
lines.push("</context_window>");
|
|
59
|
-
return `${CONTEXT_WINDOW_GUIDANCE}\n\n${lines.join("\n")}`;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export function renderContextWindowReminder(remainingTokens: number): string {
|
|
63
|
-
return `<context_window_reminder>
|
|
64
|
-
Only ${Math.max(0, Math.floor(remainingTokens))} context tokens remain. Checkpoint the active request, state and known history IDs in notes, then call new_context; no conversation summary carries over.
|
|
65
|
-
</context_window_reminder>`;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export const CONTEXT_WINDOW_FALLBACK_MESSAGE = `<context_window_reminder>
|
|
69
|
-
Context exhausted. Do not continue or answer. Make exactly one notes write or append call that checkpoints the active request, state and known history IDs, then call new_context. Use no other tools before rollover.
|
|
70
|
-
</context_window_reminder>`;
|
|
71
|
-
|
|
72
|
-
export function isContextWindowBoundary(
|
|
73
|
-
message: AgentMessage,
|
|
74
|
-
): message is Extract<AgentMessage, { role: "custom" }> & {
|
|
75
|
-
details: CodexContextManagementMessageDetails;
|
|
76
|
-
} {
|
|
77
|
-
return (
|
|
78
|
-
message.role === "custom" &&
|
|
79
|
-
message.customType === CODEX_CONTEXT_WINDOW_MESSAGE_TYPE &&
|
|
80
|
-
isCodexContextManagementMessageDetails(message.details) &&
|
|
81
|
-
message.details.contextManagement.kind === "window"
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export function sendContextWindowMessage(
|
|
86
|
-
pi: ExtensionAPI,
|
|
87
|
-
content: string,
|
|
88
|
-
kind: ContextManagementMessageKind,
|
|
89
|
-
identity: ContextWindowIdentity,
|
|
90
|
-
options: { triggerTurn: boolean; sessionId?: string },
|
|
91
|
-
trimPreviousWindow = false,
|
|
92
|
-
): void {
|
|
93
|
-
const details: CodexContextManagementMessageDetails = {
|
|
94
|
-
protocol: CONTEXT_MANAGEMENT_PROTOCOL,
|
|
95
|
-
id: randomUUID(),
|
|
96
|
-
...(options.sessionId ? { sessionId: options.sessionId } : {}),
|
|
97
|
-
contextManagement: {
|
|
98
|
-
protocol: CONTEXT_MANAGEMENT_PROTOCOL,
|
|
99
|
-
kind,
|
|
100
|
-
...identity,
|
|
101
|
-
...(trimPreviousWindow ? { trimPreviousWindow: true as const } : {}),
|
|
102
|
-
},
|
|
103
|
-
};
|
|
104
|
-
pi.sendMessage({
|
|
105
|
-
customType: CODEX_CONTEXT_WINDOW_MESSAGE_TYPE,
|
|
106
|
-
content,
|
|
107
|
-
display: true,
|
|
108
|
-
details,
|
|
109
|
-
}, options.triggerTurn
|
|
110
|
-
? { deliverAs: "steer", triggerTurn: true }
|
|
111
|
-
: { triggerTurn: false });
|
|
112
|
-
}
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
3
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import {
|
|
5
|
+
CODEX_CONTEXT_WINDOW_MESSAGE_TYPE,
|
|
6
|
+
CONTEXT_MANAGEMENT_PROTOCOL,
|
|
7
|
+
CONTEXT_WINDOW_COMPACTION_STRATEGY,
|
|
8
|
+
CONTEXT_WINDOW_FALLBACK_BUFFER,
|
|
9
|
+
CONTEXT_WINDOW_REMINDER_THRESHOLD,
|
|
10
|
+
type CodexContextManagementMessageDetails,
|
|
11
|
+
type ContextManagementMessageKind,
|
|
12
|
+
type ContextWindowCompactionDetails,
|
|
13
|
+
type ContextWindowIdentity,
|
|
14
|
+
type NotesCheckpointReceipt,
|
|
15
|
+
isCodexContextManagementMessageDetails,
|
|
16
|
+
isContextWindowCompactionDetails,
|
|
17
|
+
} from "./types";
|
|
18
|
+
|
|
19
|
+
export {
|
|
20
|
+
CODEX_CONTEXT_WINDOW_MESSAGE_TYPE,
|
|
21
|
+
CONTEXT_WINDOW_COMPACTION_STRATEGY,
|
|
22
|
+
CONTEXT_WINDOW_FALLBACK_BUFFER,
|
|
23
|
+
CONTEXT_WINDOW_REMINDER_THRESHOLD,
|
|
24
|
+
isCodexContextManagementMessageDetails,
|
|
25
|
+
isContextWindowCompactionDetails,
|
|
26
|
+
};
|
|
27
|
+
export type {
|
|
28
|
+
CodexContextManagementMessageDetails,
|
|
29
|
+
ContextManagementMessageKind,
|
|
30
|
+
ContextWindowCompactionDetails,
|
|
31
|
+
ContextWindowIdentity,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const CONTEXT_WINDOW_COMPACTION_SUMMARY =
|
|
35
|
+
"[Pi Codex context-window boundary; no conversation summary was generated.]";
|
|
36
|
+
|
|
37
|
+
const CONTEXT_WINDOW_GUIDANCE = `<context_window_guidance>
|
|
38
|
+
Checkpoint the active request, known history IDs, decisions, progress, learnings and next steps in notes before new_context, and wait for that notes result to be persisted; only a persisted successful result in the current window unlocks the rollover, and no summary carries over. If new_context reports that a rollover is already scheduled, do not call it again in the same window. After rollover, read the checkpoint receipt note first when present; use a thread hint only as supplemental guidance. Use history only for a missing detail.
|
|
39
|
+
</context_window_guidance>`;
|
|
40
|
+
|
|
41
|
+
export function renderContextWindowMessage(
|
|
42
|
+
identity: ContextWindowIdentity,
|
|
43
|
+
threadHint?: string,
|
|
44
|
+
checkpoint?: NotesCheckpointReceipt,
|
|
45
|
+
): string {
|
|
46
|
+
const lines = [
|
|
47
|
+
"<context_window>",
|
|
48
|
+
"Agent name: /root",
|
|
49
|
+
`First context window id: ${identity.firstWindowId}`,
|
|
50
|
+
`Current context window id: ${identity.currentWindowId}`,
|
|
51
|
+
];
|
|
52
|
+
if (identity.previousWindowId) lines.push(`Previous context window id: ${identity.previousWindowId}`);
|
|
53
|
+
if (checkpoint) {
|
|
54
|
+
lines.push("Checkpoint successfully written:");
|
|
55
|
+
lines.push(` Read this note before doing anything else with notes action "read_file": ${JSON.stringify(checkpoint.path)}`);
|
|
56
|
+
}
|
|
57
|
+
if (threadHint) lines.push(threadHint);
|
|
58
|
+
lines.push("</context_window>");
|
|
59
|
+
return `${CONTEXT_WINDOW_GUIDANCE}\n\n${lines.join("\n")}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function renderContextWindowReminder(remainingTokens: number): string {
|
|
63
|
+
return `<context_window_reminder>
|
|
64
|
+
Only ${Math.max(0, Math.floor(remainingTokens))} context tokens remain. Checkpoint the active request, state and known history IDs in notes, then call new_context; no conversation summary carries over.
|
|
65
|
+
</context_window_reminder>`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const CONTEXT_WINDOW_FALLBACK_MESSAGE = `<context_window_reminder>
|
|
69
|
+
Context exhausted. Do not continue or answer. Make exactly one notes write or append call that checkpoints the active request, state and known history IDs, then call new_context. Use no other tools before rollover.
|
|
70
|
+
</context_window_reminder>`;
|
|
71
|
+
|
|
72
|
+
export function isContextWindowBoundary(
|
|
73
|
+
message: AgentMessage,
|
|
74
|
+
): message is Extract<AgentMessage, { role: "custom" }> & {
|
|
75
|
+
details: CodexContextManagementMessageDetails;
|
|
76
|
+
} {
|
|
77
|
+
return (
|
|
78
|
+
message.role === "custom" &&
|
|
79
|
+
message.customType === CODEX_CONTEXT_WINDOW_MESSAGE_TYPE &&
|
|
80
|
+
isCodexContextManagementMessageDetails(message.details) &&
|
|
81
|
+
message.details.contextManagement.kind === "window"
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function sendContextWindowMessage(
|
|
86
|
+
pi: ExtensionAPI,
|
|
87
|
+
content: string,
|
|
88
|
+
kind: ContextManagementMessageKind,
|
|
89
|
+
identity: ContextWindowIdentity,
|
|
90
|
+
options: { triggerTurn: boolean; sessionId?: string },
|
|
91
|
+
trimPreviousWindow = false,
|
|
92
|
+
): void {
|
|
93
|
+
const details: CodexContextManagementMessageDetails = {
|
|
94
|
+
protocol: CONTEXT_MANAGEMENT_PROTOCOL,
|
|
95
|
+
id: randomUUID(),
|
|
96
|
+
...(options.sessionId ? { sessionId: options.sessionId } : {}),
|
|
97
|
+
contextManagement: {
|
|
98
|
+
protocol: CONTEXT_MANAGEMENT_PROTOCOL,
|
|
99
|
+
kind,
|
|
100
|
+
...identity,
|
|
101
|
+
...(trimPreviousWindow ? { trimPreviousWindow: true as const } : {}),
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
pi.sendMessage({
|
|
105
|
+
customType: CODEX_CONTEXT_WINDOW_MESSAGE_TYPE,
|
|
106
|
+
content,
|
|
107
|
+
display: true,
|
|
108
|
+
details,
|
|
109
|
+
}, options.triggerTurn
|
|
110
|
+
? { deliverAs: "steer", triggerTurn: true }
|
|
111
|
+
: { triggerTurn: false });
|
|
112
|
+
}
|