@ycodium-ai/agent-pi 0.0.0-stage → 0.2.3576
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 +99 -2
- package/assets/icon.svg +1 -0
- package/assets/ui.js +66 -0
- package/dist/index.js +1388 -0
- package/locales/en.json +45 -0
- package/locales/zh-CN.json +42 -0
- package/package.json +76 -3
package/dist/index.js
ADDED
|
@@ -0,0 +1,1388 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { definePlugin } from "@ycodium-ai/plugin-api";
|
|
3
|
+
|
|
4
|
+
// src/config.ts
|
|
5
|
+
import * as NodeCrypto from "node:crypto";
|
|
6
|
+
import * as NodeOS from "node:os";
|
|
7
|
+
import * as NodePath from "node:path";
|
|
8
|
+
|
|
9
|
+
// src/protocol.ts
|
|
10
|
+
var PI_API_PROTOCOLS = [
|
|
11
|
+
"openai-completions",
|
|
12
|
+
"openai-responses",
|
|
13
|
+
"anthropic-messages"
|
|
14
|
+
];
|
|
15
|
+
var DEFAULT_PI_API_PROTOCOL = "openai-completions";
|
|
16
|
+
function isPiApiProtocol(value) {
|
|
17
|
+
return PI_API_PROTOCOLS.some((protocol) => protocol === value);
|
|
18
|
+
}
|
|
19
|
+
function readPiApiProtocol(value) {
|
|
20
|
+
if (value === void 0) return DEFAULT_PI_API_PROTOCOL;
|
|
21
|
+
if (isPiApiProtocol(value)) return value;
|
|
22
|
+
throw new Error("Unsupported Pi API protocol.");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// src/config.ts
|
|
26
|
+
var PI_API_KEY_ENV = "YCODIUM_PI_API_KEY";
|
|
27
|
+
var record = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
|
|
28
|
+
var string = (value) => typeof value === "string" ? value : "";
|
|
29
|
+
function parseConfig(value, instanceId) {
|
|
30
|
+
const envelope = record(value);
|
|
31
|
+
const config = record(envelope.config ?? value);
|
|
32
|
+
const environment = Object.fromEntries(
|
|
33
|
+
(Array.isArray(envelope.environment) ? envelope.environment : []).map((row) => {
|
|
34
|
+
const entry = record(row);
|
|
35
|
+
return [string(entry.name), string(entry.value)];
|
|
36
|
+
}).filter(([name]) => name)
|
|
37
|
+
);
|
|
38
|
+
const configuredHome = string(config.homePath).trim();
|
|
39
|
+
const homePath = configuredHome.startsWith("~/") || configuredHome.startsWith("~\\") ? NodePath.join(NodeOS.homedir(), configuredHome.slice(2)) : configuredHome;
|
|
40
|
+
return {
|
|
41
|
+
binaryPath: string(config.binaryPath).trim() || "pi",
|
|
42
|
+
apiBaseUrl: string(config.apiBaseUrl).trim() || environment.YCODIUM_PI_API_BASE_URL?.trim() || "",
|
|
43
|
+
apiProtocol: readPiApiProtocol(config.apiProtocol),
|
|
44
|
+
model: string(config.model).trim(),
|
|
45
|
+
environment,
|
|
46
|
+
homePath: homePath ? NodePath.resolve(homePath) : NodePath.join(NodeOS.homedir(), ".pi", "ycodium", identifier(instanceId))
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
var identifier = (value) => NodeCrypto.createHash("sha256").update(value).digest("hex").slice(0, 32);
|
|
50
|
+
function nativeApiBaseUrl(baseUrl, apiProtocol) {
|
|
51
|
+
const url = new URL(baseUrl.trim());
|
|
52
|
+
url.pathname = url.pathname.replace(/\/+$/, "");
|
|
53
|
+
if (apiProtocol === "anthropic-messages") url.pathname = url.pathname.replace(/\/v1$/, "");
|
|
54
|
+
return url.toString().replace(/\/$/, "");
|
|
55
|
+
}
|
|
56
|
+
function validateConnection(config) {
|
|
57
|
+
let url;
|
|
58
|
+
try {
|
|
59
|
+
url = new URL(config.apiBaseUrl);
|
|
60
|
+
} catch {
|
|
61
|
+
throw new Error("Pi API base URL is required and must be an HTTP(S) URL.");
|
|
62
|
+
}
|
|
63
|
+
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.search || url.hash)
|
|
64
|
+
throw new Error("Pi API base URL must be HTTP(S), without credentials, query or fragment.");
|
|
65
|
+
if (!config.model) throw new Error("Pi model ID is required.");
|
|
66
|
+
if (!config.environment[PI_API_KEY_ENV]?.trim()) throw new Error("Pi API key is required.");
|
|
67
|
+
}
|
|
68
|
+
function redact(config, value) {
|
|
69
|
+
let text = value instanceof Error ? value.message : String(value);
|
|
70
|
+
for (const secret of Object.values(config.environment).filter(Boolean).sort((a, b) => b.length - a.length))
|
|
71
|
+
text = text.replaceAll(secret, "[redacted]");
|
|
72
|
+
return text;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// src/card.ts
|
|
76
|
+
var PI_CARD = {
|
|
77
|
+
driverKind: "pi",
|
|
78
|
+
displayName: "Pi",
|
|
79
|
+
iconUrl: "./assets/icon.svg",
|
|
80
|
+
commandName: "pi",
|
|
81
|
+
launchArgs: "",
|
|
82
|
+
homeEnvVar: "",
|
|
83
|
+
environment: [],
|
|
84
|
+
install: { kind: "npm" },
|
|
85
|
+
signIn: [],
|
|
86
|
+
deviceCodeSignInArgs: [],
|
|
87
|
+
npmPackage: "@earendil-works/pi-coding-agent",
|
|
88
|
+
homebrewFormula: "",
|
|
89
|
+
selfUpdateArgs: [],
|
|
90
|
+
docsUrl: "https://pi.dev",
|
|
91
|
+
setupNote: "setupNote",
|
|
92
|
+
defaultModel: "",
|
|
93
|
+
customModelIdExample: "",
|
|
94
|
+
modelSlugAliases: null,
|
|
95
|
+
mcpTransports: ["stdio", "http"],
|
|
96
|
+
pluginManaged: true,
|
|
97
|
+
verifiedSkills: false,
|
|
98
|
+
everydayHarness: false,
|
|
99
|
+
capabilities: {
|
|
100
|
+
sessionModelSwitch: "unsupported",
|
|
101
|
+
runtimeModeSwitch: "next-turn",
|
|
102
|
+
contextCompaction: "native",
|
|
103
|
+
turnSteering: true
|
|
104
|
+
},
|
|
105
|
+
credentialSources: [],
|
|
106
|
+
diagnostics: [],
|
|
107
|
+
configSchema: {
|
|
108
|
+
type: "object",
|
|
109
|
+
additionalProperties: true,
|
|
110
|
+
"x-ycodium-form-order": ["apiProtocol", "apiBaseUrl", "model", "binaryPath", "homePath"],
|
|
111
|
+
properties: {
|
|
112
|
+
apiProtocol: {
|
|
113
|
+
type: "string",
|
|
114
|
+
enum: PI_API_PROTOCOLS,
|
|
115
|
+
default: DEFAULT_PI_API_PROTOCOL,
|
|
116
|
+
"x-ycodium-form": {
|
|
117
|
+
titleKey: "form.apiProtocol.title",
|
|
118
|
+
descriptionKey: "form.apiProtocol.description"
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
...Object.fromEntries(
|
|
122
|
+
["apiBaseUrl", "model", "binaryPath", "homePath"].map((key) => [
|
|
123
|
+
key,
|
|
124
|
+
{
|
|
125
|
+
type: "string",
|
|
126
|
+
"x-ycodium-form": {
|
|
127
|
+
titleKey: `form.${key}.title`,
|
|
128
|
+
descriptionKey: `form.${key}.description`,
|
|
129
|
+
...key === "binaryPath" ? { control: "executable-path" } : {}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
])
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
modelConnectivity: {
|
|
137
|
+
dialects: ["openai-chat-completions", "openai-responses", "anthropic-messages"],
|
|
138
|
+
acceptsExternalEndpoints: true,
|
|
139
|
+
baseUrlEnvKey: "YCODIUM_PI_API_BASE_URL",
|
|
140
|
+
credentialEnvKey: PI_API_KEY_ENV
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// src/instance.ts
|
|
145
|
+
import * as NodeCrypto5 from "node:crypto";
|
|
146
|
+
import {
|
|
147
|
+
describeExecutorSpawnFailure,
|
|
148
|
+
ExecutorSpawnAbortedError,
|
|
149
|
+
ExecutorSpawnRefusedError,
|
|
150
|
+
isExecutorSpawnFailure
|
|
151
|
+
} from "@ycodium-ai/plugin-api/executor";
|
|
152
|
+
import { runProcessOperation } from "@ycodium-ai/plugin-api/process-operation";
|
|
153
|
+
|
|
154
|
+
// src/connection.ts
|
|
155
|
+
import * as NodeFSP from "node:fs/promises";
|
|
156
|
+
import * as NodePath2 from "node:path";
|
|
157
|
+
import * as NodeCrypto2 from "node:crypto";
|
|
158
|
+
import {
|
|
159
|
+
isOperationCleanupError,
|
|
160
|
+
withProcessOperation
|
|
161
|
+
} from "@ycodium-ai/plugin-api/process-operation";
|
|
162
|
+
|
|
163
|
+
// src/rpc.ts
|
|
164
|
+
var PiResponseError = class extends Error {
|
|
165
|
+
};
|
|
166
|
+
var PiRpc = class {
|
|
167
|
+
pending = /* @__PURE__ */ new Map();
|
|
168
|
+
nextId = 0;
|
|
169
|
+
failure;
|
|
170
|
+
failureResult = Promise.withResolvers();
|
|
171
|
+
failed = this.failureResult.promise;
|
|
172
|
+
drained;
|
|
173
|
+
process;
|
|
174
|
+
constructor(process, onEvent) {
|
|
175
|
+
this.process = process;
|
|
176
|
+
const read = async () => {
|
|
177
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
178
|
+
let fragments = [];
|
|
179
|
+
let length = 0;
|
|
180
|
+
const consume = async (text) => {
|
|
181
|
+
let start = 0;
|
|
182
|
+
for (let end = text.indexOf("\n"); end !== -1; end = text.indexOf("\n", start)) {
|
|
183
|
+
fragments.push(text.slice(start, end));
|
|
184
|
+
length += end - start;
|
|
185
|
+
if (length > 16 * 1024 * 1024) throw new Error("Pi protocol record is too large.");
|
|
186
|
+
const line = fragments.join("");
|
|
187
|
+
fragments = [];
|
|
188
|
+
length = 0;
|
|
189
|
+
start = end + 1;
|
|
190
|
+
if (!line.trim()) continue;
|
|
191
|
+
const event = record(JSON.parse(line));
|
|
192
|
+
if (!string(event.type)) throw new Error("Invalid Pi protocol record.");
|
|
193
|
+
if (event.type === "response") {
|
|
194
|
+
const id = string(event.id), pending = this.pending.get(id);
|
|
195
|
+
if (!pending) continue;
|
|
196
|
+
this.pending.delete(id);
|
|
197
|
+
if (event.success === true) pending.resolve(event.data);
|
|
198
|
+
else
|
|
199
|
+
pending.reject(
|
|
200
|
+
new PiResponseError(string(event.error) || "Pi rejected the command.")
|
|
201
|
+
);
|
|
202
|
+
} else await onEvent(event);
|
|
203
|
+
}
|
|
204
|
+
if (start < text.length) {
|
|
205
|
+
fragments.push(text.slice(start));
|
|
206
|
+
length += text.length - start;
|
|
207
|
+
if (length > 16 * 1024 * 1024) throw new Error("Pi protocol record is too large.");
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
for await (const chunk of process.stdout)
|
|
211
|
+
await consume(decoder.decode(chunk, { stream: true }));
|
|
212
|
+
await consume(decoder.decode());
|
|
213
|
+
if (fragments.join("").trim())
|
|
214
|
+
throw new Error("Pi ended with an incomplete protocol record.");
|
|
215
|
+
this.fail(new Error("Pi protocol stream ended."));
|
|
216
|
+
};
|
|
217
|
+
const stderr = async () => {
|
|
218
|
+
for await (const _ of process.stderr) {
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
const stdout = read();
|
|
222
|
+
const readers = [stdout, stderr()].map(
|
|
223
|
+
(task) => task.catch((cause) => {
|
|
224
|
+
this.fail(cause);
|
|
225
|
+
throw cause;
|
|
226
|
+
})
|
|
227
|
+
);
|
|
228
|
+
this.drained = Promise.allSettled(readers).then((results) => {
|
|
229
|
+
const failure = results.find((result) => result.status === "rejected");
|
|
230
|
+
if (failure?.status === "rejected") throw failure.reason;
|
|
231
|
+
});
|
|
232
|
+
void this.drained.catch(() => {
|
|
233
|
+
});
|
|
234
|
+
void process.exited.then(
|
|
235
|
+
async (code) => {
|
|
236
|
+
let timer;
|
|
237
|
+
try {
|
|
238
|
+
await Promise.race([
|
|
239
|
+
stdout.catch(() => {
|
|
240
|
+
}),
|
|
241
|
+
new Promise((resolve3) => {
|
|
242
|
+
timer = setTimeout(resolve3, 1e3);
|
|
243
|
+
})
|
|
244
|
+
]);
|
|
245
|
+
this.fail(new Error(`Pi process exited (${String(code)}).`));
|
|
246
|
+
} finally {
|
|
247
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
(cause) => this.fail(cause)
|
|
251
|
+
).catch((cause) => this.fail(cause));
|
|
252
|
+
}
|
|
253
|
+
fail(cause) {
|
|
254
|
+
if (this.failure) return;
|
|
255
|
+
this.failure = cause instanceof Error ? cause : new Error(String(cause));
|
|
256
|
+
this.failureResult.resolve(this.failure);
|
|
257
|
+
for (const pending of this.pending.values()) pending.reject(this.failure);
|
|
258
|
+
this.pending.clear();
|
|
259
|
+
}
|
|
260
|
+
async request(type, data = {}, signal, timeoutMs = 3e4) {
|
|
261
|
+
signal?.throwIfAborted();
|
|
262
|
+
if (this.failure) throw this.failure;
|
|
263
|
+
const id = String(++this.nextId), result = Promise.withResolvers();
|
|
264
|
+
this.pending.set(id, result);
|
|
265
|
+
const abort = () => result.reject(signal?.reason ?? new Error("Pi request cancelled."));
|
|
266
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
267
|
+
if (signal?.aborted) abort();
|
|
268
|
+
const timer = setTimeout(
|
|
269
|
+
() => result.reject(new Error(`Pi ${type} response timed out.`)),
|
|
270
|
+
timeoutMs
|
|
271
|
+
);
|
|
272
|
+
try {
|
|
273
|
+
const write = Promise.resolve().then(() => {
|
|
274
|
+
signal?.throwIfAborted();
|
|
275
|
+
return this.process.writeStdin(JSON.stringify({ ...data, type, id }) + "\n");
|
|
276
|
+
});
|
|
277
|
+
await Promise.race([write, result.promise]);
|
|
278
|
+
return await result.promise;
|
|
279
|
+
} finally {
|
|
280
|
+
clearTimeout(timer);
|
|
281
|
+
signal?.removeEventListener("abort", abort);
|
|
282
|
+
this.pending.delete(id);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
async respond(data) {
|
|
286
|
+
if (this.failure) throw this.failure;
|
|
287
|
+
let timer;
|
|
288
|
+
try {
|
|
289
|
+
await Promise.race([
|
|
290
|
+
Promise.resolve().then(
|
|
291
|
+
() => this.process.writeStdin(
|
|
292
|
+
JSON.stringify({ ...data, type: "extension_ui_response" }) + "\n"
|
|
293
|
+
)
|
|
294
|
+
),
|
|
295
|
+
this.failed.then((cause) => {
|
|
296
|
+
throw cause;
|
|
297
|
+
}),
|
|
298
|
+
new Promise((_, reject) => {
|
|
299
|
+
timer = setTimeout(() => reject(new Error("Pi UI response timed out.")), 3e4);
|
|
300
|
+
})
|
|
301
|
+
]);
|
|
302
|
+
} finally {
|
|
303
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
// src/connection.ts
|
|
309
|
+
function resumeCursor(config, input) {
|
|
310
|
+
const stored = record(input.discardResumeCursor ? void 0 : input.resumeCursor);
|
|
311
|
+
const sessionId = string(stored.sessionId);
|
|
312
|
+
const sessionDirectory = string(stored.sessionDirectory);
|
|
313
|
+
if (sessionId && sessionDirectory) {
|
|
314
|
+
if (!/^[a-zA-Z0-9](?:[a-zA-Z0-9._-]*[a-zA-Z0-9])?$/.test(sessionId) || !NodePath2.isAbsolute(sessionDirectory) || NodePath2.resolve(sessionDirectory) !== NodePath2.resolve(config.homePath, "sessions"))
|
|
315
|
+
throw new Error("Invalid Pi session resume cursor.");
|
|
316
|
+
const sessionFile = string(stored.sessionFile);
|
|
317
|
+
const fileRelative = NodePath2.relative(sessionDirectory, sessionFile);
|
|
318
|
+
if (sessionFile && (!NodePath2.isAbsolute(sessionFile) || !fileRelative || fileRelative.startsWith("..") || NodePath2.isAbsolute(fileRelative)))
|
|
319
|
+
throw new Error("Invalid Pi session resume cursor.");
|
|
320
|
+
return { sessionId, sessionDirectory, ...sessionFile ? { sessionFile } : {} };
|
|
321
|
+
}
|
|
322
|
+
if (Object.keys(stored).length) throw new Error("Invalid Pi session resume cursor.");
|
|
323
|
+
return {
|
|
324
|
+
sessionId: `ycodium-${identifier(input.threadId)}${input.discardResumeCursor ? `-${identifier(NodeCrypto2.randomUUID())}` : ""}`,
|
|
325
|
+
sessionDirectory: NodePath2.join(config.homePath, "sessions")
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
function initializeResumeCursor(cursor, state) {
|
|
329
|
+
const sessionFile = string(state.sessionFile);
|
|
330
|
+
if (!sessionFile || !NodePath2.isAbsolute(sessionFile)) return;
|
|
331
|
+
const relative2 = NodePath2.relative(cursor.sessionDirectory, sessionFile);
|
|
332
|
+
if (!relative2 || relative2.startsWith("..") || NodePath2.isAbsolute(relative2))
|
|
333
|
+
throw new Error("Invalid Pi session file location.");
|
|
334
|
+
if (cursor.sessionFile && NodePath2.resolve(cursor.sessionFile) !== NodePath2.resolve(sessionFile))
|
|
335
|
+
throw new Error("Pi resumed a different session file.");
|
|
336
|
+
cursor.sessionFile = sessionFile;
|
|
337
|
+
}
|
|
338
|
+
async function createLaunch(config, input, cursor) {
|
|
339
|
+
const apiProtocol = config.apiProtocol;
|
|
340
|
+
const baseUrl = nativeApiBaseUrl(config.apiBaseUrl, apiProtocol);
|
|
341
|
+
await NodeFSP.mkdir(config.homePath, { recursive: true });
|
|
342
|
+
const directory = await NodeFSP.mkdtemp(NodePath2.join(config.homePath, "launch-"));
|
|
343
|
+
try {
|
|
344
|
+
await NodeFSP.writeFile(
|
|
345
|
+
NodePath2.join(directory, "models.json"),
|
|
346
|
+
JSON.stringify({
|
|
347
|
+
providers: {
|
|
348
|
+
ycodium: {
|
|
349
|
+
baseUrl,
|
|
350
|
+
api: apiProtocol,
|
|
351
|
+
apiKey: `\${${PI_API_KEY_ENV}}`,
|
|
352
|
+
models: [{ id: config.model }]
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}),
|
|
356
|
+
{ mode: 384 }
|
|
357
|
+
);
|
|
358
|
+
const servers = Object.fromEntries(
|
|
359
|
+
(input?.mcpServers ?? []).map((server) => {
|
|
360
|
+
const t = server.transport;
|
|
361
|
+
if (t.type === "sse") throw new Error("Pi does not support legacy SSE MCP servers.");
|
|
362
|
+
return [
|
|
363
|
+
server.name,
|
|
364
|
+
t.type === "stdio" ? {
|
|
365
|
+
command: t.command,
|
|
366
|
+
args: t.args ?? [],
|
|
367
|
+
env: t.env ?? {},
|
|
368
|
+
cwd: input?.cwd,
|
|
369
|
+
exposure: "deferred"
|
|
370
|
+
} : { url: t.url, headers: t.headers ?? {}, exposure: "deferred" }
|
|
371
|
+
];
|
|
372
|
+
})
|
|
373
|
+
);
|
|
374
|
+
await NodeFSP.writeFile(
|
|
375
|
+
NodePath2.join(directory, "mcp.json"),
|
|
376
|
+
JSON.stringify({ mcpServers: servers }),
|
|
377
|
+
{ mode: 384 }
|
|
378
|
+
);
|
|
379
|
+
const args = [
|
|
380
|
+
"--mode",
|
|
381
|
+
"rpc",
|
|
382
|
+
"--no-approve",
|
|
383
|
+
"--model",
|
|
384
|
+
`ycodium/${config.model}`,
|
|
385
|
+
"--no-extensions",
|
|
386
|
+
"--no-skills"
|
|
387
|
+
];
|
|
388
|
+
if (cursor) {
|
|
389
|
+
await NodeFSP.mkdir(cursor.sessionDirectory, { recursive: true });
|
|
390
|
+
args.push("--session-dir", cursor.sessionDirectory, "--session-id", cursor.sessionId);
|
|
391
|
+
} else args.push("--no-session", "--no-prompt-templates", "--no-context-files");
|
|
392
|
+
if (input?.mcpServers?.length) args.push("-e", "builtin:mcp", "-e", "builtin:tool-search");
|
|
393
|
+
if (input?.skills)
|
|
394
|
+
for (const name of input.skills.names) {
|
|
395
|
+
const skill = NodePath2.resolve(input.skills.skillsDir, name);
|
|
396
|
+
const relative2 = NodePath2.relative(NodePath2.resolve(input.skills.skillsDir), skill);
|
|
397
|
+
if (!relative2 || relative2.startsWith("..") || NodePath2.isAbsolute(relative2))
|
|
398
|
+
throw new Error("Invalid Pi skill path.");
|
|
399
|
+
args.push("--skill", skill);
|
|
400
|
+
}
|
|
401
|
+
return {
|
|
402
|
+
directory,
|
|
403
|
+
args,
|
|
404
|
+
env: {
|
|
405
|
+
...config.environment,
|
|
406
|
+
PI_CODING_AGENT_DIR: directory,
|
|
407
|
+
PI_OFFLINE: "1",
|
|
408
|
+
PI_SKIP_VERSION_CHECK: "1"
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
} catch (cause) {
|
|
412
|
+
await NodeFSP.rm(directory, { recursive: true, force: true });
|
|
413
|
+
throw cause;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
async function openConnection(input) {
|
|
417
|
+
input.signal?.throwIfAborted();
|
|
418
|
+
if (input.start?.resumeCursor && !input.start.discardResumeCursor && !input.cursor?.sessionFile)
|
|
419
|
+
throw Object.assign(new Error("The Pi session file no longer exists."), {
|
|
420
|
+
code: "resume-thread-missing"
|
|
421
|
+
});
|
|
422
|
+
if (input.cursor?.sessionFile) {
|
|
423
|
+
try {
|
|
424
|
+
if (!(await NodeFSP.stat(input.cursor.sessionFile)).isFile())
|
|
425
|
+
throw Object.assign(new Error("Invalid Pi session file."), { code: "ENOENT" });
|
|
426
|
+
} catch (cause) {
|
|
427
|
+
if (record(cause).code !== "ENOENT") throw cause;
|
|
428
|
+
throw Object.assign(new Error("The Pi session file no longer exists."), {
|
|
429
|
+
code: "resume-thread-missing"
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
const launch = await createLaunch(input.config, input.start, input.cursor);
|
|
434
|
+
const controller = new AbortController();
|
|
435
|
+
const forward = () => controller.abort(input.signal?.reason);
|
|
436
|
+
input.signal?.addEventListener("abort", forward, { once: true });
|
|
437
|
+
if (input.signal?.aborted) forward();
|
|
438
|
+
const ready = Promise.withResolvers();
|
|
439
|
+
const close = Promise.withResolvers();
|
|
440
|
+
void close.promise.catch(() => {
|
|
441
|
+
});
|
|
442
|
+
let requestedStop = false;
|
|
443
|
+
let didStart = false;
|
|
444
|
+
const completed = withProcessOperation(
|
|
445
|
+
input.host,
|
|
446
|
+
{ signal: controller.signal },
|
|
447
|
+
async (operation) => {
|
|
448
|
+
const handle = await operation.spawn({
|
|
449
|
+
program: input.config.binaryPath,
|
|
450
|
+
args: launch.args,
|
|
451
|
+
cwd: input.cwd,
|
|
452
|
+
env: launch.env,
|
|
453
|
+
owner: { bindingId: input.host.bindingId }
|
|
454
|
+
});
|
|
455
|
+
let startupFailure;
|
|
456
|
+
const rpc = new PiRpc(handle, (event) => {
|
|
457
|
+
if (!didStart && (event.type === "extension_error" || input.start?.mcpServers?.length && event.type === "extension_ui_request" && event.method === "notify" && (event.notifyType === "error" || event.notifyType === "warning")))
|
|
458
|
+
startupFailure = new Error("Pi MCP servers could not initialize.");
|
|
459
|
+
return input.onEvent(event);
|
|
460
|
+
});
|
|
461
|
+
operation.track(rpc.drained);
|
|
462
|
+
operation.defer(() => handle.closeStdin());
|
|
463
|
+
const abort = () => {
|
|
464
|
+
rpc.fail(operation.signal.reason);
|
|
465
|
+
close.resolve();
|
|
466
|
+
};
|
|
467
|
+
operation.signal.addEventListener("abort", abort, { once: true });
|
|
468
|
+
if (operation.signal.aborted) abort();
|
|
469
|
+
const failed = rpc.failed.then((cause) => {
|
|
470
|
+
if (!requestedStop) {
|
|
471
|
+
if (didStart) input.onExit?.(cause);
|
|
472
|
+
close.reject(cause);
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
operation.track(failed);
|
|
476
|
+
try {
|
|
477
|
+
const state = record(await rpc.request("get_state", {}, operation.signal));
|
|
478
|
+
if (input.start?.mcpServers?.length) {
|
|
479
|
+
const inventory = record(await rpc.request("get_commands", {}, operation.signal));
|
|
480
|
+
const commands = Array.isArray(inventory.commands) ? inventory.commands : [];
|
|
481
|
+
if (!commands.some((command) => record(command).name === "mcp"))
|
|
482
|
+
throw new Error("Pi MCP command is unavailable.");
|
|
483
|
+
const response = record(
|
|
484
|
+
await rpc.request("prompt", { message: "/mcp" }, operation.signal)
|
|
485
|
+
);
|
|
486
|
+
if (response.disposition !== "handled")
|
|
487
|
+
throw new Error("Pi MCP initialization was not handled.");
|
|
488
|
+
}
|
|
489
|
+
if (startupFailure) throw startupFailure;
|
|
490
|
+
didStart = true;
|
|
491
|
+
ready.resolve({ rpc, state });
|
|
492
|
+
await close.promise;
|
|
493
|
+
} finally {
|
|
494
|
+
operation.signal.removeEventListener("abort", abort);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
).catch((cause) => {
|
|
498
|
+
ready.reject(cause);
|
|
499
|
+
throw cause;
|
|
500
|
+
}).finally(() => {
|
|
501
|
+
input.signal?.removeEventListener("abort", forward);
|
|
502
|
+
});
|
|
503
|
+
const cleaned = completed.then(
|
|
504
|
+
async () => {
|
|
505
|
+
await NodeFSP.rm(launch.directory, { recursive: true, force: true });
|
|
506
|
+
},
|
|
507
|
+
async (cause) => {
|
|
508
|
+
if (!isOperationCleanupError(cause))
|
|
509
|
+
await NodeFSP.rm(launch.directory, { recursive: true, force: true });
|
|
510
|
+
throw cause;
|
|
511
|
+
}
|
|
512
|
+
);
|
|
513
|
+
void cleaned.catch(() => {
|
|
514
|
+
});
|
|
515
|
+
try {
|
|
516
|
+
const established = await ready.promise;
|
|
517
|
+
input.signal?.removeEventListener("abort", forward);
|
|
518
|
+
input.signal?.throwIfAborted();
|
|
519
|
+
let stopping;
|
|
520
|
+
return {
|
|
521
|
+
...established,
|
|
522
|
+
completed: cleaned,
|
|
523
|
+
stop: () => {
|
|
524
|
+
stopping ??= (async () => {
|
|
525
|
+
requestedStop = true;
|
|
526
|
+
close.resolve();
|
|
527
|
+
await cleaned;
|
|
528
|
+
})();
|
|
529
|
+
return stopping;
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
} catch (cause) {
|
|
533
|
+
requestedStop = true;
|
|
534
|
+
close.resolve();
|
|
535
|
+
controller.abort(cause);
|
|
536
|
+
try {
|
|
537
|
+
await cleaned;
|
|
538
|
+
} catch (cleanupCause) {
|
|
539
|
+
if (isOperationCleanupError(cleanupCause)) throw cleanupCause;
|
|
540
|
+
}
|
|
541
|
+
throw cause;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// src/events.ts
|
|
546
|
+
import * as NodeCrypto3 from "node:crypto";
|
|
547
|
+
var EventQueue = class {
|
|
548
|
+
queue = [];
|
|
549
|
+
waiters = [];
|
|
550
|
+
closed = false;
|
|
551
|
+
push(event) {
|
|
552
|
+
if (this.closed) return;
|
|
553
|
+
const waiter = this.waiters.shift();
|
|
554
|
+
if (waiter) waiter.resolve({ done: false, value: event });
|
|
555
|
+
else this.queue.push(event);
|
|
556
|
+
}
|
|
557
|
+
close() {
|
|
558
|
+
this.closed = true;
|
|
559
|
+
for (const waiter of this.waiters.splice(0)) waiter.resolve({ done: true, value: void 0 });
|
|
560
|
+
}
|
|
561
|
+
[Symbol.asyncIterator]() {
|
|
562
|
+
const owner = {};
|
|
563
|
+
let returned = false;
|
|
564
|
+
return {
|
|
565
|
+
next: () => {
|
|
566
|
+
if (returned) return Promise.resolve({ done: true, value: void 0 });
|
|
567
|
+
const value = this.queue.shift();
|
|
568
|
+
if (value) return Promise.resolve({ done: false, value });
|
|
569
|
+
if (this.closed) return Promise.resolve({ done: true, value: void 0 });
|
|
570
|
+
return new Promise((resolve3) => this.waiters.push({ owner, resolve: resolve3 }));
|
|
571
|
+
},
|
|
572
|
+
return: () => {
|
|
573
|
+
returned = true;
|
|
574
|
+
const pending = this.waiters.filter((waiter) => waiter.owner === owner);
|
|
575
|
+
this.waiters = this.waiters.filter((waiter) => waiter.owner !== owner);
|
|
576
|
+
for (const waiter of pending) waiter.resolve({ done: true, value: void 0 });
|
|
577
|
+
return Promise.resolve({ done: true, value: void 0 });
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
};
|
|
582
|
+
function emitter(host, events, session) {
|
|
583
|
+
return (type, payload, refs = {}) => {
|
|
584
|
+
events.push({
|
|
585
|
+
eventId: NodeCrypto3.randomUUID(),
|
|
586
|
+
provider: "pi",
|
|
587
|
+
providerInstanceId: host.instanceId,
|
|
588
|
+
threadId: session.threadId,
|
|
589
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
590
|
+
type,
|
|
591
|
+
payload,
|
|
592
|
+
...refs,
|
|
593
|
+
...session.bindingGeneration === void 0 ? {} : { originBindingGeneration: session.bindingGeneration },
|
|
594
|
+
...session.resumeCursor === void 0 ? {} : { originResumeCursor: session.resumeCursor }
|
|
595
|
+
});
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// src/permission.ts
|
|
600
|
+
var PI_FULL_ACCESS_MODE_ID = "full-access";
|
|
601
|
+
var PI_NATIVE_MODE_REPORT = {
|
|
602
|
+
nativeModes: [{ id: PI_FULL_ACCESS_MODE_ID, name: "Full access" }],
|
|
603
|
+
currentNativeModeId: PI_FULL_ACCESS_MODE_ID
|
|
604
|
+
};
|
|
605
|
+
function piNativeModeFor(runtimeMode, nativeModeId) {
|
|
606
|
+
if (nativeModeId && nativeModeId !== PI_FULL_ACCESS_MODE_ID)
|
|
607
|
+
throw new Error("Pi only supports full access mode.");
|
|
608
|
+
if (!nativeModeId && runtimeMode !== PI_FULL_ACCESS_MODE_ID)
|
|
609
|
+
throw new Error("Pi does not provide the requested approval mode. Select full access.");
|
|
610
|
+
return PI_FULL_ACCESS_MODE_ID;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// src/turn.ts
|
|
614
|
+
import * as NodeCrypto4 from "node:crypto";
|
|
615
|
+
var count = (value) => typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
|
|
616
|
+
var toolType = (name) => name === "bash" || name === "powershell" ? "command_execution" : name === "edit" || name === "write" ? "file_change" : name.startsWith("mcp__") ? "mcp_tool_call" : "dynamic_tool_call";
|
|
617
|
+
var TOOL_SNAPSHOT_CHARS = 16384;
|
|
618
|
+
function snapshot(value) {
|
|
619
|
+
if (value === void 0) return void 0;
|
|
620
|
+
const json = JSON.stringify(value);
|
|
621
|
+
return json.length <= TOOL_SNAPSHOT_CHARS ? value : { truncated: true, preview: json.slice(0, TOOL_SNAPSHOT_CHARS) };
|
|
622
|
+
}
|
|
623
|
+
function resultText(value) {
|
|
624
|
+
const content = record(value).content;
|
|
625
|
+
if (!Array.isArray(content))
|
|
626
|
+
return typeof value === "string" ? value.slice(0, TOOL_SNAPSHOT_CHARS) : "";
|
|
627
|
+
let text = "";
|
|
628
|
+
for (const entry of content) {
|
|
629
|
+
const part = record(entry);
|
|
630
|
+
if (part.type === "text") text += string(part.text).slice(0, TOOL_SNAPSHOT_CHARS - text.length);
|
|
631
|
+
if (text.length >= TOOL_SNAPSHOT_CHARS) break;
|
|
632
|
+
}
|
|
633
|
+
return text;
|
|
634
|
+
}
|
|
635
|
+
var PiTurn = class {
|
|
636
|
+
id = NodeCrypto4.randomUUID();
|
|
637
|
+
settled = Promise.withResolvers();
|
|
638
|
+
completed = false;
|
|
639
|
+
started = false;
|
|
640
|
+
messageId = NodeCrypto4.randomUUID();
|
|
641
|
+
messageEnded = false;
|
|
642
|
+
blocks = /* @__PURE__ */ new Map();
|
|
643
|
+
tools = /* @__PURE__ */ new Map();
|
|
644
|
+
lastError;
|
|
645
|
+
aborted = false;
|
|
646
|
+
tokens = {
|
|
647
|
+
uncachedInputTokens: 0,
|
|
648
|
+
cachedInputTokens: 0,
|
|
649
|
+
cacheCreationTokens: 0,
|
|
650
|
+
outputTokens: 0,
|
|
651
|
+
reasoningTokens: 0
|
|
652
|
+
};
|
|
653
|
+
hasUsage = false;
|
|
654
|
+
emit;
|
|
655
|
+
model;
|
|
656
|
+
sessionId;
|
|
657
|
+
onFinish;
|
|
658
|
+
constructor(emit, model, sessionId, onFinish) {
|
|
659
|
+
this.emit = emit;
|
|
660
|
+
this.model = model;
|
|
661
|
+
this.sessionId = sessionId;
|
|
662
|
+
this.onFinish = onFinish;
|
|
663
|
+
}
|
|
664
|
+
start() {
|
|
665
|
+
if (this.started || this.completed) return;
|
|
666
|
+
this.started = true;
|
|
667
|
+
this.emit("turn.started", { model: this.model }, { turnId: this.id });
|
|
668
|
+
}
|
|
669
|
+
finish(state, errorMessage) {
|
|
670
|
+
if (this.completed) return;
|
|
671
|
+
this.completed = true;
|
|
672
|
+
this.completeBlocks(state === "failed" ? "failed" : "completed");
|
|
673
|
+
for (const tool of this.tools.values()) this.emitTool("item.completed", tool, "failed");
|
|
674
|
+
this.tools.clear();
|
|
675
|
+
this.emit(
|
|
676
|
+
"turn.completed",
|
|
677
|
+
{
|
|
678
|
+
state,
|
|
679
|
+
...errorMessage?.trim() ? { errorMessage } : {},
|
|
680
|
+
...this.hasUsage ? {
|
|
681
|
+
tokenUsage: {
|
|
682
|
+
provider: "pi",
|
|
683
|
+
model: this.model,
|
|
684
|
+
sessionId: this.sessionId,
|
|
685
|
+
totals: { ...this.tokens }
|
|
686
|
+
}
|
|
687
|
+
} : {}
|
|
688
|
+
},
|
|
689
|
+
{ turnId: this.id }
|
|
690
|
+
);
|
|
691
|
+
try {
|
|
692
|
+
this.onFinish();
|
|
693
|
+
} finally {
|
|
694
|
+
this.settled.resolve();
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
block(index, kind) {
|
|
698
|
+
let block = this.blocks.get(index);
|
|
699
|
+
if (!block) {
|
|
700
|
+
block = { id: `${this.id}:${this.messageId}:${index}`, kind, text: "" };
|
|
701
|
+
this.blocks.set(index, block);
|
|
702
|
+
this.emit(
|
|
703
|
+
"item.started",
|
|
704
|
+
{ itemType: kind, status: "inProgress" },
|
|
705
|
+
{ turnId: this.id, itemId: block.id }
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
return block;
|
|
709
|
+
}
|
|
710
|
+
completeBlock(block, status) {
|
|
711
|
+
this.emit(
|
|
712
|
+
"item.completed",
|
|
713
|
+
{
|
|
714
|
+
itemType: block.kind,
|
|
715
|
+
status,
|
|
716
|
+
...block.text.trim() ? { detail: block.text } : {}
|
|
717
|
+
},
|
|
718
|
+
{ turnId: this.id, itemId: block.id }
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
completeBlocks(status) {
|
|
722
|
+
for (const block of this.blocks.values()) this.completeBlock(block, status);
|
|
723
|
+
this.blocks.clear();
|
|
724
|
+
}
|
|
725
|
+
endMessage(message) {
|
|
726
|
+
if (this.messageEnded) return;
|
|
727
|
+
this.messageEnded = true;
|
|
728
|
+
const failed = message.stopReason === "error";
|
|
729
|
+
const content = Array.isArray(message.content) ? message.content : [];
|
|
730
|
+
content.forEach((value, index) => {
|
|
731
|
+
const part = record(value);
|
|
732
|
+
if (part.type !== "text" && part.type !== "thinking") return;
|
|
733
|
+
const full = string(part.type === "text" ? part.text : part.thinking);
|
|
734
|
+
if (!full && !this.blocks.has(index)) return;
|
|
735
|
+
const kind = part.type === "text" ? "assistant_message" : "reasoning";
|
|
736
|
+
const block = this.block(index, kind);
|
|
737
|
+
if (full.startsWith(block.text) && full.length > block.text.length)
|
|
738
|
+
this.emit(
|
|
739
|
+
"content.delta",
|
|
740
|
+
{
|
|
741
|
+
streamKind: kind === "reasoning" ? "reasoning_text" : "assistant_text",
|
|
742
|
+
delta: full.slice(block.text.length)
|
|
743
|
+
},
|
|
744
|
+
{ turnId: this.id, itemId: block.id }
|
|
745
|
+
);
|
|
746
|
+
block.text = full;
|
|
747
|
+
this.completeBlock(block, failed ? "failed" : "completed");
|
|
748
|
+
this.blocks.delete(index);
|
|
749
|
+
});
|
|
750
|
+
this.completeBlocks(failed ? "failed" : "completed");
|
|
751
|
+
this.lastError = failed ? string(message.errorMessage).trim() || "Pi model request failed." : void 0;
|
|
752
|
+
this.aborted = message.stopReason === "aborted";
|
|
753
|
+
const usage = record(message.usage);
|
|
754
|
+
if (![usage.input, usage.output, usage.cacheRead, usage.cacheWrite].some(
|
|
755
|
+
(value) => typeof value === "number" && Number.isFinite(value)
|
|
756
|
+
))
|
|
757
|
+
return;
|
|
758
|
+
this.hasUsage = true;
|
|
759
|
+
const input = count(usage.input), cacheRead = count(usage.cacheRead), cacheWrite = count(usage.cacheWrite), output = count(usage.output);
|
|
760
|
+
this.tokens.uncachedInputTokens += input;
|
|
761
|
+
this.tokens.cachedInputTokens += cacheRead;
|
|
762
|
+
this.tokens.cacheCreationTokens += cacheWrite;
|
|
763
|
+
this.tokens.outputTokens += output;
|
|
764
|
+
this.tokens.reasoningTokens += count(usage.reasoning);
|
|
765
|
+
this.emit(
|
|
766
|
+
"thread.token-usage.updated",
|
|
767
|
+
{
|
|
768
|
+
usage: {
|
|
769
|
+
usedTokens: input + cacheRead + cacheWrite + output,
|
|
770
|
+
inputTokens: input,
|
|
771
|
+
cachedInputTokens: cacheRead + cacheWrite,
|
|
772
|
+
outputTokens: output
|
|
773
|
+
}
|
|
774
|
+
},
|
|
775
|
+
{ turnId: this.id }
|
|
776
|
+
);
|
|
777
|
+
}
|
|
778
|
+
emitTool(type, tool, status) {
|
|
779
|
+
this.emit(
|
|
780
|
+
type,
|
|
781
|
+
{
|
|
782
|
+
itemType: toolType(tool.name),
|
|
783
|
+
title: tool.name,
|
|
784
|
+
status,
|
|
785
|
+
...tool.detail?.trim() ? { detail: tool.detail } : {},
|
|
786
|
+
data: {
|
|
787
|
+
tool: tool.name,
|
|
788
|
+
state: status === "inProgress" ? "running" : status,
|
|
789
|
+
...tool.args === void 0 ? {} : { input: tool.args },
|
|
790
|
+
...tool.output === void 0 ? {} : { output: tool.output }
|
|
791
|
+
}
|
|
792
|
+
},
|
|
793
|
+
{ turnId: this.id, itemId: tool.id }
|
|
794
|
+
);
|
|
795
|
+
}
|
|
796
|
+
event(event) {
|
|
797
|
+
if (this.completed) return;
|
|
798
|
+
const message = record(event.message);
|
|
799
|
+
if (event.type === "message_start" && message.role === "assistant") {
|
|
800
|
+
this.completeBlocks("failed");
|
|
801
|
+
this.messageId = NodeCrypto4.randomUUID();
|
|
802
|
+
this.messageEnded = false;
|
|
803
|
+
}
|
|
804
|
+
if (event.type === "message_update" && !this.messageEnded) {
|
|
805
|
+
const update = record(event.assistantMessageEvent);
|
|
806
|
+
if ((update.type === "text_delta" || update.type === "thinking_delta") && string(update.delta)) {
|
|
807
|
+
const kind = update.type === "text_delta" ? "assistant_message" : "reasoning";
|
|
808
|
+
const block = this.block(count(update.contentIndex), kind);
|
|
809
|
+
const delta = string(update.delta);
|
|
810
|
+
block.text += delta;
|
|
811
|
+
this.emit(
|
|
812
|
+
"content.delta",
|
|
813
|
+
{ streamKind: kind === "reasoning" ? "reasoning_text" : "assistant_text", delta },
|
|
814
|
+
{ turnId: this.id, itemId: block.id }
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
if (event.type === "message_end" && message.role === "assistant") this.endMessage(message);
|
|
819
|
+
if (event.type === "tool_execution_start") {
|
|
820
|
+
const nativeId = string(event.toolCallId), name = string(event.toolName) || "tool";
|
|
821
|
+
if (!nativeId || this.tools.has(nativeId)) return;
|
|
822
|
+
const tool = { id: `${this.id}:tool:${nativeId}`, name, args: snapshot(event.args) };
|
|
823
|
+
this.tools.set(nativeId, tool);
|
|
824
|
+
this.emitTool("item.started", tool, "inProgress");
|
|
825
|
+
}
|
|
826
|
+
if (event.type === "tool_execution_update" || event.type === "tool_execution_end") {
|
|
827
|
+
const nativeId = string(event.toolCallId), tool = this.tools.get(nativeId);
|
|
828
|
+
if (tool) {
|
|
829
|
+
const done = event.type === "tool_execution_end";
|
|
830
|
+
const result = done ? event.result : event.partialResult;
|
|
831
|
+
tool.output = snapshot(result);
|
|
832
|
+
tool.detail = resultText(result);
|
|
833
|
+
const update = JSON.stringify([tool.output, tool.detail]);
|
|
834
|
+
if (done || update !== tool.lastUpdate)
|
|
835
|
+
this.emitTool(
|
|
836
|
+
done ? "item.completed" : "item.updated",
|
|
837
|
+
tool,
|
|
838
|
+
done ? event.isError ? "failed" : "completed" : "inProgress"
|
|
839
|
+
);
|
|
840
|
+
tool.lastUpdate = update;
|
|
841
|
+
if (done) this.tools.delete(nativeId);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
if (event.type === "auto_retry_end" && event.success === false)
|
|
845
|
+
this.lastError = string(event.finalError).trim() || "Pi retries failed.";
|
|
846
|
+
if (event.type === "agent_settled")
|
|
847
|
+
this.finish(
|
|
848
|
+
event.aborted || this.aborted ? "interrupted" : this.lastError ? "failed" : "completed",
|
|
849
|
+
this.lastError
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
};
|
|
853
|
+
|
|
854
|
+
// src/instance.ts
|
|
855
|
+
function createPiInstance(value, host) {
|
|
856
|
+
const config = parseConfig(value, host.instanceId);
|
|
857
|
+
const events = new EventQueue();
|
|
858
|
+
const threads = /* @__PURE__ */ new Map();
|
|
859
|
+
const probes = /* @__PURE__ */ new Set();
|
|
860
|
+
const starts = /* @__PURE__ */ new Map();
|
|
861
|
+
const startEpochs = /* @__PURE__ */ new Map();
|
|
862
|
+
const disposed = new AbortController();
|
|
863
|
+
const now = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
864
|
+
const nativeMode = PI_NATIVE_MODE_REPORT;
|
|
865
|
+
const validateModel = (selection) => {
|
|
866
|
+
const model = string(record(selection).model).trim();
|
|
867
|
+
if (model && model !== config.model && model !== `ycodium/${config.model}`)
|
|
868
|
+
throw Object.assign(new Error("Pi can only run the model configured for this instance."), {
|
|
869
|
+
code: "provider-rejected"
|
|
870
|
+
});
|
|
871
|
+
};
|
|
872
|
+
const safeError = (cause) => {
|
|
873
|
+
if (cause instanceof ExecutorSpawnRefusedError || cause instanceof ExecutorSpawnAbortedError)
|
|
874
|
+
return cause;
|
|
875
|
+
return Object.assign(
|
|
876
|
+
new Error(redact(config, cause)),
|
|
877
|
+
record(cause).code === "resume-thread-missing" ? { code: "resume-thread-missing" } : cause instanceof PiResponseError || record(cause).code === "provider-rejected" ? { code: "provider-rejected" } : {}
|
|
878
|
+
);
|
|
879
|
+
};
|
|
880
|
+
const ensureActive = () => {
|
|
881
|
+
if (disposed.signal.aborted) throw new Error("Pi executor is disposed.");
|
|
882
|
+
};
|
|
883
|
+
const requireThread = (id) => {
|
|
884
|
+
const value2 = threads.get(id);
|
|
885
|
+
if (!value2) throw new Error("Pi session is not running.");
|
|
886
|
+
return value2;
|
|
887
|
+
};
|
|
888
|
+
const connection = (booked) => {
|
|
889
|
+
if (!booked.connection || booked.exited || booked.closing)
|
|
890
|
+
throw new Error("Pi session is not ready.");
|
|
891
|
+
return booked.connection;
|
|
892
|
+
};
|
|
893
|
+
const endQuestions = (booked) => {
|
|
894
|
+
for (const id of booked.questions.keys())
|
|
895
|
+
booked.emit("user-input.resolved", { decision: "cancelled" }, { requestId: id });
|
|
896
|
+
booked.questions.clear();
|
|
897
|
+
};
|
|
898
|
+
const exited = (booked, cause) => {
|
|
899
|
+
if (booked.exited) return;
|
|
900
|
+
booked.turn?.finish(
|
|
901
|
+
cause ? "failed" : "interrupted",
|
|
902
|
+
cause ? redact(config, cause) : void 0
|
|
903
|
+
);
|
|
904
|
+
endQuestions(booked);
|
|
905
|
+
booked.exited = true;
|
|
906
|
+
booked.session.status = "stopped";
|
|
907
|
+
booked.emit("session.exited", { reason: cause ? redact(config, cause) : "stopped" });
|
|
908
|
+
};
|
|
909
|
+
const onEvent = (booked, event) => {
|
|
910
|
+
if (booked.exited) return;
|
|
911
|
+
if (event.type === "extension_ui_request") {
|
|
912
|
+
const method = string(event.method), nativeId = string(event.id);
|
|
913
|
+
if (["select", "confirm", "input", "editor"].includes(method) && nativeId) {
|
|
914
|
+
const requestId = NodeCrypto5.randomUUID();
|
|
915
|
+
const options = method === "confirm" ? ["Yes", "No"] : Array.isArray(event.options) ? event.options.filter((v) => typeof v === "string") : [];
|
|
916
|
+
booked.questions.set(requestId, { nativeId, method, options });
|
|
917
|
+
const title = string(event.title) || "Pi";
|
|
918
|
+
booked.emit(
|
|
919
|
+
"user-input.requested",
|
|
920
|
+
{
|
|
921
|
+
questions: [
|
|
922
|
+
{
|
|
923
|
+
id: "value",
|
|
924
|
+
header: title,
|
|
925
|
+
question: string(event.message) || title,
|
|
926
|
+
options: options.map((label) => ({ label, description: "" })),
|
|
927
|
+
allowFreeform: method === "input" || method === "editor",
|
|
928
|
+
multiSelect: false
|
|
929
|
+
}
|
|
930
|
+
]
|
|
931
|
+
},
|
|
932
|
+
{ requestId, ...booked.turn ? { turnId: booked.turn.id } : {} }
|
|
933
|
+
);
|
|
934
|
+
}
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
if (event.type === "compaction_start")
|
|
938
|
+
booked.compaction = {
|
|
939
|
+
compactionId: NodeCrypto5.randomUUID(),
|
|
940
|
+
trigger: event.reason === "manual" ? "manual" : "automatic",
|
|
941
|
+
startedAt: Date.now()
|
|
942
|
+
};
|
|
943
|
+
if (event.type === "compaction_end") {
|
|
944
|
+
const snapshot2 = booked.compaction ?? {
|
|
945
|
+
compactionId: NodeCrypto5.randomUUID(),
|
|
946
|
+
trigger: event.reason === "manual" ? "manual" : "automatic",
|
|
947
|
+
startedAt: Date.now()
|
|
948
|
+
};
|
|
949
|
+
booked.compaction = void 0;
|
|
950
|
+
if (!event.aborted && !event.errorMessage && event.result) {
|
|
951
|
+
const result = record(event.result), summary = string(result.summary);
|
|
952
|
+
booked.emit("thread.state.changed", {
|
|
953
|
+
state: "compacted",
|
|
954
|
+
compaction: {
|
|
955
|
+
compactionId: snapshot2.compactionId,
|
|
956
|
+
trigger: snapshot2.trigger,
|
|
957
|
+
durationMs: Math.max(0, Date.now() - snapshot2.startedAt),
|
|
958
|
+
...typeof result.tokensBefore === "number" && Number.isFinite(result.tokensBefore) && result.tokensBefore >= 0 ? { preTokens: result.tokensBefore } : {},
|
|
959
|
+
...summary ? { summary: summary.slice(0, 16e3), summaryTruncated: summary.length > 16e3 } : {}
|
|
960
|
+
}
|
|
961
|
+
});
|
|
962
|
+
} else if (event.errorMessage)
|
|
963
|
+
booked.emit("runtime.warning", { message: redact(config, event.errorMessage) });
|
|
964
|
+
}
|
|
965
|
+
const message = record(event.message);
|
|
966
|
+
const mapped = event.type === "message_end" && typeof message.errorMessage === "string" ? { ...event, message: { ...message, errorMessage: redact(config, message.errorMessage) } } : event.type === "auto_retry_end" && typeof event.finalError === "string" ? { ...event, finalError: redact(config, event.finalError) } : event;
|
|
967
|
+
booked.turn?.event(mapped);
|
|
968
|
+
if (event.type === "agent_settled") endQuestions(booked);
|
|
969
|
+
};
|
|
970
|
+
const waitForSettled = async (booked, turn, signal) => {
|
|
971
|
+
let timer;
|
|
972
|
+
let abort = () => {
|
|
973
|
+
};
|
|
974
|
+
try {
|
|
975
|
+
await Promise.race([
|
|
976
|
+
turn.settled.promise,
|
|
977
|
+
booked.connection.rpc.failed.then((cause) => {
|
|
978
|
+
throw cause;
|
|
979
|
+
}),
|
|
980
|
+
new Promise((_, reject) => {
|
|
981
|
+
timer = setTimeout(() => reject(new Error("Pi did not settle after abort.")), 1e4);
|
|
982
|
+
abort = () => reject(signal?.reason ?? new Error("Pi abort cancelled."));
|
|
983
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
984
|
+
if (signal?.aborted) abort();
|
|
985
|
+
})
|
|
986
|
+
]);
|
|
987
|
+
} finally {
|
|
988
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
989
|
+
signal?.removeEventListener("abort", abort);
|
|
990
|
+
}
|
|
991
|
+
};
|
|
992
|
+
const abortTurn = async (booked, signal) => {
|
|
993
|
+
const turn = booked.turn, conn = booked.connection;
|
|
994
|
+
if (!turn || turn.completed || !conn) return;
|
|
995
|
+
await conn.rpc.request("clear_queue", {}, signal, 1e4);
|
|
996
|
+
await conn.rpc.request("abort", {}, signal, 1e4);
|
|
997
|
+
await waitForSettled(booked, turn, signal);
|
|
998
|
+
};
|
|
999
|
+
const close = async (booked) => {
|
|
1000
|
+
if (booked.closing) return booked.closing;
|
|
1001
|
+
booked.startup.abort(new Error("Pi session is stopping."));
|
|
1002
|
+
booked.closing = (async () => {
|
|
1003
|
+
await booked.starting?.catch(() => {
|
|
1004
|
+
});
|
|
1005
|
+
try {
|
|
1006
|
+
if (!booked.exited) await abortTurn(booked, AbortSignal.timeout(2e3)).catch(() => {
|
|
1007
|
+
});
|
|
1008
|
+
await booked.connection?.stop();
|
|
1009
|
+
exited(booked);
|
|
1010
|
+
if (threads.get(booked.session.threadId) === booked)
|
|
1011
|
+
threads.delete(booked.session.threadId);
|
|
1012
|
+
} catch (cause) {
|
|
1013
|
+
exited(booked, cause);
|
|
1014
|
+
throw cause;
|
|
1015
|
+
}
|
|
1016
|
+
})();
|
|
1017
|
+
return booked.closing;
|
|
1018
|
+
};
|
|
1019
|
+
const startOne = async (input, signal) => {
|
|
1020
|
+
ensureActive();
|
|
1021
|
+
validateConnection(config);
|
|
1022
|
+
validateModel(input.modelSelection);
|
|
1023
|
+
signal?.throwIfAborted();
|
|
1024
|
+
const nativeModeId = piNativeModeFor(input.runtimeMode, input.nativeModeId);
|
|
1025
|
+
const existing = threads.get(input.threadId);
|
|
1026
|
+
if (existing) await close(existing);
|
|
1027
|
+
ensureActive();
|
|
1028
|
+
signal?.throwIfAborted();
|
|
1029
|
+
const cwd = input.cwd ?? host.workspacePath(input.threadId);
|
|
1030
|
+
if (!cwd) throw new Error("Pi workspace is missing.");
|
|
1031
|
+
const cursor = resumeCursor(config, input);
|
|
1032
|
+
const session = {
|
|
1033
|
+
provider: "pi",
|
|
1034
|
+
providerInstanceId: host.instanceId,
|
|
1035
|
+
threadId: input.threadId,
|
|
1036
|
+
cwd,
|
|
1037
|
+
runtimeMode: input.runtimeMode,
|
|
1038
|
+
nativeModeId,
|
|
1039
|
+
status: "starting",
|
|
1040
|
+
model: config.model,
|
|
1041
|
+
createdAt: now(),
|
|
1042
|
+
updatedAt: now(),
|
|
1043
|
+
resumeCursor: cursor,
|
|
1044
|
+
...input.bindingGeneration === void 0 ? {} : { bindingGeneration: input.bindingGeneration }
|
|
1045
|
+
};
|
|
1046
|
+
const booked = {
|
|
1047
|
+
session,
|
|
1048
|
+
emit: emitter(host, events, session),
|
|
1049
|
+
startup: new AbortController(),
|
|
1050
|
+
questions: /* @__PURE__ */ new Map(),
|
|
1051
|
+
exited: false,
|
|
1052
|
+
stopping: false
|
|
1053
|
+
};
|
|
1054
|
+
threads.set(input.threadId, booked);
|
|
1055
|
+
const task = (async () => {
|
|
1056
|
+
try {
|
|
1057
|
+
booked.connection = await openConnection({
|
|
1058
|
+
config,
|
|
1059
|
+
host,
|
|
1060
|
+
cwd,
|
|
1061
|
+
start: { ...input, cwd },
|
|
1062
|
+
cursor,
|
|
1063
|
+
signal: AbortSignal.any([
|
|
1064
|
+
...signal ? [signal] : [],
|
|
1065
|
+
disposed.signal,
|
|
1066
|
+
booked.startup.signal
|
|
1067
|
+
]),
|
|
1068
|
+
onEvent: (event) => onEvent(booked, event),
|
|
1069
|
+
onExit: (cause) => exited(booked, cause)
|
|
1070
|
+
});
|
|
1071
|
+
if (booked.exited || booked.startup.signal.aborted || disposed.signal.aborted) {
|
|
1072
|
+
await booked.connection.stop();
|
|
1073
|
+
throw new Error("Pi startup was cancelled.");
|
|
1074
|
+
}
|
|
1075
|
+
initializeResumeCursor(cursor, booked.connection.state);
|
|
1076
|
+
session.status = "idle";
|
|
1077
|
+
booked.emit("session.started", {});
|
|
1078
|
+
return { ...session };
|
|
1079
|
+
} catch (cause) {
|
|
1080
|
+
let failure = cause;
|
|
1081
|
+
if (booked.connection) {
|
|
1082
|
+
try {
|
|
1083
|
+
await booked.connection.stop();
|
|
1084
|
+
} catch (cleanupCause) {
|
|
1085
|
+
failure = cleanupCause;
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
exited(booked, failure);
|
|
1089
|
+
throw safeError(failure);
|
|
1090
|
+
}
|
|
1091
|
+
})();
|
|
1092
|
+
booked.starting = task;
|
|
1093
|
+
return task;
|
|
1094
|
+
};
|
|
1095
|
+
const start = (input, signal) => {
|
|
1096
|
+
const previous = starts.get(input.threadId);
|
|
1097
|
+
const epoch = startEpochs.get(input.threadId) ?? 0;
|
|
1098
|
+
const task = (previous ? previous.catch(() => {
|
|
1099
|
+
}) : Promise.resolve()).then(() => {
|
|
1100
|
+
if ((startEpochs.get(input.threadId) ?? 0) !== epoch)
|
|
1101
|
+
throw new Error("Pi startup was cancelled.");
|
|
1102
|
+
return startOne(input, signal);
|
|
1103
|
+
});
|
|
1104
|
+
starts.set(input.threadId, task);
|
|
1105
|
+
void task.finally(() => {
|
|
1106
|
+
if (starts.get(input.threadId) === task) starts.delete(input.threadId);
|
|
1107
|
+
}).catch(() => {
|
|
1108
|
+
});
|
|
1109
|
+
return task;
|
|
1110
|
+
};
|
|
1111
|
+
const instance = {
|
|
1112
|
+
probe: async (signal) => {
|
|
1113
|
+
ensureActive();
|
|
1114
|
+
const task = (async () => {
|
|
1115
|
+
const combined = signal ? AbortSignal.any([signal, disposed.signal]) : disposed.signal;
|
|
1116
|
+
let version = null;
|
|
1117
|
+
try {
|
|
1118
|
+
const check = await runProcessOperation(host, {
|
|
1119
|
+
program: config.binaryPath,
|
|
1120
|
+
args: ["--version"],
|
|
1121
|
+
owner: { bindingId: host.bindingId },
|
|
1122
|
+
signal: combined,
|
|
1123
|
+
timeoutMs: 1e4
|
|
1124
|
+
});
|
|
1125
|
+
if (check.exitCode !== 0) throw new Error("Pi version check failed.");
|
|
1126
|
+
version = check.stdout.trim();
|
|
1127
|
+
const match = version.match(/(?:^|\s)v?(\d+)\.(\d+)\.(\d+)(?:\s|$)/);
|
|
1128
|
+
if (!match || Number(match[1]) < 1 || Number(match[1]) === 1 && Number(match[2]) < 4)
|
|
1129
|
+
return {
|
|
1130
|
+
...nativeMode,
|
|
1131
|
+
installed: true,
|
|
1132
|
+
version,
|
|
1133
|
+
readiness: "error",
|
|
1134
|
+
auth: { status: "unknown" },
|
|
1135
|
+
tooOld: { minimumVersion: "1.4.0" },
|
|
1136
|
+
detail: "Pi 1.4.0 or newer is required."
|
|
1137
|
+
};
|
|
1138
|
+
validateConnection(config);
|
|
1139
|
+
const conn = await openConnection({
|
|
1140
|
+
config,
|
|
1141
|
+
host,
|
|
1142
|
+
cwd: config.homePath,
|
|
1143
|
+
signal: combined,
|
|
1144
|
+
onEvent: () => {
|
|
1145
|
+
}
|
|
1146
|
+
});
|
|
1147
|
+
try {
|
|
1148
|
+
const result = record(await conn.rpc.request("get_available_models", {}, combined));
|
|
1149
|
+
const models = (Array.isArray(result.models) ? result.models : []).map(record).filter((m) => m.provider === "ycodium" && m.id === config.model).map((m) => ({
|
|
1150
|
+
slug: config.model,
|
|
1151
|
+
name: string(m.name) || config.model,
|
|
1152
|
+
isDefault: true
|
|
1153
|
+
}));
|
|
1154
|
+
if (!models.length) throw new Error("The configured Pi model is unavailable.");
|
|
1155
|
+
return {
|
|
1156
|
+
installed: true,
|
|
1157
|
+
version,
|
|
1158
|
+
readiness: "ready",
|
|
1159
|
+
auth: { status: "unknown" },
|
|
1160
|
+
models,
|
|
1161
|
+
...nativeMode,
|
|
1162
|
+
notice: { messageKey: "connectionNotVerified" }
|
|
1163
|
+
};
|
|
1164
|
+
} finally {
|
|
1165
|
+
await conn.stop();
|
|
1166
|
+
}
|
|
1167
|
+
} catch (cause) {
|
|
1168
|
+
combined.throwIfAborted();
|
|
1169
|
+
if (cause instanceof ExecutorSpawnRefusedError || cause instanceof ExecutorSpawnAbortedError)
|
|
1170
|
+
throw cause;
|
|
1171
|
+
if (isExecutorSpawnFailure(cause)) {
|
|
1172
|
+
const failure = describeExecutorSpawnFailure(cause);
|
|
1173
|
+
return {
|
|
1174
|
+
...nativeMode,
|
|
1175
|
+
installed: failure.readiness !== "not-installed",
|
|
1176
|
+
version,
|
|
1177
|
+
readiness: failure.readiness,
|
|
1178
|
+
auth: { status: "unknown" },
|
|
1179
|
+
detail: redact(config, failure.detail)
|
|
1180
|
+
};
|
|
1181
|
+
}
|
|
1182
|
+
return {
|
|
1183
|
+
...nativeMode,
|
|
1184
|
+
installed: true,
|
|
1185
|
+
version,
|
|
1186
|
+
readiness: "error",
|
|
1187
|
+
auth: { status: "unknown" },
|
|
1188
|
+
detail: redact(config, cause)
|
|
1189
|
+
};
|
|
1190
|
+
}
|
|
1191
|
+
})();
|
|
1192
|
+
probes.add(task);
|
|
1193
|
+
try {
|
|
1194
|
+
return await task;
|
|
1195
|
+
} finally {
|
|
1196
|
+
probes.delete(task);
|
|
1197
|
+
}
|
|
1198
|
+
},
|
|
1199
|
+
startSession: start,
|
|
1200
|
+
sendTurn: async (input, signal) => {
|
|
1201
|
+
signal?.throwIfAborted();
|
|
1202
|
+
validateModel(input.modelSelection);
|
|
1203
|
+
const booked = requireThread(input.threadId), conn = connection(booked);
|
|
1204
|
+
if (booked.stopping) throw new Error("Pi is stopping the current turn.");
|
|
1205
|
+
if (input.expectedTurnId !== void 0) {
|
|
1206
|
+
if (!booked.turn || booked.turn.id !== input.expectedTurnId || booked.turn.completed)
|
|
1207
|
+
throw Object.assign(new Error("The target Pi turn has ended."), {
|
|
1208
|
+
code: "provider-rejected"
|
|
1209
|
+
});
|
|
1210
|
+
try {
|
|
1211
|
+
await conn.rpc.request("steer", { message: input.input ?? "" }, signal);
|
|
1212
|
+
} catch (cause) {
|
|
1213
|
+
if (!(cause instanceof PiResponseError)) await close(booked);
|
|
1214
|
+
throw safeError(cause);
|
|
1215
|
+
}
|
|
1216
|
+
return {
|
|
1217
|
+
threadId: input.threadId,
|
|
1218
|
+
turnId: input.expectedTurnId,
|
|
1219
|
+
resumeCursor: booked.session.resumeCursor
|
|
1220
|
+
};
|
|
1221
|
+
}
|
|
1222
|
+
if (booked.turn) throw new Error("Pi already has an active turn.");
|
|
1223
|
+
const cursor = record(booked.session.resumeCursor);
|
|
1224
|
+
const turn = new PiTurn(booked.emit, config.model, string(cursor.sessionId), () => {
|
|
1225
|
+
if (booked.turn === turn) booked.turn = void 0;
|
|
1226
|
+
booked.session.status = "idle";
|
|
1227
|
+
booked.session.updatedAt = now();
|
|
1228
|
+
});
|
|
1229
|
+
booked.turn = turn;
|
|
1230
|
+
booked.session.status = "running";
|
|
1231
|
+
turn.start();
|
|
1232
|
+
try {
|
|
1233
|
+
const response = record(
|
|
1234
|
+
await conn.rpc.request("prompt", { message: input.input ?? "" }, signal)
|
|
1235
|
+
);
|
|
1236
|
+
if (response.disposition === "handled") turn.finish("completed");
|
|
1237
|
+
return {
|
|
1238
|
+
threadId: input.threadId,
|
|
1239
|
+
turnId: turn.id,
|
|
1240
|
+
resumeCursor: booked.session.resumeCursor
|
|
1241
|
+
};
|
|
1242
|
+
} catch (cause) {
|
|
1243
|
+
turn.finish("failed", redact(config, cause));
|
|
1244
|
+
if (!(cause instanceof PiResponseError)) await close(booked);
|
|
1245
|
+
throw safeError(cause);
|
|
1246
|
+
}
|
|
1247
|
+
},
|
|
1248
|
+
interruptTurn: async (threadId, turnId, signal) => {
|
|
1249
|
+
const booked = requireThread(threadId), turn = booked.turn;
|
|
1250
|
+
if (!turn || turnId !== void 0 && turn.id !== turnId) return;
|
|
1251
|
+
booked.stopping = true;
|
|
1252
|
+
try {
|
|
1253
|
+
await abortTurn(booked, signal);
|
|
1254
|
+
} catch (cause) {
|
|
1255
|
+
await close(booked);
|
|
1256
|
+
throw safeError(cause);
|
|
1257
|
+
} finally {
|
|
1258
|
+
booked.stopping = false;
|
|
1259
|
+
}
|
|
1260
|
+
},
|
|
1261
|
+
respondToRequest: async () => {
|
|
1262
|
+
throw new Error("Pi does not expose native tool approvals.");
|
|
1263
|
+
},
|
|
1264
|
+
respondToUserInput: async (threadId, requestId, answers) => {
|
|
1265
|
+
const booked = requireThread(threadId), question = booked.questions.get(requestId);
|
|
1266
|
+
if (!question) return;
|
|
1267
|
+
const data = record(answers), entry = record(record(data.answers ?? data).value);
|
|
1268
|
+
const candidates = Array.isArray(entry.answers) ? entry.answers : [];
|
|
1269
|
+
const value2 = string(candidates[0]) || string(record(data.answers ?? data).value);
|
|
1270
|
+
const response = !value2 ? { cancelled: true } : question.method === "confirm" ? { confirmed: value2 === "Yes" } : { value: value2 };
|
|
1271
|
+
try {
|
|
1272
|
+
await connection(booked).rpc.respond({ id: question.nativeId, ...response });
|
|
1273
|
+
} catch (cause) {
|
|
1274
|
+
await close(booked);
|
|
1275
|
+
throw safeError(cause);
|
|
1276
|
+
}
|
|
1277
|
+
booked.questions.delete(requestId);
|
|
1278
|
+
booked.emit(
|
|
1279
|
+
"user-input.resolved",
|
|
1280
|
+
{ decision: value2 ? "answered" : "cancelled" },
|
|
1281
|
+
{ requestId }
|
|
1282
|
+
);
|
|
1283
|
+
},
|
|
1284
|
+
compactContext: async (threadId) => {
|
|
1285
|
+
const booked = requireThread(threadId);
|
|
1286
|
+
try {
|
|
1287
|
+
await connection(booked).rpc.request("compact", {}, void 0, 12e4);
|
|
1288
|
+
} catch (cause) {
|
|
1289
|
+
if (!(cause instanceof PiResponseError)) await close(booked);
|
|
1290
|
+
throw safeError(cause);
|
|
1291
|
+
}
|
|
1292
|
+
},
|
|
1293
|
+
stopSession: async (threadId) => {
|
|
1294
|
+
startEpochs.set(threadId, (startEpochs.get(threadId) ?? 0) + 1);
|
|
1295
|
+
const starting = starts.get(threadId), booked = threads.get(threadId);
|
|
1296
|
+
if (booked) {
|
|
1297
|
+
try {
|
|
1298
|
+
await close(booked);
|
|
1299
|
+
} catch (cause) {
|
|
1300
|
+
throw safeError(cause);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
await starting?.catch(() => {
|
|
1304
|
+
});
|
|
1305
|
+
const late = threads.get(threadId);
|
|
1306
|
+
if (late) {
|
|
1307
|
+
try {
|
|
1308
|
+
await close(late);
|
|
1309
|
+
} catch (cause) {
|
|
1310
|
+
throw safeError(cause);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
},
|
|
1314
|
+
readThread: async (threadId) => {
|
|
1315
|
+
const booked = requireThread(threadId);
|
|
1316
|
+
let result;
|
|
1317
|
+
try {
|
|
1318
|
+
result = record(await connection(booked).rpc.request("get_messages"));
|
|
1319
|
+
} catch (cause) {
|
|
1320
|
+
throw safeError(cause);
|
|
1321
|
+
}
|
|
1322
|
+
return {
|
|
1323
|
+
threadId,
|
|
1324
|
+
turns: [
|
|
1325
|
+
{
|
|
1326
|
+
id: booked.turn?.id ?? string(record(booked.session.resumeCursor).sessionId),
|
|
1327
|
+
items: Array.isArray(result.messages) ? result.messages : []
|
|
1328
|
+
}
|
|
1329
|
+
]
|
|
1330
|
+
};
|
|
1331
|
+
},
|
|
1332
|
+
events,
|
|
1333
|
+
listLiveProcesses: async () => [...threads.values()].filter((b) => b.connection && !b.exited).map((b) => ({
|
|
1334
|
+
pid: b.connection.rpc.process.pid,
|
|
1335
|
+
threadId: b.session.threadId,
|
|
1336
|
+
bindingId: host.bindingId,
|
|
1337
|
+
command: config.binaryPath
|
|
1338
|
+
})),
|
|
1339
|
+
dispose: async () => {
|
|
1340
|
+
disposed.abort();
|
|
1341
|
+
const closing = [...threads.values()].map(close);
|
|
1342
|
+
await Promise.allSettled([...probes, ...starts.values()]);
|
|
1343
|
+
const results = await Promise.allSettled(closing);
|
|
1344
|
+
events.close();
|
|
1345
|
+
const failure = results.find((result) => result.status === "rejected");
|
|
1346
|
+
if (failure?.status === "rejected") throw new Error(redact(config, failure.reason));
|
|
1347
|
+
},
|
|
1348
|
+
forceStop: async () => {
|
|
1349
|
+
await Promise.all(
|
|
1350
|
+
[.../* @__PURE__ */ new Set([...threads.keys(), ...starts.keys()])].map((id) => instance.stopSession(id))
|
|
1351
|
+
);
|
|
1352
|
+
}
|
|
1353
|
+
};
|
|
1354
|
+
const safely = (method) => async (...args) => {
|
|
1355
|
+
try {
|
|
1356
|
+
return await method(...args);
|
|
1357
|
+
} catch (cause) {
|
|
1358
|
+
throw safeError(cause);
|
|
1359
|
+
}
|
|
1360
|
+
};
|
|
1361
|
+
return {
|
|
1362
|
+
...instance,
|
|
1363
|
+
startSession: safely(instance.startSession),
|
|
1364
|
+
sendTurn: safely(instance.sendTurn),
|
|
1365
|
+
interruptTurn: safely(instance.interruptTurn),
|
|
1366
|
+
respondToUserInput: safely(instance.respondToUserInput),
|
|
1367
|
+
compactContext: safely(instance.compactContext),
|
|
1368
|
+
stopSession: safely(instance.stopSession),
|
|
1369
|
+
readThread: safely(instance.readThread),
|
|
1370
|
+
dispose: safely(instance.dispose),
|
|
1371
|
+
forceStop: safely(instance.forceStop)
|
|
1372
|
+
};
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
// src/index.ts
|
|
1376
|
+
var index_default = definePlugin({
|
|
1377
|
+
id: "agent-pi",
|
|
1378
|
+
apiVersion: 47,
|
|
1379
|
+
executors: [
|
|
1380
|
+
{ card: PI_CARD, createInstance: async (config, host) => createPiInstance(config, host) }
|
|
1381
|
+
],
|
|
1382
|
+
activate(ctx) {
|
|
1383
|
+
ctx.log.info("Pi executor ready");
|
|
1384
|
+
}
|
|
1385
|
+
});
|
|
1386
|
+
export {
|
|
1387
|
+
index_default as default
|
|
1388
|
+
};
|