@ian-pascoe/pi-codemode 0.1.0 → 0.3.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 +96 -24
- package/package.json +6 -6
- package/src/codemode-console-output.ts +11 -0
- package/src/codemode-observer-ui.ts +19 -38
- package/src/codemode-session-coordinator.ts +313 -158
- package/src/codemode-tool-catalog.ts +4 -6
- package/src/codemode-tool-contract.ts +108 -14
- package/src/codemode-tool-rendering.ts +138 -39
- package/src/codemode-worker-protocol.ts +141 -33
- package/src/codemode-worker.ts +283 -18
- package/src/pi-codemode-extension.ts +31 -28
- package/src/pi-tool-bridge.ts +5 -38
package/README.md
CHANGED
|
@@ -62,11 +62,44 @@ codemode_cancel({ sessionId: string });
|
|
|
62
62
|
Stops the session process and frees its capacity. The cancel call succeeds;
|
|
63
63
|
subsequent polling returns the retained `cancellation` failure.
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
### `codemode_sessions`
|
|
66
66
|
|
|
67
67
|
```ts
|
|
68
|
+
codemode_sessions({});
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Lists every live Session, with idle Sessions first in least-recently-used order,
|
|
72
|
+
then running Sessions in least-recently-used order. Listing does not refresh
|
|
73
|
+
Session recency.
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
type CodeModeSessionsResult = {
|
|
77
|
+
result: "success";
|
|
78
|
+
sessions: Array<{
|
|
79
|
+
sessionId: string;
|
|
80
|
+
state: "idle" | "running";
|
|
81
|
+
cellCount: number;
|
|
82
|
+
lastActivityAtMs: number;
|
|
83
|
+
}>;
|
|
84
|
+
};
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The execute, result, and cancel tools return:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
type CodeModeConsoleEntry = {
|
|
91
|
+
method: "log" | "info" | "warn" | "error" | "debug";
|
|
92
|
+
text: string;
|
|
93
|
+
};
|
|
94
|
+
|
|
68
95
|
type CodeModeResult =
|
|
69
|
-
| {
|
|
96
|
+
| {
|
|
97
|
+
result: "success";
|
|
98
|
+
sessionId: string;
|
|
99
|
+
data?: JsonValue;
|
|
100
|
+
reclaimedSessionId?: string;
|
|
101
|
+
console?: CodeModeConsoleEntry[];
|
|
102
|
+
}
|
|
70
103
|
| { result: "pending"; sessionId: string }
|
|
71
104
|
| {
|
|
72
105
|
result: "failed";
|
|
@@ -76,6 +109,7 @@ type CodeModeResult =
|
|
|
76
109
|
| "unknown"
|
|
77
110
|
| "busy"
|
|
78
111
|
| "capacity"
|
|
112
|
+
| "eviction"
|
|
79
113
|
| "script"
|
|
80
114
|
| "serialization"
|
|
81
115
|
| "timeout"
|
|
@@ -84,6 +118,7 @@ type CodeModeResult =
|
|
|
84
118
|
| "runtime";
|
|
85
119
|
message: string;
|
|
86
120
|
};
|
|
121
|
+
console?: CodeModeConsoleEntry[];
|
|
87
122
|
};
|
|
88
123
|
```
|
|
89
124
|
|
|
@@ -91,29 +126,57 @@ type CodeModeResult =
|
|
|
91
126
|
result text returned to the model. Pi retains additional bounded Presentation
|
|
92
127
|
Snapshots in tool-result details for Transcript replay and the TUI.
|
|
93
128
|
|
|
129
|
+
Cells may call `console.log`, `console.info`, `console.warn`, `console.error`,
|
|
130
|
+
and `console.debug`. One call creates one ordered entry without a trailing
|
|
131
|
+
newline, while embedded newlines remain intact:
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
console.log("answer:", 42);
|
|
135
|
+
return 42;
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
```json
|
|
139
|
+
{
|
|
140
|
+
"result": "success",
|
|
141
|
+
"sessionId": "...",
|
|
142
|
+
"data": 42,
|
|
143
|
+
"console": [{ "method": "log", "text": "answer: 42" }]
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Formatting matches the pinned Deno Console for format tokens, primitives,
|
|
148
|
+
spacing, and multiline inspection. Getters, coercion hooks, and custom
|
|
149
|
+
inspectors do not run; CodeMode uses safe inspection instead when that differs
|
|
150
|
+
from Deno. Console output arrives only with terminal results. Ordinary script,
|
|
151
|
+
serialization, and worker-reported runtime failures retain prior calls. A
|
|
152
|
+
timeout, cancellation, termination, or process death may omit them because the
|
|
153
|
+
parent kills the worker before it can reply.
|
|
154
|
+
|
|
94
155
|
## Transcript and Observer UI
|
|
95
156
|
|
|
96
|
-
The CodeMode Transcript gives all
|
|
157
|
+
The CodeMode Transcript gives all four tools semantic collapsed and expanded
|
|
97
158
|
rendering. Collapsed rows prioritize Cell lifecycle, a short Session ID, Cell
|
|
98
|
-
Ordinal, returned-value shape, nested-tool count, and elapsed
|
|
99
|
-
rows show the full Session ID, explicit call arguments,
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
presentation.
|
|
159
|
+
Ordinal, returned-value shape, Console-call count, nested-tool count, and elapsed
|
|
160
|
+
time. Expanded rows show the full Session ID, explicit call arguments,
|
|
161
|
+
TypeScript source, bounded Console output before structured returned data or the
|
|
162
|
+
error, and bounded nested-tool names, outcomes, and durations. Nested arguments
|
|
163
|
+
and raw nested outputs are never copied into the presentation.
|
|
103
164
|
|
|
104
165
|
Status always uses a symbol and text together:
|
|
105
166
|
|
|
106
167
|
```text
|
|
107
168
|
◉ running ○ idle ✓ completed
|
|
108
|
-
× failed ■ cancelled ! timed out
|
|
169
|
+
× failed ■ cancelled ■ reclaimed ! timed out
|
|
109
170
|
```
|
|
110
171
|
|
|
111
172
|
Awaited Cells publish a presentation update immediately and once per second.
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
173
|
+
Collapsed calls show one highlighted line inline or the first eight highlighted
|
|
174
|
+
lines of a multi-line Cell. Expanded calls show the complete TypeScript source.
|
|
175
|
+
Returned-data display uses Pi's 2,000-line/50-KB limit; complete oversized data
|
|
176
|
+
is written to a private Result Spill while the model-facing result remains
|
|
177
|
+
unchanged. Result Spill files last for the live Pi session. Replayed history
|
|
178
|
+
falls back to its retained bounded data when a prior spill is no longer
|
|
179
|
+
available.
|
|
117
180
|
|
|
118
181
|
In TUI mode, the read-only CodeMode Observer UI appears above the editor during
|
|
119
182
|
Cell activity. It shows up to eight running, idle, or recently terminal
|
|
@@ -204,12 +267,18 @@ last match wins. Project `tools` replaces the global array, while project
|
|
|
204
267
|
|
|
205
268
|
An unmatched active tool defaults to `direct-and-codemode`; an unmatched
|
|
206
269
|
inactive tool remains unavailable. An explicit rule may expose an inactive tool
|
|
207
|
-
or activate direct access. The
|
|
270
|
+
or activate direct access. The four `codemode_*` tools are always direct-only.
|
|
208
271
|
Pi's global allowed/excluded registry remains authoritative. Invalid fields or
|
|
209
272
|
patterns disable CodeMode for that session without changing Pi's active tools.
|
|
210
273
|
|
|
211
|
-
`maxSessions` defaults to 8 and counts only live Deno processes.
|
|
212
|
-
|
|
274
|
+
`maxSessions` defaults to 8 and counts only live Deno processes. When capacity
|
|
275
|
+
is full, a new Session gracefully stops the least-recently-used idle Session
|
|
276
|
+
before starting; its Notebook Bindings are discarded, the new success reports
|
|
277
|
+
`reclaimedSessionId`, and polling the old ID returns `eviction`. Running Sessions
|
|
278
|
+
are never reclaimed, so admission still returns `capacity` when every process is
|
|
279
|
+
busy. Executing or polling refreshes recency; listing and Observer rendering do
|
|
280
|
+
not. Up to 64 recent worker-free terminal or failed-admission records remain
|
|
281
|
+
pollable.
|
|
213
282
|
|
|
214
283
|
## Isolation and limits
|
|
215
284
|
|
|
@@ -219,13 +288,16 @@ generated helper source out of ordinary source locations. Every operating-system
|
|
|
219
288
|
permission class is denied: filesystem read/write, network, environment, system
|
|
220
289
|
information, subprocesses, FFI, and remote imports.
|
|
221
290
|
|
|
222
|
-
Guest code receives ECMAScript built-ins, a read-only `tools` object,
|
|
223
|
-
frozen `Deno.version` identity. Raw
|
|
224
|
-
|
|
225
|
-
withheld.
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
291
|
+
Guest code receives ECMAScript built-ins, a read-only `tools` object, a frozen
|
|
292
|
+
five-method Console facade, and only a frozen `Deno.version` identity. Raw
|
|
293
|
+
process and standard-stream access, `Worker`, timers, filesystem/network APIs,
|
|
294
|
+
and module loading are withheld. Console calls never write to worker streams and
|
|
295
|
+
do not include output from registered Pi tool handlers. The parent watchdog
|
|
296
|
+
terminates the subprocess for timeout or an infinite loop. Deno/V8 bounds each
|
|
297
|
+
Session to a 128 MiB old-space heap and a 1 MiB stack. Protocol inputs, tool
|
|
298
|
+
results, returned data, and Console entries share one 8 MiB UTF-8 worker-message
|
|
299
|
+
limit. An oversized response becomes the bounded `serialization` failure and
|
|
300
|
+
may omit its Console entries.
|
|
229
301
|
|
|
230
302
|
Registered Pi tools still execute in Pi's parent process with their normal
|
|
231
303
|
permissions and lifecycle hooks. Cancellation aborts them through Pi's
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ian-pascoe/pi-codemode",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Process-isolated persistent TypeScript tool composition for Pi",
|
|
6
6
|
"keywords": [
|
|
@@ -32,10 +32,6 @@
|
|
|
32
32
|
"access": "public",
|
|
33
33
|
"provenance": true
|
|
34
34
|
},
|
|
35
|
-
"scripts": {
|
|
36
|
-
"test": "vitest run --config ../../vitest.config.ts --root .",
|
|
37
|
-
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
38
|
-
},
|
|
39
35
|
"dependencies": {
|
|
40
36
|
"deno": "2.9.5",
|
|
41
37
|
"minimatch": "^10.2.5",
|
|
@@ -55,5 +51,9 @@
|
|
|
55
51
|
"extensions": [
|
|
56
52
|
"./src/index.ts"
|
|
57
53
|
]
|
|
54
|
+
},
|
|
55
|
+
"scripts": {
|
|
56
|
+
"test": "vitest run --config ../../vitest.config.ts --root .",
|
|
57
|
+
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
58
58
|
}
|
|
59
|
-
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Console methods captured from a CodeMode Cell in call order. */
|
|
2
|
+
export const CODEMODE_CONSOLE_METHODS = ["log", "info", "warn", "error", "debug"] as const;
|
|
3
|
+
|
|
4
|
+
/** One supported CodeMode Cell Console method. */
|
|
5
|
+
export type CodeModeConsoleMethod = (typeof CODEMODE_CONSOLE_METHODS)[number];
|
|
6
|
+
|
|
7
|
+
/** One captured CodeMode Cell Console call without a synthetic trailing newline. */
|
|
8
|
+
export type CodeModeConsoleEntry = {
|
|
9
|
+
readonly method: CodeModeConsoleMethod;
|
|
10
|
+
readonly text: string;
|
|
11
|
+
};
|
|
@@ -12,6 +12,11 @@ import type {
|
|
|
12
12
|
CodeModeObserverSnapshot,
|
|
13
13
|
CodeModeUnexpectedFailure,
|
|
14
14
|
} from "./codemode-session-coordinator.js";
|
|
15
|
+
import {
|
|
16
|
+
boundedCodeModeElapsedMs,
|
|
17
|
+
formatCodeModeDuration,
|
|
18
|
+
shortestUniqueCodeModeSessionPrefix,
|
|
19
|
+
} from "./codemode-session-coordinator.js";
|
|
15
20
|
|
|
16
21
|
const CODEMODE_OBSERVER_UI_KEY = "codemode-observer";
|
|
17
22
|
const CODEMODE_OBSERVER_ROW_LIMIT = 8;
|
|
@@ -72,7 +77,7 @@ export type CodeModeObserverRow = {
|
|
|
72
77
|
/** Idle Session summary retained only during relevant Observer activity. */
|
|
73
78
|
| { readonly state: "idle"; readonly cellCount: number }
|
|
74
79
|
| {
|
|
75
|
-
readonly state: "failed" | "cancelled" | "timed_out";
|
|
80
|
+
readonly state: "failed" | "cancelled" | "reclaimed" | "timed_out";
|
|
76
81
|
/** Most recent one-based Cell Ordinal, or Session Cell count when no final Cell exists. */
|
|
77
82
|
readonly cellOrdinal: number;
|
|
78
83
|
}
|
|
@@ -102,6 +107,7 @@ const CODEMODE_OBSERVER_STATE_PRESENTATION = {
|
|
|
102
107
|
idle: { symbol: "○", label: "idle", color: "muted" },
|
|
103
108
|
failed: { symbol: "×", label: "failed", color: "error" },
|
|
104
109
|
cancelled: { symbol: "■", label: "cancelled", color: "warning" },
|
|
110
|
+
reclaimed: { symbol: "■", label: "reclaimed", color: "warning" },
|
|
105
111
|
timed_out: { symbol: "!", label: "timed out", color: "error" },
|
|
106
112
|
} satisfies Record<CodeModeObserverState, CodeModeObserverStatePresentation>;
|
|
107
113
|
|
|
@@ -130,33 +136,21 @@ function shortestUniqueCodeModeSessionPrefixes(
|
|
|
130
136
|
const safe = sanitizeCodeModeObserverText(session.sessionId);
|
|
131
137
|
return safe.length === 0 ? "unknown" : safe;
|
|
132
138
|
});
|
|
133
|
-
return identifiers.map((identifier, index) =>
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
candidateIndex !== index && candidate.startsWith(identifier.slice(0, length)),
|
|
140
|
-
)
|
|
141
|
-
) {
|
|
142
|
-
length += 1;
|
|
143
|
-
}
|
|
144
|
-
return identifier.slice(0, length);
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function boundedElapsedMilliseconds(nowMs: number, startedAtMs: number): number {
|
|
149
|
-
const elapsed = Math.round(nowMs - startedAtMs);
|
|
150
|
-
if (!Number.isFinite(elapsed)) return 0;
|
|
151
|
-
return Math.min(Number.MAX_SAFE_INTEGER, Math.max(0, elapsed));
|
|
139
|
+
return identifiers.map((identifier, index) =>
|
|
140
|
+
shortestUniqueCodeModeSessionPrefix(
|
|
141
|
+
identifier,
|
|
142
|
+
identifiers.filter((_candidate, candidateIndex) => candidateIndex !== index),
|
|
143
|
+
),
|
|
144
|
+
);
|
|
152
145
|
}
|
|
153
146
|
|
|
154
147
|
function codeModeTerminalObserverState(
|
|
155
148
|
session: CodeModeObservedSession,
|
|
156
|
-
): Extract<CodeModeObserverState, "failed" | "cancelled" | "timed_out"> {
|
|
149
|
+
): Extract<CodeModeObserverState, "failed" | "cancelled" | "reclaimed" | "timed_out"> {
|
|
157
150
|
if (session.terminal_error_code === "cancellation" || session.last_cell?.state === "cancelled") {
|
|
158
151
|
return "cancelled";
|
|
159
152
|
}
|
|
153
|
+
if (session.terminal_error_code === "eviction") return "reclaimed";
|
|
160
154
|
if (session.terminal_error_code === "timeout" || session.last_cell?.state === "timed_out") {
|
|
161
155
|
return "timed_out";
|
|
162
156
|
}
|
|
@@ -180,7 +174,7 @@ function projectCodeModeObserverRow(
|
|
|
180
174
|
sessionPrefix,
|
|
181
175
|
state: "running",
|
|
182
176
|
cellOrdinal: session.current_cell.ordinal,
|
|
183
|
-
elapsedMs:
|
|
177
|
+
elapsedMs: boundedCodeModeElapsedMs(session.current_cell.started_at_ms, nowMs),
|
|
184
178
|
activeToolNames,
|
|
185
179
|
activeToolCount: Math.max(session.current_cell.active_tool_count, activeToolNames.length),
|
|
186
180
|
};
|
|
@@ -230,13 +224,6 @@ export function buildCodeModeObserverView(
|
|
|
230
224
|
};
|
|
231
225
|
}
|
|
232
226
|
|
|
233
|
-
function formatCodeModeObserverDuration(elapsedMs: number): string {
|
|
234
|
-
if (elapsedMs < 1_000) return `${elapsedMs}ms`;
|
|
235
|
-
if (elapsedMs < 60_000) return `${(elapsedMs / 1_000).toFixed(1)}s`;
|
|
236
|
-
const seconds = Math.floor(elapsedMs / 1_000);
|
|
237
|
-
return `${Math.floor(seconds / 60)}m ${String(seconds % 60).padStart(2, "0")}s`;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
227
|
function formatCodeModeObserverToolActivity(
|
|
241
228
|
row: Extract<CodeModeObserverRow, { state: "running" }>,
|
|
242
229
|
): string | undefined {
|
|
@@ -254,10 +241,6 @@ function codeModeObserverRowDetail(row: CodeModeObserverRow): string {
|
|
|
254
241
|
: `Cell ${row.cellOrdinal}`;
|
|
255
242
|
}
|
|
256
243
|
|
|
257
|
-
function joinCodeModeObserverRow(parts: readonly string[], separator: string): string {
|
|
258
|
-
return parts.join(separator);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
244
|
function renderCodeModeObserverRow(
|
|
262
245
|
row: CodeModeObserverRow,
|
|
263
246
|
width: number,
|
|
@@ -268,9 +251,7 @@ function renderCodeModeObserverRow(
|
|
|
268
251
|
const status = theme.fg(presentation.color, presentation.label);
|
|
269
252
|
const detail = theme.fg("muted", codeModeObserverRowDetail(row));
|
|
270
253
|
const duration =
|
|
271
|
-
row.state === "running"
|
|
272
|
-
? theme.fg("muted", formatCodeModeObserverDuration(row.elapsedMs))
|
|
273
|
-
: undefined;
|
|
254
|
+
row.state === "running" ? theme.fg("muted", formatCodeModeDuration(row.elapsedMs)) : undefined;
|
|
274
255
|
const toolActivity =
|
|
275
256
|
row.state === "running" ? formatCodeModeObserverToolActivity(row) : undefined;
|
|
276
257
|
const themedToolActivity =
|
|
@@ -283,13 +264,13 @@ function renderCodeModeObserverRow(
|
|
|
283
264
|
[identity, status],
|
|
284
265
|
].map((parts) => parts.filter((part): part is string => part !== undefined));
|
|
285
266
|
for (const parts of candidates) {
|
|
286
|
-
const line =
|
|
267
|
+
const line = parts.join(separator);
|
|
287
268
|
if (visibleWidth(line) <= width) return line;
|
|
288
269
|
}
|
|
289
270
|
|
|
290
271
|
const compactSeparator = theme.fg("dim", CODEMODE_OBSERVER_COMPACT_SEPARATOR_TEXT);
|
|
291
272
|
const compactIdentity = `${theme.fg(presentation.color, presentation.symbol)} ${theme.bold(row.sessionPrefix)}`;
|
|
292
|
-
const compact =
|
|
273
|
+
const compact = [compactIdentity, status].join(compactSeparator);
|
|
293
274
|
if (visibleWidth(compact) <= width) return compact;
|
|
294
275
|
return truncateToWidth(compact, width, CODEMODE_OBSERVER_ELLIPSIS);
|
|
295
276
|
}
|