@ian-pascoe/pi-codemode 0.4.0 → 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 CHANGED
@@ -154,14 +154,19 @@ parent kills the worker before it can reply.
154
154
 
155
155
  ## Transcript and Observer UI
156
156
 
157
- The CodeMode Transcript gives all four tools semantic collapsed and expanded
158
- rendering. Collapsed rows prioritize Cell lifecycle, a short Session ID, Cell
157
+ The CodeMode Transcript gives all five tools semantic collapsed and expanded
158
+ rendering. Session rows prioritize Cell lifecycle, a short Session ID, Cell
159
159
  Ordinal, returned-value shape, Console-call count, nested-tool count, and elapsed
160
160
  time. Expanded rows show the full Session ID, explicit call arguments,
161
161
  TypeScript source, bounded Console output before structured returned data or the
162
162
  error, and bounded nested-tool names, outcomes, and durations. Nested arguments
163
163
  and raw nested outputs are never copied into the presentation.
164
164
 
165
+ Search rows show the query, result range, and next offset without exposing raw
166
+ JSON. Expanding a search shows each exact tool name, display group, highlighted
167
+ TypeScript declaration, and any declaration-size failure. Search declarations
168
+ are bounded only in the Transcript; the model-facing result remains unchanged.
169
+
165
170
  Status always uses a symbol and text together:
166
171
 
167
172
  ```text
@@ -228,18 +233,50 @@ termination, or process failure destroys that session's heap.
228
233
 
229
234
  ## Registered tools
230
235
 
231
- The `codemode_execute` description contains generated TypeScript declarations
232
- for the currently exposed registered tools. Guest calls resolve to:
236
+ The `codemode_execute` description contains a token-bounded catalogue of
237
+ complete generated TypeScript declarations for currently exposed registered
238
+ tools. It marks the catalogue `COMPLETE` or `PARTIAL`; a partial catalogue keeps
239
+ the remaining declarations available through:
233
240
 
234
241
  ```ts
235
- type PiToolResult = {
242
+ const page = await tools.codemode_search({
243
+ query: "intent or exact registered name",
244
+ group: "optional display group",
245
+ limit: 10,
246
+ offset: 0,
247
+ });
248
+ ```
249
+
250
+ Search returns stable pages with `items`, `total`, `hasMore`, and `nextOffset`.
251
+ Each item contains its exact flat `name`, display-only `group`, bounded
252
+ `description`, and complete `declaration`. A pathological declaration above the
253
+ search response bound instead has an explicit `declarationError`, without
254
+ stalling pagination. Call a discovered tool with `tools[item.name](input)`;
255
+ every exposed name remains callable even when its declaration is omitted from
256
+ the inline catalogue.
257
+
258
+ `codemode_search` is also a direct Pi tool, so declarations can be discovered
259
+ before starting a Cell. Direct search reads the current exposure catalogue;
260
+ in-Cell search reads the Cell's frozen exposure snapshot. Both expose only
261
+ CodeMode-callable tools and use the same search implementation.
262
+
263
+ Ordinary guest tool calls resolve to:
264
+
265
+ ```ts
266
+ type PiToolResult<Output = unknown> = {
236
267
  content: Array<
237
268
  { type: "text"; text: string } | { type: "image"; data: string; mimeType: string }
238
269
  >;
239
- details?: unknown;
270
+ details?: Output;
240
271
  };
241
272
  ```
242
273
 
274
+ Each declaration derives its input from `parameters` and its `details` type
275
+ from the registered definition's optional `outputSchema`. Source-gated fallback
276
+ schemas cover Pi's built-in tools and `@howaboua/pi-codex-conversion` 3.0.23;
277
+ tool-provided schemas take precedence. Other missing or unsupported output
278
+ schemas remain `unknown`.
279
+
243
280
  Ordinary tool failures reject with a catchable `CodeModeToolError`. A Pi result
244
281
  that requests termination stops the complete CodeMode Session and cannot be
245
282
  caught by guest code.
@@ -268,7 +305,8 @@ last match wins. Project `tools` replaces the global array, while project
268
305
 
269
306
  An unmatched active tool defaults to `direct-and-codemode`; an unmatched
270
307
  inactive tool remains unavailable. An explicit rule may expose an inactive tool
271
- or activate direct access. The four `codemode_*` tools are always direct-only.
308
+ or activate direct access. The five registered `codemode_*` tools are always
309
+ direct-only.
272
310
  Pi's global allowed/excluded registry remains authoritative. Invalid fields or
273
311
  patterns disable CodeMode for that session without changing Pi's active tools.
274
312
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ian-pascoe/pi-codemode",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "private": false,
5
5
  "description": "Process-isolated persistent TypeScript tool composition for Pi",
6
6
  "keywords": [
@@ -35,16 +35,26 @@
35
35
  },
36
36
  "dependencies": {
37
37
  "deno": "2.9.5",
38
- "minimatch": "^10.2.5",
39
- "typescript": "6.0.3"
38
+ "minimatch": "^10.2.6",
39
+ "runtime-typescript": "npm:typescript@6.0.3",
40
+ "typescript": "^7.0.2"
41
+ },
42
+ "devDependencies": {
43
+ "@howaboua/pi-codex-conversion": "3.0.23"
40
44
  },
41
45
  "peerDependencies": {
42
46
  "@earendil-works/pi-agent-core": "*",
43
47
  "@earendil-works/pi-ai": "*",
44
48
  "@earendil-works/pi-coding-agent": "*",
45
49
  "@earendil-works/pi-tui": "*",
50
+ "@howaboua/pi-codex-conversion": "3.0.23",
46
51
  "typebox": "*"
47
52
  },
53
+ "peerDependenciesMeta": {
54
+ "@howaboua/pi-codex-conversion": {
55
+ "optional": true
56
+ }
57
+ },
48
58
  "engines": {
49
59
  "node": ">=22.19.0"
50
60
  },
@@ -1,4 +1,4 @@
1
- import ts from "typescript";
1
+ import ts from "runtime-typescript";
2
2
 
3
3
  /** Successful pure transform output consumed only by the worker-owned Notebook runtime. */
4
4
  export type TransformedCodeModeCell = {
@@ -0,0 +1,222 @@
1
+ import type { ToolInfo } from "@earendil-works/pi-coding-agent";
2
+ import { Type, type TSchema } from "typebox";
3
+ import type { CodeModeToolSchema } from "./codemode-tool-catalog.js";
4
+
5
+ const ClosedObject = { additionalProperties: false } as const;
6
+ const OptionalUndefinedString = Type.Optional(Type.Union([Type.String(), Type.Undefined()]));
7
+ const OptionalUndefinedNumber = Type.Optional(Type.Union([Type.Number(), Type.Undefined()]));
8
+
9
+ const TruncationResultSchema = Type.Object(
10
+ {
11
+ content: Type.String(),
12
+ truncated: Type.Boolean(),
13
+ truncatedBy: Type.Union([Type.Literal("lines"), Type.Literal("bytes"), Type.Null()]),
14
+ totalLines: Type.Number(),
15
+ totalBytes: Type.Number(),
16
+ outputLines: Type.Number(),
17
+ outputBytes: Type.Number(),
18
+ lastLinePartial: Type.Boolean(),
19
+ firstLineExceedsLimit: Type.Boolean(),
20
+ maxLines: Type.Number(),
21
+ maxBytes: Type.Number(),
22
+ },
23
+ ClosedObject,
24
+ );
25
+ const ShellOutputSchema = Type.Object(
26
+ {
27
+ truncation: Type.Optional(TruncationResultSchema),
28
+ fullOutputPath: Type.Optional(Type.String()),
29
+ },
30
+ ClosedObject,
31
+ );
32
+ const ReadOutputSchema = Type.Object(
33
+ { truncation: Type.Optional(TruncationResultSchema) },
34
+ ClosedObject,
35
+ );
36
+ const EditOutputSchema = Type.Object(
37
+ {
38
+ diff: Type.String(),
39
+ patch: Type.String(),
40
+ firstChangedLine: Type.Optional(Type.Number()),
41
+ },
42
+ ClosedObject,
43
+ );
44
+ const GrepOutputSchema = Type.Object(
45
+ {
46
+ truncation: Type.Optional(TruncationResultSchema),
47
+ matchLimitReached: Type.Optional(Type.Number()),
48
+ linesTruncated: Type.Optional(Type.Boolean()),
49
+ },
50
+ ClosedObject,
51
+ );
52
+ const FindOutputSchema = Type.Object(
53
+ {
54
+ truncation: Type.Optional(TruncationResultSchema),
55
+ resultLimitReached: Type.Optional(Type.Number()),
56
+ },
57
+ ClosedObject,
58
+ );
59
+ const LsOutputSchema = Type.Object(
60
+ {
61
+ truncation: Type.Optional(TruncationResultSchema),
62
+ entryLimitReached: Type.Optional(Type.Number()),
63
+ },
64
+ ClosedObject,
65
+ );
66
+
67
+ const ExecutePatchResultSchema = Type.Object(
68
+ {
69
+ changedFiles: Type.Array(Type.String()),
70
+ createdFiles: Type.Array(Type.String()),
71
+ deletedFiles: Type.Array(Type.String()),
72
+ movedFiles: Type.Array(Type.String()),
73
+ fuzz: Type.Number(),
74
+ },
75
+ ClosedObject,
76
+ );
77
+ const ApplyPatchOutputSchema = Type.Union([
78
+ Type.Object({ status: Type.Literal("success"), result: ExecutePatchResultSchema }, ClosedObject),
79
+ Type.Object(
80
+ {
81
+ status: Type.Literal("partial_failure"),
82
+ result: ExecutePatchResultSchema,
83
+ failedTargets: Type.Optional(Type.Union([Type.Array(Type.String()), Type.Undefined()])),
84
+ },
85
+ ClosedObject,
86
+ ),
87
+ ]);
88
+ const UnifiedExecOutputSchema = Type.Object(
89
+ {
90
+ chunk_id: Type.String(),
91
+ wall_time_seconds: Type.Number(),
92
+ output: Type.String(),
93
+ exit_code: OptionalUndefinedNumber,
94
+ session_id: OptionalUndefinedNumber,
95
+ original_token_count: OptionalUndefinedNumber,
96
+ },
97
+ ClosedObject,
98
+ );
99
+ const ImagegenOutputSchema = Type.Object(
100
+ {
101
+ path: Type.String(),
102
+ latest_path: Type.String(),
103
+ images: Type.Array(
104
+ Type.Object(
105
+ {
106
+ path: Type.String(),
107
+ absolute_path: Type.String(),
108
+ latest_path: Type.Optional(Type.String()),
109
+ latest_absolute_path: Type.Optional(Type.String()),
110
+ },
111
+ ClosedObject,
112
+ ),
113
+ ),
114
+ background: OptionalUndefinedString,
115
+ quality: OptionalUndefinedString,
116
+ size: OptionalUndefinedString,
117
+ },
118
+ ClosedObject,
119
+ );
120
+ // oxlint-disable-next-line anti-slop/no-unsafe-dictionary-type -- SAFETY: pi-codex-conversion intentionally exposes provider-defined web_run fields alongside these known text fields.
121
+ type CodexWebRunPayload = Record<string, unknown> & {
122
+ encrypted_output?: string | undefined;
123
+ output_text?: string | undefined;
124
+ output?: string | undefined;
125
+ text?: string | undefined;
126
+ };
127
+ const WebRunPayloadSchema = Type.Unsafe<CodexWebRunPayload>(
128
+ Type.Object(
129
+ {
130
+ encrypted_output: OptionalUndefinedString,
131
+ output_text: OptionalUndefinedString,
132
+ output: OptionalUndefinedString,
133
+ text: OptionalUndefinedString,
134
+ },
135
+ { additionalProperties: true },
136
+ ),
137
+ );
138
+ const WebRunOutputSchema = Type.Object({ webRun: WebRunPayloadSchema }, ClosedObject);
139
+ const ViewImageContentSchema = Type.Object(
140
+ {
141
+ type: Type.Literal("image"),
142
+ data: Type.String(),
143
+ mimeType: Type.String(),
144
+ detail: Type.Union([Type.Literal("high"), Type.Literal("original")]),
145
+ },
146
+ ClosedObject,
147
+ );
148
+ const ViewImageOutputSchema = Type.Union([
149
+ Type.Object({ viewImage: Type.Literal(true) }, ClosedObject),
150
+ Type.Object(
151
+ {
152
+ viewImageDescription: Type.Object(
153
+ {
154
+ image: ViewImageContentSchema,
155
+ path: Type.String(),
156
+ description: Type.String(),
157
+ },
158
+ ClosedObject,
159
+ ),
160
+ },
161
+ ClosedObject,
162
+ ),
163
+ ]);
164
+
165
+ /** Source-gated fallback output schemas for Pi and pi-codex-conversion tool details. */
166
+ export const CodeModeKnownOutputSchemas = {
167
+ builtin: {
168
+ bash: ShellOutputSchema,
169
+ powershell: ShellOutputSchema,
170
+ read: ReadOutputSchema,
171
+ edit: EditOutputSchema,
172
+ write: Type.Undefined(),
173
+ grep: GrepOutputSchema,
174
+ find: FindOutputSchema,
175
+ ls: LsOutputSchema,
176
+ },
177
+ codexConversion: {
178
+ apply_patch: ApplyPatchOutputSchema,
179
+ exec_command: UnifiedExecOutputSchema,
180
+ write_stdin: UnifiedExecOutputSchema,
181
+ view_image: ViewImageOutputSchema,
182
+ web_run: WebRunOutputSchema,
183
+ imagegen: ImagegenOutputSchema,
184
+ },
185
+ } as const satisfies {
186
+ readonly builtin: Readonly<Record<string, TSchema>>;
187
+ readonly codexConversion: Readonly<Record<string, TSchema>>;
188
+ };
189
+
190
+ const CODEX_CONVERSION_SOURCE = "npm:@howaboua/pi-codex-conversion";
191
+
192
+ function schemaByName(
193
+ schemas: Readonly<Record<string, TSchema>>,
194
+ name: string,
195
+ ): TSchema | undefined {
196
+ return schemas[name];
197
+ }
198
+
199
+ function isCodexConversionSource(sourceInfo: ToolInfo["sourceInfo"]): boolean {
200
+ return (
201
+ sourceInfo.origin === "package" &&
202
+ (sourceInfo.source === CODEX_CONVERSION_SOURCE ||
203
+ sourceInfo.source.startsWith(`${CODEX_CONVERSION_SOURCE}@`))
204
+ );
205
+ }
206
+
207
+ /** Returns a known output schema only when both the registered tool name and owner match. */
208
+ export function resolveKnownToolOutputSchema(
209
+ tool: Pick<ToolInfo, "name" | "sourceInfo">,
210
+ ): CodeModeToolSchema | undefined {
211
+ if (
212
+ tool.sourceInfo.source === "builtin" &&
213
+ tool.sourceInfo.path === `<builtin:${tool.name}>` &&
214
+ tool.sourceInfo.scope === "temporary" &&
215
+ tool.sourceInfo.origin === "top-level"
216
+ ) {
217
+ return schemaByName(CodeModeKnownOutputSchemas.builtin, tool.name);
218
+ }
219
+ return isCodexConversionSource(tool.sourceInfo)
220
+ ? schemaByName(CodeModeKnownOutputSchemas.codexConversion, tool.name)
221
+ : undefined;
222
+ }
@@ -7,7 +7,9 @@ import { CodeModeWorkerProcess } from "./codemode-deno-process.js";
7
7
  import { formatCodeModePresentationData } from "./codemode-presentation-output.js";
8
8
  import type { CodeModeRuntime, CodeModeTimerHandle } from "./codemode-runtime.js";
9
9
  import type { CodeModeResultSpillWriter } from "./codemode-session-files.js";
10
+ import type { CodeModeToolSearchEntry } from "./codemode-tool-catalog.js";
10
11
  import {
12
+ CODEMODE_SEARCH_TOOL_NAME,
11
13
  createCodeModeFailure,
12
14
  createCodeModePending,
13
15
  createCodeModeSuccess,
@@ -123,6 +125,10 @@ export type CodeModeNestedToolBatch = {
123
125
  readonly sessionId: string;
124
126
  readonly batchId: string;
125
127
  readonly calls: readonly CodeModeNestedToolCall[];
128
+ /** Exact guest-callable name snapshot installed for this Cell. */
129
+ readonly exposedToolNames: readonly string[];
130
+ /** Immutable searchable declaration snapshot installed with this Cell's names. */
131
+ readonly searchEntries: readonly CodeModeToolSearchEntry[];
126
132
  readonly signal: AbortSignal;
127
133
  readonly onUpdate?: (update: CodeModeNestedToolUpdate) => void;
128
134
  };
@@ -159,7 +165,11 @@ export type CodeModeSessionOperationResult = {
159
165
  /** Construction capabilities and limits for one CodeMode Session coordinator. */
160
166
  export type CodeModeSessionCoordinatorOptions = {
161
167
  readonly maxSessions: number;
162
- readonly getToolNames: () => readonly string[];
168
+ /** Captures guest-callable names and searchable declarations atomically for each Cell. */
169
+ readonly getToolSnapshot: () => {
170
+ readonly names: readonly string[];
171
+ readonly searchEntries: readonly CodeModeToolSearchEntry[];
172
+ };
163
173
  readonly executeToolBatch: ExecuteCodeModeNestedToolBatch;
164
174
  /** Private Result Spill storage for complete oversized presentation data. */
165
175
  readonly resultSpillWriter: CodeModeResultSpillWriter;
@@ -198,6 +208,8 @@ type ActiveCodeModeCell = {
198
208
  readonly resolveCompletion: (result: CodeModeResult) => void;
199
209
  readonly metadata: CodeModeMetadataAccumulator;
200
210
  readonly nestedTools: CodeModePresentationSnapshot["nested_tools"];
211
+ exposedToolNames: readonly string[];
212
+ searchEntries: readonly CodeModeToolSearchEntry[];
201
213
  activeToolNames: readonly string[];
202
214
  activeToolCount: number;
203
215
  failedNestedToolCount: number;
@@ -739,6 +751,8 @@ export class CodeModeSessionCoordinator {
739
751
  resolveCompletion: completion.resolve,
740
752
  metadata: emptyMetadataAccumulator(),
741
753
  nestedTools: [],
754
+ exposedToolNames: [],
755
+ searchEntries: [],
742
756
  activeToolNames: [],
743
757
  activeToolCount: 0,
744
758
  failedNestedToolCount: 0,
@@ -789,7 +803,13 @@ export class CodeModeSessionCoordinator {
789
803
  }, input.timeoutMs + CODEMODE_WATCHDOG_GRACE_MS);
790
804
  }
791
805
  try {
792
- const toolNames = [...new Set(this.options.getToolNames())];
806
+ const toolSnapshot = this.options.getToolSnapshot();
807
+ const toolNames = [...new Set(toolSnapshot.names)];
808
+ cell.exposedToolNames = toolNames;
809
+ const exposedNames = new Set(toolNames);
810
+ cell.searchEntries = toolSnapshot.searchEntries.filter((entry) =>
811
+ exposedNames.has(entry.name),
812
+ );
793
813
  const requestBase = {
794
814
  version: 1,
795
815
  type: "execute",
@@ -900,9 +920,12 @@ export class CodeModeSessionCoordinator {
900
920
  cell: ActiveCodeModeCell,
901
921
  response: Extract<CodeModeWorkerResponse, { readonly type: "tool-batch" }>,
902
922
  ): Promise<void> {
903
- cell.activeToolNames = [...new Set(response.calls.map((call) => call.toolName))];
904
- cell.activeToolCount = response.calls.length;
905
- cell.nestedToolCount += response.calls.length;
923
+ const piToolCalls = response.calls.filter(
924
+ (call) => call.toolName !== CODEMODE_SEARCH_TOOL_NAME,
925
+ );
926
+ cell.activeToolNames = [...new Set(piToolCalls.map((call) => call.toolName))];
927
+ cell.activeToolCount = piToolCalls.length;
928
+ cell.nestedToolCount += piToolCalls.length;
906
929
  record.lastActivityAtMs = this.runtime.now();
907
930
  this.publishObserverSnapshot();
908
931
 
@@ -927,6 +950,8 @@ export class CodeModeSessionCoordinator {
927
950
  sessionId: record.sessionId,
928
951
  batchId: response.batchId,
929
952
  calls: parsedCalls,
953
+ exposedToolNames: cell.exposedToolNames,
954
+ searchEntries: cell.searchEntries,
930
955
  signal: cell.abortController.signal,
931
956
  } as const;
932
957
  const onUpdate = (_update: CodeModeNestedToolUpdate): void => {
@@ -951,7 +976,7 @@ export class CodeModeSessionCoordinator {
951
976
  };
952
977
  }
953
978
  if (!this.isCurrentCell(record, cell)) return;
954
- this.recordNestedToolPresentation(cell, response.calls, batchResult);
979
+ this.recordNestedToolPresentation(cell, piToolCalls, batchResult);
955
980
  cell.activeToolNames = [];
956
981
  cell.activeToolCount = 0;
957
982
  record.lastActivityAtMs = this.runtime.now();