pi-openai-toolkit 0.14.8 → 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.
Files changed (39) hide show
  1. package/README.md +1 -1
  2. package/README.zh.md +1 -1
  3. package/docs/internals.md +18 -0
  4. package/extensions/auto-mode.ts +1 -1
  5. package/extensions/codex-astra.ts +1 -1
  6. package/extensions/compaction.ts +1 -1
  7. package/extensions/image-generation.ts +1 -1
  8. package/extensions/web-search.ts +1 -1
  9. package/package.json +2 -1
  10. package/src/auto-mode/classifier.ts +165 -165
  11. package/src/auto-mode/extension.ts +129 -20
  12. package/src/auto-mode/policy.ts +146 -146
  13. package/src/auto-mode/prompt.ts +279 -279
  14. package/src/auto-mode/reviewer.ts +325 -325
  15. package/src/auto-mode/tool-review-tui.ts +211 -0
  16. package/src/auto-mode/transcript.ts +133 -133
  17. package/src/auto-mode/types.ts +137 -137
  18. package/src/codex-astra/effort-planner.ts +175 -175
  19. package/src/codex-astra/extension.ts +122 -122
  20. package/src/compaction-projection.ts +31 -31
  21. package/src/context-management/messages.ts +112 -112
  22. package/src/context-management/namespace-tools.ts +426 -426
  23. package/src/context-management/tool-contract.ts +23 -23
  24. package/src/context-management/tools.ts +291 -291
  25. package/src/context-management/window-budget.ts +83 -83
  26. package/src/context-management/window-request.ts +125 -125
  27. package/src/image-generation/artifacts.ts +237 -237
  28. package/src/image-generation/client.ts +248 -248
  29. package/src/image-generation/eligibility.ts +18 -18
  30. package/src/image-generation/references.ts +194 -194
  31. package/src/image-generation/render.ts +252 -252
  32. package/src/model-scope.ts +17 -17
  33. package/src/pi-context-hook.ts +116 -116
  34. package/src/provider-headers.ts +23 -23
  35. package/src/web-search/alpha-client.ts +875 -875
  36. package/src/web-search/extension.ts +523 -523
  37. package/src/web-search/payload.ts +295 -295
  38. package/src/web-search/prompt.ts +59 -59
  39. package/src/web-search/types.ts +144 -144
@@ -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
+ }
@@ -1,31 +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
- }
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
+ }
@@ -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, 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
- }
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
+ }