@ian-pascoe/pi-codemode 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +241 -0
- package/package.json +59 -0
- package/src/codemode-cell-transform.ts +612 -0
- package/src/codemode-deno-launch.ts +59 -0
- package/src/codemode-deno-process.ts +208 -0
- package/src/codemode-observer-ui.ts +517 -0
- package/src/codemode-presentation-output.ts +16 -0
- package/src/codemode-runtime.ts +24 -0
- package/src/codemode-session-coordinator.ts +1297 -0
- package/src/codemode-session-files.ts +80 -0
- package/src/codemode-tool-catalog.ts +350 -0
- package/src/codemode-tool-contract.ts +480 -0
- package/src/codemode-tool-exposure.ts +159 -0
- package/src/codemode-tool-rendering.ts +487 -0
- package/src/codemode-worker-protocol.ts +480 -0
- package/src/codemode-worker.ts +1092 -0
- package/src/index.ts +1 -0
- package/src/pi-agent-session-capture.ts +157 -0
- package/src/pi-codemode-extension.ts +469 -0
- package/src/pi-codemode-settings.ts +168 -0
- package/src/pi-tool-bridge.ts +744 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { resolveCodeModeDenoLaunch } from "./codemode-deno-launch.js";
|
|
5
|
+
import type { CodeModeRuntime, CodeModeTimerHandle } from "./codemode-runtime.js";
|
|
6
|
+
import {
|
|
7
|
+
CODEMODE_WORKER_MESSAGE_LIMIT_BYTES,
|
|
8
|
+
parseCodeModeWorkerResponse,
|
|
9
|
+
serializeCodeModeWorkerRequest,
|
|
10
|
+
type CodeModeWorkerRequest,
|
|
11
|
+
type CodeModeWorkerResponse,
|
|
12
|
+
} from "./codemode-worker-protocol.js";
|
|
13
|
+
|
|
14
|
+
const CODEMODE_PROCESS_START_TIMEOUT_MS = 30_000;
|
|
15
|
+
const CODEMODE_PROCESS_STOP_GRACE_MS = 2_000;
|
|
16
|
+
const CODEMODE_STDERR_LIMIT_BYTES = 64 * 1024;
|
|
17
|
+
|
|
18
|
+
/** Construction options for one Deno-native persistent notebook process. */
|
|
19
|
+
export type CodeModeWorkerProcessOptions = {
|
|
20
|
+
readonly sessionId: string;
|
|
21
|
+
readonly runtime: CodeModeRuntime;
|
|
22
|
+
readonly onResponse: (response: CodeModeWorkerResponse) => void;
|
|
23
|
+
readonly onFailure: (message: string) => void;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** Owns process protocol I/O and deterministic acquisition/release for one CodeMode Session. */
|
|
27
|
+
export class CodeModeWorkerProcess {
|
|
28
|
+
/** Settles only after the worker confirms the expected CodeMode Session ID. */
|
|
29
|
+
readonly ready: Promise<void>;
|
|
30
|
+
private readonly child: ChildProcessWithoutNullStreams;
|
|
31
|
+
private readonly exitPromise: Promise<void>;
|
|
32
|
+
private readonly readyPromise: PromiseWithResolvers<void>;
|
|
33
|
+
private readonly exitPromiseResolvers: PromiseWithResolvers<void>;
|
|
34
|
+
private exitMode: "running" | "graceful" | "forced" = "running";
|
|
35
|
+
private readySettled = false;
|
|
36
|
+
private failed = false;
|
|
37
|
+
private stdoutBuffer = "";
|
|
38
|
+
private stderr = "";
|
|
39
|
+
|
|
40
|
+
/** Starts the pinned Deno process and its startup watchdog. */
|
|
41
|
+
constructor(private readonly options: CodeModeWorkerProcessOptions) {
|
|
42
|
+
this.readyPromise = Promise.withResolvers<void>();
|
|
43
|
+
this.ready = this.readyPromise.promise;
|
|
44
|
+
this.exitPromiseResolvers = Promise.withResolvers<void>();
|
|
45
|
+
this.exitPromise = this.exitPromiseResolvers.promise;
|
|
46
|
+
const workerPath = fileURLToPath(new URL("./codemode-worker.ts", import.meta.url));
|
|
47
|
+
const launch = resolveCodeModeDenoLaunch(workerPath, options.sessionId);
|
|
48
|
+
this.child = spawn(launch.command, launch.args, {
|
|
49
|
+
env: { DENO_NO_UPDATE_CHECK: "1", NO_COLOR: "1" },
|
|
50
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
51
|
+
});
|
|
52
|
+
this.child.stdout.setEncoding("utf8");
|
|
53
|
+
this.child.stderr.setEncoding("utf8");
|
|
54
|
+
this.child.stdout.on("data", (chunk: string) => this.acceptStdout(chunk));
|
|
55
|
+
this.child.stderr.on("data", (chunk: string) => {
|
|
56
|
+
if (Buffer.byteLength(this.stderr, "utf8") < CODEMODE_STDERR_LIMIT_BYTES)
|
|
57
|
+
this.stderr += chunk;
|
|
58
|
+
});
|
|
59
|
+
this.child.on("error", (cause) => {
|
|
60
|
+
const message = `CodeMode Deno process error: ${cause.message}`;
|
|
61
|
+
this.settleReadyFailure(message);
|
|
62
|
+
this.exitPromiseResolvers.resolve();
|
|
63
|
+
this.fail(message);
|
|
64
|
+
});
|
|
65
|
+
this.child.on("exit", (code, signal) => {
|
|
66
|
+
const stderr = this.stderr.trim();
|
|
67
|
+
const outcome = `${signal ?? code ?? "unknown"}${stderr.length === 0 ? "" : `: ${stderr}`}`;
|
|
68
|
+
this.settleReadyFailure(`CodeMode Deno process exited before ready (${outcome})`);
|
|
69
|
+
if (this.exitMode === "graceful" && code !== 0) {
|
|
70
|
+
this.exitPromiseResolvers.reject(
|
|
71
|
+
new Error(`Pi CodeMode: Deno process failed graceful cleanup (${outcome})`),
|
|
72
|
+
);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
this.exitPromiseResolvers.resolve();
|
|
76
|
+
if (this.exitMode === "running") this.fail(`CodeMode Deno process exited (${outcome})`);
|
|
77
|
+
});
|
|
78
|
+
const startTimeout = options.runtime.setTimeout(() => {
|
|
79
|
+
this.fail("CodeMode Deno process did not become ready");
|
|
80
|
+
this.child.kill();
|
|
81
|
+
}, CODEMODE_PROCESS_START_TIMEOUT_MS);
|
|
82
|
+
this.ready.then(
|
|
83
|
+
() => options.runtime.clearTimeout(startTimeout),
|
|
84
|
+
() => options.runtime.clearTimeout(startTimeout),
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Writes one bounded request to the worker process. */
|
|
89
|
+
send(
|
|
90
|
+
request: CodeModeWorkerRequest,
|
|
91
|
+
): { readonly ok: true } | { readonly ok: false; readonly message: string } {
|
|
92
|
+
const serialized = serializeCodeModeWorkerRequest(request);
|
|
93
|
+
if (!serialized.ok) return serialized;
|
|
94
|
+
if (this.child.stdin.destroyed) {
|
|
95
|
+
return { ok: false, message: "CodeMode Deno process input is unavailable" };
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
this.child.stdin.write(`${serialized.value}\n`);
|
|
99
|
+
return { ok: true };
|
|
100
|
+
} catch {
|
|
101
|
+
return { ok: false, message: "CodeMode Deno process input is unavailable" };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Force-stops the process and settles startup even when no ready message arrived. */
|
|
106
|
+
async terminate(): Promise<void> {
|
|
107
|
+
if (this.exitMode !== "running") return this.exitPromise;
|
|
108
|
+
this.exitMode = "forced";
|
|
109
|
+
this.settleReadyFailure("CodeMode Deno process was terminated before ready");
|
|
110
|
+
this.child.kill();
|
|
111
|
+
await this.exitPromise;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Requests graceful idle shutdown, then force-stops after the fixed grace period. */
|
|
115
|
+
async shutdown(): Promise<void> {
|
|
116
|
+
if (this.exitMode !== "running") return this.exitPromise;
|
|
117
|
+
this.exitMode = "graceful";
|
|
118
|
+
const sent = this.send({
|
|
119
|
+
version: 1,
|
|
120
|
+
type: "shutdown",
|
|
121
|
+
sessionId: this.options.sessionId,
|
|
122
|
+
});
|
|
123
|
+
this.child.stdin.end();
|
|
124
|
+
if (!sent.ok) this.child.kill();
|
|
125
|
+
let stopTimer: CodeModeTimerHandle | undefined;
|
|
126
|
+
try {
|
|
127
|
+
await Promise.race([
|
|
128
|
+
this.exitPromise,
|
|
129
|
+
new Promise<void>((resolvePromise) => {
|
|
130
|
+
stopTimer = this.options.runtime.setTimeout(() => {
|
|
131
|
+
this.settleReadyFailure("CodeMode Deno process was stopped before ready");
|
|
132
|
+
this.child.kill();
|
|
133
|
+
resolvePromise();
|
|
134
|
+
}, CODEMODE_PROCESS_STOP_GRACE_MS);
|
|
135
|
+
}),
|
|
136
|
+
]);
|
|
137
|
+
await this.exitPromise;
|
|
138
|
+
} finally {
|
|
139
|
+
if (stopTimer !== undefined) this.options.runtime.clearTimeout(stopTimer);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private acceptStdout(chunk: string): void {
|
|
144
|
+
this.stdoutBuffer += chunk;
|
|
145
|
+
if (Buffer.byteLength(this.stdoutBuffer, "utf8") > CODEMODE_WORKER_MESSAGE_LIMIT_BYTES + 1) {
|
|
146
|
+
this.fail("CodeMode worker response exceeds 8 MiB");
|
|
147
|
+
void this.terminate();
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
let newlineIndex = this.stdoutBuffer.indexOf("\n");
|
|
151
|
+
while (newlineIndex !== -1) {
|
|
152
|
+
const line = this.stdoutBuffer.slice(0, newlineIndex);
|
|
153
|
+
this.stdoutBuffer = this.stdoutBuffer.slice(newlineIndex + 1);
|
|
154
|
+
if (line.length > 0) this.acceptLine(line);
|
|
155
|
+
newlineIndex = this.stdoutBuffer.indexOf("\n");
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private acceptLine(line: string): void {
|
|
160
|
+
const parsed = parseCodeModeWorkerResponse(line);
|
|
161
|
+
if (!parsed.ok) {
|
|
162
|
+
this.fail(parsed.message);
|
|
163
|
+
void this.terminate();
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (parsed.value.sessionId !== this.options.sessionId) {
|
|
167
|
+
this.fail("CodeMode worker response has a stale Session ID");
|
|
168
|
+
void this.terminate();
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (parsed.value.type === "ready") {
|
|
172
|
+
this.settleReadySuccess();
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (parsed.value.type === "protocol-error") {
|
|
176
|
+
this.fail(parsed.value.message);
|
|
177
|
+
void this.terminate();
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
this.options.onResponse(parsed.value);
|
|
182
|
+
} catch (cause) {
|
|
183
|
+
const message =
|
|
184
|
+
cause instanceof Error ? cause.message : "CodeMode worker response handling failed";
|
|
185
|
+
this.fail(message);
|
|
186
|
+
void this.terminate();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private settleReadySuccess(): void {
|
|
191
|
+
if (this.readySettled) return;
|
|
192
|
+
this.readySettled = true;
|
|
193
|
+
this.readyPromise.resolve();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private settleReadyFailure(message: string): void {
|
|
197
|
+
if (this.readySettled) return;
|
|
198
|
+
this.readySettled = true;
|
|
199
|
+
this.readyPromise.reject(new Error(`Pi CodeMode: ${message}`));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private fail(message: string): void {
|
|
203
|
+
if (this.failed || this.exitMode !== "running") return;
|
|
204
|
+
this.failed = true;
|
|
205
|
+
this.settleReadyFailure(message);
|
|
206
|
+
this.options.onFailure(message);
|
|
207
|
+
}
|
|
208
|
+
}
|