pi-openai-toolkit 0.23.2 → 0.24.0
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 +4 -15
- package/README.zh.md +4 -15
- package/config.schema.json +5 -19
- package/docs/configuration.md +19 -21
- package/docs/internals.md +34 -120
- package/package.json +3 -16
- package/src/config/legacy.ts +12 -62
- package/src/config/migration.ts +19 -7
- package/src/config/notifications.ts +10 -4
- package/src/config/policy.ts +9 -10
- package/src/config/v2.ts +12 -51
- package/src/config.ts +20 -10
- package/src/extension-runtime.ts +163 -987
- package/src/native-fallback.ts +11 -23
- package/src/responses-headers.ts +1 -1
- package/src/retired-remote-windows.ts +35 -0
- package/src/runtime.ts +12 -2
- package/src/types.ts +4 -28
- package/src/context-management/codex-provider.ts +0 -270
- package/src/context-management/history-notes.ts +0 -357
- package/src/context-management/manual-compact.ts +0 -247
- package/src/context-management/messages.ts +0 -238
- package/src/context-management/namespace-tools.ts +0 -426
- package/src/context-management/responses-tools.ts +0 -238
- package/src/context-management/scoped-compaction.ts +0 -89
- package/src/context-management/tool-contract.ts +0 -62
- package/src/context-management/tools.ts +0 -371
- package/src/context-management/types.ts +0 -168
- package/src/context-management/window-budget.ts +0 -83
- package/src/context-management/window-bulk.ts +0 -155
- package/src/context-management/window-manager.ts +0 -912
- package/src/context-management/window-request.ts +0 -125
|
@@ -1,238 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Convert Pi tool declarations into Responses-API wire tools.
|
|
3
|
-
*
|
|
4
|
-
* Pi 1.3 exports `convertResponsesTools` only from
|
|
5
|
-
* `@earendil-works/pi-ai/api/openai-responses-shared`, but Pi's extension module
|
|
6
|
-
* aliases map a fixed allowlist of specifiers and do not include that subpath.
|
|
7
|
-
* Importing it from an extension therefore fails at load time with
|
|
8
|
-
* "Cannot find module", which silently disables every hook the extension owns.
|
|
9
|
-
*
|
|
10
|
-
* This module reproduces that conversion without the subpath import. The strict
|
|
11
|
-
* JSON-schema rewrite is a faithful port of Pi's `makeStrictJsonSchema`: it must
|
|
12
|
-
* stay byte-identical, because a compaction request has to declare the same tool
|
|
13
|
-
* schema as the live request it continues.
|
|
14
|
-
*/
|
|
15
|
-
import type { Tool } from "@earendil-works/pi-ai";
|
|
16
|
-
|
|
17
|
-
export type ResponsesWireTool =
|
|
18
|
-
| { type: "function"; name: string; description: string; parameters: unknown; strict?: boolean | null }
|
|
19
|
-
| { type: "custom"; name: string; description: string; format: { type: string; syntax: string; definition: string } };
|
|
20
|
-
|
|
21
|
-
export type ConvertResponsesToolsOptions = {
|
|
22
|
-
/** Value applied when a tool does not opt in explicitly. `null` means no default. */
|
|
23
|
-
strict?: boolean | null;
|
|
24
|
-
supportsStrictMode?: boolean;
|
|
25
|
-
supportsOpenAIGrammarTools?: boolean;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
type JsonSchema = Record<string, unknown>;
|
|
29
|
-
|
|
30
|
-
type ConstrainedSampling = {
|
|
31
|
-
type: "json_schema" | "grammar";
|
|
32
|
-
strict?: "require" | "prefer" | "ignore";
|
|
33
|
-
variants?: { openai_lark?: string; openai_regex?: string };
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
/** Keywords the strict subset cannot express. Kept in sync with Pi's implementation. */
|
|
37
|
-
const UNSUPPORTED_STRICT_SCHEMA_KEYS = [
|
|
38
|
-
"$ref",
|
|
39
|
-
"$defs",
|
|
40
|
-
"definitions",
|
|
41
|
-
"allOf",
|
|
42
|
-
"oneOf",
|
|
43
|
-
"patternProperties",
|
|
44
|
-
"dependentSchemas",
|
|
45
|
-
"dependencies",
|
|
46
|
-
"unevaluatedProperties",
|
|
47
|
-
"propertyNames",
|
|
48
|
-
"contains",
|
|
49
|
-
"prefixItems",
|
|
50
|
-
"not",
|
|
51
|
-
"if",
|
|
52
|
-
"then",
|
|
53
|
-
"else",
|
|
54
|
-
];
|
|
55
|
-
|
|
56
|
-
class UnsupportedStrictJsonSchemaError extends Error {}
|
|
57
|
-
|
|
58
|
-
function isJsonSchemaObject(value: unknown): value is JsonSchema {
|
|
59
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function isStructuredSchema(schema: unknown): boolean {
|
|
63
|
-
if (!isJsonSchemaObject(schema)) return false;
|
|
64
|
-
const types = typeof schema["type"] === "string"
|
|
65
|
-
? [schema["type"]]
|
|
66
|
-
: Array.isArray(schema["type"]) ? schema["type"] : [];
|
|
67
|
-
return types.includes("object")
|
|
68
|
-
|| types.includes("array")
|
|
69
|
-
|| schema["properties"] !== undefined
|
|
70
|
-
|| schema["items"] !== undefined;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function schemaAllowsNull(schema: unknown): boolean {
|
|
74
|
-
if (!isJsonSchemaObject(schema)) return false;
|
|
75
|
-
if (schema["type"] === "null" || (Array.isArray(schema["type"]) && schema["type"].includes("null"))) return true;
|
|
76
|
-
if (schema["const"] === null || (Array.isArray(schema["enum"]) && schema["enum"].includes(null))) return true;
|
|
77
|
-
return Array.isArray(schema["anyOf"]) && schema["anyOf"].some((variant) => schemaAllowsNull(variant));
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function makeJsonSchemaNodeStrict(schema: unknown): void { if (!isJsonSchemaObject(schema)) throw new UnsupportedStrictJsonSchemaError("boolean schemas are unsupported");
|
|
81
|
-
for (const key of UNSUPPORTED_STRICT_SCHEMA_KEYS) {
|
|
82
|
-
if (schema[key] !== undefined) throw new UnsupportedStrictJsonSchemaError(`${key} schemas are unsupported`);
|
|
83
|
-
}
|
|
84
|
-
if (schema["anyOf"] !== undefined) {
|
|
85
|
-
if (!Array.isArray(schema["anyOf"]) || schema["anyOf"].length === 0) {
|
|
86
|
-
throw new UnsupportedStrictJsonSchemaError("anyOf must contain at least one schema");
|
|
87
|
-
}
|
|
88
|
-
for (const variant of schema["anyOf"]) {
|
|
89
|
-
if (isStructuredSchema(variant)) throw new UnsupportedStrictJsonSchemaError("object and array unions are unsupported");
|
|
90
|
-
makeJsonSchemaNodeStrict(variant);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
if (schema["items"] !== undefined) {
|
|
94
|
-
if (Array.isArray(schema["items"])) throw new UnsupportedStrictJsonSchemaError("tuple schemas are unsupported");
|
|
95
|
-
makeJsonSchemaNodeStrict(schema["items"]);
|
|
96
|
-
}
|
|
97
|
-
const isObjectSchema = schema["type"] === "object";
|
|
98
|
-
if (schema["properties"] !== undefined && !isObjectSchema) {
|
|
99
|
-
throw new UnsupportedStrictJsonSchemaError("properties require type object");
|
|
100
|
-
}
|
|
101
|
-
if (!isObjectSchema) return;
|
|
102
|
-
if (schema["additionalProperties"] !== undefined && schema["additionalProperties"] !== false) {
|
|
103
|
-
throw new UnsupportedStrictJsonSchemaError("schema-valued or true additionalProperties is unsupported");
|
|
104
|
-
}
|
|
105
|
-
if (schema["properties"] !== undefined && !isJsonSchemaObject(schema["properties"])) {
|
|
106
|
-
throw new UnsupportedStrictJsonSchemaError("object properties must be a schema map");
|
|
107
|
-
}
|
|
108
|
-
if (schema["required"] !== undefined
|
|
109
|
-
&& (!Array.isArray(schema["required"]) || schema["required"].some((key) => typeof key !== "string"))) {
|
|
110
|
-
throw new UnsupportedStrictJsonSchemaError("object required must be a string array");
|
|
111
|
-
}
|
|
112
|
-
const properties = isJsonSchemaObject(schema["properties"]) ? schema["properties"] : {};
|
|
113
|
-
const propertyNames = Object.keys(properties);
|
|
114
|
-
const required = new Set(Array.isArray(schema["required"]) ? schema["required"] as string[] : []);
|
|
115
|
-
if ([...required].some((key) => !propertyNames.includes(key))) {
|
|
116
|
-
throw new UnsupportedStrictJsonSchemaError("required contains an unknown property");
|
|
117
|
-
}
|
|
118
|
-
for (const [key, property] of Object.entries(properties)) {
|
|
119
|
-
makeJsonSchemaNodeStrict(property);
|
|
120
|
-
if (!required.has(key) && !schemaAllowsNull(property)) {
|
|
121
|
-
properties[key] = { anyOf: [property, { type: "null" }] };
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
schema["required"] = propertyNames;
|
|
125
|
-
schema["additionalProperties"] = false;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/** Convert a tool schema to the strict subset expected by provider constrained sampling. */
|
|
129
|
-
function makeStrictJsonSchema(schema: unknown): unknown {
|
|
130
|
-
const cloned = structuredClone(schema);
|
|
131
|
-
if (!isJsonSchemaObject(cloned)) throw new UnsupportedStrictJsonSchemaError("root schema must have type object");
|
|
132
|
-
makeJsonSchemaNodeStrict(cloned);
|
|
133
|
-
if (cloned["type"] !== "object") throw new UnsupportedStrictJsonSchemaError("root schema must have type object");
|
|
134
|
-
return cloned;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function constrainedSamplingOf(tool: Tool): ConstrainedSampling | undefined { const value = (tool as { constrainedSampling?: unknown }).constrainedSampling;
|
|
138
|
-
if (!value || typeof value !== "object") return undefined;
|
|
139
|
-
const type = (value as { type?: unknown }).type;
|
|
140
|
-
if (type !== "json_schema" && type !== "grammar") return undefined;
|
|
141
|
-
return value as ConstrainedSampling;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/** Only `openai_lark` / `openai_regex` variants can be expressed on the Responses wire. */
|
|
145
|
-
function grammarOf(tool: Tool): { syntax: string; definition: string } | undefined {
|
|
146
|
-
const sampling = constrainedSamplingOf(tool);
|
|
147
|
-
if (sampling?.type !== "grammar") return undefined;
|
|
148
|
-
const lark = sampling.variants?.openai_lark;
|
|
149
|
-
const regex = sampling.variants?.openai_regex;
|
|
150
|
-
const hasLark = typeof lark === "string" && lark.trim().length > 0;
|
|
151
|
-
const hasRegex = typeof regex === "string" && regex.trim().length > 0;
|
|
152
|
-
if (!hasLark && !hasRegex) {
|
|
153
|
-
throw new Error(`Tool "${tool.name}" cannot use grammar constrained sampling: no supported grammar variant was provided.`);
|
|
154
|
-
}
|
|
155
|
-
return hasLark ? { syntax: "lark", definition: lark! } : { syntax: "regex", definition: regex! };
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/** Whether the schema can be expressed in the strict subset at all. */
|
|
159
|
-
function isStrictExpressible(parameters: unknown): boolean {
|
|
160
|
-
try {
|
|
161
|
-
makeStrictJsonSchema(parameters);
|
|
162
|
-
return true;
|
|
163
|
-
} catch (error) {
|
|
164
|
-
if (error instanceof UnsupportedStrictJsonSchemaError) return false;
|
|
165
|
-
throw error;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/** The reason `makeStrictJsonSchema` refused, for fail-closed error text. */
|
|
170
|
-
function strictUnsupportedReason(parameters: unknown): string {
|
|
171
|
-
try {
|
|
172
|
-
makeStrictJsonSchema(parameters);
|
|
173
|
-
return "the schema is unsupported";
|
|
174
|
-
} catch (error) {
|
|
175
|
-
return error instanceof Error ? error.message : String(error);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Resolve the effective strict flag for one tool, mirroring Pi's
|
|
181
|
-
* `resolveJsonSchemaStrictSampling` + `defaultStrict` composition.
|
|
182
|
-
*
|
|
183
|
-
* For any `json_schema` constrained sampling, Pi ignores the declared
|
|
184
|
-
* `strict` mode when the schema is expressible: it simply returns `true`.
|
|
185
|
-
* The mode only matters on the failure path, where `require` throws and
|
|
186
|
-
* everything else falls back to the caller's default. Pi's `defaultStrict`
|
|
187
|
-
* is `options.strict === undefined ? false : options.strict`, so an explicit
|
|
188
|
-
* `null` is forwarded verbatim onto the wire.
|
|
189
|
-
*/
|
|
190
|
-
function strictFor(tool: Tool, options: ConvertResponsesToolsOptions): boolean | null {
|
|
191
|
-
const supportsStrictMode = options.supportsStrictMode ?? true;
|
|
192
|
-
const sampling = constrainedSamplingOf(tool);
|
|
193
|
-
const fallback = options.strict === undefined ? false : options.strict;
|
|
194
|
-
if (sampling?.type !== "json_schema") return fallback;
|
|
195
|
-
if (supportsStrictMode) {
|
|
196
|
-
if (isStrictExpressible(tool.parameters)) return true;
|
|
197
|
-
if (sampling.strict === "require") {
|
|
198
|
-
throw new Error(`Tool "${tool.name}" requires JSON-schema constrained sampling, but ${strictUnsupportedReason(tool.parameters)}.`);
|
|
199
|
-
}
|
|
200
|
-
return fallback;
|
|
201
|
-
}
|
|
202
|
-
if (sampling.strict === "require") {
|
|
203
|
-
throw new Error(`Tool "${tool.name}" requires JSON-schema constrained sampling, but strict tools are unsupported.`);
|
|
204
|
-
}
|
|
205
|
-
return fallback;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/** Mirror Pi's wire shape so compaction requests match ordinary Responses requests. */
|
|
209
|
-
export function convertResponsesTools(
|
|
210
|
-
tools: readonly Tool[],
|
|
211
|
-
options: ConvertResponsesToolsOptions = {},
|
|
212
|
-
): ResponsesWireTool[] {
|
|
213
|
-
const supportsStrictMode = options.supportsStrictMode ?? true;
|
|
214
|
-
const supportsOpenAIGrammarTools = options.supportsOpenAIGrammarTools ?? false;
|
|
215
|
-
return tools.map((tool) => {
|
|
216
|
-
if (supportsOpenAIGrammarTools) {
|
|
217
|
-
const grammar = grammarOf(tool);
|
|
218
|
-
if (grammar) {
|
|
219
|
-
return {
|
|
220
|
-
type: "custom" as const,
|
|
221
|
-
name: tool.name,
|
|
222
|
-
description: tool.description,
|
|
223
|
-
format: { type: "grammar", ...grammar },
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
const strict = strictFor(tool, options);
|
|
228
|
-
const wire: ResponsesWireTool = {
|
|
229
|
-
type: "function",
|
|
230
|
-
name: tool.name,
|
|
231
|
-
description: tool.description,
|
|
232
|
-
parameters: strict === true ? makeStrictJsonSchema(tool.parameters) : tool.parameters,
|
|
233
|
-
};
|
|
234
|
-
// Pi writes `strict` whenever the transport supports it, including `false` and `null`.
|
|
235
|
-
if (supportsStrictMode) wire.strict = strict;
|
|
236
|
-
return wire;
|
|
237
|
-
});
|
|
238
|
-
}
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
2
|
-
import {
|
|
3
|
-
estimateTokens,
|
|
4
|
-
findCutPoint,
|
|
5
|
-
type FileOperations,
|
|
6
|
-
type SessionBeforeCompactEvent,
|
|
7
|
-
type SessionEntry,
|
|
8
|
-
} from "@earendil-works/pi-coding-agent";
|
|
9
|
-
import { isRecord } from "./types";
|
|
10
|
-
import type { RemoteWindowCompactionInput } from "./window-manager";
|
|
11
|
-
|
|
12
|
-
function addFileOperation(name: string, args: unknown, files: FileOperations): void {
|
|
13
|
-
if (!isRecord(args) || typeof args.path !== "string") return;
|
|
14
|
-
if (name === "read") files.read.add(args.path);
|
|
15
|
-
else if (name === "write") files.written.add(args.path);
|
|
16
|
-
else if (name === "edit") files.edited.add(args.path);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** Only canonical, current-window operations may contribute summary file metadata. */
|
|
20
|
-
function collectFileOperations(messages: readonly AgentMessage[], scope: RemoteWindowCompactionInput): FileOperations {
|
|
21
|
-
const files: FileOperations = { read: new Set(), written: new Set(), edited: new Set() };
|
|
22
|
-
const previous = scope.owningCompaction;
|
|
23
|
-
if (previous && isRecord(previous.details)) {
|
|
24
|
-
for (const path of Array.isArray(previous.details.readFiles) ? previous.details.readFiles : []) {
|
|
25
|
-
if (typeof path === "string") files.read.add(path);
|
|
26
|
-
}
|
|
27
|
-
for (const path of Array.isArray(previous.details.modifiedFiles) ? previous.details.modifiedFiles : []) {
|
|
28
|
-
if (typeof path === "string") files.edited.add(path);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
for (const message of messages) {
|
|
32
|
-
if (message.role === "assistant") {
|
|
33
|
-
for (const block of message.content) if (block.type === "toolCall") addFileOperation(block.name, block.arguments, files);
|
|
34
|
-
} else if (message.role === "toolResult") {
|
|
35
|
-
for (const call of message.nestedCalls?.calls ?? []) addFileOperation(call.name, call.arguments, files);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
return files;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Adapt canonical contributions to the public cut-point primitive. These are
|
|
43
|
-
* detached in-memory views with REAL source IDs, never entries to persist.
|
|
44
|
-
* Omitted entries contribute nothing; edited messages replace their raw values.
|
|
45
|
-
*/
|
|
46
|
-
export function prepareScopedNativeCompaction(
|
|
47
|
-
event: SessionBeforeCompactEvent,
|
|
48
|
-
scope: RemoteWindowCompactionInput,
|
|
49
|
-
): SessionBeforeCompactEvent {
|
|
50
|
-
const entries: SessionEntry[] = [];
|
|
51
|
-
for (const projected of scope.entries) {
|
|
52
|
-
if (projected.sourceEntry.type === "compaction") continue;
|
|
53
|
-
const messages = projected.messages.filter((message) => message.role !== "system");
|
|
54
|
-
if (!messages.length) continue;
|
|
55
|
-
// A single retained ID cannot safely identify a split within one source.
|
|
56
|
-
if (messages.length !== 1) throw new Error("compaction-source-projection-unavailable");
|
|
57
|
-
entries.push({ type: "message", id: projected.sourceEntry.id, parentId: projected.sourceEntry.parentId,
|
|
58
|
-
timestamp: projected.sourceEntry.timestamp, message: structuredClone(messages[0]!) });
|
|
59
|
-
}
|
|
60
|
-
if (!entries.length) throw new Error("unsafe-compaction-retained-start");
|
|
61
|
-
const settings = event.preparation.settings;
|
|
62
|
-
if (!settings || !Number.isFinite(settings.keepRecentTokens) || !Number.isFinite(settings.reserveTokens)) {
|
|
63
|
-
throw new Error("compaction-settings-unavailable");
|
|
64
|
-
}
|
|
65
|
-
const owner = scope.owningCompaction;
|
|
66
|
-
const ownerIndex = owner ? event.branchEntries.findIndex((entry) => entry.id === owner.id) : -1;
|
|
67
|
-
if (owner && ownerIndex < scope.windowBoundaryIndex) throw new Error("compaction-owner-mismatch");
|
|
68
|
-
const lowerIndex = Math.max(scope.windowBoundaryIndex, ownerIndex);
|
|
69
|
-
const minimumKeptIndex = entries.findIndex((entry) => event.branchEntries.findIndex((raw) => raw.id === entry.id) >= lowerIndex);
|
|
70
|
-
if (minimumKeptIndex < 0) throw new Error("compaction-current-window-too-small");
|
|
71
|
-
const cut = findCutPoint(entries, minimumKeptIndex, entries.length, settings.keepRecentTokens);
|
|
72
|
-
const kept = entries[cut.firstKeptEntryIndex];
|
|
73
|
-
if (kept?.type !== "message" || kept.message.role === "toolResult") throw new Error("unsafe-compaction-retained-start");
|
|
74
|
-
const firstKeptEntryId = kept.id;
|
|
75
|
-
if (!firstKeptEntryId) throw new Error("unsafe-compaction-retained-start");
|
|
76
|
-
const messages = (start: number, end: number) => entries.slice(start, end).flatMap((entry) => entry.type === "message" ? [entry.message] : []);
|
|
77
|
-
const messagesToSummarize = messages(0, cut.isSplitTurn ? cut.turnStartIndex : cut.firstKeptEntryIndex);
|
|
78
|
-
const turnPrefixMessages = cut.isSplitTurn ? messages(cut.turnStartIndex, cut.firstKeptEntryIndex) : [];
|
|
79
|
-
return {
|
|
80
|
-
...event,
|
|
81
|
-
preparation: {
|
|
82
|
-
firstKeptEntryId, messagesToSummarize, turnPrefixMessages, isSplitTurn: cut.isSplitTurn,
|
|
83
|
-
tokensBefore: scope.messages.reduce((tokens, message) => tokens + estimateTokens(message), 0),
|
|
84
|
-
previousSummary: scope.owningCompaction?.summary,
|
|
85
|
-
fileOps: collectFileOperations([...messagesToSummarize, ...turnPrefixMessages], scope),
|
|
86
|
-
settings: { ...settings },
|
|
87
|
-
},
|
|
88
|
-
};
|
|
89
|
-
}
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
export const HISTORY_ACTIONS = [
|
|
2
|
-
"list_windows",
|
|
3
|
-
"list_items",
|
|
4
|
-
"read_item",
|
|
5
|
-
"search_contents",
|
|
6
|
-
] as const;
|
|
7
|
-
|
|
8
|
-
export const NOTES_ACTIONS = [
|
|
9
|
-
"list_files_by_prefix",
|
|
10
|
-
"read_file",
|
|
11
|
-
"search_contents",
|
|
12
|
-
"append_to_file",
|
|
13
|
-
"write_file",
|
|
14
|
-
] as const;
|
|
15
|
-
|
|
16
|
-
/** Stable names shared by Toolkit registration and an external Pi launcher. */
|
|
17
|
-
export const CONTEXT_MANAGEMENT_TOOL_NAMES = [
|
|
18
|
-
"new_context",
|
|
19
|
-
"get_context_remaining",
|
|
20
|
-
"history",
|
|
21
|
-
"notes",
|
|
22
|
-
] as const;
|
|
23
|
-
|
|
24
|
-
export type HistoryAction = (typeof HISTORY_ACTIONS)[number];
|
|
25
|
-
export type NotesAction = (typeof NOTES_ACTIONS)[number];
|
|
26
|
-
export type ContextManagementToolName = (typeof CONTEXT_MANAGEMENT_TOOL_NAMES)[number];
|
|
27
|
-
|
|
28
|
-
export type ContextToolSelection = {
|
|
29
|
-
/** Pi's explicit tool allowlist. Omitted means use Pi's default selection. */
|
|
30
|
-
tools?: readonly string[];
|
|
31
|
-
/** Pi's default suppression mode. Both values are explicit host policy. */
|
|
32
|
-
noTools?: "all" | "builtin";
|
|
33
|
-
/** Pi applies this denylist after the allowlist. */
|
|
34
|
-
excludeTools?: readonly string[];
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Add Toolkit context tools to a host-provided allowlist without overriding an
|
|
39
|
-
* explicit no-tools or exclude-tools decision. The launcher owns model eligibility;
|
|
40
|
-
* this helper only owns the shared selection contract.
|
|
41
|
-
*/
|
|
42
|
-
export function ensureContextManagementToolsInAllowlist(
|
|
43
|
-
selection: ContextToolSelection,
|
|
44
|
-
eligible: boolean,
|
|
45
|
-
): string[] | undefined {
|
|
46
|
-
if (!eligible || selection.noTools !== undefined || selection.tools === undefined) {
|
|
47
|
-
return selection.tools === undefined ? undefined : [...selection.tools];
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const excluded = new Set(selection.excludeTools ?? []);
|
|
51
|
-
const names = [...selection.tools];
|
|
52
|
-
for (const name of CONTEXT_MANAGEMENT_TOOL_NAMES) {
|
|
53
|
-
if (!excluded.has(name) && !names.includes(name)) names.push(name);
|
|
54
|
-
}
|
|
55
|
-
return names;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export const HISTORY_DESCRIPTION =
|
|
59
|
-
"Prior-window detail. Pass IDs unchanged. Search, never browse.";
|
|
60
|
-
|
|
61
|
-
export const NOTES_DESCRIPTION =
|
|
62
|
-
"Cross-window checkpoints on virtual paths. Relative uses current agent; cross-agent uses <agent>/notes[/path].";
|