@ian-pascoe/pi-codemode 0.4.1 → 0.5.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 +45 -7
- package/package.json +10 -1
- package/src/codemode-known-output-schemas.ts +222 -0
- package/src/codemode-session-coordinator.ts +31 -6
- package/src/codemode-tool-catalog.ts +369 -51
- package/src/codemode-tool-contract.ts +77 -3
- package/src/codemode-tool-rendering.ts +171 -5
- package/src/pi-codemode-extension.ts +114 -19
|
@@ -2,39 +2,77 @@ import { Buffer } from "node:buffer";
|
|
|
2
2
|
import { Type } from "typebox";
|
|
3
3
|
import { Value } from "typebox/value";
|
|
4
4
|
import {
|
|
5
|
+
CODEMODE_SEARCH_TOOL_NAME,
|
|
5
6
|
isCodeModeJsonObject,
|
|
6
7
|
isReservedCodeModeToolName,
|
|
7
8
|
type CodeModeJsonObject,
|
|
8
9
|
type CodeModeJsonValue,
|
|
10
|
+
type CodeModeToolSearchPage,
|
|
11
|
+
type CodeModeToolSearchParameters,
|
|
12
|
+
CodeModeToolSearchParametersSchema,
|
|
9
13
|
} from "./codemode-tool-contract.js";
|
|
10
14
|
|
|
11
15
|
const CODEMODE_CATALOGUE_LIMIT_BYTES = 1024 * 1024;
|
|
16
|
+
const CODEMODE_CATALOGUE_TOKEN_BUDGET = 2_000;
|
|
17
|
+
const CODEMODE_CATALOGUE_GROUP_SUMMARY_TOKEN_BUDGET = 256;
|
|
12
18
|
const CODEMODE_JSDOC_LIMIT_BYTES = 2 * 1024;
|
|
19
|
+
const CODEMODE_GROUP_LIMIT_BYTES = 512;
|
|
13
20
|
const CODEMODE_SCHEMA_DEPTH_LIMIT = 16;
|
|
21
|
+
const CODEMODE_SEARCH_DEFAULT_LIMIT = 10;
|
|
22
|
+
const CODEMODE_SEARCH_INDEX_LIMIT_BYTES = 8 * 1024;
|
|
23
|
+
const CODEMODE_SEARCH_RESULT_LIMIT_BYTES = 1024 * 1024;
|
|
14
24
|
|
|
15
25
|
/** A structural JSON Schema document accepted from TypeBox or another producer. */
|
|
16
|
-
export type
|
|
26
|
+
export type CodeModeToolSchema = boolean | object;
|
|
17
27
|
|
|
18
|
-
/** One CodeMode-callable Pi tool and its structural input
|
|
28
|
+
/** One CodeMode-callable Pi tool and its structural input/output schemas. */
|
|
19
29
|
export type CodeModeToolCatalogueTool = {
|
|
20
30
|
readonly name: string;
|
|
21
|
-
|
|
31
|
+
/** Stable display-only source group; it never changes the exact flat tool name. */
|
|
32
|
+
readonly group: string;
|
|
33
|
+
readonly inputSchema: CodeModeToolSchema;
|
|
34
|
+
readonly outputSchema?: CodeModeToolSchema;
|
|
22
35
|
readonly description?: string;
|
|
23
36
|
};
|
|
24
37
|
|
|
25
|
-
/**
|
|
38
|
+
/** One complete declaration retained for progressive CodeMode tool search. */
|
|
39
|
+
export type CodeModeToolSearchEntry = {
|
|
40
|
+
readonly name: string;
|
|
41
|
+
readonly group: string;
|
|
42
|
+
readonly description?: string;
|
|
43
|
+
readonly declaration: string;
|
|
44
|
+
/** Pre-normalized bounded terms used only by the parent-side search index. */
|
|
45
|
+
readonly searchIndex?: string;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/** A bounded inline catalogue plus every searchable complete declaration. */
|
|
26
49
|
export type CodeModeToolCatalogueResult =
|
|
27
|
-
| {
|
|
28
|
-
|
|
50
|
+
| {
|
|
51
|
+
readonly ok: true;
|
|
52
|
+
readonly text: string;
|
|
53
|
+
readonly complete: boolean;
|
|
54
|
+
readonly shownCount: number;
|
|
55
|
+
readonly totalCount: number;
|
|
56
|
+
readonly searchEntries: readonly CodeModeToolSearchEntry[];
|
|
57
|
+
}
|
|
58
|
+
| { readonly ok: false; readonly reason: "catalogue-exceeds-outer-limit" };
|
|
29
59
|
|
|
30
|
-
|
|
60
|
+
/** Successfully rendered progressive CodeMode tool catalogue. */
|
|
61
|
+
export type CodeModeToolCatalogue = Extract<CodeModeToolCatalogueResult, { readonly ok: true }>;
|
|
62
|
+
|
|
63
|
+
/** Expected progressive declaration search outcome at the guest input boundary. */
|
|
64
|
+
export type CodeModeToolSearchResult =
|
|
65
|
+
| { readonly ok: true; readonly page: CodeModeToolSearchPage }
|
|
66
|
+
| { readonly ok: false; readonly code: "validation" | "serialization"; readonly message: string };
|
|
67
|
+
|
|
68
|
+
type ParsedCodeModeToolSchema = boolean | CodeModeJsonObject;
|
|
31
69
|
type RenderedTool = {
|
|
32
70
|
readonly name: string;
|
|
71
|
+
readonly group: string;
|
|
33
72
|
readonly description: string | undefined;
|
|
34
73
|
readonly input: string;
|
|
74
|
+
readonly output: string;
|
|
35
75
|
};
|
|
36
|
-
type CodeModeCatalogueDescriptionMode = "include-descriptions" | "omit-descriptions";
|
|
37
|
-
|
|
38
76
|
const JsonStringSchema = Type.String();
|
|
39
77
|
const JsonNumberSchema = Type.Number();
|
|
40
78
|
const JsonBooleanSchema = Type.Boolean();
|
|
@@ -102,13 +140,40 @@ function quotedName(name: string): string {
|
|
|
102
140
|
function boundedDescription(description: string | undefined): string | undefined {
|
|
103
141
|
if (description === undefined) return undefined;
|
|
104
142
|
let output = "";
|
|
143
|
+
let bytes = 0;
|
|
105
144
|
for (const character of description) {
|
|
106
|
-
|
|
145
|
+
const characterBytes = Buffer.byteLength(character, "utf8");
|
|
146
|
+
if (bytes + characterBytes > CODEMODE_JSDOC_LIMIT_BYTES) break;
|
|
107
147
|
output += character;
|
|
148
|
+
bytes += characterBytes;
|
|
108
149
|
}
|
|
109
150
|
return output.replaceAll("*/", "*\\/");
|
|
110
151
|
}
|
|
111
152
|
|
|
153
|
+
function boundedGroup(group: string): string {
|
|
154
|
+
let output = "";
|
|
155
|
+
let bytes = 0;
|
|
156
|
+
for (const character of group) {
|
|
157
|
+
const characterBytes = Buffer.byteLength(character, "utf8");
|
|
158
|
+
if (bytes + characterBytes > CODEMODE_GROUP_LIMIT_BYTES) break;
|
|
159
|
+
output += character;
|
|
160
|
+
bytes += characterBytes;
|
|
161
|
+
}
|
|
162
|
+
return output;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function boundedSearchIndex(value: string): string {
|
|
166
|
+
let output = "";
|
|
167
|
+
let bytes = 0;
|
|
168
|
+
for (const character of value) {
|
|
169
|
+
const characterBytes = Buffer.byteLength(character, "utf8");
|
|
170
|
+
if (bytes + characterBytes > CODEMODE_SEARCH_INDEX_LIMIT_BYTES) break;
|
|
171
|
+
output += character;
|
|
172
|
+
bytes += characterBytes;
|
|
173
|
+
}
|
|
174
|
+
return normalizeSearchText(output);
|
|
175
|
+
}
|
|
176
|
+
|
|
112
177
|
function jsdoc(description: string | undefined): string {
|
|
113
178
|
if (description === undefined || description.length === 0) return "";
|
|
114
179
|
return ` /** ${description.replaceAll("\n", " ")} */\n`;
|
|
@@ -220,6 +285,8 @@ function primitiveOrStructuredType(
|
|
|
220
285
|
return "boolean";
|
|
221
286
|
case "null":
|
|
222
287
|
return "null";
|
|
288
|
+
case "undefined":
|
|
289
|
+
return "undefined";
|
|
223
290
|
case "object":
|
|
224
291
|
return objectType(record, root, seen, depth);
|
|
225
292
|
case "array":
|
|
@@ -286,63 +353,314 @@ function arrayType(
|
|
|
286
353
|
return `readonly ${schemaType(record.items, root, seen, depth + 1)}[]`;
|
|
287
354
|
}
|
|
288
355
|
|
|
289
|
-
function renderTool(tool: RenderedTool
|
|
290
|
-
return `${
|
|
356
|
+
function renderTool(tool: RenderedTool): string {
|
|
357
|
+
return `${jsdoc(tool.description)} readonly [${quotedName(tool.name)}]: (input: ${tool.input}) => Promise<PiToolResult<${tool.output}>>;\n`;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function isWithinCatalogueLimit(text: string): boolean {
|
|
361
|
+
return Buffer.byteLength(text, "utf8") <= CODEMODE_CATALOGUE_LIMIT_BYTES;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function renderSchema(
|
|
365
|
+
// oxlint-disable-next-line anti-slop/no-unknown-parameters -- SAFETY: Registered schema metadata is parsed structurally here before catalogue rendering.
|
|
366
|
+
schema: unknown,
|
|
367
|
+
): string {
|
|
368
|
+
const parsed = parseStructuralJsonValue(schema);
|
|
369
|
+
const structural: ParsedCodeModeToolSchema | undefined =
|
|
370
|
+
parsed === true || parsed === false || isStructuralJsonObject(parsed) ? parsed : undefined;
|
|
371
|
+
return schemaType(structural, schemaRecord(structural), new Set(), 0);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function estimatedTokens(text: string): number {
|
|
375
|
+
return Math.ceil(Buffer.byteLength(text, "utf8") / 4);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
type CodeModeToolGroupSelection = {
|
|
379
|
+
readonly group: string;
|
|
380
|
+
readonly tools: readonly RenderedTool[];
|
|
381
|
+
readonly selectedNames: ReadonlySet<string>;
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
function selectInlineToolNames(tools: readonly RenderedTool[]): ReadonlySet<string> {
|
|
385
|
+
const groups = new Map<string, RenderedTool[]>();
|
|
386
|
+
for (const tool of tools) {
|
|
387
|
+
const group = groups.get(tool.group) ?? [];
|
|
388
|
+
group.push(tool);
|
|
389
|
+
groups.set(tool.group, group);
|
|
390
|
+
}
|
|
391
|
+
const queues = [...groups]
|
|
392
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
393
|
+
.map(([group, groupedTools]) => ({
|
|
394
|
+
group,
|
|
395
|
+
tools: groupedTools
|
|
396
|
+
.map((tool) => ({ tool, declaration: renderTool(tool) }))
|
|
397
|
+
.sort(
|
|
398
|
+
(left, right) =>
|
|
399
|
+
estimatedTokens(left.declaration) - estimatedTokens(right.declaration) ||
|
|
400
|
+
left.tool.name.localeCompare(right.tool.name),
|
|
401
|
+
),
|
|
402
|
+
}));
|
|
403
|
+
const selected = new Set<string>();
|
|
404
|
+
const selectionOrder: string[] = [];
|
|
405
|
+
let usedTokens = estimatedTokens(renderCatalogue(tools, selected));
|
|
406
|
+
let active = queues.filter(({ tools: groupedTools }) => groupedTools.length > 0);
|
|
407
|
+
while (active.length > 0) {
|
|
408
|
+
const nextActive: typeof active = [];
|
|
409
|
+
for (const queue of active) {
|
|
410
|
+
const candidate = queue.tools[0];
|
|
411
|
+
if (candidate === undefined) continue;
|
|
412
|
+
const cost = estimatedTokens(candidate.declaration);
|
|
413
|
+
if (usedTokens + cost > CODEMODE_CATALOGUE_TOKEN_BUDGET) continue;
|
|
414
|
+
queue.tools.shift();
|
|
415
|
+
selected.add(candidate.tool.name);
|
|
416
|
+
selectionOrder.push(candidate.tool.name);
|
|
417
|
+
usedTokens += cost;
|
|
418
|
+
if (queue.tools.length > 0) nextActive.push(queue);
|
|
419
|
+
}
|
|
420
|
+
active = nextActive;
|
|
421
|
+
}
|
|
422
|
+
while (estimatedTokens(renderCatalogue(tools, selected)) > CODEMODE_CATALOGUE_TOKEN_BUDGET) {
|
|
423
|
+
const removed = selectionOrder.pop();
|
|
424
|
+
if (removed === undefined) break;
|
|
425
|
+
selected.delete(removed);
|
|
426
|
+
}
|
|
427
|
+
return selected;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function groupSelections(
|
|
431
|
+
tools: readonly RenderedTool[],
|
|
432
|
+
selectedNames: ReadonlySet<string>,
|
|
433
|
+
): readonly CodeModeToolGroupSelection[] {
|
|
434
|
+
const groups = new Map<string, RenderedTool[]>();
|
|
435
|
+
for (const tool of tools) {
|
|
436
|
+
const group = groups.get(tool.group) ?? [];
|
|
437
|
+
group.push(tool);
|
|
438
|
+
groups.set(tool.group, group);
|
|
439
|
+
}
|
|
440
|
+
return [...groups]
|
|
441
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
442
|
+
.map(([group, groupedTools]) => ({
|
|
443
|
+
group,
|
|
444
|
+
tools: groupedTools,
|
|
445
|
+
selectedNames: new Set(
|
|
446
|
+
groupedTools.filter((tool) => selectedNames.has(tool.name)).map((tool) => tool.name),
|
|
447
|
+
),
|
|
448
|
+
}));
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function groupSummaryLines(selections: readonly CodeModeToolGroupSelection[]): readonly string[] {
|
|
452
|
+
const lines = ["// Groups:"];
|
|
453
|
+
let shownGroups = 0;
|
|
454
|
+
for (const { group, tools, selectedNames } of selections) {
|
|
455
|
+
const line = `// - ${JSON.stringify(group)}: ${selectedNames.size} of ${tools.length} shown`;
|
|
456
|
+
const remaining = selections.length - shownGroups - 1;
|
|
457
|
+
const candidate = [
|
|
458
|
+
...lines,
|
|
459
|
+
line,
|
|
460
|
+
...(remaining > 0 ? [`// - ... ${remaining} more groups`] : []),
|
|
461
|
+
].join("\n");
|
|
462
|
+
if (estimatedTokens(candidate) > CODEMODE_CATALOGUE_GROUP_SUMMARY_TOKEN_BUDGET) break;
|
|
463
|
+
lines.push(line);
|
|
464
|
+
shownGroups += 1;
|
|
465
|
+
}
|
|
466
|
+
const omittedGroups = selections.length - shownGroups;
|
|
467
|
+
if (omittedGroups > 0) lines.push(`// - ... ${omittedGroups} more groups`);
|
|
468
|
+
return lines;
|
|
291
469
|
}
|
|
292
470
|
|
|
293
471
|
function renderCatalogue(
|
|
294
472
|
tools: readonly RenderedTool[],
|
|
295
|
-
|
|
473
|
+
selectedNames: ReadonlySet<string>,
|
|
296
474
|
): string {
|
|
297
|
-
|
|
475
|
+
const selectedTools = tools.filter((tool) => selectedNames.has(tool.name));
|
|
476
|
+
const complete = selectedTools.length === tools.length;
|
|
477
|
+
const groups = groupSelections(tools, selectedNames);
|
|
478
|
+
const summary = [
|
|
479
|
+
`// CodeMode tool catalogue: ${complete ? "COMPLETE" : "PARTIAL"} (${selectedTools.length} of ${tools.length} declarations shown).`,
|
|
480
|
+
...(complete
|
|
481
|
+
? []
|
|
482
|
+
: [
|
|
483
|
+
'// Find omitted tools with tools.codemode_search({ query: "<intent or exact name>" }).',
|
|
484
|
+
"// Call a returned exact flat name with tools[result.name](input); groups are display-only.",
|
|
485
|
+
]),
|
|
486
|
+
...groupSummaryLines(groups),
|
|
487
|
+
].join("\n");
|
|
488
|
+
return `${summary}\n\ntype PiToolResult<Output = unknown> = {\n content: Array<{ type: "text"; text: string } | { type: "image"; data: string; mimeType: string }> ;\n details?: Output;\n};\n\ntype CodeModeToolSearchItem = {\n readonly name: string;\n readonly group: string;\n readonly description?: string;\n readonly declaration?: string;\n readonly declarationError?: string;\n};\n\ntype CodeModeToolSearchPage = {\n readonly items: readonly CodeModeToolSearchItem[];\n readonly total: number;\n readonly hasMore: boolean;\n readonly nextOffset: number | null;\n};\n\ndeclare const tools: Readonly<{\n /** Search every CodeMode-exposed Pi tool and return complete declarations for exact flat names. */\n readonly [${quotedName(CODEMODE_SEARCH_TOOL_NAME)}]: (input: { readonly query?: string; readonly group?: string; readonly limit?: number; readonly offset?: number }) => Promise<CodeModeToolSearchPage>;\n${selectedTools.map((tool) => renderTool(tool)).join("")}}>;\n`;
|
|
298
489
|
}
|
|
299
490
|
|
|
300
|
-
function
|
|
301
|
-
return
|
|
491
|
+
function normalizeSearchText(value: string): string {
|
|
492
|
+
return value
|
|
493
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
494
|
+
.replace(/[^a-zA-Z0-9]+/g, " ")
|
|
495
|
+
.toLowerCase()
|
|
496
|
+
.trim();
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function searchTerms(query: string): readonly string[] {
|
|
500
|
+
return normalizeSearchText(query).split(" ").filter(Boolean);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function searchScore(entry: CodeModeToolSearchEntry, terms: readonly string[]): number {
|
|
504
|
+
const name = normalizeSearchText(entry.name);
|
|
505
|
+
const group = normalizeSearchText(entry.group);
|
|
506
|
+
const description = normalizeSearchText(entry.description ?? "");
|
|
507
|
+
const declaration = entry.searchIndex ?? boundedSearchIndex(entry.declaration);
|
|
508
|
+
let score = 0;
|
|
509
|
+
for (const term of terms) {
|
|
510
|
+
if (name === term) score += 20;
|
|
511
|
+
else if (name.split(" ").includes(term)) score += 12;
|
|
512
|
+
else if (name.includes(term)) score += 8;
|
|
513
|
+
if (description.includes(term)) score += 4;
|
|
514
|
+
if (group.includes(term)) score += 2;
|
|
515
|
+
if (declaration.includes(term)) score += 1;
|
|
516
|
+
}
|
|
517
|
+
return score;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function searchItem(entry: CodeModeToolSearchEntry): CodeModeToolSearchPage["items"][number] {
|
|
521
|
+
return entry.description === undefined
|
|
522
|
+
? {
|
|
523
|
+
name: entry.name,
|
|
524
|
+
group: entry.group,
|
|
525
|
+
declaration: entry.declaration,
|
|
526
|
+
}
|
|
527
|
+
: {
|
|
528
|
+
name: entry.name,
|
|
529
|
+
group: entry.group,
|
|
530
|
+
description: entry.description,
|
|
531
|
+
declaration: entry.declaration,
|
|
532
|
+
};
|
|
302
533
|
}
|
|
303
534
|
|
|
304
|
-
|
|
535
|
+
function unavailableDeclarationSearchItem(
|
|
536
|
+
entry: CodeModeToolSearchEntry,
|
|
537
|
+
): CodeModeToolSearchPage["items"][number] {
|
|
538
|
+
return {
|
|
539
|
+
name: entry.name,
|
|
540
|
+
group: entry.group,
|
|
541
|
+
...(entry.description !== undefined && { description: entry.description }),
|
|
542
|
+
declarationError: "Complete declaration exceeds the 1 MiB CodeMode search result limit",
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function createSearchPage(
|
|
547
|
+
ranked: readonly CodeModeToolSearchEntry[],
|
|
548
|
+
input: CodeModeToolSearchParameters,
|
|
549
|
+
): CodeModeToolSearchResult {
|
|
550
|
+
const offset = input.offset ?? 0;
|
|
551
|
+
const limit = input.limit ?? CODEMODE_SEARCH_DEFAULT_LIMIT;
|
|
552
|
+
const items: CodeModeToolSearchPage["items"][number][] = [];
|
|
553
|
+
for (const entry of ranked.slice(offset, offset + limit)) {
|
|
554
|
+
let item = searchItem(entry);
|
|
555
|
+
let nextItems = [...items, item];
|
|
556
|
+
const consumed = offset + nextItems.length;
|
|
557
|
+
let candidate: CodeModeToolSearchPage = {
|
|
558
|
+
items: nextItems,
|
|
559
|
+
total: ranked.length,
|
|
560
|
+
hasMore: consumed < ranked.length,
|
|
561
|
+
nextOffset: consumed < ranked.length ? consumed : null,
|
|
562
|
+
};
|
|
563
|
+
if (Buffer.byteLength(JSON.stringify(candidate), "utf8") > CODEMODE_SEARCH_RESULT_LIMIT_BYTES) {
|
|
564
|
+
if (items.length > 0) break;
|
|
565
|
+
item = unavailableDeclarationSearchItem(entry);
|
|
566
|
+
nextItems = [...items, item];
|
|
567
|
+
candidate = { ...candidate, items: nextItems };
|
|
568
|
+
}
|
|
569
|
+
if (Buffer.byteLength(JSON.stringify(candidate), "utf8") > CODEMODE_SEARCH_RESULT_LIMIT_BYTES) {
|
|
570
|
+
return {
|
|
571
|
+
ok: false,
|
|
572
|
+
code: "serialization",
|
|
573
|
+
message: `Pi CodeMode search metadata exceeds the 1 MiB result limit: ${entry.name}`,
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
items.push(item);
|
|
577
|
+
}
|
|
578
|
+
const consumed = offset + items.length;
|
|
579
|
+
return {
|
|
580
|
+
ok: true,
|
|
581
|
+
page: {
|
|
582
|
+
items,
|
|
583
|
+
total: ranked.length,
|
|
584
|
+
hasMore: consumed < ranked.length,
|
|
585
|
+
nextOffset: consumed < ranked.length ? consumed : null,
|
|
586
|
+
},
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/** Searches complete declarations for the exact flat names exposed to one CodeMode Cell. */
|
|
591
|
+
export function searchCodeModeToolCatalogue(
|
|
592
|
+
entries: readonly CodeModeToolSearchEntry[],
|
|
593
|
+
input: CodeModeJsonValue,
|
|
594
|
+
): CodeModeToolSearchResult {
|
|
595
|
+
if (!Value.Check(CodeModeToolSearchParametersSchema, input)) {
|
|
596
|
+
return {
|
|
597
|
+
ok: false,
|
|
598
|
+
code: "validation",
|
|
599
|
+
message:
|
|
600
|
+
"Pi CodeMode search input must be an object with optional query, group, limit (1-20), and offset fields",
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
const scoped =
|
|
604
|
+
input.group === undefined ? entries : entries.filter((entry) => entry.group === input.group);
|
|
605
|
+
const query = input.query?.trim() ?? "";
|
|
606
|
+
const exact = scoped.find(
|
|
607
|
+
(entry) => entry.name === query || `tools[${quotedName(entry.name)}]` === query,
|
|
608
|
+
);
|
|
609
|
+
if (exact !== undefined) return createSearchPage([exact], input);
|
|
610
|
+
const terms = searchTerms(query);
|
|
611
|
+
const ranked =
|
|
612
|
+
terms.length === 0
|
|
613
|
+
? [...scoped].sort((left, right) => left.name.localeCompare(right.name))
|
|
614
|
+
: scoped
|
|
615
|
+
.map((entry) => ({ entry, score: searchScore(entry, terms) }))
|
|
616
|
+
.filter(({ score }) => score > 0)
|
|
617
|
+
.sort(
|
|
618
|
+
(left, right) =>
|
|
619
|
+
right.score - left.score || left.entry.name.localeCompare(right.entry.name),
|
|
620
|
+
)
|
|
621
|
+
.map(({ entry }) => entry);
|
|
622
|
+
return createSearchPage(ranked, input);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/** Renders a token-budgeted inline catalogue and retains complete declarations for search. */
|
|
305
626
|
export function renderCodeModeToolCatalogue(
|
|
306
627
|
tools: readonly CodeModeToolCatalogueTool[],
|
|
307
628
|
): CodeModeToolCatalogueResult {
|
|
308
629
|
const candidates = tools
|
|
309
630
|
.filter((tool) => !isReservedCodeModeToolName(tool.name))
|
|
310
|
-
.sort(
|
|
631
|
+
.sort(
|
|
632
|
+
(left, right) => left.name.localeCompare(right.name) || left.group.localeCompare(right.group),
|
|
633
|
+
);
|
|
311
634
|
const unique = candidates.filter(
|
|
312
635
|
(tool, index) => index === 0 || tool.name !== candidates[index - 1]?.name,
|
|
313
636
|
);
|
|
314
|
-
const rendered = unique.map((tool) => {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
let text = renderCatalogue(rendered, "include-descriptions");
|
|
326
|
-
if (isWithinCatalogueLimit(text)) return { ok: true, text };
|
|
327
|
-
|
|
328
|
-
const simplified = [...rendered];
|
|
329
|
-
const schemaOrder = simplified
|
|
330
|
-
.map((tool, index) => ({
|
|
331
|
-
index,
|
|
332
|
-
bytes: Buffer.byteLength(tool.input, "utf8"),
|
|
333
|
-
name: tool.name,
|
|
334
|
-
}))
|
|
335
|
-
.sort((left, right) => right.bytes - left.bytes || left.name.localeCompare(right.name));
|
|
336
|
-
for (const candidate of schemaOrder) {
|
|
337
|
-
const current = simplified[candidate.index];
|
|
338
|
-
if (current === undefined) continue;
|
|
339
|
-
simplified[candidate.index] = { ...current, input: "unknown" };
|
|
340
|
-
text = renderCatalogue(simplified, "include-descriptions");
|
|
341
|
-
if (isWithinCatalogueLimit(text)) return { ok: true, text };
|
|
637
|
+
const rendered = unique.map((tool) => ({
|
|
638
|
+
name: tool.name,
|
|
639
|
+
group: boundedGroup(tool.group),
|
|
640
|
+
description: boundedDescription(tool.description),
|
|
641
|
+
input: renderSchema(tool.inputSchema),
|
|
642
|
+
output: renderSchema(tool.outputSchema),
|
|
643
|
+
}));
|
|
644
|
+
const selectedNames = selectInlineToolNames(rendered);
|
|
645
|
+
const text = renderCatalogue(rendered, selectedNames);
|
|
646
|
+
if (!isWithinCatalogueLimit(text)) {
|
|
647
|
+
return { ok: false, reason: "catalogue-exceeds-outer-limit" };
|
|
342
648
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
:
|
|
649
|
+
const searchEntries = rendered.map((tool) => ({
|
|
650
|
+
name: tool.name,
|
|
651
|
+
group: tool.group,
|
|
652
|
+
...(tool.description !== undefined && { description: tool.description }),
|
|
653
|
+
declaration: renderTool(tool),
|
|
654
|
+
searchIndex: boundedSearchIndex(
|
|
655
|
+
`${tool.name}\n${tool.group}\n${tool.description ?? ""}\n${tool.input}\n${tool.output}`,
|
|
656
|
+
),
|
|
657
|
+
}));
|
|
658
|
+
return {
|
|
659
|
+
ok: true,
|
|
660
|
+
text,
|
|
661
|
+
complete: selectedNames.size === rendered.length,
|
|
662
|
+
shownCount: selectedNames.size,
|
|
663
|
+
totalCount: rendered.length,
|
|
664
|
+
searchEntries,
|
|
665
|
+
};
|
|
348
666
|
}
|
|
@@ -19,7 +19,12 @@ const CODEMODE_TOOL_NAMES = {
|
|
|
19
19
|
result: "codemode_result",
|
|
20
20
|
cancel: "codemode_cancel",
|
|
21
21
|
sessions: "codemode_sessions",
|
|
22
|
+
search: "codemode_search",
|
|
22
23
|
} as const;
|
|
24
|
+
|
|
25
|
+
/** Exact direct and guest name for progressive CodeMode tool declaration search. */
|
|
26
|
+
export const CODEMODE_SEARCH_TOOL_NAME = CODEMODE_TOOL_NAMES.search;
|
|
27
|
+
|
|
23
28
|
const RESERVED_CODEMODE_TOOL_NAMES = new Set<string>(Object.values(CODEMODE_TOOL_NAMES));
|
|
24
29
|
|
|
25
30
|
/** Reports whether a registered name belongs to CodeMode itself and must remain direct-only. */
|
|
@@ -126,6 +131,17 @@ export const CodeModeCancelParametersSchema = Type.Object(
|
|
|
126
131
|
/** Strict empty arguments accepted by the read-only `codemode_sessions` tool. */
|
|
127
132
|
export const CodeModeSessionsParametersSchema = Type.Object({}, { additionalProperties: false });
|
|
128
133
|
|
|
134
|
+
/** Strict arguments shared by direct and in-Cell `codemode_search`. */
|
|
135
|
+
export const CodeModeToolSearchParametersSchema = Type.Object(
|
|
136
|
+
{
|
|
137
|
+
query: Type.Optional(Type.String({ maxLength: 512 })),
|
|
138
|
+
group: Type.Optional(Type.String({ minLength: 1, maxLength: 512 })),
|
|
139
|
+
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 20 })),
|
|
140
|
+
offset: Type.Optional(NonNegativeSafeIntegerSchema),
|
|
141
|
+
},
|
|
142
|
+
{ additionalProperties: false },
|
|
143
|
+
);
|
|
144
|
+
|
|
129
145
|
/** Parsed arguments for `codemode_execute`. */
|
|
130
146
|
export type CodeModeExecuteParameters = Static<typeof CodeModeExecuteParametersSchema>;
|
|
131
147
|
/** Parsed arguments for `codemode_result`. */
|
|
@@ -134,6 +150,8 @@ export type CodeModeResultParameters = Static<typeof CodeModeResultParametersSch
|
|
|
134
150
|
export type CodeModeCancelParameters = Static<typeof CodeModeCancelParametersSchema>;
|
|
135
151
|
/** Parsed arguments for the read-only `codemode_sessions` tool. */
|
|
136
152
|
export type CodeModeSessionsParameters = Static<typeof CodeModeSessionsParametersSchema>;
|
|
153
|
+
/** Parsed arguments shared by direct and in-Cell `codemode_search`. */
|
|
154
|
+
export type CodeModeToolSearchParameters = Static<typeof CodeModeToolSearchParametersSchema>;
|
|
137
155
|
|
|
138
156
|
/** A JSON object accepted in a successful CodeMode result. */
|
|
139
157
|
export type CodeModeJsonObject = { readonly [key: string]: CodeModeJsonValue };
|
|
@@ -228,6 +246,38 @@ export const CodeModeSessionsResultSchema = Type.Object(
|
|
|
228
246
|
/** Schema-derived live Session list ordered by reclamation priority. */
|
|
229
247
|
export type CodeModeSessionsResult = Static<typeof CodeModeSessionsResultSchema>;
|
|
230
248
|
|
|
249
|
+
const CodeModeToolSearchItemBaseSchema = {
|
|
250
|
+
name: Type.String(),
|
|
251
|
+
group: Type.String(),
|
|
252
|
+
description: Type.Optional(Type.String()),
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
/** One exact CodeMode tool declaration, or an explicit size-bound failure for that declaration. */
|
|
256
|
+
export const CodeModeToolSearchItemSchema = Type.Union([
|
|
257
|
+
Type.Object(
|
|
258
|
+
{ ...CodeModeToolSearchItemBaseSchema, declaration: Type.String({ minLength: 1 }) },
|
|
259
|
+
{ additionalProperties: false },
|
|
260
|
+
),
|
|
261
|
+
Type.Object(
|
|
262
|
+
{ ...CodeModeToolSearchItemBaseSchema, declarationError: Type.String({ minLength: 1 }) },
|
|
263
|
+
{ additionalProperties: false },
|
|
264
|
+
),
|
|
265
|
+
]);
|
|
266
|
+
|
|
267
|
+
/** Stable progressive declaration-search page shared by direct and in-Cell search. */
|
|
268
|
+
export const CodeModeToolSearchPageSchema = Type.Object(
|
|
269
|
+
{
|
|
270
|
+
items: Type.Array(CodeModeToolSearchItemSchema, { maxItems: 20 }),
|
|
271
|
+
total: NonNegativeSafeIntegerSchema,
|
|
272
|
+
hasMore: Type.Boolean(),
|
|
273
|
+
nextOffset: Type.Union([NonNegativeSafeIntegerSchema, Type.Null()]),
|
|
274
|
+
},
|
|
275
|
+
{ additionalProperties: false },
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
/** Schema-derived progressive declaration-search page. */
|
|
279
|
+
export type CodeModeToolSearchPage = Static<typeof CodeModeToolSearchPageSchema>;
|
|
280
|
+
|
|
231
281
|
const CodeModePendingSchema = Type.Object(
|
|
232
282
|
{
|
|
233
283
|
result: Type.Literal("pending"),
|
|
@@ -465,7 +515,7 @@ export type CodeModeToolOperationResult = {
|
|
|
465
515
|
readonly presentation?: CodeModePresentationSnapshot;
|
|
466
516
|
};
|
|
467
517
|
|
|
468
|
-
/** Operations supplied by the session coordinator to build the
|
|
518
|
+
/** Operations supplied by the session coordinator and catalogue to build the five Pi tools. */
|
|
469
519
|
export interface CodeModeToolOperations {
|
|
470
520
|
execute(
|
|
471
521
|
input: CodeModeExecuteParameters,
|
|
@@ -476,6 +526,7 @@ export interface CodeModeToolOperations {
|
|
|
476
526
|
result(input: CodeModeResultParameters): Promise<CodeModeToolOperationResult>;
|
|
477
527
|
cancel(input: CodeModeCancelParameters): Promise<CodeModeToolOperationResult>;
|
|
478
528
|
sessions(): Promise<CodeModeSessionsResult>;
|
|
529
|
+
search(input: CodeModeToolSearchParameters): Promise<CodeModeToolSearchPage>;
|
|
479
530
|
}
|
|
480
531
|
|
|
481
532
|
function structuredCodeModeResult(
|
|
@@ -503,6 +554,9 @@ type CodeModeToolDefinitions = readonly [
|
|
|
503
554
|
ToolDefinition<typeof CodeModeResultParametersSchema, CodeModeResultDetails>,
|
|
504
555
|
ToolDefinition<typeof CodeModeCancelParametersSchema, CodeModeResultDetails>,
|
|
505
556
|
ToolDefinition<typeof CodeModeSessionsParametersSchema, CodeModeSessionsResult>,
|
|
557
|
+
ToolDefinition<typeof CodeModeToolSearchParametersSchema, CodeModeToolSearchPage> & {
|
|
558
|
+
readonly outputSchema: typeof CodeModeToolSearchPageSchema;
|
|
559
|
+
},
|
|
506
560
|
];
|
|
507
561
|
|
|
508
562
|
function structuredCodeModeSessionsResult(
|
|
@@ -514,7 +568,16 @@ function structuredCodeModeSessionsResult(
|
|
|
514
568
|
};
|
|
515
569
|
}
|
|
516
570
|
|
|
517
|
-
|
|
571
|
+
function structuredCodeModeToolSearchPage(
|
|
572
|
+
page: CodeModeToolSearchPage,
|
|
573
|
+
): AgentToolResult<CodeModeToolSearchPage> {
|
|
574
|
+
return {
|
|
575
|
+
content: [{ type: "text", text: JSON.stringify(page) }],
|
|
576
|
+
details: page,
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/** Creates the five stable Pi definitions while leaving admission and session policy to the coordinator. */
|
|
518
581
|
export function createCodeModeToolDefinitions(
|
|
519
582
|
operations: CodeModeToolOperations,
|
|
520
583
|
executeDescription = "Execute TypeScript in a persistent isolated Deno CodeMode Session.",
|
|
@@ -570,5 +633,16 @@ export function createCodeModeToolDefinitions(
|
|
|
570
633
|
return structuredCodeModeSessionsResult(await operations.sessions());
|
|
571
634
|
},
|
|
572
635
|
};
|
|
573
|
-
|
|
636
|
+
const searchTool: CodeModeToolDefinitions[4] = {
|
|
637
|
+
name: CODEMODE_TOOL_NAMES.search,
|
|
638
|
+
label: "Search CodeMode Tools",
|
|
639
|
+
description:
|
|
640
|
+
"Search CodeMode-exposed Pi tools and return exact flat names with complete TypeScript declarations.",
|
|
641
|
+
parameters: CodeModeToolSearchParametersSchema,
|
|
642
|
+
outputSchema: CodeModeToolSearchPageSchema,
|
|
643
|
+
async execute(_toolCallId, input) {
|
|
644
|
+
return structuredCodeModeToolSearchPage(await operations.search(input));
|
|
645
|
+
},
|
|
646
|
+
};
|
|
647
|
+
return [executeTool, resultTool, cancelTool, sessionsTool, searchTool];
|
|
574
648
|
}
|