pi-webdesk 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/README.md +111 -0
- package/dist/apps/daemon/src/appearance-preferences.js +218 -0
- package/dist/apps/daemon/src/auth.js +88 -0
- package/dist/apps/daemon/src/bin.js +123 -0
- package/dist/apps/daemon/src/cli.js +48 -0
- package/dist/apps/daemon/src/event-hub.js +155 -0
- package/dist/apps/daemon/src/index.js +102 -0
- package/dist/apps/daemon/src/launcher-control.js +114 -0
- package/dist/apps/daemon/src/launcher.js +73 -0
- package/dist/apps/daemon/src/pi-auth.js +290 -0
- package/dist/apps/daemon/src/pi-resources.js +182 -0
- package/dist/apps/daemon/src/pi-runtime-factory.js +19 -0
- package/dist/apps/daemon/src/pi-sessions.js +265 -0
- package/dist/apps/daemon/src/runtime-process.js +241 -0
- package/dist/apps/daemon/src/secret.js +71 -0
- package/dist/apps/daemon/src/server.js +1662 -0
- package/dist/apps/daemon/src/session-projection.js +117 -0
- package/dist/apps/daemon/src/state-lock.js +31 -0
- package/dist/apps/daemon/src/static-web.js +53 -0
- package/dist/apps/daemon/src/task-archive.js +152 -0
- package/dist/apps/daemon/src/task-commit.js +503 -0
- package/dist/apps/daemon/src/task-merge.js +912 -0
- package/dist/apps/daemon/src/task-review.js +204 -0
- package/dist/apps/daemon/src/task-runtime.js +1124 -0
- package/dist/apps/daemon/src/task-validation.js +352 -0
- package/dist/apps/daemon/src/workspace-store.js +140 -0
- package/dist/apps/daemon/src/workspace.js +795 -0
- package/dist/extensions/webdesk.js +34 -0
- package/dist/packages/git/src/commit.js +675 -0
- package/dist/packages/git/src/errors.js +55 -0
- package/dist/packages/git/src/fingerprint.js +286 -0
- package/dist/packages/git/src/index.js +123 -0
- package/dist/packages/git/src/merge.js +1008 -0
- package/dist/packages/git/src/paths.js +58 -0
- package/dist/packages/git/src/repository.js +77 -0
- package/dist/packages/git/src/review.js +396 -0
- package/dist/packages/git/src/runner.js +110 -0
- package/dist/packages/git/src/validation.js +263 -0
- package/dist/packages/git/src/worktree.js +233 -0
- package/dist/packages/pi-bridge/extensions/pita-policy.js +117 -0
- package/dist/packages/pi-bridge/src/auth.js +80 -0
- package/dist/packages/pi-bridge/src/errors.js +19 -0
- package/dist/packages/pi-bridge/src/handshake.js +43 -0
- package/dist/packages/pi-bridge/src/index.js +76 -0
- package/dist/packages/pi-bridge/src/jsonl.js +105 -0
- package/dist/packages/pi-bridge/src/policy-approval.js +62 -0
- package/dist/packages/pi-bridge/src/resolve.js +59 -0
- package/dist/packages/pi-bridge/src/resources-child.mjs +23 -0
- package/dist/packages/pi-bridge/src/resources.js +481 -0
- package/dist/packages/pi-bridge/src/rpc/client.js +480 -0
- package/dist/packages/pi-bridge/src/rpc/runtime.js +496 -0
- package/dist/packages/pi-bridge/src/rpc/supervisor.mjs +129 -0
- package/dist/packages/pi-bridge/src/rpc/tool-events.js +78 -0
- package/dist/packages/pi-bridge/src/rpc/wire.js +263 -0
- package/dist/packages/pi-bridge/src/runtime.js +0 -0
- package/dist/packages/pi-bridge/src/sessions-child.mjs +38 -0
- package/dist/packages/pi-bridge/src/sessions.js +314 -0
- package/dist/packages/pi-bridge/src/tool-activity.js +56 -0
- package/dist/packages/protocol/src/index.js +1863 -0
- package/dist/web/assets/index-BOw_fhvO.css +2 -0
- package/dist/web/assets/index-oXs7yAAo.js +119 -0
- package/dist/web/index.html +14 -0
- package/package.json +69 -0
- package/scripts/prepare.mjs +7 -0
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
// packages/pi-bridge/src/rpc/runtime.ts
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { extractToolCalls } from "../tool-activity.js";
|
|
4
|
+
import { extractToolResult, ToolEventProjection } from "./tool-events.js";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { PiBridgeError } from "../errors.js";
|
|
7
|
+
import { decodePolicyApproval } from "../policy-approval.js";
|
|
8
|
+
import {
|
|
9
|
+
PITA_POLICY_STATUS_KEY,
|
|
10
|
+
PITA_POLICY_VERSION,
|
|
11
|
+
decodePolicyStatus,
|
|
12
|
+
isSupportedPolicyVersion
|
|
13
|
+
} from "../handshake.js";
|
|
14
|
+
import { PiRpcClient } from "./client.js";
|
|
15
|
+
import {
|
|
16
|
+
extractEntryText,
|
|
17
|
+
getAvailableModelsDataSchema,
|
|
18
|
+
getEntriesDataSchema,
|
|
19
|
+
getStateDataSchema,
|
|
20
|
+
getTreeDataSchema,
|
|
21
|
+
rpcExtensionErrorEventSchema,
|
|
22
|
+
wireModelSchema
|
|
23
|
+
} from "./wire.js";
|
|
24
|
+
var DEFAULT_POLICY_EXTENSION_PATH = fileURLToPath(
|
|
25
|
+
new URL(import.meta.url.endsWith(".ts") ? "../../extensions/pita-policy.ts" : "../../extensions/pita-policy.js", import.meta.url)
|
|
26
|
+
);
|
|
27
|
+
var DEFAULT_STARTUP_TIMEOUT_MS = 3e4;
|
|
28
|
+
function createPiRpcRuntime(options) {
|
|
29
|
+
return new PiRpcRuntime(options);
|
|
30
|
+
}
|
|
31
|
+
var PiRpcRuntime = class {
|
|
32
|
+
#options;
|
|
33
|
+
#mode;
|
|
34
|
+
#client;
|
|
35
|
+
#listeners = /* @__PURE__ */ new Set();
|
|
36
|
+
#toolEvents = new ToolEventProjection();
|
|
37
|
+
#state = "created";
|
|
38
|
+
#startPromise = null;
|
|
39
|
+
#handshakeSettle = null;
|
|
40
|
+
constructor(options) {
|
|
41
|
+
if (options.sessionFile !== void 0) {
|
|
42
|
+
if ((options.sessionMode ?? "persistent") === "ephemeral") {
|
|
43
|
+
throw new PiBridgeError(
|
|
44
|
+
"PI_INVALID_CONFIGURATION",
|
|
45
|
+
"A Pi session file cannot be resumed in ephemeral mode."
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
if (!path.isAbsolute(options.sessionFile)) {
|
|
49
|
+
throw new PiBridgeError(
|
|
50
|
+
"PI_INVALID_CONFIGURATION",
|
|
51
|
+
"The Pi session file must be an absolute path."
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
this.#options = options;
|
|
56
|
+
this.#mode = options.mode ?? "supervised";
|
|
57
|
+
this.#client = new PiRpcClient({
|
|
58
|
+
executable: options.executable,
|
|
59
|
+
args: this.#buildArgs(),
|
|
60
|
+
cwd: options.cwd,
|
|
61
|
+
env: {
|
|
62
|
+
...options.env ?? process.env,
|
|
63
|
+
PITA_RUNTIME_MODE: this.#mode
|
|
64
|
+
},
|
|
65
|
+
...options.spawnProcess !== void 0 ? { spawnProcess: options.spawnProcess } : {},
|
|
66
|
+
...options.requestTimeoutMs !== void 0 ? { requestTimeoutMs: options.requestTimeoutMs } : {},
|
|
67
|
+
...options.maxRecordBytes !== void 0 ? { maxRecordBytes: options.maxRecordBytes } : {},
|
|
68
|
+
...options.supervisorIgnoreEofForTests === true ? { supervisorIgnoreEofForTests: true } : {},
|
|
69
|
+
handlers: {
|
|
70
|
+
onEvent: (record) => this.#handleEvent(record),
|
|
71
|
+
onExtensionUiRequest: (record) => this.#handleExtensionUiRequest(record),
|
|
72
|
+
onProtocolIssue: (issue) => {
|
|
73
|
+
if (issue.fatal && this.#state !== "exited") this.#setState("failed");
|
|
74
|
+
this.#emit({ kind: "protocol-issue", ...issue });
|
|
75
|
+
},
|
|
76
|
+
onExit: (info) => this.#handleExit(info)
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
get lifecycleState() {
|
|
81
|
+
return this.#state;
|
|
82
|
+
}
|
|
83
|
+
start(options = {}) {
|
|
84
|
+
if (this.#startPromise !== null) return this.#startPromise;
|
|
85
|
+
this.#startPromise = this.#start(options);
|
|
86
|
+
return this.#startPromise;
|
|
87
|
+
}
|
|
88
|
+
async #start(options) {
|
|
89
|
+
this.#setState("starting");
|
|
90
|
+
const timeoutMs = this.#options.startupTimeoutMs ?? DEFAULT_STARTUP_TIMEOUT_MS;
|
|
91
|
+
const handshakePromise = new Promise((resolve, reject) => {
|
|
92
|
+
this.#handshakeSettle = { resolve, reject };
|
|
93
|
+
});
|
|
94
|
+
void handshakePromise.catch(() => void 0);
|
|
95
|
+
const timer = setTimeout(() => {
|
|
96
|
+
this.#failStartup(
|
|
97
|
+
new PiBridgeError(
|
|
98
|
+
"PI_STARTUP_TIMEOUT",
|
|
99
|
+
`Did not receive the pita-policy handshake within ${timeoutMs}ms. Ensure the Webdesk policy extension is loaded with -e and that this Pi version emits extension_ui_request setStatus records in RPC mode.`,
|
|
100
|
+
{ details: { timeoutMs } }
|
|
101
|
+
)
|
|
102
|
+
);
|
|
103
|
+
}, timeoutMs);
|
|
104
|
+
timer.unref?.();
|
|
105
|
+
try {
|
|
106
|
+
await this.#client.start();
|
|
107
|
+
const pid = this.#client.processId;
|
|
108
|
+
if (pid === null) {
|
|
109
|
+
throw new PiBridgeError(
|
|
110
|
+
"PI_SPAWN_FAILED",
|
|
111
|
+
"Pi spawned without a valid operating-system process id"
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
await options.onProcessStarted?.(pid);
|
|
115
|
+
this.#client.activateSupervisor();
|
|
116
|
+
const handshake = await handshakePromise;
|
|
117
|
+
this.#setState("ready");
|
|
118
|
+
return handshake;
|
|
119
|
+
} catch (error) {
|
|
120
|
+
if (this.#state !== "exited") this.#setState("failed");
|
|
121
|
+
try {
|
|
122
|
+
this.#client.kill("SIGKILL");
|
|
123
|
+
} catch {
|
|
124
|
+
}
|
|
125
|
+
throw error;
|
|
126
|
+
} finally {
|
|
127
|
+
clearTimeout(timer);
|
|
128
|
+
this.#handshakeSettle = null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
async prompt(message, options) {
|
|
132
|
+
this.#requireReady("prompt");
|
|
133
|
+
await this.#client.request({
|
|
134
|
+
type: "prompt",
|
|
135
|
+
message,
|
|
136
|
+
...options?.streamingBehavior !== void 0 ? { streamingBehavior: options.streamingBehavior } : {}
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
async steer(message) {
|
|
140
|
+
this.#requireReady("steer");
|
|
141
|
+
await this.#client.request({ type: "steer", message });
|
|
142
|
+
}
|
|
143
|
+
async followUp(message) {
|
|
144
|
+
this.#requireReady("follow_up");
|
|
145
|
+
await this.#client.request({ type: "follow_up", message });
|
|
146
|
+
}
|
|
147
|
+
async abort() {
|
|
148
|
+
this.#requireReady("abort");
|
|
149
|
+
await this.#client.request({ type: "abort" });
|
|
150
|
+
}
|
|
151
|
+
async getState() {
|
|
152
|
+
this.#requireReady("get_state");
|
|
153
|
+
const response = await this.#client.request({ type: "get_state" });
|
|
154
|
+
const data = this.#parseData(getStateDataSchema, response.data, "get_state");
|
|
155
|
+
return {
|
|
156
|
+
model: data.model === null || data.model === void 0 ? null : { id: data.model.id, provider: data.model.provider, name: data.model.name },
|
|
157
|
+
isStreaming: data.isStreaming,
|
|
158
|
+
messageCount: data.messageCount,
|
|
159
|
+
sessionFile: data.sessionFile ?? null,
|
|
160
|
+
sessionId: data.sessionId ?? null,
|
|
161
|
+
sessionName: data.sessionName ?? null
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
async getAvailableModels() {
|
|
165
|
+
this.#requireReady("get_available_models");
|
|
166
|
+
const response = await this.#client.request({ type: "get_available_models" });
|
|
167
|
+
const data = this.#parseData(
|
|
168
|
+
getAvailableModelsDataSchema,
|
|
169
|
+
response.data,
|
|
170
|
+
"get_available_models"
|
|
171
|
+
);
|
|
172
|
+
return data.models.map(mapModel);
|
|
173
|
+
}
|
|
174
|
+
async setModel(provider, modelId) {
|
|
175
|
+
this.#requireReady("set_model");
|
|
176
|
+
const response = await this.#client.request({
|
|
177
|
+
type: "set_model",
|
|
178
|
+
provider,
|
|
179
|
+
modelId
|
|
180
|
+
});
|
|
181
|
+
const model = this.#parseData(wireModelSchema, response.data, "set_model");
|
|
182
|
+
return mapModel(model);
|
|
183
|
+
}
|
|
184
|
+
async getEntries(since) {
|
|
185
|
+
this.#requireReady("get_entries");
|
|
186
|
+
const response = await this.#client.request({
|
|
187
|
+
type: "get_entries",
|
|
188
|
+
...since !== void 0 ? { since } : {}
|
|
189
|
+
});
|
|
190
|
+
const data = this.#parseData(getEntriesDataSchema, response.data, "get_entries");
|
|
191
|
+
return {
|
|
192
|
+
entries: data.entries.map((entry) => mapEntrySummary(entry)),
|
|
193
|
+
leafId: data.leafId
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
async getTree() {
|
|
197
|
+
this.#requireReady("get_tree");
|
|
198
|
+
const response = await this.#client.request({ type: "get_tree" });
|
|
199
|
+
const data = this.#parseData(getTreeDataSchema, response.data, "get_tree");
|
|
200
|
+
return {
|
|
201
|
+
roots: data.tree.map((node) => mapTreeNode(node)),
|
|
202
|
+
leafId: data.leafId,
|
|
203
|
+
truncated: data.truncated
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
subscribe(listener) {
|
|
207
|
+
this.#listeners.add(listener);
|
|
208
|
+
return () => this.#listeners.delete(listener);
|
|
209
|
+
}
|
|
210
|
+
respondToDialog(reply) {
|
|
211
|
+
switch (reply.kind) {
|
|
212
|
+
case "confirm":
|
|
213
|
+
this.#client.sendExtensionUiResponse({
|
|
214
|
+
type: "extension_ui_response",
|
|
215
|
+
id: reply.requestId,
|
|
216
|
+
confirmed: reply.confirmed
|
|
217
|
+
});
|
|
218
|
+
return;
|
|
219
|
+
case "value":
|
|
220
|
+
this.#client.sendExtensionUiResponse({
|
|
221
|
+
type: "extension_ui_response",
|
|
222
|
+
id: reply.requestId,
|
|
223
|
+
value: reply.value
|
|
224
|
+
});
|
|
225
|
+
return;
|
|
226
|
+
case "cancel":
|
|
227
|
+
this.#client.sendExtensionUiResponse({
|
|
228
|
+
type: "extension_ui_response",
|
|
229
|
+
id: reply.requestId,
|
|
230
|
+
cancelled: true
|
|
231
|
+
});
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
async dispose() {
|
|
236
|
+
if (this.#state === "created") {
|
|
237
|
+
this.#setState("exited");
|
|
238
|
+
return { code: null, signal: null };
|
|
239
|
+
}
|
|
240
|
+
const info = await this.#client.stop();
|
|
241
|
+
return info;
|
|
242
|
+
}
|
|
243
|
+
#buildArgs() {
|
|
244
|
+
return [
|
|
245
|
+
...this.#options.prependArgs ?? [],
|
|
246
|
+
"--mode",
|
|
247
|
+
"rpc",
|
|
248
|
+
...(this.#options.sessionMode ?? "persistent") === "ephemeral" ? ["--no-session"] : this.#options.sessionFile === void 0 ? [] : ["--session", path.normalize(this.#options.sessionFile)],
|
|
249
|
+
"-e",
|
|
250
|
+
this.#options.policyExtensionPath ?? DEFAULT_POLICY_EXTENSION_PATH,
|
|
251
|
+
...this.#options.extraArgs ?? []
|
|
252
|
+
];
|
|
253
|
+
}
|
|
254
|
+
#requireReady(operation) {
|
|
255
|
+
if (this.#state !== "ready") {
|
|
256
|
+
throw new PiBridgeError(
|
|
257
|
+
"PI_RUNTIME_NOT_READY",
|
|
258
|
+
`Cannot ${operation}: runtime is ${this.#state} (requires ready)`,
|
|
259
|
+
{ details: { state: this.#state } }
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
#parseData(schema, value, commandType) {
|
|
264
|
+
const result = schema.safeParse(value);
|
|
265
|
+
if (!result.success) {
|
|
266
|
+
throw new PiBridgeError(
|
|
267
|
+
"PI_PROTOCOL_VIOLATION",
|
|
268
|
+
`Pi returned malformed ${commandType} data: ${result.error.message}`,
|
|
269
|
+
{ details: { commandType } }
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
return result.data;
|
|
273
|
+
}
|
|
274
|
+
#handleExtensionUiRequest(record) {
|
|
275
|
+
if (record.method === "setStatus" && record.statusKey === PITA_POLICY_STATUS_KEY) {
|
|
276
|
+
this.#handlePolicyStatus(record);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const request = mapDialogRequest(record);
|
|
280
|
+
if (request !== null) {
|
|
281
|
+
this.#emit({ kind: "dialog-requested", request });
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
if (record.method === "notify") {
|
|
285
|
+
this.#emit({
|
|
286
|
+
kind: "notification",
|
|
287
|
+
message: record.message ?? "",
|
|
288
|
+
level: record.notifyType ?? "info"
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
#handlePolicyStatus(record) {
|
|
293
|
+
const decoded = decodePolicyStatus(record.statusText);
|
|
294
|
+
if (!decoded.ok) {
|
|
295
|
+
this.#failStartup(
|
|
296
|
+
new PiBridgeError(
|
|
297
|
+
"PI_POLICY_HANDSHAKE_INCOMPATIBLE",
|
|
298
|
+
`Webdesk policy handshake is invalid: ${decoded.reason}`
|
|
299
|
+
)
|
|
300
|
+
);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
if (!isSupportedPolicyVersion(decoded.status.version)) {
|
|
304
|
+
this.#failStartup(
|
|
305
|
+
new PiBridgeError(
|
|
306
|
+
"PI_POLICY_HANDSHAKE_INCOMPATIBLE",
|
|
307
|
+
`Webdesk policy extension announced version ${decoded.status.version}, but this bridge supports version ${PITA_POLICY_VERSION}`,
|
|
308
|
+
{ details: { announced: decoded.status.version, supported: PITA_POLICY_VERSION } }
|
|
309
|
+
)
|
|
310
|
+
);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (decoded.status.mode !== this.#mode) {
|
|
314
|
+
this.#failStartup(
|
|
315
|
+
new PiBridgeError(
|
|
316
|
+
"PI_POLICY_HANDSHAKE_INCOMPATIBLE",
|
|
317
|
+
`Webdesk policy extension announced ${decoded.status.mode} mode, but this runtime requires ${this.#mode} mode`,
|
|
318
|
+
{ details: { announced: decoded.status.mode, required: this.#mode } }
|
|
319
|
+
)
|
|
320
|
+
);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
this.#emit({ kind: "policy-status", status: decoded.status });
|
|
324
|
+
this.#handshakeSettle?.resolve({
|
|
325
|
+
version: decoded.status.version,
|
|
326
|
+
mode: decoded.status.mode
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
#handleEvent(record) {
|
|
330
|
+
for (const tool of this.#toolEvents.accept(record)) this.#emit({ kind: "tool-activity", tool });
|
|
331
|
+
if (record.type === "extension_error") {
|
|
332
|
+
const parsed = rpcExtensionErrorEventSchema.safeParse(record);
|
|
333
|
+
const message = parsed.success ? parsed.data.error ?? "unknown extension error" : "unknown extension error";
|
|
334
|
+
const extensionPath = parsed.success ? parsed.data.extensionPath ?? null : null;
|
|
335
|
+
if (this.#handshakeSettle !== null) {
|
|
336
|
+
this.#failStartup(
|
|
337
|
+
new PiBridgeError(
|
|
338
|
+
"PI_STARTUP_FAILED",
|
|
339
|
+
`Extension error during startup${extensionPath === null ? "" : ` (${extensionPath})`}: ${message}`
|
|
340
|
+
)
|
|
341
|
+
);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
this.#emit({ kind: "extension-error", message, extensionPath });
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
switch (record.type) {
|
|
348
|
+
case "agent_start":
|
|
349
|
+
this.#emit({ kind: "agent-activity", phase: "agent-start" });
|
|
350
|
+
return;
|
|
351
|
+
case "agent_end":
|
|
352
|
+
this.#emit({ kind: "agent-activity", phase: "agent-end" });
|
|
353
|
+
return;
|
|
354
|
+
case "agent_settled":
|
|
355
|
+
this.#emit({ kind: "agent-activity", phase: "agent-settled" });
|
|
356
|
+
return;
|
|
357
|
+
case "turn_start":
|
|
358
|
+
this.#emit({ kind: "agent-activity", phase: "turn-start" });
|
|
359
|
+
return;
|
|
360
|
+
case "turn_end":
|
|
361
|
+
this.#emit({ kind: "agent-activity", phase: "turn-end" });
|
|
362
|
+
return;
|
|
363
|
+
case "message_update": {
|
|
364
|
+
const delta = extractTextDelta(record);
|
|
365
|
+
if (delta !== null) this.#emit({ kind: "assistant-delta", text: delta });
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
default:
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
#handleExit(info) {
|
|
373
|
+
const exitedDuringStartup = this.#state === "starting";
|
|
374
|
+
if (this.#handshakeSettle !== null) {
|
|
375
|
+
const stderr = this.#client.stderrTail.trim();
|
|
376
|
+
this.#failStartup(
|
|
377
|
+
new PiBridgeError(
|
|
378
|
+
"PI_STARTUP_FAILED",
|
|
379
|
+
`Pi exited during startup (code ${info.code}, signal ${info.signal})` + (stderr === "" ? "" : `; stderr tail:
|
|
380
|
+
${stderr}`),
|
|
381
|
+
{ details: { code: info.code, signal: info.signal } }
|
|
382
|
+
)
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
if (exitedDuringStartup) {
|
|
386
|
+
this.#setState("failed");
|
|
387
|
+
} else if (this.#state !== "failed") {
|
|
388
|
+
this.#setState("exited");
|
|
389
|
+
}
|
|
390
|
+
this.#emit({ kind: "exited", code: info.code, signal: info.signal });
|
|
391
|
+
}
|
|
392
|
+
#failStartup(error) {
|
|
393
|
+
const settle = this.#handshakeSettle;
|
|
394
|
+
this.#handshakeSettle = null;
|
|
395
|
+
settle?.reject(error);
|
|
396
|
+
}
|
|
397
|
+
#setState(state) {
|
|
398
|
+
if (this.#state === state) return;
|
|
399
|
+
this.#state = state;
|
|
400
|
+
this.#emit({ kind: "lifecycle", state });
|
|
401
|
+
}
|
|
402
|
+
#emit(event) {
|
|
403
|
+
for (const listener of this.#listeners) listener(event);
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
function mapModel(model) {
|
|
407
|
+
return {
|
|
408
|
+
id: model.id,
|
|
409
|
+
...model.provider === void 0 ? {} : { provider: model.provider },
|
|
410
|
+
...model.name === void 0 ? {} : { name: model.name }
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
function mapDialogRequest(record) {
|
|
414
|
+
switch (record.method) {
|
|
415
|
+
case "confirm": {
|
|
416
|
+
const approval = decodePolicyApproval(record.title, record.message);
|
|
417
|
+
if (approval !== null) {
|
|
418
|
+
const transportTimeoutMs = Number.isSafeInteger(record.timeout) && (record.timeout ?? 0) > 0 ? record.timeout : approval.timeoutMs;
|
|
419
|
+
return {
|
|
420
|
+
requestId: record.id,
|
|
421
|
+
kind: "approval",
|
|
422
|
+
toolCallId: approval.toolCallId,
|
|
423
|
+
toolName: approval.toolName,
|
|
424
|
+
summary: approval.summary,
|
|
425
|
+
requestedAtMs: approval.requestedAtMs,
|
|
426
|
+
timeoutMs: Math.min(approval.timeoutMs, transportTimeoutMs)
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
return {
|
|
430
|
+
requestId: record.id,
|
|
431
|
+
kind: "confirm",
|
|
432
|
+
title: record.title ?? "",
|
|
433
|
+
message: record.message ?? "",
|
|
434
|
+
timeoutMs: record.timeout
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
case "select":
|
|
438
|
+
return {
|
|
439
|
+
requestId: record.id,
|
|
440
|
+
kind: "select",
|
|
441
|
+
title: record.title ?? "",
|
|
442
|
+
options: record.options ?? [],
|
|
443
|
+
timeoutMs: record.timeout
|
|
444
|
+
};
|
|
445
|
+
case "input":
|
|
446
|
+
return {
|
|
447
|
+
requestId: record.id,
|
|
448
|
+
kind: "input",
|
|
449
|
+
title: record.title ?? "",
|
|
450
|
+
placeholder: record.placeholder,
|
|
451
|
+
timeoutMs: record.timeout
|
|
452
|
+
};
|
|
453
|
+
case "editor":
|
|
454
|
+
return {
|
|
455
|
+
requestId: record.id,
|
|
456
|
+
kind: "editor",
|
|
457
|
+
title: record.title ?? "",
|
|
458
|
+
prefill: record.prefill
|
|
459
|
+
};
|
|
460
|
+
default:
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
function mapEntrySummary(entry) {
|
|
465
|
+
const text = entry.message === void 0 ? null : extractEntryText(entry.message.content);
|
|
466
|
+
const toolCalls = entry.message?.role === "assistant" ? extractToolCalls(entry.message.content) : [];
|
|
467
|
+
const toolResult = extractToolResult(entry.message);
|
|
468
|
+
return {
|
|
469
|
+
id: entry.id,
|
|
470
|
+
parentId: entry.parentId ?? null,
|
|
471
|
+
type: entry.type,
|
|
472
|
+
...entry.message === void 0 ? {} : { role: entry.message.role },
|
|
473
|
+
...text === null ? {} : { text },
|
|
474
|
+
...toolCalls.length === 0 ? {} : { toolCalls },
|
|
475
|
+
...toolResult === void 0 ? {} : { toolResult }
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
function mapTreeNode(node) {
|
|
479
|
+
return {
|
|
480
|
+
entry: mapEntrySummary(node.entry),
|
|
481
|
+
children: node.children.map((child) => mapTreeNode(child)),
|
|
482
|
+
label: node.label
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
function extractTextDelta(record) {
|
|
486
|
+
const event = record["assistantMessageEvent"];
|
|
487
|
+
if (typeof event !== "object" || event === null) return null;
|
|
488
|
+
const delta = event;
|
|
489
|
+
if (delta["type"] !== "text_delta") return null;
|
|
490
|
+
return typeof delta["delta"] === "string" ? delta["delta"] : null;
|
|
491
|
+
}
|
|
492
|
+
export {
|
|
493
|
+
DEFAULT_POLICY_EXTENSION_PATH,
|
|
494
|
+
PiRpcRuntime,
|
|
495
|
+
createPiRpcRuntime
|
|
496
|
+
};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// packages/pi-bridge/src/rpc/supervisor.mjs
|
|
4
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
5
|
+
import { constants as osConstants } from "node:os";
|
|
6
|
+
var ACTIVATION_LINE = "PITA_ACTIVATE\n";
|
|
7
|
+
var MAX_GATE_BYTES = 128;
|
|
8
|
+
var SHUTDOWN_GRACE_MS = 3e3;
|
|
9
|
+
var executable = process.env.PITA_SUPERVISOR_EXECUTABLE;
|
|
10
|
+
var encodedArgs = process.env.PITA_SUPERVISOR_ARGS;
|
|
11
|
+
var launchId = process.argv[2] === "--launch-id" ? process.argv[3] : void 0;
|
|
12
|
+
if (executable === void 0 || encodedArgs === void 0 || launchId === void 0 || !/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(
|
|
13
|
+
launchId
|
|
14
|
+
)) {
|
|
15
|
+
process.exit(70);
|
|
16
|
+
}
|
|
17
|
+
var args;
|
|
18
|
+
try {
|
|
19
|
+
const parsed = JSON.parse(encodedArgs);
|
|
20
|
+
if (!Array.isArray(parsed) || !parsed.every((value) => typeof value === "string")) {
|
|
21
|
+
throw new Error("invalid args");
|
|
22
|
+
}
|
|
23
|
+
args = parsed;
|
|
24
|
+
} catch {
|
|
25
|
+
process.exit(70);
|
|
26
|
+
}
|
|
27
|
+
var child = null;
|
|
28
|
+
var gate = Buffer.alloc(0);
|
|
29
|
+
var shuttingDown = false;
|
|
30
|
+
var gracefulTimer = null;
|
|
31
|
+
var exitCode = 0;
|
|
32
|
+
function groupMembers() {
|
|
33
|
+
try {
|
|
34
|
+
const output = execFileSync("ps", ["-axo", "pid=,pgid=,stat="], {
|
|
35
|
+
encoding: "utf8",
|
|
36
|
+
env: { ...process.env, LC_ALL: "C", LANG: "C" },
|
|
37
|
+
timeout: 1e3,
|
|
38
|
+
maxBuffer: 1024 * 1024,
|
|
39
|
+
// Otherwise the ps probe inherits this supervisor's group and observes
|
|
40
|
+
// itself as the final live member forever.
|
|
41
|
+
detached: true
|
|
42
|
+
});
|
|
43
|
+
return output.split("\n").map((line) => line.trim().split(/\s+/)).filter(
|
|
44
|
+
([pid, pgid, stat]) => Number(pid) !== process.pid && Number(pgid) === process.pid && stat !== void 0 && !stat.startsWith("Z")
|
|
45
|
+
).map(([pid]) => Number(pid)).filter((pid) => Number.isSafeInteger(pid) && pid > 0);
|
|
46
|
+
} catch {
|
|
47
|
+
return [1];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function finishWhenGroupIsEmpty(deadline) {
|
|
51
|
+
if (groupMembers().length === 0) process.exit(exitCode);
|
|
52
|
+
if (Date.now() >= deadline) {
|
|
53
|
+
try {
|
|
54
|
+
process.kill(-process.pid, "SIGKILL");
|
|
55
|
+
} catch {
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
setTimeout(() => finishWhenGroupIsEmpty(deadline), 25);
|
|
61
|
+
}
|
|
62
|
+
function shutdown(code = exitCode) {
|
|
63
|
+
if (shuttingDown) return;
|
|
64
|
+
shuttingDown = true;
|
|
65
|
+
exitCode = code;
|
|
66
|
+
if (gracefulTimer !== null) clearTimeout(gracefulTimer);
|
|
67
|
+
gracefulTimer = null;
|
|
68
|
+
if (child !== null) process.stdin.unpipe(child.stdin);
|
|
69
|
+
try {
|
|
70
|
+
process.kill(-process.pid, "SIGTERM");
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (error?.code !== "ESRCH") exitCode = 1;
|
|
73
|
+
}
|
|
74
|
+
finishWhenGroupIsEmpty(Date.now() + SHUTDOWN_GRACE_MS);
|
|
75
|
+
}
|
|
76
|
+
function beginGracefulShutdown() {
|
|
77
|
+
if (shuttingDown || gracefulTimer !== null) return;
|
|
78
|
+
if (child === null) process.exit(0);
|
|
79
|
+
process.stdin.unpipe(child.stdin);
|
|
80
|
+
child.stdin.end();
|
|
81
|
+
gracefulTimer = setTimeout(() => shutdown(), SHUTDOWN_GRACE_MS);
|
|
82
|
+
}
|
|
83
|
+
function childResult(code, signal) {
|
|
84
|
+
if (code !== null) return code;
|
|
85
|
+
const signalNumber = signal === null ? void 0 : osConstants.signals[signal];
|
|
86
|
+
return signalNumber === void 0 ? 1 : 128 + signalNumber;
|
|
87
|
+
}
|
|
88
|
+
process.on("SIGTERM", () => shutdown());
|
|
89
|
+
process.on("SIGINT", () => shutdown());
|
|
90
|
+
process.stdout.on("error", () => shutdown(1));
|
|
91
|
+
process.stderr.on("error", () => shutdown(1));
|
|
92
|
+
process.on("uncaughtException", () => shutdown(1));
|
|
93
|
+
function activate(remainder) {
|
|
94
|
+
const childEnv = { ...process.env };
|
|
95
|
+
delete childEnv.PITA_SUPERVISOR_EXECUTABLE;
|
|
96
|
+
delete childEnv.PITA_SUPERVISOR_ARGS;
|
|
97
|
+
delete childEnv.PITA_SUPERVISOR_IGNORE_EOF;
|
|
98
|
+
child = spawn(executable, args, {
|
|
99
|
+
cwd: process.cwd(),
|
|
100
|
+
env: childEnv,
|
|
101
|
+
shell: false,
|
|
102
|
+
detached: false,
|
|
103
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
104
|
+
});
|
|
105
|
+
child.stdout?.pipe(process.stdout);
|
|
106
|
+
child.stderr?.pipe(process.stderr);
|
|
107
|
+
child.stdin?.on("error", () => shutdown(72));
|
|
108
|
+
if (remainder.length > 0) child.stdin?.write(remainder);
|
|
109
|
+
process.stdin.pipe(child.stdin);
|
|
110
|
+
child.on("error", () => shutdown(71));
|
|
111
|
+
child.on("exit", (code, signal) => shutdown(childResult(code, signal)));
|
|
112
|
+
}
|
|
113
|
+
function onGateData(chunk) {
|
|
114
|
+
gate = Buffer.concat([gate, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)]);
|
|
115
|
+
if (gate.length > MAX_GATE_BYTES) process.exit(70);
|
|
116
|
+
const newline = gate.indexOf(10);
|
|
117
|
+
if (newline < 0) return;
|
|
118
|
+
process.stdin.off("data", onGateData);
|
|
119
|
+
const line = gate.subarray(0, newline + 1).toString("utf8");
|
|
120
|
+
if (line !== ACTIVATION_LINE) process.exit(70);
|
|
121
|
+
activate(gate.subarray(newline + 1));
|
|
122
|
+
gate = Buffer.alloc(0);
|
|
123
|
+
}
|
|
124
|
+
process.stdin.on("data", onGateData);
|
|
125
|
+
process.stdin.on("end", () => {
|
|
126
|
+
if (child === null) process.exit(0);
|
|
127
|
+
if (process.env.PITA_SUPERVISOR_IGNORE_EOF !== "1") beginGracefulShutdown();
|
|
128
|
+
});
|
|
129
|
+
process.stdin.resume();
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// packages/pi-bridge/src/rpc/tool-events.ts
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { boundedToolText, extractToolCalls, toolCallProjection, toolResultText } from "../tool-activity.js";
|
|
4
|
+
var resultMessageSchema = z.object({
|
|
5
|
+
role: z.literal("toolResult"),
|
|
6
|
+
toolCallId: z.string().min(1).max(500),
|
|
7
|
+
toolName: z.string().min(1).max(200),
|
|
8
|
+
content: z.unknown(),
|
|
9
|
+
isError: z.boolean()
|
|
10
|
+
});
|
|
11
|
+
function extractToolResult(message) {
|
|
12
|
+
const parsed = resultMessageSchema.safeParse(message);
|
|
13
|
+
if (!parsed.success) return void 0;
|
|
14
|
+
const output = toolResultText(parsed.data.content);
|
|
15
|
+
return {
|
|
16
|
+
toolCallId: parsed.data.toolCallId,
|
|
17
|
+
name: boundedToolText(parsed.data.toolName, 200).text,
|
|
18
|
+
output: output.text,
|
|
19
|
+
omittedChars: output.omitted,
|
|
20
|
+
isError: parsed.data.isError
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
var executionSchema = z.object({
|
|
24
|
+
type: z.enum(["tool_execution_start", "tool_execution_update", "tool_execution_end"]),
|
|
25
|
+
toolCallId: z.string().min(1).max(500),
|
|
26
|
+
toolName: z.string().min(1).max(200),
|
|
27
|
+
args: z.unknown().optional(),
|
|
28
|
+
partialResult: z.object({ content: z.unknown() }).optional(),
|
|
29
|
+
result: z.object({ content: z.unknown() }).optional(),
|
|
30
|
+
isError: z.boolean().optional()
|
|
31
|
+
});
|
|
32
|
+
var ToolEventProjection = class {
|
|
33
|
+
#pending = /* @__PURE__ */ new Map();
|
|
34
|
+
accept(record) {
|
|
35
|
+
if (record.type === "message_end") {
|
|
36
|
+
const message = z.object({ role: z.string(), content: z.unknown() }).safeParse(record.message);
|
|
37
|
+
if (message.success && message.data.role === "assistant") {
|
|
38
|
+
const calls = extractToolCalls(message.data.content);
|
|
39
|
+
for (const call3 of calls) this.#remember(call3);
|
|
40
|
+
return calls;
|
|
41
|
+
}
|
|
42
|
+
const result = extractToolResult(record.message);
|
|
43
|
+
const call2 = result && this.#pending.get(result.toolCallId);
|
|
44
|
+
if (call2 && result) {
|
|
45
|
+
this.#pending.delete(call2.toolCallId);
|
|
46
|
+
return [{ ...call2, status: result.isError ? "failed" : "succeeded", output: result.output, outputOmittedChars: result.omittedChars }];
|
|
47
|
+
}
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
if (record.type === "agent_end" || record.type === "agent_settled") {
|
|
51
|
+
const interrupted = [...this.#pending.values()].map((call2) => ({ ...call2, status: "incomplete" }));
|
|
52
|
+
this.#pending.clear();
|
|
53
|
+
return interrupted;
|
|
54
|
+
}
|
|
55
|
+
const parsed = executionSchema.safeParse(record);
|
|
56
|
+
if (!parsed.success) return [];
|
|
57
|
+
const event = parsed.data;
|
|
58
|
+
if (event.type === "tool_execution_end" && (event.result === void 0 || event.isError === void 0)) return [];
|
|
59
|
+
const call = this.#pending.get(event.toolCallId) ?? toolCallProjection(event.toolCallId, event.toolName, event.args);
|
|
60
|
+
const output = toolResultText((event.result ?? event.partialResult)?.content);
|
|
61
|
+
const next = {
|
|
62
|
+
...call,
|
|
63
|
+
status: event.type === "tool_execution_end" ? event.isError ? "failed" : "succeeded" : "running",
|
|
64
|
+
...event.result || event.partialResult ? { output: output.text, outputOmittedChars: output.omitted } : {}
|
|
65
|
+
};
|
|
66
|
+
if (event.type === "tool_execution_end") this.#pending.delete(call.toolCallId);
|
|
67
|
+
else this.#remember(next);
|
|
68
|
+
return [next];
|
|
69
|
+
}
|
|
70
|
+
#remember(call) {
|
|
71
|
+
this.#pending.set(call.toolCallId, call);
|
|
72
|
+
if (this.#pending.size > 200) this.#pending.delete(this.#pending.keys().next().value);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
export {
|
|
76
|
+
ToolEventProjection,
|
|
77
|
+
extractToolResult
|
|
78
|
+
};
|