@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.
@@ -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 CodeModeToolInputSchema = boolean | object;
26
+ export type CodeModeToolSchema = boolean | object;
17
27
 
18
- /** One CodeMode-callable Pi tool and its structural input schema. */
28
+ /** One CodeMode-callable Pi tool and its structural input/output schemas. */
19
29
  export type CodeModeToolCatalogueTool = {
20
30
  readonly name: string;
21
- readonly inputSchema: CodeModeToolInputSchema;
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
- /** A complete catalogue or an explicit refusal before exposure changes. */
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
- | { readonly ok: true; readonly text: string }
28
- | { readonly ok: false; readonly reason: "names-exceed-catalogue-limit" };
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
- type ParsedCodeModeToolInputSchema = boolean | CodeModeJsonObject;
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
- if (Buffer.byteLength(output + character, "utf8") > CODEMODE_JSDOC_LIMIT_BYTES) break;
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, descriptionMode: CodeModeCatalogueDescriptionMode): string {
290
- return `${descriptionMode === "include-descriptions" ? jsdoc(tool.description) : ""} readonly [${quotedName(tool.name)}]: (input: ${tool.input}) => Promise<PiToolResult>;\n`;
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
- descriptionMode: CodeModeCatalogueDescriptionMode,
473
+ selectedNames: ReadonlySet<string>,
296
474
  ): string {
297
- 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`;
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 isWithinCatalogueLimit(text: string): boolean {
301
- return Buffer.byteLength(text, "utf8") <= CODEMODE_CATALOGUE_LIMIT_BYTES;
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
- /** Renders all guest-callable names once or refuses a name-only overflow. */
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((left, right) => left.name.localeCompare(right.name));
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
- const parsed = parseStructuralJsonValue(tool.inputSchema);
316
- const inputSchema: ParsedCodeModeToolInputSchema | undefined =
317
- parsed === true || parsed === false || isStructuralJsonObject(parsed) ? parsed : undefined;
318
- return {
319
- name: tool.name,
320
- description: boundedDescription(tool.description),
321
- input: schemaType(inputSchema, schemaRecord(inputSchema), new Set(), 0),
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
- text = renderCatalogue(simplified, "omit-descriptions");
345
- return isWithinCatalogueLimit(text)
346
- ? { ok: true, text }
347
- : { ok: false, reason: "names-exceed-catalogue-limit" };
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 four Pi tools. */
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
- /** Creates the four stable Pi definitions while leaving admission and session policy to the coordinator. */
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
- return [executeTool, resultTool, cancelTool, sessionsTool];
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
  }