@ian-pascoe/pi-codemode 0.1.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/LICENSE +21 -0
- package/README.md +241 -0
- package/package.json +59 -0
- package/src/codemode-cell-transform.ts +612 -0
- package/src/codemode-deno-launch.ts +59 -0
- package/src/codemode-deno-process.ts +208 -0
- package/src/codemode-observer-ui.ts +517 -0
- package/src/codemode-presentation-output.ts +16 -0
- package/src/codemode-runtime.ts +24 -0
- package/src/codemode-session-coordinator.ts +1297 -0
- package/src/codemode-session-files.ts +80 -0
- package/src/codemode-tool-catalog.ts +350 -0
- package/src/codemode-tool-contract.ts +480 -0
- package/src/codemode-tool-exposure.ts +159 -0
- package/src/codemode-tool-rendering.ts +487 -0
- package/src/codemode-worker-protocol.ts +480 -0
- package/src/codemode-worker.ts +1092 -0
- package/src/index.ts +1 -0
- package/src/pi-agent-session-capture.ts +157 -0
- package/src/pi-codemode-extension.ts +469 -0
- package/src/pi-codemode-settings.ts +168 -0
- package/src/pi-tool-bridge.ts +744 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { chmod, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
/** One queued Result Spill path and its presentation-only write completion. */
|
|
6
|
+
export interface CodeModeResultSpill {
|
|
7
|
+
/** Private file path available to retain in presentation details immediately. */
|
|
8
|
+
readonly path: string;
|
|
9
|
+
/** File write completion that never gates the model-facing Cell result. */
|
|
10
|
+
readonly completion: Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Writes complete CodeMode presentation data when the Transcript view is bounded. */
|
|
14
|
+
export interface CodeModeResultSpillWriter {
|
|
15
|
+
/** Queue complete presentation data without delaying the model-facing Cell result. */
|
|
16
|
+
writeResultSpill(output: string): CodeModeResultSpill;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Owns private Result Spill files for one live Pi CodeMode session. */
|
|
20
|
+
export interface CodeModeSessionFiles extends CodeModeResultSpillWriter {
|
|
21
|
+
/** Private directory removed after the CodeMode session shuts down. */
|
|
22
|
+
readonly directoryPath: string;
|
|
23
|
+
/** Finish queued writes and remove every private Result Spill file. */
|
|
24
|
+
close(): Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
class CodeModeSessionFileStore implements CodeModeSessionFiles {
|
|
28
|
+
private closed = false;
|
|
29
|
+
private closePromise: Promise<void> | undefined;
|
|
30
|
+
private nextFileIndex = 0;
|
|
31
|
+
private writeQueue: Promise<void> = Promise.resolve();
|
|
32
|
+
|
|
33
|
+
/** Creates a private Result Spill store rooted at the given directory. */
|
|
34
|
+
constructor(readonly directoryPath: string) {}
|
|
35
|
+
|
|
36
|
+
writeResultSpill(output: string): CodeModeResultSpill {
|
|
37
|
+
const spillPath = join(this.directoryPath, `result-spill-${this.nextFileIndex++}.txt`);
|
|
38
|
+
const completion = this.enqueueSessionFileWrite(async () => {
|
|
39
|
+
await writeFile(spillPath, output, { encoding: "utf8", mode: 0o600 });
|
|
40
|
+
await chmod(spillPath, 0o600);
|
|
41
|
+
});
|
|
42
|
+
return { path: spillPath, completion };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
close(): Promise<void> {
|
|
46
|
+
if (this.closePromise !== undefined) return this.closePromise;
|
|
47
|
+
this.closed = true;
|
|
48
|
+
this.closePromise = this.writeQueue.then(
|
|
49
|
+
() => rm(this.directoryPath, { force: true, recursive: true }),
|
|
50
|
+
() => rm(this.directoryPath, { force: true, recursive: true }),
|
|
51
|
+
);
|
|
52
|
+
return this.closePromise;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private enqueueSessionFileWrite<T>(write: () => Promise<T>): Promise<T> {
|
|
56
|
+
if (this.closed) return Promise.reject(new Error("Pi CodeMode: session files are closed"));
|
|
57
|
+
const result = this.writeQueue.then(write);
|
|
58
|
+
this.writeQueue = result.then(
|
|
59
|
+
() => undefined,
|
|
60
|
+
() => undefined,
|
|
61
|
+
);
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Create a private Result Spill directory below Pi session storage or the system temporary directory. */
|
|
67
|
+
export async function createCodeModeSessionFiles(
|
|
68
|
+
sessionDirectory: string,
|
|
69
|
+
): Promise<CodeModeSessionFiles> {
|
|
70
|
+
const parentDirectory = sessionDirectory.length > 0 ? sessionDirectory : tmpdir();
|
|
71
|
+
await mkdir(parentDirectory, { mode: 0o700, recursive: true });
|
|
72
|
+
const directoryPath = await mkdtemp(join(parentDirectory, "pi-codemode-"));
|
|
73
|
+
try {
|
|
74
|
+
await chmod(directoryPath, 0o700);
|
|
75
|
+
return new CodeModeSessionFileStore(directoryPath);
|
|
76
|
+
} catch (cause) {
|
|
77
|
+
await rm(directoryPath, { force: true, recursive: true });
|
|
78
|
+
throw cause;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
import { Value } from "typebox/value";
|
|
4
|
+
import {
|
|
5
|
+
isCodeModeJsonObject,
|
|
6
|
+
isReservedCodeModeToolName,
|
|
7
|
+
type CodeModeJsonObject,
|
|
8
|
+
type CodeModeJsonValue,
|
|
9
|
+
} from "./codemode-tool-contract.js";
|
|
10
|
+
|
|
11
|
+
const CODEMODE_CATALOGUE_LIMIT_BYTES = 1024 * 1024;
|
|
12
|
+
const CODEMODE_JSDOC_LIMIT_BYTES = 2 * 1024;
|
|
13
|
+
const CODEMODE_SCHEMA_DEPTH_LIMIT = 16;
|
|
14
|
+
|
|
15
|
+
/** A structural JSON Schema document accepted from TypeBox or another producer. */
|
|
16
|
+
export type CodeModeToolInputSchema = boolean | object;
|
|
17
|
+
|
|
18
|
+
/** One CodeMode-callable Pi tool and its structural input schema. */
|
|
19
|
+
export type CodeModeToolCatalogueTool = {
|
|
20
|
+
readonly name: string;
|
|
21
|
+
readonly inputSchema: CodeModeToolInputSchema;
|
|
22
|
+
readonly description?: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** A complete catalogue or an explicit refusal before exposure changes. */
|
|
26
|
+
export type CodeModeToolCatalogueResult =
|
|
27
|
+
| { readonly ok: true; readonly text: string }
|
|
28
|
+
| { readonly ok: false; readonly reason: "names-exceed-catalogue-limit" };
|
|
29
|
+
|
|
30
|
+
type ParsedCodeModeToolInputSchema = boolean | CodeModeJsonObject;
|
|
31
|
+
type RenderedTool = {
|
|
32
|
+
readonly name: string;
|
|
33
|
+
readonly description: string | undefined;
|
|
34
|
+
readonly input: string;
|
|
35
|
+
};
|
|
36
|
+
type CodeModeCatalogueDescriptionMode = "include-descriptions" | "omit-descriptions";
|
|
37
|
+
|
|
38
|
+
const JsonStringSchema = Type.String();
|
|
39
|
+
const JsonNumberSchema = Type.Number();
|
|
40
|
+
const JsonBooleanSchema = Type.Boolean();
|
|
41
|
+
const StructuralJsonObjectSchema = Type.Object({}, { additionalProperties: true });
|
|
42
|
+
|
|
43
|
+
function isJsonString(value: CodeModeJsonValue): value is string {
|
|
44
|
+
return Value.Check(JsonStringSchema, value);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parseStructuralJsonValue(
|
|
48
|
+
// oxlint-disable-next-line anti-slop/no-unknown-parameters -- SAFETY: This is the catalogue's sole structural JSON ingress; the TypeBox, non-TypeBox, and boolean-schema test proves supported producers remain accepted without invoking schema accessors.
|
|
49
|
+
value: unknown,
|
|
50
|
+
seen: WeakSet<object> = new WeakSet(),
|
|
51
|
+
depth = 0,
|
|
52
|
+
): CodeModeJsonValue | undefined {
|
|
53
|
+
if (depth > CODEMODE_SCHEMA_DEPTH_LIMIT * 4) return undefined;
|
|
54
|
+
if (value === null) return null;
|
|
55
|
+
if (Value.Check(JsonStringSchema, value) || Value.Check(JsonBooleanSchema, value)) return value;
|
|
56
|
+
if (Value.Check(JsonNumberSchema, value)) return Number.isFinite(value) ? value : undefined;
|
|
57
|
+
if (Array.isArray(value)) {
|
|
58
|
+
if (seen.has(value)) return undefined;
|
|
59
|
+
seen.add(value);
|
|
60
|
+
try {
|
|
61
|
+
const output: CodeModeJsonValue[] = [];
|
|
62
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
63
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
|
|
64
|
+
if (descriptor === undefined || !("value" in descriptor)) return undefined;
|
|
65
|
+
const item = parseStructuralJsonValue(descriptor.value, seen, depth + 1);
|
|
66
|
+
if (item === undefined) return undefined;
|
|
67
|
+
output.push(item);
|
|
68
|
+
}
|
|
69
|
+
return output;
|
|
70
|
+
} finally {
|
|
71
|
+
seen.delete(value);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (!Value.Check(StructuralJsonObjectSchema, value) || seen.has(value)) return undefined;
|
|
75
|
+
seen.add(value);
|
|
76
|
+
try {
|
|
77
|
+
const output: Array<readonly [string, CodeModeJsonValue]> = [];
|
|
78
|
+
for (const key of Object.keys(value)) {
|
|
79
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
80
|
+
if (descriptor === undefined || !("value" in descriptor)) continue;
|
|
81
|
+
const property = parseStructuralJsonValue(descriptor.value, seen, depth + 1);
|
|
82
|
+
if (property !== undefined) output.push([key, property]);
|
|
83
|
+
}
|
|
84
|
+
return Object.fromEntries(output);
|
|
85
|
+
} finally {
|
|
86
|
+
seen.delete(value);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function isStructuralJsonObject(value: CodeModeJsonValue | undefined): value is CodeModeJsonObject {
|
|
91
|
+
return Value.Check(StructuralJsonObjectSchema, value);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function schemaRecord(value: CodeModeJsonValue | undefined): CodeModeJsonObject | undefined {
|
|
95
|
+
return value !== undefined && isCodeModeJsonObject(value) ? value : undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function jsonLiteral(value: CodeModeJsonValue): string | undefined {
|
|
99
|
+
return JSON.stringify(value);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function quotedName(name: string): string {
|
|
103
|
+
return JSON.stringify(name);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function boundedDescription(description: string | undefined): string | undefined {
|
|
107
|
+
if (description === undefined) return undefined;
|
|
108
|
+
let output = "";
|
|
109
|
+
for (const character of description) {
|
|
110
|
+
if (Buffer.byteLength(output + character, "utf8") > CODEMODE_JSDOC_LIMIT_BYTES) break;
|
|
111
|
+
output += character;
|
|
112
|
+
}
|
|
113
|
+
return output.replaceAll("*/", "*\\/");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function jsdoc(description: string | undefined): string {
|
|
117
|
+
if (description === undefined || description.length === 0) return "";
|
|
118
|
+
return ` /** ${description.replaceAll("\n", " ")} */\n`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function schemaType(
|
|
122
|
+
schema: CodeModeJsonValue | undefined,
|
|
123
|
+
root: CodeModeJsonObject | undefined,
|
|
124
|
+
seen: ReadonlySet<string>,
|
|
125
|
+
depth: number,
|
|
126
|
+
): string {
|
|
127
|
+
if (depth > CODEMODE_SCHEMA_DEPTH_LIMIT) return "unknown";
|
|
128
|
+
if (schema === true) return "unknown";
|
|
129
|
+
if (schema === false) return "never";
|
|
130
|
+
const record = schemaRecord(schema);
|
|
131
|
+
if (record === undefined) return "unknown";
|
|
132
|
+
|
|
133
|
+
const reference = record.$ref;
|
|
134
|
+
if (reference !== undefined && isJsonString(reference)) {
|
|
135
|
+
const referenced = resolveLocalReference(reference, root);
|
|
136
|
+
if (referenced === undefined || seen.has(reference)) return "unknown";
|
|
137
|
+
return schemaType(referenced, root, new Set([...seen, reference]), depth + 1);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const constant = record.const;
|
|
141
|
+
if (Object.hasOwn(record, "const") && constant !== undefined) {
|
|
142
|
+
return jsonLiteral(constant) ?? "unknown";
|
|
143
|
+
}
|
|
144
|
+
const enumValues = Array.isArray(record.enum) ? record.enum.map(jsonLiteral) : undefined;
|
|
145
|
+
if (
|
|
146
|
+
enumValues !== undefined &&
|
|
147
|
+
enumValues.length > 0 &&
|
|
148
|
+
enumValues.every((value) => value !== undefined)
|
|
149
|
+
) {
|
|
150
|
+
return enumValues.join(" | ");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const union =
|
|
154
|
+
unionType(record.anyOf, root, seen, depth) ?? unionType(record.oneOf, root, seen, depth);
|
|
155
|
+
if (union !== undefined) return union;
|
|
156
|
+
const intersection = intersectionType(record.allOf, root, seen, depth);
|
|
157
|
+
if (intersection !== undefined) return intersection;
|
|
158
|
+
|
|
159
|
+
const type = record.type;
|
|
160
|
+
if (Array.isArray(type)) {
|
|
161
|
+
const types = type.filter(isJsonString);
|
|
162
|
+
return types.length === type.length && types.length > 0
|
|
163
|
+
? types
|
|
164
|
+
.map((entry) => primitiveOrStructuredType(entry, record, root, seen, depth))
|
|
165
|
+
.join(" | ")
|
|
166
|
+
: "unknown";
|
|
167
|
+
}
|
|
168
|
+
return type !== undefined && isJsonString(type)
|
|
169
|
+
? primitiveOrStructuredType(type, record, root, seen, depth)
|
|
170
|
+
: hasObjectKeywords(record)
|
|
171
|
+
? objectType(record, root, seen, depth)
|
|
172
|
+
: hasArrayKeywords(record)
|
|
173
|
+
? arrayType(record, root, seen, depth)
|
|
174
|
+
: "unknown";
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function resolveLocalReference(
|
|
178
|
+
reference: string,
|
|
179
|
+
root: CodeModeJsonObject | undefined,
|
|
180
|
+
): CodeModeJsonValue | undefined {
|
|
181
|
+
if (root === undefined || !reference.startsWith("#/$defs/")) return undefined;
|
|
182
|
+
const name = reference.slice("#/$defs/".length);
|
|
183
|
+
if (name.length === 0 || name.includes("/")) return undefined;
|
|
184
|
+
const definitions = schemaRecord(root.$defs);
|
|
185
|
+
return definitions?.[name];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function unionType(
|
|
189
|
+
value: CodeModeJsonValue | undefined,
|
|
190
|
+
root: CodeModeJsonObject | undefined,
|
|
191
|
+
seen: ReadonlySet<string>,
|
|
192
|
+
depth: number,
|
|
193
|
+
): string | undefined {
|
|
194
|
+
if (!Array.isArray(value) || value.length === 0) return undefined;
|
|
195
|
+
return value.map((entry) => schemaType(entry, root, seen, depth + 1)).join(" | ");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function intersectionType(
|
|
199
|
+
value: CodeModeJsonValue | undefined,
|
|
200
|
+
root: CodeModeJsonObject | undefined,
|
|
201
|
+
seen: ReadonlySet<string>,
|
|
202
|
+
depth: number,
|
|
203
|
+
): string | undefined {
|
|
204
|
+
if (!Array.isArray(value) || value.length === 0) return undefined;
|
|
205
|
+
return value.map((entry) => schemaType(entry, root, seen, depth + 1)).join(" & ");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function primitiveOrStructuredType(
|
|
209
|
+
type: string,
|
|
210
|
+
record: CodeModeJsonObject,
|
|
211
|
+
root: CodeModeJsonObject | undefined,
|
|
212
|
+
seen: ReadonlySet<string>,
|
|
213
|
+
depth: number,
|
|
214
|
+
): string {
|
|
215
|
+
switch (type) {
|
|
216
|
+
case "string":
|
|
217
|
+
return "string";
|
|
218
|
+
case "number":
|
|
219
|
+
case "integer":
|
|
220
|
+
return "number";
|
|
221
|
+
case "boolean":
|
|
222
|
+
return "boolean";
|
|
223
|
+
case "null":
|
|
224
|
+
return "null";
|
|
225
|
+
case "object":
|
|
226
|
+
return objectType(record, root, seen, depth);
|
|
227
|
+
case "array":
|
|
228
|
+
return arrayType(record, root, seen, depth);
|
|
229
|
+
default:
|
|
230
|
+
return "unknown";
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function hasObjectKeywords(record: CodeModeJsonObject): boolean {
|
|
235
|
+
return (
|
|
236
|
+
Object.hasOwn(record, "properties") ||
|
|
237
|
+
Object.hasOwn(record, "additionalProperties") ||
|
|
238
|
+
Object.hasOwn(record, "required")
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function hasArrayKeywords(record: CodeModeJsonObject): boolean {
|
|
243
|
+
return Object.hasOwn(record, "items") || Object.hasOwn(record, "prefixItems");
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function objectType(
|
|
247
|
+
record: CodeModeJsonObject,
|
|
248
|
+
root: CodeModeJsonObject | undefined,
|
|
249
|
+
seen: ReadonlySet<string>,
|
|
250
|
+
depth: number,
|
|
251
|
+
): string {
|
|
252
|
+
const properties = schemaRecord(record.properties);
|
|
253
|
+
const required = new Set(
|
|
254
|
+
Array.isArray(record.required) ? record.required.filter(isJsonString) : [],
|
|
255
|
+
);
|
|
256
|
+
const members = Object.entries(properties ?? {})
|
|
257
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
258
|
+
.map(
|
|
259
|
+
([name, property]) =>
|
|
260
|
+
`readonly [${quotedName(name)}]${required.has(name) ? "" : "?"}: ${schemaType(property, root, seen, depth + 1)};`,
|
|
261
|
+
);
|
|
262
|
+
const additional = record.additionalProperties;
|
|
263
|
+
if (additional !== false) {
|
|
264
|
+
const additionalType =
|
|
265
|
+
members.length === 0 && schemaRecord(additional) !== undefined
|
|
266
|
+
? schemaType(additional, root, seen, depth + 1)
|
|
267
|
+
: "unknown";
|
|
268
|
+
members.push(`readonly [key: string]: ${additionalType};`);
|
|
269
|
+
}
|
|
270
|
+
return members.length === 0 ? "Readonly<Record<string, never>>" : `{ ${members.join(" ")} }`;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function arrayType(
|
|
274
|
+
record: CodeModeJsonObject,
|
|
275
|
+
root: CodeModeJsonObject | undefined,
|
|
276
|
+
seen: ReadonlySet<string>,
|
|
277
|
+
depth: number,
|
|
278
|
+
): string {
|
|
279
|
+
const prefixItems = record.prefixItems;
|
|
280
|
+
if (Array.isArray(prefixItems)) {
|
|
281
|
+
const tuple = prefixItems.map((item) => schemaType(item, root, seen, depth + 1));
|
|
282
|
+
const items = record.items;
|
|
283
|
+
if (schemaRecord(items) !== undefined || items === true) {
|
|
284
|
+
tuple.push(`...${items === true ? "unknown" : schemaType(items, root, seen, depth + 1)}[]`);
|
|
285
|
+
}
|
|
286
|
+
return `readonly [${tuple.join(", ")}]`;
|
|
287
|
+
}
|
|
288
|
+
return `readonly ${schemaType(record.items, root, seen, depth + 1)}[]`;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function renderTool(tool: RenderedTool, descriptionMode: CodeModeCatalogueDescriptionMode): string {
|
|
292
|
+
return `${descriptionMode === "include-descriptions" ? jsdoc(tool.description) : ""} readonly [${quotedName(tool.name)}]: (input: ${tool.input}) => Promise<PiToolResult>;\n`;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function renderCatalogue(
|
|
296
|
+
tools: readonly RenderedTool[],
|
|
297
|
+
descriptionMode: CodeModeCatalogueDescriptionMode,
|
|
298
|
+
): string {
|
|
299
|
+
return `type PiToolResult = {\n content: Array<{ type: "text"; text: string } | { type: "image"; data: string; mimeType: string }> ;\n details?: unknown;\n};\n\ndeclare const tools: Readonly<{\n${tools.map((tool) => renderTool(tool, descriptionMode)).join("")}}>;\n`;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function isWithinCatalogueLimit(text: string): boolean {
|
|
303
|
+
return Buffer.byteLength(text, "utf8") <= CODEMODE_CATALOGUE_LIMIT_BYTES;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** Renders all guest-callable names once or refuses a name-only overflow. */
|
|
307
|
+
export function renderCodeModeToolCatalogue(
|
|
308
|
+
tools: readonly CodeModeToolCatalogueTool[],
|
|
309
|
+
): CodeModeToolCatalogueResult {
|
|
310
|
+
const candidates = tools
|
|
311
|
+
.filter((tool) => !isReservedCodeModeToolName(tool.name))
|
|
312
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
313
|
+
const unique = candidates.filter(
|
|
314
|
+
(tool, index) => index === 0 || tool.name !== candidates[index - 1]?.name,
|
|
315
|
+
);
|
|
316
|
+
const rendered = unique.map((tool) => {
|
|
317
|
+
const parsed = parseStructuralJsonValue(tool.inputSchema);
|
|
318
|
+
const inputSchema: ParsedCodeModeToolInputSchema | undefined =
|
|
319
|
+
parsed === true || parsed === false || isStructuralJsonObject(parsed) ? parsed : undefined;
|
|
320
|
+
return {
|
|
321
|
+
name: tool.name,
|
|
322
|
+
description: boundedDescription(tool.description),
|
|
323
|
+
input: schemaType(inputSchema, schemaRecord(inputSchema), new Set(), 0),
|
|
324
|
+
};
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
let text = renderCatalogue(rendered, "include-descriptions");
|
|
328
|
+
if (isWithinCatalogueLimit(text)) return { ok: true, text };
|
|
329
|
+
|
|
330
|
+
const simplified = [...rendered];
|
|
331
|
+
const schemaOrder = simplified
|
|
332
|
+
.map((tool, index) => ({
|
|
333
|
+
index,
|
|
334
|
+
bytes: Buffer.byteLength(tool.input, "utf8"),
|
|
335
|
+
name: tool.name,
|
|
336
|
+
}))
|
|
337
|
+
.sort((left, right) => right.bytes - left.bytes || left.name.localeCompare(right.name));
|
|
338
|
+
for (const candidate of schemaOrder) {
|
|
339
|
+
const current = simplified[candidate.index];
|
|
340
|
+
if (current === undefined) continue;
|
|
341
|
+
simplified[candidate.index] = { ...current, input: "unknown" };
|
|
342
|
+
text = renderCatalogue(simplified, "include-descriptions");
|
|
343
|
+
if (isWithinCatalogueLimit(text)) return { ok: true, text };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
text = renderCatalogue(simplified, "omit-descriptions");
|
|
347
|
+
return isWithinCatalogueLimit(text)
|
|
348
|
+
? { ok: true, text }
|
|
349
|
+
: { ok: false, reason: "names-exceed-catalogue-limit" };
|
|
350
|
+
}
|