pi-openai-toolkit 0.14.8 → 0.14.10
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 +1 -1
- package/README.zh.md +1 -1
- package/docs/internals.md +20 -0
- package/package.json +6 -19
- 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 +335 -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 -31
- package/src/context-management/messages.ts +115 -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 +298 -291
- package/src/context-management/window-budget.ts +83 -83
- package/src/context-management/window-request.ts +125 -125
- package/src/extension-runtime.ts +1 -0
- 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 +15 -3
- package/src/pi-context-hook.ts +116 -116
- package/src/provider-headers.ts +23 -23
- package/src/serializer.ts +127 -5
- package/src/web-search/alpha-client.ts +875 -875
- package/src/web-search/extension.ts +523 -523
- package/src/web-search/payload.ts +295 -295
- package/src/web-search/prompt.ts +59 -59
- package/src/web-search/types.ts +144 -144
- package/tsconfig.check.json +0 -4
- package/extensions/auto-mode.ts +0 -1
- package/extensions/codex-astra.ts +0 -1
- package/extensions/compaction.ts +0 -1
- package/extensions/image-generation.ts +0 -1
- package/extensions/web-search.ts +0 -1
|
@@ -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,115 @@
|
|
|
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
|
-
|
|
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("
|
|
55
|
-
lines.push(
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
message.
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
+
Before calling new_context, checkpoint the active request, known history IDs, decisions, progress, learnings and next steps in notes, and wait for that result to be persisted; only a persisted successful result in the current window unlocks the rollover. If new_context reports that a rollover is already scheduled, do not call it again in the same window. When this message includes a completed context-switch handoff, follow that post-rollover section first instead of restarting the pre-rollover checkpoint steps. A thread hint is supplemental and must not replace the local checkpoint receipt. No conversation summary carries over, and history is only for missing details.
|
|
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("Context switch completed. This is the first turn in the new context window.");
|
|
55
|
+
lines.push("Checkpoint successfully written:");
|
|
56
|
+
lines.push(` Read this note before doing anything else with notes action "read_file": ${JSON.stringify(checkpoint.path)}`);
|
|
57
|
+
lines.push("After reading the checkpoint receipt, resume the active user task.");
|
|
58
|
+
lines.push("Do not immediately create another checkpoint or call new_context as part of this handoff. Only prepare a new checkpoint when a later context rollover is actually needed.");
|
|
59
|
+
}
|
|
60
|
+
if (threadHint) lines.push(threadHint);
|
|
61
|
+
lines.push("</context_window>");
|
|
62
|
+
return `${CONTEXT_WINDOW_GUIDANCE}\n\n${lines.join("\n")}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function renderContextWindowReminder(remainingTokens: number): string {
|
|
66
|
+
return `<context_window_reminder>
|
|
67
|
+
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.
|
|
68
|
+
</context_window_reminder>`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const CONTEXT_WINDOW_FALLBACK_MESSAGE = `<context_window_reminder>
|
|
72
|
+
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.
|
|
73
|
+
</context_window_reminder>`;
|
|
74
|
+
|
|
75
|
+
export function isContextWindowBoundary(
|
|
76
|
+
message: AgentMessage,
|
|
77
|
+
): message is Extract<AgentMessage, { role: "custom" }> & {
|
|
78
|
+
details: CodexContextManagementMessageDetails;
|
|
79
|
+
} {
|
|
80
|
+
return (
|
|
81
|
+
message.role === "custom" &&
|
|
82
|
+
message.customType === CODEX_CONTEXT_WINDOW_MESSAGE_TYPE &&
|
|
83
|
+
isCodexContextManagementMessageDetails(message.details) &&
|
|
84
|
+
message.details.contextManagement.kind === "window"
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function sendContextWindowMessage(
|
|
89
|
+
pi: ExtensionAPI,
|
|
90
|
+
content: string,
|
|
91
|
+
kind: ContextManagementMessageKind,
|
|
92
|
+
identity: ContextWindowIdentity,
|
|
93
|
+
options: { triggerTurn: boolean; sessionId?: string },
|
|
94
|
+
trimPreviousWindow = false,
|
|
95
|
+
): void {
|
|
96
|
+
const details: CodexContextManagementMessageDetails = {
|
|
97
|
+
protocol: CONTEXT_MANAGEMENT_PROTOCOL,
|
|
98
|
+
id: randomUUID(),
|
|
99
|
+
...(options.sessionId ? { sessionId: options.sessionId } : {}),
|
|
100
|
+
contextManagement: {
|
|
101
|
+
protocol: CONTEXT_MANAGEMENT_PROTOCOL,
|
|
102
|
+
kind,
|
|
103
|
+
...identity,
|
|
104
|
+
...(trimPreviousWindow ? { trimPreviousWindow: true as const } : {}),
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
pi.sendMessage({
|
|
108
|
+
customType: CODEX_CONTEXT_WINDOW_MESSAGE_TYPE,
|
|
109
|
+
content,
|
|
110
|
+
display: true,
|
|
111
|
+
details,
|
|
112
|
+
}, options.triggerTurn
|
|
113
|
+
? { deliverAs: "steer", triggerTurn: true }
|
|
114
|
+
: { triggerTurn: false });
|
|
115
|
+
}
|