@averyyy/pi-server 0.80.3-piclient.10
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/CHANGELOG.md +21 -0
- package/README.md +46 -0
- package/bin/pi-server.js +26 -0
- package/bin/update.js +86 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +4 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +8 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +40 -0
- package/dist/config.js.map +1 -0
- package/dist/event-encoding.d.ts +5 -0
- package/dist/event-encoding.d.ts.map +1 -0
- package/dist/event-encoding.js +15 -0
- package/dist/event-encoding.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/pi-server-protocol.d.ts +13 -0
- package/dist/pi-server-protocol.d.ts.map +1 -0
- package/dist/pi-server-protocol.js +32 -0
- package/dist/pi-server-protocol.js.map +1 -0
- package/dist/request-chunks.d.ts +37 -0
- package/dist/request-chunks.d.ts.map +1 -0
- package/dist/request-chunks.js +167 -0
- package/dist/request-chunks.js.map +1 -0
- package/dist/server.d.ts +23 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +735 -0
- package/dist/server.js.map +1 -0
- package/dist/session-persistence.d.ts +5 -0
- package/dist/session-persistence.d.ts.map +1 -0
- package/dist/session-persistence.js +259 -0
- package/dist/session-persistence.js.map +1 -0
- package/dist/session-store.d.ts +75 -0
- package/dist/session-store.d.ts.map +1 -0
- package/dist/session-store.js +293 -0
- package/dist/session-store.js.map +1 -0
- package/package.json +58 -0
package/dist/server.js
ADDED
|
@@ -0,0 +1,735 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import { compact, DEFAULT_COMPACTION_SETTINGS, prepareCompaction, } from "@earendil-works/pi-agent-core";
|
|
3
|
+
import { createModels, createProvider, } from "@earendil-works/pi-ai";
|
|
4
|
+
import { streamSimple } from "@earendil-works/pi-ai/compat";
|
|
5
|
+
import { loadConfig } from "./config.js";
|
|
6
|
+
import { encodeErrorEvent, encodeProxyEvent } from "./event-encoding.js";
|
|
7
|
+
import { CHUNK_ENDPOINT, receiveRequestChunk } from "./request-chunks.js";
|
|
8
|
+
import { deletePersistedSession, loadPersistedSessions, savePersistedSession } from "./session-persistence.js";
|
|
9
|
+
import { appendCompactionEntry, appendMessages, appendSessionEntries, deleteSession as deleteSessionFromStore, dropLastAssistantError, getOrCreateSession, getSession, getSessionBranch, listSessions, replaceMessages, replaceSessionTree, setStaticContext, switchSessionLeaf, } from "./session-store.js";
|
|
10
|
+
export { loadConfig } from "./config.js";
|
|
11
|
+
function createRequestModels(model, options) {
|
|
12
|
+
const models = createModels();
|
|
13
|
+
models.setProvider(createProvider({
|
|
14
|
+
id: model.provider,
|
|
15
|
+
name: model.provider,
|
|
16
|
+
models: [model],
|
|
17
|
+
auth: {
|
|
18
|
+
apiKey: {
|
|
19
|
+
name: "pi-server request auth",
|
|
20
|
+
resolve: async () => ({ auth: { apiKey: options.apiKey, headers: options.headers } }),
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
api: {
|
|
24
|
+
stream: (requestModel, context, streamOptions) => streamSimple(requestModel, context, streamOptions),
|
|
25
|
+
streamSimple: (requestModel, context, streamOptions) => streamSimple(requestModel, context, streamOptions),
|
|
26
|
+
},
|
|
27
|
+
}));
|
|
28
|
+
return models;
|
|
29
|
+
}
|
|
30
|
+
const STREAM_HEARTBEAT = ": keep-alive\n\n";
|
|
31
|
+
const JSON_HEARTBEAT = " \n";
|
|
32
|
+
const STREAM_HEARTBEAT_INTERVAL_MS = 25_000;
|
|
33
|
+
const streamRuns = new Map();
|
|
34
|
+
function readBody(req) {
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
const chunks = [];
|
|
37
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
38
|
+
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
|
|
39
|
+
req.on("error", reject);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
function sendJson(res, status, body) {
|
|
43
|
+
const data = JSON.stringify(body);
|
|
44
|
+
res.writeHead(status, { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(data) });
|
|
45
|
+
res.end(data);
|
|
46
|
+
}
|
|
47
|
+
function logRequestError(req, error) {
|
|
48
|
+
const message = error instanceof Error ? error.stack || error.message : String(error);
|
|
49
|
+
console.error(`${req.method ?? "UNKNOWN"} ${req.url ?? "/"} failed: ${message}`);
|
|
50
|
+
}
|
|
51
|
+
function authenticate(config, req) {
|
|
52
|
+
if (!config.authToken)
|
|
53
|
+
return true;
|
|
54
|
+
const header = req.headers.authorization;
|
|
55
|
+
if (!header)
|
|
56
|
+
return false;
|
|
57
|
+
const token = header.startsWith("Bearer ") ? header.slice(7) : header;
|
|
58
|
+
return token === config.authToken;
|
|
59
|
+
}
|
|
60
|
+
function sessionResponseBody(session) {
|
|
61
|
+
return {
|
|
62
|
+
sessionId: session.sessionId,
|
|
63
|
+
staticContextHash: session.staticContextHash,
|
|
64
|
+
treeHash: session.treeHash,
|
|
65
|
+
messageCount: session.messages.length,
|
|
66
|
+
entryCount: session.entries.length,
|
|
67
|
+
leafId: session.leafId,
|
|
68
|
+
revision: session.revision,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function persistSession(config, session) {
|
|
72
|
+
savePersistedSession(config.sessionStoreDir, session);
|
|
73
|
+
}
|
|
74
|
+
function runKey(sessionId, runId) {
|
|
75
|
+
return `${sessionId}\0${runId}`;
|
|
76
|
+
}
|
|
77
|
+
function getStreamRun(sessionId, runId) {
|
|
78
|
+
return streamRuns.get(runKey(sessionId, runId));
|
|
79
|
+
}
|
|
80
|
+
function startStreamRun(sessionId, runId) {
|
|
81
|
+
const existing = getStreamRun(sessionId, runId);
|
|
82
|
+
if (existing?.status === "completed" || existing?.status === "failed")
|
|
83
|
+
return existing;
|
|
84
|
+
const now = Date.now();
|
|
85
|
+
const run = existing ?? {
|
|
86
|
+
sessionId,
|
|
87
|
+
runId,
|
|
88
|
+
status: "running",
|
|
89
|
+
events: [],
|
|
90
|
+
createdAt: now,
|
|
91
|
+
updatedAt: now,
|
|
92
|
+
};
|
|
93
|
+
run.status = "running";
|
|
94
|
+
run.updatedAt = now;
|
|
95
|
+
streamRuns.set(runKey(sessionId, runId), run);
|
|
96
|
+
return run;
|
|
97
|
+
}
|
|
98
|
+
function recordStreamRunEvent(run, event) {
|
|
99
|
+
if (!run)
|
|
100
|
+
return;
|
|
101
|
+
run.events.push(event);
|
|
102
|
+
run.updatedAt = Date.now();
|
|
103
|
+
}
|
|
104
|
+
function completeStreamRun(run, message) {
|
|
105
|
+
if (!run)
|
|
106
|
+
return;
|
|
107
|
+
run.status = "completed";
|
|
108
|
+
run.message = message;
|
|
109
|
+
run.errorMessage = undefined;
|
|
110
|
+
run.updatedAt = Date.now();
|
|
111
|
+
}
|
|
112
|
+
function failStreamRun(run, errorMessage) {
|
|
113
|
+
if (!run)
|
|
114
|
+
return;
|
|
115
|
+
run.status = "failed";
|
|
116
|
+
run.errorMessage = errorMessage;
|
|
117
|
+
run.updatedAt = Date.now();
|
|
118
|
+
}
|
|
119
|
+
function sessionHistoryFullResponseBody(session, baseMessageCount) {
|
|
120
|
+
return {
|
|
121
|
+
sessionId: session.sessionId,
|
|
122
|
+
staticContext: session.staticContext,
|
|
123
|
+
staticContextHash: session.staticContextHash,
|
|
124
|
+
treeHash: session.treeHash,
|
|
125
|
+
messageCount: session.messages.length,
|
|
126
|
+
entryCount: session.entries.length,
|
|
127
|
+
leafId: session.leafId,
|
|
128
|
+
revision: session.revision,
|
|
129
|
+
entries: session.entries,
|
|
130
|
+
baseMessageCount,
|
|
131
|
+
messages: session.messages.slice(baseMessageCount),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function sessionTreePatchResponseBody(session, baseMessageCount, entriesFrom, baseRevision) {
|
|
135
|
+
return {
|
|
136
|
+
sessionId: session.sessionId,
|
|
137
|
+
staticContext: session.staticContext,
|
|
138
|
+
staticContextHash: session.staticContextHash,
|
|
139
|
+
treeHash: session.treeHash,
|
|
140
|
+
messageCount: session.messages.length,
|
|
141
|
+
entryCount: session.entries.length,
|
|
142
|
+
leafId: session.leafId,
|
|
143
|
+
revision: session.revision,
|
|
144
|
+
baseMessageCount,
|
|
145
|
+
messages: session.messages.slice(baseMessageCount),
|
|
146
|
+
treePatch: {
|
|
147
|
+
entriesFrom,
|
|
148
|
+
baseRevision,
|
|
149
|
+
entries: session.entries.slice(entriesFrom),
|
|
150
|
+
leafId: session.leafId,
|
|
151
|
+
revision: session.revision,
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
export function resolveStreamOptions(_config, model, body) {
|
|
156
|
+
return { model, options: { ...(body.options ?? {}) } };
|
|
157
|
+
}
|
|
158
|
+
function handleSessionInit(config, body, res) {
|
|
159
|
+
if (!body.sessionId) {
|
|
160
|
+
sendJson(res, 400, { error: "sessionId is required" });
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (body.staticContext) {
|
|
164
|
+
setStaticContext(body.sessionId, body.staticContext);
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
getOrCreateSession(body.sessionId);
|
|
168
|
+
}
|
|
169
|
+
const session = getSession(body.sessionId);
|
|
170
|
+
persistSession(config, session);
|
|
171
|
+
sendJson(res, 200, sessionResponseBody(session));
|
|
172
|
+
}
|
|
173
|
+
function handleSessionUpdate(config, body, res) {
|
|
174
|
+
if (!body.sessionId) {
|
|
175
|
+
sendJson(res, 400, { error: "sessionId is required" });
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (!body.staticContext) {
|
|
179
|
+
sendJson(res, 400, { error: "staticContext is required for update" });
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
setStaticContext(body.sessionId, body.staticContext);
|
|
183
|
+
const session = getSession(body.sessionId);
|
|
184
|
+
persistSession(config, session);
|
|
185
|
+
sendJson(res, 200, sessionResponseBody(session));
|
|
186
|
+
}
|
|
187
|
+
function handleSessionSync(config, body, res) {
|
|
188
|
+
if (!body.sessionId) {
|
|
189
|
+
sendJson(res, 400, { error: "sessionId is required" });
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (!Array.isArray(body.messages)) {
|
|
193
|
+
sendJson(res, 400, { error: "messages is required" });
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (body.staticContext) {
|
|
197
|
+
setStaticContext(body.sessionId, body.staticContext);
|
|
198
|
+
}
|
|
199
|
+
const session = replaceMessages(body.sessionId, body.messages);
|
|
200
|
+
persistSession(config, session);
|
|
201
|
+
sendJson(res, 200, sessionResponseBody(session));
|
|
202
|
+
}
|
|
203
|
+
function handleSessionAppend(config, body, res) {
|
|
204
|
+
if (!body.sessionId) {
|
|
205
|
+
sendJson(res, 400, { error: "sessionId is required" });
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (!Array.isArray(body.messages)) {
|
|
209
|
+
sendJson(res, 400, { error: "messages is required" });
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (body.staticContext) {
|
|
213
|
+
setStaticContext(body.sessionId, body.staticContext);
|
|
214
|
+
}
|
|
215
|
+
const session = appendMessages(body.sessionId, body.messages);
|
|
216
|
+
persistSession(config, session);
|
|
217
|
+
sendJson(res, 200, sessionResponseBody(session));
|
|
218
|
+
}
|
|
219
|
+
function handleSessionTreeSync(config, body, res) {
|
|
220
|
+
if (!body.sessionId) {
|
|
221
|
+
sendJson(res, 400, { error: "sessionId is required" });
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (!Array.isArray(body.entries)) {
|
|
225
|
+
sendJson(res, 400, { error: "entries is required" });
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (body.staticContext) {
|
|
229
|
+
setStaticContext(body.sessionId, body.staticContext);
|
|
230
|
+
}
|
|
231
|
+
const session = replaceSessionTree(body.sessionId, body.entries, body.leafId ?? null);
|
|
232
|
+
persistSession(config, session);
|
|
233
|
+
sendJson(res, 200, sessionResponseBody(session));
|
|
234
|
+
}
|
|
235
|
+
function handleSessionTreeAppend(config, body, res) {
|
|
236
|
+
if (!body.sessionId) {
|
|
237
|
+
sendJson(res, 400, { error: "sessionId is required" });
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (!Array.isArray(body.entries)) {
|
|
241
|
+
sendJson(res, 400, { error: "entries is required" });
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (body.staticContext) {
|
|
245
|
+
setStaticContext(body.sessionId, body.staticContext);
|
|
246
|
+
}
|
|
247
|
+
const session = appendSessionEntries(body.sessionId, body.entries, body.leafId ?? null);
|
|
248
|
+
persistSession(config, session);
|
|
249
|
+
sendJson(res, 200, sessionResponseBody(session));
|
|
250
|
+
}
|
|
251
|
+
function handleSessionTreeSwitch(config, body, res) {
|
|
252
|
+
if (!body.sessionId) {
|
|
253
|
+
sendJson(res, 400, { error: "sessionId is required" });
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
const session = switchSessionLeaf(body.sessionId, body.leafId ?? null);
|
|
257
|
+
persistSession(config, session);
|
|
258
|
+
sendJson(res, 200, sessionResponseBody(session));
|
|
259
|
+
}
|
|
260
|
+
function prepareSessionCompact(body) {
|
|
261
|
+
if (!body.sessionId) {
|
|
262
|
+
return { status: 400, body: { error: "sessionId is required" } };
|
|
263
|
+
}
|
|
264
|
+
if (!body.model) {
|
|
265
|
+
return { status: 400, body: { error: "model is required" } };
|
|
266
|
+
}
|
|
267
|
+
const session = getSession(body.sessionId);
|
|
268
|
+
if (!session) {
|
|
269
|
+
return { status: 404, body: { error: "session not found" } };
|
|
270
|
+
}
|
|
271
|
+
const entries = getSessionBranch(session);
|
|
272
|
+
const preparationResult = prepareCompaction(entries, body.settings ?? DEFAULT_COMPACTION_SETTINGS, body.preparation);
|
|
273
|
+
if (!preparationResult.ok) {
|
|
274
|
+
return { status: 400, body: { error: preparationResult.error.message } };
|
|
275
|
+
}
|
|
276
|
+
if (!preparationResult.value) {
|
|
277
|
+
return { status: 400, body: { error: "Nothing to compact" } };
|
|
278
|
+
}
|
|
279
|
+
const options = body.options ?? {};
|
|
280
|
+
return {
|
|
281
|
+
session,
|
|
282
|
+
preparation: preparationResult.value,
|
|
283
|
+
options,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
async function completeSessionCompact(config, body, prepared) {
|
|
287
|
+
const result = await compact(prepared.preparation, createRequestModels(body.model, prepared.options), body.model, body.customInstructions, undefined, prepared.options.reasoning);
|
|
288
|
+
if (!result.ok) {
|
|
289
|
+
return { status: 500, body: { error: result.error.message } };
|
|
290
|
+
}
|
|
291
|
+
const baseTreeHash = prepared.session.treeHash;
|
|
292
|
+
const baseEntryCount = prepared.session.entries.length;
|
|
293
|
+
const { session: updatedSession, entry: compactionEntry } = appendCompactionEntry(body.sessionId, result.value);
|
|
294
|
+
persistSession(config, updatedSession);
|
|
295
|
+
if (!body.fullResponse && body.baseTreeHash === baseTreeHash) {
|
|
296
|
+
return {
|
|
297
|
+
status: 200,
|
|
298
|
+
body: {
|
|
299
|
+
success: true,
|
|
300
|
+
compaction: result.value,
|
|
301
|
+
compactionEntry,
|
|
302
|
+
...sessionResponseBody(updatedSession),
|
|
303
|
+
staticContext: updatedSession.staticContext,
|
|
304
|
+
treePatch: {
|
|
305
|
+
baseTreeHash,
|
|
306
|
+
entriesFrom: baseEntryCount,
|
|
307
|
+
entries: [compactionEntry],
|
|
308
|
+
leafId: updatedSession.leafId,
|
|
309
|
+
revision: updatedSession.revision,
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
return {
|
|
315
|
+
status: 200,
|
|
316
|
+
body: {
|
|
317
|
+
success: true,
|
|
318
|
+
compaction: result.value,
|
|
319
|
+
compactionEntry,
|
|
320
|
+
...sessionResponseBody(updatedSession),
|
|
321
|
+
staticContext: updatedSession.staticContext,
|
|
322
|
+
entries: updatedSession.entries,
|
|
323
|
+
messages: updatedSession.messages,
|
|
324
|
+
},
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
function writeServerSentEvent(res, event, body) {
|
|
328
|
+
res.write(`event: ${event}\n`);
|
|
329
|
+
res.write(`data: ${JSON.stringify(body)}\n\n`);
|
|
330
|
+
}
|
|
331
|
+
async function handleSessionCompactStream(config, body, prepared, res) {
|
|
332
|
+
res.writeHead(200, {
|
|
333
|
+
"Content-Type": "text/event-stream",
|
|
334
|
+
"Cache-Control": "no-cache",
|
|
335
|
+
Connection: "keep-alive",
|
|
336
|
+
});
|
|
337
|
+
res.flushHeaders();
|
|
338
|
+
res.write(STREAM_HEARTBEAT);
|
|
339
|
+
const heartbeat = setInterval(() => {
|
|
340
|
+
if (!res.writableEnded) {
|
|
341
|
+
res.write(STREAM_HEARTBEAT);
|
|
342
|
+
}
|
|
343
|
+
}, STREAM_HEARTBEAT_INTERVAL_MS);
|
|
344
|
+
heartbeat.unref();
|
|
345
|
+
try {
|
|
346
|
+
const result = await completeSessionCompact(config, body, prepared);
|
|
347
|
+
writeServerSentEvent(res, result.status >= 400 ? "error" : "result", result.body);
|
|
348
|
+
}
|
|
349
|
+
catch (err) {
|
|
350
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
351
|
+
writeServerSentEvent(res, "error", { error: message });
|
|
352
|
+
}
|
|
353
|
+
finally {
|
|
354
|
+
clearInterval(heartbeat);
|
|
355
|
+
res.end();
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
async function handleSessionCompactJsonStream(config, body, prepared, res) {
|
|
359
|
+
res.writeHead(200, {
|
|
360
|
+
"Content-Type": "application/json",
|
|
361
|
+
"Cache-Control": "no-cache",
|
|
362
|
+
Connection: "keep-alive",
|
|
363
|
+
});
|
|
364
|
+
res.flushHeaders();
|
|
365
|
+
res.write(JSON_HEARTBEAT);
|
|
366
|
+
const heartbeat = setInterval(() => {
|
|
367
|
+
if (!res.writableEnded) {
|
|
368
|
+
res.write(JSON_HEARTBEAT);
|
|
369
|
+
}
|
|
370
|
+
}, STREAM_HEARTBEAT_INTERVAL_MS);
|
|
371
|
+
heartbeat.unref();
|
|
372
|
+
try {
|
|
373
|
+
const result = await completeSessionCompact(config, body, prepared);
|
|
374
|
+
res.write(JSON.stringify(result.body));
|
|
375
|
+
}
|
|
376
|
+
catch (err) {
|
|
377
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
378
|
+
res.write(JSON.stringify({ error: message }));
|
|
379
|
+
}
|
|
380
|
+
finally {
|
|
381
|
+
clearInterval(heartbeat);
|
|
382
|
+
res.end();
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
async function handleSessionCompact(config, body, res) {
|
|
386
|
+
const prepared = prepareSessionCompact(body);
|
|
387
|
+
if ("status" in prepared) {
|
|
388
|
+
sendJson(res, prepared.status, prepared.body);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
if (body.streamResponse) {
|
|
392
|
+
await handleSessionCompactStream(config, body, prepared, res);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
await handleSessionCompactJsonStream(config, body, prepared, res);
|
|
396
|
+
}
|
|
397
|
+
function handleDropLastAssistantError(config, body, res) {
|
|
398
|
+
if (!body.sessionId) {
|
|
399
|
+
sendJson(res, 400, { error: "sessionId is required" });
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
const dropped = dropLastAssistantError(body.sessionId);
|
|
403
|
+
const session = getSession(body.sessionId);
|
|
404
|
+
if (session) {
|
|
405
|
+
persistSession(config, session);
|
|
406
|
+
}
|
|
407
|
+
const messageCount = session?.messages.length ?? 0;
|
|
408
|
+
sendJson(res, 200, { success: true, dropped, messageCount });
|
|
409
|
+
}
|
|
410
|
+
function handleSessionHistory(sessionId, from, entriesFrom, revision, baseTreeHash, res) {
|
|
411
|
+
const session = getSession(sessionId);
|
|
412
|
+
if (!session) {
|
|
413
|
+
sendJson(res, 404, { error: "session not found" });
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
const baseMessageCount = from ?? 0;
|
|
417
|
+
if (entriesFrom !== undefined &&
|
|
418
|
+
entriesFrom <= session.entries.length &&
|
|
419
|
+
(revision === undefined || revision <= session.revision) &&
|
|
420
|
+
(baseTreeHash === undefined || baseTreeHash === session.prefixHashes[entriesFrom])) {
|
|
421
|
+
sendJson(res, 200, sessionTreePatchResponseBody(session, baseMessageCount, entriesFrom, revision));
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
sendJson(res, 200, sessionHistoryFullResponseBody(session, baseMessageCount));
|
|
425
|
+
}
|
|
426
|
+
function handleSessionRun(sessionId, runId, res) {
|
|
427
|
+
const run = getStreamRun(sessionId, runId);
|
|
428
|
+
if (!run) {
|
|
429
|
+
sendJson(res, 404, { error: "run not found" });
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
sendJson(res, 200, run);
|
|
433
|
+
}
|
|
434
|
+
export function buildStreamContext(session, body) {
|
|
435
|
+
const messages = body.contextOverlay ?? [...session.messages, ...(body.ephemeralMessages ?? [])];
|
|
436
|
+
return {
|
|
437
|
+
systemPrompt: session.staticContext?.systemPrompt,
|
|
438
|
+
messages,
|
|
439
|
+
tools: session.staticContext?.tools,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
function handleStream(config, body, res) {
|
|
443
|
+
if (!body.sessionId) {
|
|
444
|
+
sendJson(res, 400, { error: "sessionId is required" });
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
if (!body.model) {
|
|
448
|
+
sendJson(res, 400, { error: "model is required" });
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
const session = getOrCreateSession(body.sessionId);
|
|
452
|
+
if (body.staticContext) {
|
|
453
|
+
setStaticContext(body.sessionId, body.staticContext);
|
|
454
|
+
persistSession(config, session);
|
|
455
|
+
}
|
|
456
|
+
if (!session.staticContext && !body.staticContext) {
|
|
457
|
+
sendJson(res, 400, { error: "Session has no static context. Initialize with /api/session/init first." });
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
if (body.ephemeralMessages !== undefined && !Array.isArray(body.ephemeralMessages)) {
|
|
461
|
+
sendJson(res, 400, { error: "ephemeralMessages must be an array" });
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
if (body.contextOverlay !== undefined && !Array.isArray(body.contextOverlay)) {
|
|
465
|
+
sendJson(res, 400, { error: "contextOverlay must be an array" });
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
const context = buildStreamContext(session, body);
|
|
469
|
+
const existingRun = body.runId ? getStreamRun(body.sessionId, body.runId) : undefined;
|
|
470
|
+
const { model: resolvedModel, options: streamOptions } = resolveStreamOptions(config, body.model, body);
|
|
471
|
+
res.writeHead(200, {
|
|
472
|
+
"Content-Type": "text/event-stream",
|
|
473
|
+
"Cache-Control": "no-cache",
|
|
474
|
+
Connection: "keep-alive",
|
|
475
|
+
});
|
|
476
|
+
res.flushHeaders();
|
|
477
|
+
res.write(STREAM_HEARTBEAT);
|
|
478
|
+
if (existingRun?.status === "completed") {
|
|
479
|
+
for (const event of existingRun.events) {
|
|
480
|
+
res.write(encodeProxyEvent(event));
|
|
481
|
+
}
|
|
482
|
+
res.end();
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
const run = body.runId ? startStreamRun(body.sessionId, body.runId) : undefined;
|
|
486
|
+
const heartbeat = setInterval(() => {
|
|
487
|
+
if (!res.writableEnded) {
|
|
488
|
+
res.write(STREAM_HEARTBEAT);
|
|
489
|
+
}
|
|
490
|
+
}, STREAM_HEARTBEAT_INTERVAL_MS);
|
|
491
|
+
heartbeat.unref();
|
|
492
|
+
let stream;
|
|
493
|
+
try {
|
|
494
|
+
stream = streamSimple(resolvedModel, context, streamOptions);
|
|
495
|
+
}
|
|
496
|
+
catch (err) {
|
|
497
|
+
clearInterval(heartbeat);
|
|
498
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
499
|
+
failStreamRun(run, message);
|
|
500
|
+
res.write(encodeErrorEvent(message));
|
|
501
|
+
res.end();
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
(async () => {
|
|
505
|
+
try {
|
|
506
|
+
for await (const event of stream) {
|
|
507
|
+
const proxyEvent = toProxyEvent(event);
|
|
508
|
+
if (proxyEvent) {
|
|
509
|
+
recordStreamRunEvent(run, proxyEvent);
|
|
510
|
+
res.write(encodeProxyEvent(proxyEvent));
|
|
511
|
+
}
|
|
512
|
+
if (event.type === "done") {
|
|
513
|
+
completeStreamRun(run, event.message);
|
|
514
|
+
}
|
|
515
|
+
else if (event.type === "error") {
|
|
516
|
+
failStreamRun(run, event.error.errorMessage ?? event.reason);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
finally {
|
|
521
|
+
clearInterval(heartbeat);
|
|
522
|
+
}
|
|
523
|
+
res.end();
|
|
524
|
+
})().catch((err) => {
|
|
525
|
+
clearInterval(heartbeat);
|
|
526
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
527
|
+
failStreamRun(run, message);
|
|
528
|
+
res.write(encodeErrorEvent(message));
|
|
529
|
+
res.end();
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
async function handlePostRequest(config, pathname, body, res) {
|
|
533
|
+
if (pathname === "/api/session/init") {
|
|
534
|
+
handleSessionInit(config, body, res);
|
|
535
|
+
return true;
|
|
536
|
+
}
|
|
537
|
+
if (pathname === "/api/session/update") {
|
|
538
|
+
handleSessionUpdate(config, body, res);
|
|
539
|
+
return true;
|
|
540
|
+
}
|
|
541
|
+
if (pathname === "/api/session/sync") {
|
|
542
|
+
handleSessionSync(config, body, res);
|
|
543
|
+
return true;
|
|
544
|
+
}
|
|
545
|
+
if (pathname === "/api/session/append") {
|
|
546
|
+
handleSessionAppend(config, body, res);
|
|
547
|
+
return true;
|
|
548
|
+
}
|
|
549
|
+
if (pathname === "/api/session/tree/sync") {
|
|
550
|
+
handleSessionTreeSync(config, body, res);
|
|
551
|
+
return true;
|
|
552
|
+
}
|
|
553
|
+
if (pathname === "/api/session/tree/append") {
|
|
554
|
+
handleSessionTreeAppend(config, body, res);
|
|
555
|
+
return true;
|
|
556
|
+
}
|
|
557
|
+
if (pathname === "/api/session/tree/switch") {
|
|
558
|
+
handleSessionTreeSwitch(config, body, res);
|
|
559
|
+
return true;
|
|
560
|
+
}
|
|
561
|
+
if (pathname === "/api/session/drop-last-assistant-error") {
|
|
562
|
+
handleDropLastAssistantError(config, body, res);
|
|
563
|
+
return true;
|
|
564
|
+
}
|
|
565
|
+
if (pathname === "/api/session/compact") {
|
|
566
|
+
await handleSessionCompact(config, body, res);
|
|
567
|
+
return true;
|
|
568
|
+
}
|
|
569
|
+
if (pathname === "/api/stream") {
|
|
570
|
+
handleStream(config, body, res);
|
|
571
|
+
return true;
|
|
572
|
+
}
|
|
573
|
+
return false;
|
|
574
|
+
}
|
|
575
|
+
export function createPiServer(configOverride) {
|
|
576
|
+
const config = loadConfig(configOverride);
|
|
577
|
+
loadPersistedSessions(config.sessionStoreDir);
|
|
578
|
+
const server = createServer(async (req, res) => {
|
|
579
|
+
const url = new URL(req.url ?? "/", `http://${config.host}:${config.port}`);
|
|
580
|
+
if (req.method === "GET" && url.pathname === "/health") {
|
|
581
|
+
sendJson(res, 200, { status: "ok" });
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
if (!authenticate(config, req)) {
|
|
585
|
+
sendJson(res, 401, { error: "Unauthorized" });
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
if (req.method === "GET" && url.pathname === "/api/sessions") {
|
|
589
|
+
sendJson(res, 200, { sessions: listSessions() });
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
const runMatch = /^\/api\/session\/([^/]+)\/runs\/([^/]+)$/.exec(url.pathname);
|
|
593
|
+
if (req.method === "GET" && runMatch) {
|
|
594
|
+
handleSessionRun(decodeURIComponent(runMatch[1]), decodeURIComponent(runMatch[2]), res);
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
if (req.method === "GET" && url.pathname.startsWith("/api/session/") && url.pathname.endsWith("/history")) {
|
|
598
|
+
const encodedSessionId = url.pathname.slice("/api/session/".length, -"/history".length);
|
|
599
|
+
const fromParam = url.searchParams.get("from");
|
|
600
|
+
const from = fromParam === null ? undefined : Number(fromParam);
|
|
601
|
+
if (from !== undefined && (!Number.isInteger(from) || from < 0)) {
|
|
602
|
+
sendJson(res, 400, { error: "from must be a non-negative integer" });
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
const entriesFromParam = url.searchParams.get("entriesFrom");
|
|
606
|
+
const entriesFrom = entriesFromParam === null ? undefined : Number(entriesFromParam);
|
|
607
|
+
if (entriesFrom !== undefined && (!Number.isInteger(entriesFrom) || entriesFrom < 0)) {
|
|
608
|
+
sendJson(res, 400, { error: "entriesFrom must be a non-negative integer" });
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
const revisionParam = url.searchParams.get("revision");
|
|
612
|
+
const revision = revisionParam === null ? undefined : Number(revisionParam);
|
|
613
|
+
if (revision !== undefined && (!Number.isInteger(revision) || revision < 0)) {
|
|
614
|
+
sendJson(res, 400, { error: "revision must be a non-negative integer" });
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
handleSessionHistory(decodeURIComponent(encodedSessionId), from, entriesFrom, revision, url.searchParams.get("baseTreeHash") ?? undefined, res);
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
if (req.method === "POST" && url.pathname === CHUNK_ENDPOINT) {
|
|
621
|
+
try {
|
|
622
|
+
const body = JSON.parse(await readBody(req));
|
|
623
|
+
const chunkResult = receiveRequestChunk(body);
|
|
624
|
+
if (!chunkResult.complete) {
|
|
625
|
+
sendJson(res, 200, chunkResult.ack);
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
await handlePostRequest(config, chunkResult.target, JSON.parse(chunkResult.bodyJson), res);
|
|
629
|
+
}
|
|
630
|
+
catch (err) {
|
|
631
|
+
logRequestError(req, err);
|
|
632
|
+
if (!res.headersSent) {
|
|
633
|
+
sendJson(res, 400, { error: err instanceof Error ? err.message : String(err) });
|
|
634
|
+
}
|
|
635
|
+
else {
|
|
636
|
+
res.write(encodeErrorEvent(err instanceof Error ? err.message : String(err)));
|
|
637
|
+
res.end();
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
if (req.method === "POST") {
|
|
643
|
+
try {
|
|
644
|
+
const body = JSON.parse(await readBody(req));
|
|
645
|
+
if (await handlePostRequest(config, url.pathname, body, res))
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
catch (err) {
|
|
649
|
+
logRequestError(req, err);
|
|
650
|
+
if (!res.headersSent) {
|
|
651
|
+
sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
|
|
652
|
+
}
|
|
653
|
+
else {
|
|
654
|
+
res.write(encodeErrorEvent(err instanceof Error ? err.message : String(err)));
|
|
655
|
+
res.end();
|
|
656
|
+
}
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
if (req.method === "DELETE" && url.pathname.startsWith("/api/session/")) {
|
|
661
|
+
const sessionId = decodeURIComponent(url.pathname.slice("/api/session/".length));
|
|
662
|
+
deleteSessionFromStore(sessionId);
|
|
663
|
+
deletePersistedSession(config.sessionStoreDir, sessionId);
|
|
664
|
+
sendJson(res, 200, { deleted: sessionId });
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
sendJson(res, 404, { error: "Not found" });
|
|
668
|
+
});
|
|
669
|
+
return server;
|
|
670
|
+
}
|
|
671
|
+
function toProxyEvent(event) {
|
|
672
|
+
switch (event.type) {
|
|
673
|
+
case "start":
|
|
674
|
+
return { type: "start" };
|
|
675
|
+
case "text_start":
|
|
676
|
+
return { type: "text_start", contentIndex: event.contentIndex };
|
|
677
|
+
case "text_delta":
|
|
678
|
+
return { type: "text_delta", contentIndex: event.contentIndex, delta: event.delta };
|
|
679
|
+
case "text_end":
|
|
680
|
+
return {
|
|
681
|
+
type: "text_end",
|
|
682
|
+
contentIndex: event.contentIndex,
|
|
683
|
+
contentSignature: event.partial.content[event.contentIndex]?.type === "text"
|
|
684
|
+
? event.partial.content[event.contentIndex].textSignature
|
|
685
|
+
: undefined,
|
|
686
|
+
};
|
|
687
|
+
case "thinking_start":
|
|
688
|
+
return { type: "thinking_start", contentIndex: event.contentIndex };
|
|
689
|
+
case "thinking_delta":
|
|
690
|
+
return { type: "thinking_delta", contentIndex: event.contentIndex, delta: event.delta };
|
|
691
|
+
case "thinking_end":
|
|
692
|
+
return {
|
|
693
|
+
type: "thinking_end",
|
|
694
|
+
contentIndex: event.contentIndex,
|
|
695
|
+
contentSignature: event.partial.content[event.contentIndex]?.type === "thinking"
|
|
696
|
+
? event.partial.content[event.contentIndex].thinkingSignature
|
|
697
|
+
: undefined,
|
|
698
|
+
};
|
|
699
|
+
case "toolcall_start":
|
|
700
|
+
return {
|
|
701
|
+
type: "toolcall_start",
|
|
702
|
+
contentIndex: event.contentIndex,
|
|
703
|
+
id: event.partial.content[event.contentIndex]?.type === "toolCall"
|
|
704
|
+
? event.partial.content[event.contentIndex].id
|
|
705
|
+
: "",
|
|
706
|
+
toolName: event.partial.content[event.contentIndex]?.type === "toolCall"
|
|
707
|
+
? event.partial.content[event.contentIndex].name
|
|
708
|
+
: "",
|
|
709
|
+
};
|
|
710
|
+
case "toolcall_delta":
|
|
711
|
+
return { type: "toolcall_delta", contentIndex: event.contentIndex, delta: event.delta };
|
|
712
|
+
case "toolcall_end":
|
|
713
|
+
return { type: "toolcall_end", contentIndex: event.contentIndex };
|
|
714
|
+
case "done":
|
|
715
|
+
return { type: "done", reason: event.reason, usage: event.message.usage };
|
|
716
|
+
case "error":
|
|
717
|
+
return {
|
|
718
|
+
type: "error",
|
|
719
|
+
reason: event.reason,
|
|
720
|
+
errorMessage: event.error.errorMessage,
|
|
721
|
+
usage: event.error.usage,
|
|
722
|
+
};
|
|
723
|
+
default:
|
|
724
|
+
return undefined;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
export function startServer(configOverride) {
|
|
728
|
+
const config = loadConfig(configOverride);
|
|
729
|
+
const server = createPiServer(configOverride);
|
|
730
|
+
server.listen(config.port, config.host, () => {
|
|
731
|
+
console.log(`pi-server listening on ${config.host}:${config.port}`);
|
|
732
|
+
});
|
|
733
|
+
return server;
|
|
734
|
+
}
|
|
735
|
+
//# sourceMappingURL=server.js.map
|