@averyyy/pi-server 0.80.3-piclient.5 → 0.80.3-piclient.7
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 +5 -1
- package/bin/update.js +6 -170
- 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 +15 -5
- package/dist/request-chunks.d.ts.map +1 -1
- package/dist/request-chunks.js +123 -27
- package/dist/request-chunks.js.map +1 -1
- package/dist/server.d.ts +6 -2
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +178 -24
- package/dist/server.js.map +1 -1
- package/dist/session-persistence.d.ts.map +1 -1
- package/dist/session-persistence.js +104 -3
- package/dist/session-persistence.js.map +1 -1
- package/dist/session-store.d.ts +10 -0
- package/dist/session-store.d.ts.map +1 -1
- package/dist/session-store.js +46 -12
- package/dist/session-store.js.map +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,10 @@
|
|
|
12
12
|
- `/api/request/chunk` endpoint for reassembling oversized client requests before dispatch.
|
|
13
13
|
- `DELETE /api/session/:id` endpoint for removing one server-side session.
|
|
14
14
|
- `/health` endpoint for health checks.
|
|
15
|
-
- `pi-server update` command with npm global package updates
|
|
15
|
+
- `pi-server update` command with npm global package updates.
|
|
16
16
|
- Configurable via `PI_SERVER_CONFIG` or environment variables: `PI_SERVER_HOST`, `PI_SERVER_PORT`, `PI_SERVER_AUTH_TOKEN`.
|
|
17
17
|
- Persistent session tree storage under `PI_SERVER_SESSION_STORE_DIR`, including exact tree hashes in session responses.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- Used `--legacy-peer-deps` for npm-global fork updates so existing upstream Pi installs do not trigger peer override warnings for forked prerelease aliases.
|
package/bin/update.js
CHANGED
|
@@ -1,19 +1,8 @@
|
|
|
1
|
-
import { Buffer } from "node:buffer";
|
|
2
1
|
import { spawnSync } from "node:child_process";
|
|
3
2
|
import { readFileSync } from "node:fs";
|
|
4
3
|
import { dirname, resolve } from "node:path";
|
|
5
4
|
import { fileURLToPath } from "node:url";
|
|
6
5
|
|
|
7
|
-
const H_PROXY_WARNING_URL_PATTERN =
|
|
8
|
-
/https?:\/\/114\.114\.114\.114:\d+\/proxycontrolwarn\/httpwarning_\d+\.html\?ori_url=[A-Za-z0-9+/=]+(?:&uid=\d+)?/;
|
|
9
|
-
const H_PROXY_WARNING_HOST = "114.114.114.114:9421";
|
|
10
|
-
const H_PROXY_HEADERS = {
|
|
11
|
-
"User-Agent":
|
|
12
|
-
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121 Safari/537.36",
|
|
13
|
-
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
14
|
-
"Accept-Language": "en-US,en;q=0.5",
|
|
15
|
-
};
|
|
16
|
-
|
|
17
6
|
function defaultPackageRoot() {
|
|
18
7
|
return dirname(dirname(fileURLToPath(import.meta.url)));
|
|
19
8
|
}
|
|
@@ -30,173 +19,23 @@ function runStep(runner, command, args, cwd, stdio = "inherit") {
|
|
|
30
19
|
return runner(command, args, { cwd, stdio, encoding: "utf-8" });
|
|
31
20
|
}
|
|
32
21
|
|
|
33
|
-
function responseStatus(response) {
|
|
34
|
-
return response.statusText ? `${response.status} ${response.statusText}` : String(response.status);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function getQueryValue(url, name) {
|
|
38
|
-
const match = new RegExp(`[?&]${name}=([^&]+)`).exec(url);
|
|
39
|
-
return match?.[1] ?? "";
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function getInputValue(html, id) {
|
|
43
|
-
const match = new RegExp(`id="${id}"[^>]*value="([^"]*)"`).exec(html);
|
|
44
|
-
return match?.[1] ?? "";
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function bitReverse(value) {
|
|
48
|
-
return (
|
|
49
|
-
((1 & value) << 7) |
|
|
50
|
-
((2 & value) << 5) |
|
|
51
|
-
((4 & value) << 3) |
|
|
52
|
-
((8 & value) << 1) |
|
|
53
|
-
((16 & value) >> 1) |
|
|
54
|
-
((32 & value) >> 3) |
|
|
55
|
-
((64 & value) >> 5) |
|
|
56
|
-
((128 & value) >> 7)
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function encodeWarningByte(value) {
|
|
61
|
-
if (value === 32) return "+";
|
|
62
|
-
if (
|
|
63
|
-
(value < 48 && value !== 45 && value !== 46) ||
|
|
64
|
-
(value < 65 && value > 57) ||
|
|
65
|
-
(value > 90 && value < 97 && value !== 95) ||
|
|
66
|
-
value > 122
|
|
67
|
-
) {
|
|
68
|
-
return `%${value.toString(16).toUpperCase().padStart(2, "0")}`;
|
|
69
|
-
}
|
|
70
|
-
return String.fromCharCode(value);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function md6(value) {
|
|
74
|
-
let result = "";
|
|
75
|
-
for (let index = 0; index < value.length; index++) {
|
|
76
|
-
result += encodeWarningByte(53 ^ bitReverse(value.charCodeAt(index)) ^ (255 & index));
|
|
77
|
-
}
|
|
78
|
-
return result;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function b64(value) {
|
|
82
|
-
return Buffer.from(value, "utf-8").toString("base64");
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
async function findHProxyWarningUrl(response) {
|
|
86
|
-
const location = response.headers.get("location") ?? "";
|
|
87
|
-
if (response.status === 302 && H_PROXY_WARNING_URL_PATTERN.test(location)) return location;
|
|
88
|
-
if (H_PROXY_WARNING_URL_PATTERN.test(response.url)) return response.url;
|
|
89
|
-
|
|
90
|
-
const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
|
|
91
|
-
if (!contentType.includes("text/html")) return undefined;
|
|
92
|
-
|
|
93
|
-
const body = await response.clone().text();
|
|
94
|
-
return H_PROXY_WARNING_URL_PATTERN.exec(body)?.[0];
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
async function approveHProxyWarning(warningUrl, fetchImpl) {
|
|
98
|
-
const warningResponse = await fetchImpl(warningUrl, {
|
|
99
|
-
method: "GET",
|
|
100
|
-
headers: H_PROXY_HEADERS,
|
|
101
|
-
redirect: "manual",
|
|
102
|
-
});
|
|
103
|
-
const html = await warningResponse.text();
|
|
104
|
-
if (!warningResponse.ok) {
|
|
105
|
-
throw new Error(`Proxy warning page fetch failed: ${responseStatus(warningResponse)}`);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const oriUrl = getQueryValue(warningUrl, "ori_url");
|
|
109
|
-
const sessionId = getInputValue(html, "sessionid");
|
|
110
|
-
if (!oriUrl || !sessionId) {
|
|
111
|
-
throw new Error("Proxy warning page did not include approval fields");
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const pid = getInputValue(html, "pid");
|
|
115
|
-
const uid = getInputValue(html, "uid");
|
|
116
|
-
const payload = `ori_url=${oriUrl}&sessionid=${sessionId}&pid=${pid}&uid=${uid}`;
|
|
117
|
-
const checkUrl = `http://${H_PROXY_WARNING_HOST}/proxycontrolwarn/check?${b64(md6(b64(payload)))}`;
|
|
118
|
-
const checkResponse = await fetchImpl(checkUrl, {
|
|
119
|
-
method: "GET",
|
|
120
|
-
headers: H_PROXY_HEADERS,
|
|
121
|
-
redirect: "manual",
|
|
122
|
-
});
|
|
123
|
-
const checkBody = await checkResponse.text();
|
|
124
|
-
if (!checkResponse.ok) {
|
|
125
|
-
throw new Error(`Proxy approval failed: ${responseStatus(checkResponse)} ${checkBody.slice(0, 80)}`);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
async function approveHProxyTarget(url, fetchImpl) {
|
|
130
|
-
let response;
|
|
131
|
-
try {
|
|
132
|
-
response = await fetchImpl(url, { method: "GET", headers: H_PROXY_HEADERS, redirect: "manual" });
|
|
133
|
-
} catch {
|
|
134
|
-
return false;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const warningUrl = await findHProxyWarningUrl(response);
|
|
138
|
-
if (!warningUrl) return false;
|
|
139
|
-
|
|
140
|
-
await approveHProxyWarning(warningUrl, fetchImpl);
|
|
141
|
-
return true;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function maybeGitHttpUrl(value) {
|
|
145
|
-
const trimmed = value.trim();
|
|
146
|
-
if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) return trimmed;
|
|
147
|
-
const ssh = /^git@([^:]+):(.+)$/.exec(trimmed);
|
|
148
|
-
if (ssh) return `https://${ssh[1]}/${ssh[2]}`;
|
|
149
|
-
return undefined;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function maybeHttpUrl(value) {
|
|
153
|
-
const trimmed = value.trim();
|
|
154
|
-
return trimmed.startsWith("http://") || trimmed.startsWith("https://") ? trimmed : undefined;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function getProxyApprovalTargets(runner, repoRoot, includeGitRemote = true) {
|
|
158
|
-
const targets = [];
|
|
159
|
-
if (includeGitRemote) {
|
|
160
|
-
const remote = runStep(runner, "git", ["config", "--get", "remote.origin.url"], repoRoot, "pipe");
|
|
161
|
-
if (remote.status === 0) {
|
|
162
|
-
const url = maybeGitHttpUrl(String(remote.stdout ?? ""));
|
|
163
|
-
if (url) targets.push(url);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const registry = runStep(runner, "npm", ["config", "get", "registry"], repoRoot, "pipe");
|
|
168
|
-
if (registry.status === 0) {
|
|
169
|
-
const url = maybeHttpUrl(String(registry.stdout ?? ""));
|
|
170
|
-
if (url) targets.push(url);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
return [...new Set(targets)];
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
async function approveUpdateProxyTargets(runner, repoRoot, stdout, fetchImpl, includeGitRemote = true) {
|
|
177
|
-
for (const target of getProxyApprovalTargets(runner, repoRoot, includeGitRemote)) {
|
|
178
|
-
if (await approveHProxyTarget(target, fetchImpl)) {
|
|
179
|
-
stdout.write(`Approved proxy warning for ${target}\n`);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
22
|
function isMissingGitCheckout(result) {
|
|
185
23
|
const text = `${String(result.stdout ?? "")}\n${String(result.stderr ?? "")}`;
|
|
186
24
|
return result.status !== 0 && text.includes("not a git repository");
|
|
187
25
|
}
|
|
188
26
|
|
|
189
|
-
async function runNpmGlobalUpdate(runner, repoRoot, stdout, stderr
|
|
190
|
-
await approveUpdateProxyTargets(runner, repoRoot, stdout, fetchImpl, false);
|
|
27
|
+
async function runNpmGlobalUpdate(runner, repoRoot, stdout, stderr) {
|
|
191
28
|
stdout.write("Updating npm packages: @averyyy/pi-client@latest @averyyy/pi-server@latest\n");
|
|
192
29
|
const result = runStep(
|
|
193
30
|
runner,
|
|
194
31
|
"npm",
|
|
195
|
-
["install", "-g", "--ignore-scripts", "@averyyy/pi-client@latest", "@averyyy/pi-server@latest"],
|
|
32
|
+
["install", "-g", "--ignore-scripts", "--legacy-peer-deps", "@averyyy/pi-client@latest", "@averyyy/pi-server@latest"],
|
|
196
33
|
repoRoot,
|
|
197
34
|
);
|
|
198
35
|
if (result.status !== 0) {
|
|
199
|
-
stderr.write(
|
|
36
|
+
stderr.write(
|
|
37
|
+
"pi-server update failed: npm install -g --ignore-scripts --legacy-peer-deps @averyyy/pi-client@latest @averyyy/pi-server@latest\n",
|
|
38
|
+
);
|
|
200
39
|
return result.status ?? 1;
|
|
201
40
|
}
|
|
202
41
|
stdout.write("pi-server update complete\n");
|
|
@@ -207,7 +46,6 @@ export async function runPiServerUpdate(_args = [], options = {}) {
|
|
|
207
46
|
const packageRoot = options.packageRoot ?? defaultPackageRoot();
|
|
208
47
|
const repoRoot = options.repoRoot ?? defaultRepoRoot();
|
|
209
48
|
const runner = options.runner ?? spawnSync;
|
|
210
|
-
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
211
49
|
const stdout = options.stdout ?? process.stdout;
|
|
212
50
|
const stderr = options.stderr ?? process.stderr;
|
|
213
51
|
const pkg = readPackageMetadata(packageRoot);
|
|
@@ -218,7 +56,7 @@ export async function runPiServerUpdate(_args = [], options = {}) {
|
|
|
218
56
|
const status = runStep(runner, "git", ["status", "--porcelain"], repoRoot, "pipe");
|
|
219
57
|
if (status.status !== 0) {
|
|
220
58
|
if (isMissingGitCheckout(status)) {
|
|
221
|
-
return runNpmGlobalUpdate(runner, repoRoot, stdout, stderr
|
|
59
|
+
return runNpmGlobalUpdate(runner, repoRoot, stdout, stderr);
|
|
222
60
|
}
|
|
223
61
|
stderr.write("pi-server update failed: unable to inspect git status\n");
|
|
224
62
|
return status.status ?? 1;
|
|
@@ -228,8 +66,6 @@ export async function runPiServerUpdate(_args = [], options = {}) {
|
|
|
228
66
|
return 1;
|
|
229
67
|
}
|
|
230
68
|
|
|
231
|
-
await approveUpdateProxyTargets(runner, repoRoot, stdout, fetchImpl);
|
|
232
|
-
|
|
233
69
|
const steps = [
|
|
234
70
|
["git", ["pull", "--ff-only"]],
|
|
235
71
|
["npm", ["install", "--ignore-scripts"]],
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { SessionTreeEntry } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import type { Tool } from "@earendil-works/pi-ai";
|
|
3
|
+
export interface PiServerStaticContext {
|
|
4
|
+
systemPrompt?: string;
|
|
5
|
+
tools?: Tool[];
|
|
6
|
+
}
|
|
7
|
+
export declare function hashPiServerStaticContext(context: PiServerStaticContext | undefined): string;
|
|
8
|
+
export declare const PI_SERVER_EMPTY_TREE_HASH: string;
|
|
9
|
+
export declare function hashPiServerTreeEntry(entry: SessionTreeEntry): string;
|
|
10
|
+
export declare function appendPiServerTreeHash(previousHash: string, entryHash: string): string;
|
|
11
|
+
export declare function buildPiServerTreePrefixHashes(entries: SessionTreeEntry[]): string[];
|
|
12
|
+
export declare function hashPiServerSessionEntries(entries: SessionTreeEntry[]): string;
|
|
13
|
+
//# sourceMappingURL=pi-server-protocol.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pi-server-protocol.d.ts","sourceRoot":"","sources":["../src/pi-server-protocol.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACtE,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAElD,MAAM,WAAW,qBAAqB;IACrC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,IAAI,EAAE,CAAC;CACf;AAED,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,qBAAqB,GAAG,SAAS,GAAG,MAAM,CAW5F;AAED,eAAO,MAAM,yBAAyB,QAA0D,CAAC;AAEjG,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,gBAAgB,GAAG,MAAM,CAErE;AAED,wBAAgB,sBAAsB,CAAC,YAAY,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAEtF;AAED,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,gBAAgB,EAAE,GAAG,MAAM,EAAE,CAMnF;AAED,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,gBAAgB,EAAE,GAAG,MAAM,CAE9E","sourcesContent":["import { createHash } from \"node:crypto\";\nimport type { SessionTreeEntry } from \"@earendil-works/pi-agent-core\";\nimport type { Tool } from \"@earendil-works/pi-ai\";\n\nexport interface PiServerStaticContext {\n\tsystemPrompt?: string;\n\ttools?: Tool[];\n}\n\nexport function hashPiServerStaticContext(context: PiServerStaticContext | undefined): string {\n\tif (!context) return \"\";\n\tconst canonical = {\n\t\tsystemPrompt: context.systemPrompt,\n\t\ttools: context.tools?.map((tool) => ({\n\t\t\tname: tool.name,\n\t\t\tdescription: tool.description,\n\t\t\tparameters: tool.parameters,\n\t\t})),\n\t};\n\treturn createHash(\"sha256\").update(JSON.stringify(canonical)).digest(\"hex\");\n}\n\nexport const PI_SERVER_EMPTY_TREE_HASH = createHash(\"sha256\").update(\"pi-tree-v1\").digest(\"hex\");\n\nexport function hashPiServerTreeEntry(entry: SessionTreeEntry): string {\n\treturn createHash(\"sha256\").update(JSON.stringify(entry)).digest(\"hex\");\n}\n\nexport function appendPiServerTreeHash(previousHash: string, entryHash: string): string {\n\treturn createHash(\"sha256\").update(`${previousHash}:${entryHash}`).digest(\"hex\");\n}\n\nexport function buildPiServerTreePrefixHashes(entries: SessionTreeEntry[]): string[] {\n\tconst hashes = [PI_SERVER_EMPTY_TREE_HASH];\n\tfor (const entry of entries) {\n\t\thashes.push(appendPiServerTreeHash(hashes[hashes.length - 1], hashPiServerTreeEntry(entry)));\n\t}\n\treturn hashes;\n}\n\nexport function hashPiServerSessionEntries(entries: SessionTreeEntry[]): string {\n\treturn buildPiServerTreePrefixHashes(entries)[entries.length];\n}\n"]}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
export function hashPiServerStaticContext(context) {
|
|
3
|
+
if (!context)
|
|
4
|
+
return "";
|
|
5
|
+
const canonical = {
|
|
6
|
+
systemPrompt: context.systemPrompt,
|
|
7
|
+
tools: context.tools?.map((tool) => ({
|
|
8
|
+
name: tool.name,
|
|
9
|
+
description: tool.description,
|
|
10
|
+
parameters: tool.parameters,
|
|
11
|
+
})),
|
|
12
|
+
};
|
|
13
|
+
return createHash("sha256").update(JSON.stringify(canonical)).digest("hex");
|
|
14
|
+
}
|
|
15
|
+
export const PI_SERVER_EMPTY_TREE_HASH = createHash("sha256").update("pi-tree-v1").digest("hex");
|
|
16
|
+
export function hashPiServerTreeEntry(entry) {
|
|
17
|
+
return createHash("sha256").update(JSON.stringify(entry)).digest("hex");
|
|
18
|
+
}
|
|
19
|
+
export function appendPiServerTreeHash(previousHash, entryHash) {
|
|
20
|
+
return createHash("sha256").update(`${previousHash}:${entryHash}`).digest("hex");
|
|
21
|
+
}
|
|
22
|
+
export function buildPiServerTreePrefixHashes(entries) {
|
|
23
|
+
const hashes = [PI_SERVER_EMPTY_TREE_HASH];
|
|
24
|
+
for (const entry of entries) {
|
|
25
|
+
hashes.push(appendPiServerTreeHash(hashes[hashes.length - 1], hashPiServerTreeEntry(entry)));
|
|
26
|
+
}
|
|
27
|
+
return hashes;
|
|
28
|
+
}
|
|
29
|
+
export function hashPiServerSessionEntries(entries) {
|
|
30
|
+
return buildPiServerTreePrefixHashes(entries)[entries.length];
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=pi-server-protocol.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pi-server-protocol.js","sourceRoot":"","sources":["../src/pi-server-protocol.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AASzC,MAAM,UAAU,yBAAyB,CAAC,OAA0C,EAAU;IAC7F,IAAI,CAAC,OAAO;QAAE,OAAO,EAAE,CAAC;IACxB,MAAM,SAAS,GAAG;QACjB,YAAY,EAAE,OAAO,CAAC,YAAY;QAClC,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;YACpC,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,WAAW,EAAE,IAAI,CAAC,WAAW;YAC7B,UAAU,EAAE,IAAI,CAAC,UAAU;SAC3B,CAAC,CAAC;KACH,CAAC;IACF,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAAA,CAC5E;AAED,MAAM,CAAC,MAAM,yBAAyB,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAEjG,MAAM,UAAU,qBAAqB,CAAC,KAAuB,EAAU;IACtE,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAAA,CACxE;AAED,MAAM,UAAU,sBAAsB,CAAC,YAAoB,EAAE,SAAiB,EAAU;IACvF,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,GAAG,YAAY,IAAI,SAAS,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAAA,CACjF;AAED,MAAM,UAAU,6BAA6B,CAAC,OAA2B,EAAY;IACpF,MAAM,MAAM,GAAG,CAAC,yBAAyB,CAAC,CAAC;IAC3C,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC7B,MAAM,CAAC,IAAI,CAAC,sBAAsB,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,EAAE,qBAAqB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAC9F,CAAC;IACD,OAAO,MAAM,CAAC;AAAA,CACd;AAED,MAAM,UAAU,0BAA0B,CAAC,OAA2B,EAAU;IAC/E,OAAO,6BAA6B,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;AAAA,CAC9D","sourcesContent":["import { createHash } from \"node:crypto\";\nimport type { SessionTreeEntry } from \"@earendil-works/pi-agent-core\";\nimport type { Tool } from \"@earendil-works/pi-ai\";\n\nexport interface PiServerStaticContext {\n\tsystemPrompt?: string;\n\ttools?: Tool[];\n}\n\nexport function hashPiServerStaticContext(context: PiServerStaticContext | undefined): string {\n\tif (!context) return \"\";\n\tconst canonical = {\n\t\tsystemPrompt: context.systemPrompt,\n\t\ttools: context.tools?.map((tool) => ({\n\t\t\tname: tool.name,\n\t\t\tdescription: tool.description,\n\t\t\tparameters: tool.parameters,\n\t\t})),\n\t};\n\treturn createHash(\"sha256\").update(JSON.stringify(canonical)).digest(\"hex\");\n}\n\nexport const PI_SERVER_EMPTY_TREE_HASH = createHash(\"sha256\").update(\"pi-tree-v1\").digest(\"hex\");\n\nexport function hashPiServerTreeEntry(entry: SessionTreeEntry): string {\n\treturn createHash(\"sha256\").update(JSON.stringify(entry)).digest(\"hex\");\n}\n\nexport function appendPiServerTreeHash(previousHash: string, entryHash: string): string {\n\treturn createHash(\"sha256\").update(`${previousHash}:${entryHash}`).digest(\"hex\");\n}\n\nexport function buildPiServerTreePrefixHashes(entries: SessionTreeEntry[]): string[] {\n\tconst hashes = [PI_SERVER_EMPTY_TREE_HASH];\n\tfor (const entry of entries) {\n\t\thashes.push(appendPiServerTreeHash(hashes[hashes.length - 1], hashPiServerTreeEntry(entry)));\n\t}\n\treturn hashes;\n}\n\nexport function hashPiServerSessionEntries(entries: SessionTreeEntry[]): string {\n\treturn buildPiServerTreePrefixHashes(entries)[entries.length];\n}\n"]}
|
package/dist/request-chunks.d.ts
CHANGED
|
@@ -2,15 +2,16 @@ export declare const CHUNK_ENDPOINT = "/api/request/chunk";
|
|
|
2
2
|
export interface RequestChunkBody {
|
|
3
3
|
requestId: string;
|
|
4
4
|
target: string;
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
chunkIndex: number;
|
|
6
|
+
totalChunks: number;
|
|
7
|
+
sha256: string;
|
|
7
8
|
chunk: string;
|
|
8
9
|
}
|
|
9
10
|
interface ChunkAck {
|
|
10
11
|
received: true;
|
|
11
12
|
requestId: string;
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
chunkIndex: number;
|
|
14
|
+
totalChunks: number;
|
|
14
15
|
}
|
|
15
16
|
interface PendingChunkResult {
|
|
16
17
|
complete: false;
|
|
@@ -21,7 +22,16 @@ interface CompleteChunkResult {
|
|
|
21
22
|
target: string;
|
|
22
23
|
bodyJson: string;
|
|
23
24
|
}
|
|
24
|
-
export declare
|
|
25
|
+
export declare const REQUEST_CHUNK_PENDING_TTL_MS: number;
|
|
26
|
+
export declare const REQUEST_CHUNK_MAX_PENDING_BYTES: number;
|
|
27
|
+
export declare const REQUEST_CHUNK_COMPLETED_TTL_MS: number;
|
|
28
|
+
interface ReceiveRequestChunkOptions {
|
|
29
|
+
nowMs?: number;
|
|
30
|
+
pendingTtlMs?: number;
|
|
31
|
+
maxPendingBytes?: number;
|
|
32
|
+
completedTtlMs?: number;
|
|
33
|
+
}
|
|
34
|
+
export declare function receiveRequestChunk(body: RequestChunkBody, options?: ReceiveRequestChunkOptions): PendingChunkResult | CompleteChunkResult;
|
|
25
35
|
export declare function clearAllRequestChunks(): void;
|
|
26
36
|
export {};
|
|
27
37
|
//# sourceMappingURL=request-chunks.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"request-chunks.d.ts","sourceRoot":"","sources":["../src/request-chunks.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"request-chunks.d.ts","sourceRoot":"","sources":["../src/request-chunks.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,cAAc,uBAAuB,CAAC;AAenD,MAAM,WAAW,gBAAgB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACd;AAwBD,UAAU,QAAQ;IACjB,QAAQ,EAAE,IAAI,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;CACpB;AAED,UAAU,kBAAkB;IAC3B,QAAQ,EAAE,KAAK,CAAC;IAChB,GAAG,EAAE,QAAQ,CAAC;CACd;AAED,UAAU,mBAAmB;IAC5B,QAAQ,EAAE,IAAI,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CACjB;AAMD,eAAO,MAAM,4BAA4B,QAAgB,CAAC;AAC1D,eAAO,MAAM,+BAA+B,QAAmB,CAAC;AAChE,eAAO,MAAM,8BAA8B,QAAY,CAAC;AAExD,UAAU,0BAA0B;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AAkED,wBAAgB,mBAAmB,CAClC,IAAI,EAAE,gBAAgB,EACtB,OAAO,GAAE,0BAA+B,GACtC,kBAAkB,GAAG,mBAAmB,CAuF1C;AAED,wBAAgB,qBAAqB,IAAI,IAAI,CAI5C","sourcesContent":["import { Buffer } from \"node:buffer\";\nimport { createHash } from \"node:crypto\";\n\nexport const CHUNK_ENDPOINT = \"/api/request/chunk\";\n\nconst ALLOWED_TARGETS = new Set([\n\t\"/api/session/init\",\n\t\"/api/session/update\",\n\t\"/api/session/sync\",\n\t\"/api/session/append\",\n\t\"/api/session/tree/sync\",\n\t\"/api/session/tree/append\",\n\t\"/api/session/tree/switch\",\n\t\"/api/session/drop-last-assistant-error\",\n\t\"/api/session/compact\",\n\t\"/api/stream\",\n]);\n\nexport interface RequestChunkBody {\n\trequestId: string;\n\ttarget: string;\n\tchunkIndex: number;\n\ttotalChunks: number;\n\tsha256: string;\n\tchunk: string;\n}\n\ninterface RequestChunk {\n\tchunk: string;\n\tsha256: string;\n}\n\ninterface PendingRequest {\n\ttarget: string;\n\ttotalChunks: number;\n\tchunks: Map<number, RequestChunk>;\n\treceivedBytes: number;\n\tupdatedAtMs: number;\n}\n\ninterface CompletedRequest {\n\ttarget: string;\n\ttotalChunks: number;\n\tbodyJson: string;\n\tcompletedChunkIndex: number;\n\tcompletedAtMs: number;\n\tchunks: Map<number, RequestChunk>;\n}\n\ninterface ChunkAck {\n\treceived: true;\n\trequestId: string;\n\tchunkIndex: number;\n\ttotalChunks: number;\n}\n\ninterface PendingChunkResult {\n\tcomplete: false;\n\tack: ChunkAck;\n}\n\ninterface CompleteChunkResult {\n\tcomplete: true;\n\ttarget: string;\n\tbodyJson: string;\n}\n\nconst pendingRequests = new Map<string, PendingRequest>();\nconst completedRequests = new Map<string, CompletedRequest>();\nlet pendingRequestBytes = 0;\n\nexport const REQUEST_CHUNK_PENDING_TTL_MS = 5 * 60 * 1000;\nexport const REQUEST_CHUNK_MAX_PENDING_BYTES = 64 * 1024 * 1024;\nexport const REQUEST_CHUNK_COMPLETED_TTL_MS = 60 * 1000;\n\ninterface ReceiveRequestChunkOptions {\n\tnowMs?: number;\n\tpendingTtlMs?: number;\n\tmaxPendingBytes?: number;\n\tcompletedTtlMs?: number;\n}\n\nfunction sha256(value: string): string {\n\treturn createHash(\"sha256\").update(value).digest(\"hex\");\n}\n\nfunction assertValidChunk(body: RequestChunkBody): void {\n\tif (!body.requestId) throw new Error(\"requestId is required\");\n\tif (!ALLOWED_TARGETS.has(body.target)) throw new Error(`Unsupported chunk target: ${body.target}`);\n\tif (!Number.isInteger(body.totalChunks) || body.totalChunks <= 0) {\n\t\tthrow new Error(\"totalChunks must be a positive integer\");\n\t}\n\tif (!Number.isInteger(body.chunkIndex) || body.chunkIndex < 0 || body.chunkIndex >= body.totalChunks) {\n\t\tthrow new Error(\"chunkIndex must be an integer within the chunk range\");\n\t}\n\tif (typeof body.chunk !== \"string\") throw new Error(\"chunk must be a string\");\n\tif (typeof body.sha256 !== \"string\" || !/^[a-f0-9]{64}$/i.test(body.sha256)) {\n\t\tthrow new Error(\"sha256 must be a 64-character hex string\");\n\t}\n\tif (sha256(body.chunk) !== body.sha256) {\n\t\tthrow new Error(`Chunk checksum mismatch: ${body.chunkIndex}`);\n\t}\n}\n\nfunction makeAck(body: RequestChunkBody): PendingChunkResult {\n\treturn {\n\t\tcomplete: false,\n\t\tack: {\n\t\t\treceived: true,\n\t\t\trequestId: body.requestId,\n\t\t\tchunkIndex: body.chunkIndex,\n\t\t\ttotalChunks: body.totalChunks,\n\t\t},\n\t};\n}\n\nfunction deletePendingRequest(requestId: string, pending: PendingRequest): void {\n\tpendingRequests.delete(requestId);\n\tpendingRequestBytes -= pending.receivedBytes;\n}\n\nfunction cleanupExpiredRequests(nowMs: number, pendingTtlMs: number, completedTtlMs: number): void {\n\tfor (const [requestId, pending] of pendingRequests) {\n\t\tif (nowMs - pending.updatedAtMs > pendingTtlMs) {\n\t\t\tdeletePendingRequest(requestId, pending);\n\t\t}\n\t}\n\tfor (const [requestId, completed] of completedRequests) {\n\t\tif (nowMs - completed.completedAtMs > completedTtlMs) {\n\t\t\tcompletedRequests.delete(requestId);\n\t\t}\n\t}\n}\n\nfunction cleanupForPendingBytes(extraBytes: number, maxPendingBytes: number, protectedRequestId: string): void {\n\tfor (const [requestId, pending] of pendingRequests) {\n\t\tif (pendingRequestBytes + extraBytes <= maxPendingBytes) return;\n\t\tif (requestId !== protectedRequestId) {\n\t\t\tdeletePendingRequest(requestId, pending);\n\t\t}\n\t}\n\tif (pendingRequestBytes + extraBytes > maxPendingBytes) {\n\t\tthrow new Error(\"Request chunk pending bytes limit exceeded\");\n\t}\n}\n\nexport function receiveRequestChunk(\n\tbody: RequestChunkBody,\n\toptions: ReceiveRequestChunkOptions = {},\n): PendingChunkResult | CompleteChunkResult {\n\tassertValidChunk(body);\n\n\tconst nowMs = options.nowMs ?? Date.now();\n\tcleanupExpiredRequests(\n\t\tnowMs,\n\t\toptions.pendingTtlMs ?? REQUEST_CHUNK_PENDING_TTL_MS,\n\t\toptions.completedTtlMs ?? REQUEST_CHUNK_COMPLETED_TTL_MS,\n\t);\n\n\tconst completed = completedRequests.get(body.requestId);\n\tif (completed) {\n\t\tif (completed.target !== body.target || completed.totalChunks !== body.totalChunks) {\n\t\t\tthrow new Error(\"Chunk metadata does not match the completed request\");\n\t\t}\n\t\tconst existing = completed.chunks.get(body.chunkIndex);\n\t\tif (!existing || existing.chunk !== body.chunk || existing.sha256 !== body.sha256) {\n\t\t\tthrow new Error(`Duplicate chunk index does not match: ${body.chunkIndex}`);\n\t\t}\n\t\tif (body.chunkIndex === completed.completedChunkIndex) {\n\t\t\treturn {\n\t\t\t\tcomplete: true,\n\t\t\t\ttarget: completed.target,\n\t\t\t\tbodyJson: completed.bodyJson,\n\t\t\t};\n\t\t}\n\t\treturn makeAck(body);\n\t}\n\n\tlet pending = pendingRequests.get(body.requestId);\n\tif (pending) {\n\t\tif (pending.target !== body.target || pending.totalChunks !== body.totalChunks) {\n\t\t\tthrow new Error(\"Chunk metadata does not match the pending request\");\n\t\t}\n\t\tconst existing = pending.chunks.get(body.chunkIndex);\n\t\tif (existing) {\n\t\t\tif (existing.chunk !== body.chunk || existing.sha256 !== body.sha256) {\n\t\t\t\tthrow new Error(`Duplicate chunk index does not match: ${body.chunkIndex}`);\n\t\t\t}\n\t\t\tpending.updatedAtMs = nowMs;\n\t\t\treturn makeAck(body);\n\t\t}\n\t}\n\n\tconst chunkBytes = Buffer.byteLength(body.chunk, \"utf-8\");\n\tcleanupForPendingBytes(chunkBytes, options.maxPendingBytes ?? REQUEST_CHUNK_MAX_PENDING_BYTES, body.requestId);\n\tif (!pending) {\n\t\tpending = {\n\t\t\ttarget: body.target,\n\t\t\ttotalChunks: body.totalChunks,\n\t\t\tchunks: new Map(),\n\t\t\treceivedBytes: 0,\n\t\t\tupdatedAtMs: nowMs,\n\t\t};\n\t\tpendingRequests.set(body.requestId, pending);\n\t}\n\tpending.chunks.set(body.chunkIndex, { chunk: body.chunk, sha256: body.sha256 });\n\tpending.receivedBytes += chunkBytes;\n\tpending.updatedAtMs = nowMs;\n\tpendingRequestBytes += chunkBytes;\n\n\tif (pending.chunks.size !== pending.totalChunks) {\n\t\treturn makeAck(body);\n\t}\n\n\tconst encodedChunks: string[] = [];\n\tfor (let index = 0; index < pending.totalChunks; index++) {\n\t\tconst chunk = pending.chunks.get(index);\n\t\tif (chunk === undefined) throw new Error(`Missing chunk index: ${index}`);\n\t\tencodedChunks.push(chunk.chunk);\n\t}\n\n\tdeletePendingRequest(body.requestId, pending);\n\tconst bodyJson = Buffer.from(encodedChunks.join(\"\"), \"base64\").toString(\"utf-8\");\n\tcompletedRequests.set(body.requestId, {\n\t\ttarget: pending.target,\n\t\ttotalChunks: pending.totalChunks,\n\t\tbodyJson,\n\t\tcompletedChunkIndex: body.chunkIndex,\n\t\tcompletedAtMs: nowMs,\n\t\tchunks: pending.chunks,\n\t});\n\treturn {\n\t\tcomplete: true,\n\t\ttarget: pending.target,\n\t\tbodyJson,\n\t};\n}\n\nexport function clearAllRequestChunks(): void {\n\tpendingRequests.clear();\n\tcompletedRequests.clear();\n\tpendingRequestBytes = 0;\n}\n"]}
|
package/dist/request-chunks.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Buffer } from "node:buffer";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
2
3
|
export const CHUNK_ENDPOINT = "/api/request/chunk";
|
|
3
4
|
const ALLOWED_TARGETS = new Set([
|
|
4
5
|
"/api/session/init",
|
|
@@ -13,59 +14,154 @@ const ALLOWED_TARGETS = new Set([
|
|
|
13
14
|
"/api/stream",
|
|
14
15
|
]);
|
|
15
16
|
const pendingRequests = new Map();
|
|
17
|
+
const completedRequests = new Map();
|
|
18
|
+
let pendingRequestBytes = 0;
|
|
19
|
+
export const REQUEST_CHUNK_PENDING_TTL_MS = 5 * 60 * 1000;
|
|
20
|
+
export const REQUEST_CHUNK_MAX_PENDING_BYTES = 64 * 1024 * 1024;
|
|
21
|
+
export const REQUEST_CHUNK_COMPLETED_TTL_MS = 60 * 1000;
|
|
22
|
+
function sha256(value) {
|
|
23
|
+
return createHash("sha256").update(value).digest("hex");
|
|
24
|
+
}
|
|
16
25
|
function assertValidChunk(body) {
|
|
17
26
|
if (!body.requestId)
|
|
18
27
|
throw new Error("requestId is required");
|
|
19
28
|
if (!ALLOWED_TARGETS.has(body.target))
|
|
20
29
|
throw new Error(`Unsupported chunk target: ${body.target}`);
|
|
21
|
-
if (!Number.isInteger(body.
|
|
22
|
-
throw new Error("
|
|
23
|
-
|
|
24
|
-
|
|
30
|
+
if (!Number.isInteger(body.totalChunks) || body.totalChunks <= 0) {
|
|
31
|
+
throw new Error("totalChunks must be a positive integer");
|
|
32
|
+
}
|
|
33
|
+
if (!Number.isInteger(body.chunkIndex) || body.chunkIndex < 0 || body.chunkIndex >= body.totalChunks) {
|
|
34
|
+
throw new Error("chunkIndex must be an integer within the chunk range");
|
|
25
35
|
}
|
|
26
36
|
if (typeof body.chunk !== "string")
|
|
27
37
|
throw new Error("chunk must be a string");
|
|
38
|
+
if (typeof body.sha256 !== "string" || !/^[a-f0-9]{64}$/i.test(body.sha256)) {
|
|
39
|
+
throw new Error("sha256 must be a 64-character hex string");
|
|
40
|
+
}
|
|
41
|
+
if (sha256(body.chunk) !== body.sha256) {
|
|
42
|
+
throw new Error(`Chunk checksum mismatch: ${body.chunkIndex}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function makeAck(body) {
|
|
46
|
+
return {
|
|
47
|
+
complete: false,
|
|
48
|
+
ack: {
|
|
49
|
+
received: true,
|
|
50
|
+
requestId: body.requestId,
|
|
51
|
+
chunkIndex: body.chunkIndex,
|
|
52
|
+
totalChunks: body.totalChunks,
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function deletePendingRequest(requestId, pending) {
|
|
57
|
+
pendingRequests.delete(requestId);
|
|
58
|
+
pendingRequestBytes -= pending.receivedBytes;
|
|
59
|
+
}
|
|
60
|
+
function cleanupExpiredRequests(nowMs, pendingTtlMs, completedTtlMs) {
|
|
61
|
+
for (const [requestId, pending] of pendingRequests) {
|
|
62
|
+
if (nowMs - pending.updatedAtMs > pendingTtlMs) {
|
|
63
|
+
deletePendingRequest(requestId, pending);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
for (const [requestId, completed] of completedRequests) {
|
|
67
|
+
if (nowMs - completed.completedAtMs > completedTtlMs) {
|
|
68
|
+
completedRequests.delete(requestId);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
28
71
|
}
|
|
29
|
-
|
|
72
|
+
function cleanupForPendingBytes(extraBytes, maxPendingBytes, protectedRequestId) {
|
|
73
|
+
for (const [requestId, pending] of pendingRequests) {
|
|
74
|
+
if (pendingRequestBytes + extraBytes <= maxPendingBytes)
|
|
75
|
+
return;
|
|
76
|
+
if (requestId !== protectedRequestId) {
|
|
77
|
+
deletePendingRequest(requestId, pending);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (pendingRequestBytes + extraBytes > maxPendingBytes) {
|
|
81
|
+
throw new Error("Request chunk pending bytes limit exceeded");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
export function receiveRequestChunk(body, options = {}) {
|
|
30
85
|
assertValidChunk(body);
|
|
86
|
+
const nowMs = options.nowMs ?? Date.now();
|
|
87
|
+
cleanupExpiredRequests(nowMs, options.pendingTtlMs ?? REQUEST_CHUNK_PENDING_TTL_MS, options.completedTtlMs ?? REQUEST_CHUNK_COMPLETED_TTL_MS);
|
|
88
|
+
const completed = completedRequests.get(body.requestId);
|
|
89
|
+
if (completed) {
|
|
90
|
+
if (completed.target !== body.target || completed.totalChunks !== body.totalChunks) {
|
|
91
|
+
throw new Error("Chunk metadata does not match the completed request");
|
|
92
|
+
}
|
|
93
|
+
const existing = completed.chunks.get(body.chunkIndex);
|
|
94
|
+
if (!existing || existing.chunk !== body.chunk || existing.sha256 !== body.sha256) {
|
|
95
|
+
throw new Error(`Duplicate chunk index does not match: ${body.chunkIndex}`);
|
|
96
|
+
}
|
|
97
|
+
if (body.chunkIndex === completed.completedChunkIndex) {
|
|
98
|
+
return {
|
|
99
|
+
complete: true,
|
|
100
|
+
target: completed.target,
|
|
101
|
+
bodyJson: completed.bodyJson,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
return makeAck(body);
|
|
105
|
+
}
|
|
31
106
|
let pending = pendingRequests.get(body.requestId);
|
|
107
|
+
if (pending) {
|
|
108
|
+
if (pending.target !== body.target || pending.totalChunks !== body.totalChunks) {
|
|
109
|
+
throw new Error("Chunk metadata does not match the pending request");
|
|
110
|
+
}
|
|
111
|
+
const existing = pending.chunks.get(body.chunkIndex);
|
|
112
|
+
if (existing) {
|
|
113
|
+
if (existing.chunk !== body.chunk || existing.sha256 !== body.sha256) {
|
|
114
|
+
throw new Error(`Duplicate chunk index does not match: ${body.chunkIndex}`);
|
|
115
|
+
}
|
|
116
|
+
pending.updatedAtMs = nowMs;
|
|
117
|
+
return makeAck(body);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const chunkBytes = Buffer.byteLength(body.chunk, "utf-8");
|
|
121
|
+
cleanupForPendingBytes(chunkBytes, options.maxPendingBytes ?? REQUEST_CHUNK_MAX_PENDING_BYTES, body.requestId);
|
|
32
122
|
if (!pending) {
|
|
33
|
-
pending = {
|
|
123
|
+
pending = {
|
|
124
|
+
target: body.target,
|
|
125
|
+
totalChunks: body.totalChunks,
|
|
126
|
+
chunks: new Map(),
|
|
127
|
+
receivedBytes: 0,
|
|
128
|
+
updatedAtMs: nowMs,
|
|
129
|
+
};
|
|
34
130
|
pendingRequests.set(body.requestId, pending);
|
|
35
131
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
pending.chunks.set(body.index, body.chunk);
|
|
43
|
-
if (pending.chunks.size !== pending.total) {
|
|
44
|
-
return {
|
|
45
|
-
complete: false,
|
|
46
|
-
ack: {
|
|
47
|
-
received: true,
|
|
48
|
-
requestId: body.requestId,
|
|
49
|
-
index: body.index,
|
|
50
|
-
total: body.total,
|
|
51
|
-
},
|
|
52
|
-
};
|
|
132
|
+
pending.chunks.set(body.chunkIndex, { chunk: body.chunk, sha256: body.sha256 });
|
|
133
|
+
pending.receivedBytes += chunkBytes;
|
|
134
|
+
pending.updatedAtMs = nowMs;
|
|
135
|
+
pendingRequestBytes += chunkBytes;
|
|
136
|
+
if (pending.chunks.size !== pending.totalChunks) {
|
|
137
|
+
return makeAck(body);
|
|
53
138
|
}
|
|
54
139
|
const encodedChunks = [];
|
|
55
|
-
for (let index = 0; index < pending.
|
|
140
|
+
for (let index = 0; index < pending.totalChunks; index++) {
|
|
56
141
|
const chunk = pending.chunks.get(index);
|
|
57
142
|
if (chunk === undefined)
|
|
58
143
|
throw new Error(`Missing chunk index: ${index}`);
|
|
59
|
-
encodedChunks.push(chunk);
|
|
144
|
+
encodedChunks.push(chunk.chunk);
|
|
60
145
|
}
|
|
61
|
-
|
|
146
|
+
deletePendingRequest(body.requestId, pending);
|
|
147
|
+
const bodyJson = Buffer.from(encodedChunks.join(""), "base64").toString("utf-8");
|
|
148
|
+
completedRequests.set(body.requestId, {
|
|
149
|
+
target: pending.target,
|
|
150
|
+
totalChunks: pending.totalChunks,
|
|
151
|
+
bodyJson,
|
|
152
|
+
completedChunkIndex: body.chunkIndex,
|
|
153
|
+
completedAtMs: nowMs,
|
|
154
|
+
chunks: pending.chunks,
|
|
155
|
+
});
|
|
62
156
|
return {
|
|
63
157
|
complete: true,
|
|
64
158
|
target: pending.target,
|
|
65
|
-
bodyJson
|
|
159
|
+
bodyJson,
|
|
66
160
|
};
|
|
67
161
|
}
|
|
68
162
|
export function clearAllRequestChunks() {
|
|
69
163
|
pendingRequests.clear();
|
|
164
|
+
completedRequests.clear();
|
|
165
|
+
pendingRequestBytes = 0;
|
|
70
166
|
}
|
|
71
167
|
//# sourceMappingURL=request-chunks.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"request-chunks.js","sourceRoot":"","sources":["../src/request-chunks.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC,MAAM,CAAC,MAAM,cAAc,GAAG,oBAAoB,CAAC;AAEnD,MAAM,eAAe,GAAG,IAAI,GAAG,CAAC;IAC/B,mBAAmB;IACnB,qBAAqB;IACrB,mBAAmB;IACnB,qBAAqB;IACrB,wBAAwB;IACxB,0BAA0B;IAC1B,0BAA0B;IAC1B,wCAAwC;IACxC,sBAAsB;IACtB,aAAa;CACb,CAAC,CAAC;AAkCH,MAAM,eAAe,GAAG,IAAI,GAAG,EAA0B,CAAC;AAE1D,SAAS,gBAAgB,CAAC,IAAsB,EAAQ;IACvD,IAAI,CAAC,IAAI,CAAC,SAAS;QAAE,MAAM,IAAI,KAAK,CAAC,uBAAuB,CAAC,CAAC;IAC9D,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,6BAA6B,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;IACnG,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,KAAK,IAAI,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,kCAAkC,CAAC,CAAC;IAC1G,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,KAAK,GAAG,CAAC,IAAI,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;QACjF,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAC;IACpE,CAAC;IACD,IAAI,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ;QAAE,MAAM,IAAI,KAAK,CAAC,wBAAwB,CAAC,CAAC;AAAA,CAC9E;AAED,MAAM,UAAU,mBAAmB,CAAC,IAAsB,EAA4C;IACrG,gBAAgB,CAAC,IAAI,CAAC,CAAC;IAEvB,IAAI,OAAO,GAAG,eAAe,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAClD,IAAI,CAAC,OAAO,EAAE,CAAC;QACd,OAAO,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,GAAG,EAAE,EAAE,CAAC;QACxE,eAAe,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;IAC9C,CAAC;IAED,IAAI,OAAO,CAAC,MAAM,KAAK,IAAI,CAAC,MAAM,IAAI,OAAO,CAAC,KAAK,KAAK,IAAI,CAAC,KAAK,EAAE,CAAC;QACpE,MAAM,IAAI,KAAK,CAAC,mDAAmD,CAAC,CAAC;IACtE,CAAC;IACD,IAAI,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QACpC,MAAM,IAAI,KAAK,CAAC,0BAA0B,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;IACzD,CAAC;IAED,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;IAE3C,IAAI,OAAO,CAAC,MAAM,CAAC,IAAI,KAAK,OAAO,CAAC,KAAK,EAAE,CAAC;QAC3C,OAAO;YACN,QAAQ,EAAE,KAAK;YACf,GAAG,EAAE;gBACJ,QAAQ,EAAE,IAAI;gBACd,SAAS,EAAE,IAAI,CAAC,SAAS;gBACzB,KAAK,EAAE,IAAI,CAAC,KAAK;gBACjB,KAAK,EAAE,IAAI,CAAC,KAAK;aACjB;SACD,CAAC;IACH,CAAC;IAED,MAAM,aAAa,GAAa,EAAE,CAAC;IACnC,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,OAAO,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC;QACpD,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACxC,IAAI,KAAK,KAAK,SAAS;YAAE,MAAM,IAAI,KAAK,CAAC,wBAAwB,KAAK,EAAE,CAAC,CAAC;QAC1E,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC3B,CAAC;IAED,eAAe,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IACvC,OAAO;QACN,QAAQ,EAAE,IAAI;QACd,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,QAAQ,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC;KACzE,CAAC;AAAA,CACF;AAED,MAAM,UAAU,qBAAqB,GAAS;IAC7C,eAAe,CAAC,KAAK,EAAE,CAAC;AAAA,CACxB","sourcesContent":["import { Buffer } from \"node:buffer\";\n\nexport const CHUNK_ENDPOINT = \"/api/request/chunk\";\n\nconst ALLOWED_TARGETS = new Set([\n\t\"/api/session/init\",\n\t\"/api/session/update\",\n\t\"/api/session/sync\",\n\t\"/api/session/append\",\n\t\"/api/session/tree/sync\",\n\t\"/api/session/tree/append\",\n\t\"/api/session/tree/switch\",\n\t\"/api/session/drop-last-assistant-error\",\n\t\"/api/session/compact\",\n\t\"/api/stream\",\n]);\n\nexport interface RequestChunkBody {\n\trequestId: string;\n\ttarget: string;\n\tindex: number;\n\ttotal: number;\n\tchunk: string;\n}\n\ninterface PendingRequest {\n\ttarget: string;\n\ttotal: number;\n\tchunks: Map<number, string>;\n}\n\ninterface ChunkAck {\n\treceived: true;\n\trequestId: string;\n\tindex: number;\n\ttotal: number;\n}\n\ninterface PendingChunkResult {\n\tcomplete: false;\n\tack: ChunkAck;\n}\n\ninterface CompleteChunkResult {\n\tcomplete: true;\n\ttarget: string;\n\tbodyJson: string;\n}\n\nconst pendingRequests = new Map<string, PendingRequest>();\n\nfunction assertValidChunk(body: RequestChunkBody): void {\n\tif (!body.requestId) throw new Error(\"requestId is required\");\n\tif (!ALLOWED_TARGETS.has(body.target)) throw new Error(`Unsupported chunk target: ${body.target}`);\n\tif (!Number.isInteger(body.total) || body.total <= 0) throw new Error(\"total must be a positive integer\");\n\tif (!Number.isInteger(body.index) || body.index < 0 || body.index >= body.total) {\n\t\tthrow new Error(\"index must be an integer within the chunk range\");\n\t}\n\tif (typeof body.chunk !== \"string\") throw new Error(\"chunk must be a string\");\n}\n\nexport function receiveRequestChunk(body: RequestChunkBody): PendingChunkResult | CompleteChunkResult {\n\tassertValidChunk(body);\n\n\tlet pending = pendingRequests.get(body.requestId);\n\tif (!pending) {\n\t\tpending = { target: body.target, total: body.total, chunks: new Map() };\n\t\tpendingRequests.set(body.requestId, pending);\n\t}\n\n\tif (pending.target !== body.target || pending.total !== body.total) {\n\t\tthrow new Error(\"Chunk metadata does not match the pending request\");\n\t}\n\tif (pending.chunks.has(body.index)) {\n\t\tthrow new Error(`Duplicate chunk index: ${body.index}`);\n\t}\n\n\tpending.chunks.set(body.index, body.chunk);\n\n\tif (pending.chunks.size !== pending.total) {\n\t\treturn {\n\t\t\tcomplete: false,\n\t\t\tack: {\n\t\t\t\treceived: true,\n\t\t\t\trequestId: body.requestId,\n\t\t\t\tindex: body.index,\n\t\t\t\ttotal: body.total,\n\t\t\t},\n\t\t};\n\t}\n\n\tconst encodedChunks: string[] = [];\n\tfor (let index = 0; index < pending.total; index++) {\n\t\tconst chunk = pending.chunks.get(index);\n\t\tif (chunk === undefined) throw new Error(`Missing chunk index: ${index}`);\n\t\tencodedChunks.push(chunk);\n\t}\n\n\tpendingRequests.delete(body.requestId);\n\treturn {\n\t\tcomplete: true,\n\t\ttarget: pending.target,\n\t\tbodyJson: Buffer.from(encodedChunks.join(\"\"), \"base64\").toString(\"utf-8\"),\n\t};\n}\n\nexport function clearAllRequestChunks(): void {\n\tpendingRequests.clear();\n}\n"]}
|
|
1
|
+
{"version":3,"file":"request-chunks.js","sourceRoot":"","sources":["../src/request-chunks.ts"],"names":[],"mappings":"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","sourcesContent":["import { Buffer } from \"node:buffer\";\nimport { createHash } from \"node:crypto\";\n\nexport const CHUNK_ENDPOINT = \"/api/request/chunk\";\n\nconst ALLOWED_TARGETS = new Set([\n\t\"/api/session/init\",\n\t\"/api/session/update\",\n\t\"/api/session/sync\",\n\t\"/api/session/append\",\n\t\"/api/session/tree/sync\",\n\t\"/api/session/tree/append\",\n\t\"/api/session/tree/switch\",\n\t\"/api/session/drop-last-assistant-error\",\n\t\"/api/session/compact\",\n\t\"/api/stream\",\n]);\n\nexport interface RequestChunkBody {\n\trequestId: string;\n\ttarget: string;\n\tchunkIndex: number;\n\ttotalChunks: number;\n\tsha256: string;\n\tchunk: string;\n}\n\ninterface RequestChunk {\n\tchunk: string;\n\tsha256: string;\n}\n\ninterface PendingRequest {\n\ttarget: string;\n\ttotalChunks: number;\n\tchunks: Map<number, RequestChunk>;\n\treceivedBytes: number;\n\tupdatedAtMs: number;\n}\n\ninterface CompletedRequest {\n\ttarget: string;\n\ttotalChunks: number;\n\tbodyJson: string;\n\tcompletedChunkIndex: number;\n\tcompletedAtMs: number;\n\tchunks: Map<number, RequestChunk>;\n}\n\ninterface ChunkAck {\n\treceived: true;\n\trequestId: string;\n\tchunkIndex: number;\n\ttotalChunks: number;\n}\n\ninterface PendingChunkResult {\n\tcomplete: false;\n\tack: ChunkAck;\n}\n\ninterface CompleteChunkResult {\n\tcomplete: true;\n\ttarget: string;\n\tbodyJson: string;\n}\n\nconst pendingRequests = new Map<string, PendingRequest>();\nconst completedRequests = new Map<string, CompletedRequest>();\nlet pendingRequestBytes = 0;\n\nexport const REQUEST_CHUNK_PENDING_TTL_MS = 5 * 60 * 1000;\nexport const REQUEST_CHUNK_MAX_PENDING_BYTES = 64 * 1024 * 1024;\nexport const REQUEST_CHUNK_COMPLETED_TTL_MS = 60 * 1000;\n\ninterface ReceiveRequestChunkOptions {\n\tnowMs?: number;\n\tpendingTtlMs?: number;\n\tmaxPendingBytes?: number;\n\tcompletedTtlMs?: number;\n}\n\nfunction sha256(value: string): string {\n\treturn createHash(\"sha256\").update(value).digest(\"hex\");\n}\n\nfunction assertValidChunk(body: RequestChunkBody): void {\n\tif (!body.requestId) throw new Error(\"requestId is required\");\n\tif (!ALLOWED_TARGETS.has(body.target)) throw new Error(`Unsupported chunk target: ${body.target}`);\n\tif (!Number.isInteger(body.totalChunks) || body.totalChunks <= 0) {\n\t\tthrow new Error(\"totalChunks must be a positive integer\");\n\t}\n\tif (!Number.isInteger(body.chunkIndex) || body.chunkIndex < 0 || body.chunkIndex >= body.totalChunks) {\n\t\tthrow new Error(\"chunkIndex must be an integer within the chunk range\");\n\t}\n\tif (typeof body.chunk !== \"string\") throw new Error(\"chunk must be a string\");\n\tif (typeof body.sha256 !== \"string\" || !/^[a-f0-9]{64}$/i.test(body.sha256)) {\n\t\tthrow new Error(\"sha256 must be a 64-character hex string\");\n\t}\n\tif (sha256(body.chunk) !== body.sha256) {\n\t\tthrow new Error(`Chunk checksum mismatch: ${body.chunkIndex}`);\n\t}\n}\n\nfunction makeAck(body: RequestChunkBody): PendingChunkResult {\n\treturn {\n\t\tcomplete: false,\n\t\tack: {\n\t\t\treceived: true,\n\t\t\trequestId: body.requestId,\n\t\t\tchunkIndex: body.chunkIndex,\n\t\t\ttotalChunks: body.totalChunks,\n\t\t},\n\t};\n}\n\nfunction deletePendingRequest(requestId: string, pending: PendingRequest): void {\n\tpendingRequests.delete(requestId);\n\tpendingRequestBytes -= pending.receivedBytes;\n}\n\nfunction cleanupExpiredRequests(nowMs: number, pendingTtlMs: number, completedTtlMs: number): void {\n\tfor (const [requestId, pending] of pendingRequests) {\n\t\tif (nowMs - pending.updatedAtMs > pendingTtlMs) {\n\t\t\tdeletePendingRequest(requestId, pending);\n\t\t}\n\t}\n\tfor (const [requestId, completed] of completedRequests) {\n\t\tif (nowMs - completed.completedAtMs > completedTtlMs) {\n\t\t\tcompletedRequests.delete(requestId);\n\t\t}\n\t}\n}\n\nfunction cleanupForPendingBytes(extraBytes: number, maxPendingBytes: number, protectedRequestId: string): void {\n\tfor (const [requestId, pending] of pendingRequests) {\n\t\tif (pendingRequestBytes + extraBytes <= maxPendingBytes) return;\n\t\tif (requestId !== protectedRequestId) {\n\t\t\tdeletePendingRequest(requestId, pending);\n\t\t}\n\t}\n\tif (pendingRequestBytes + extraBytes > maxPendingBytes) {\n\t\tthrow new Error(\"Request chunk pending bytes limit exceeded\");\n\t}\n}\n\nexport function receiveRequestChunk(\n\tbody: RequestChunkBody,\n\toptions: ReceiveRequestChunkOptions = {},\n): PendingChunkResult | CompleteChunkResult {\n\tassertValidChunk(body);\n\n\tconst nowMs = options.nowMs ?? Date.now();\n\tcleanupExpiredRequests(\n\t\tnowMs,\n\t\toptions.pendingTtlMs ?? REQUEST_CHUNK_PENDING_TTL_MS,\n\t\toptions.completedTtlMs ?? REQUEST_CHUNK_COMPLETED_TTL_MS,\n\t);\n\n\tconst completed = completedRequests.get(body.requestId);\n\tif (completed) {\n\t\tif (completed.target !== body.target || completed.totalChunks !== body.totalChunks) {\n\t\t\tthrow new Error(\"Chunk metadata does not match the completed request\");\n\t\t}\n\t\tconst existing = completed.chunks.get(body.chunkIndex);\n\t\tif (!existing || existing.chunk !== body.chunk || existing.sha256 !== body.sha256) {\n\t\t\tthrow new Error(`Duplicate chunk index does not match: ${body.chunkIndex}`);\n\t\t}\n\t\tif (body.chunkIndex === completed.completedChunkIndex) {\n\t\t\treturn {\n\t\t\t\tcomplete: true,\n\t\t\t\ttarget: completed.target,\n\t\t\t\tbodyJson: completed.bodyJson,\n\t\t\t};\n\t\t}\n\t\treturn makeAck(body);\n\t}\n\n\tlet pending = pendingRequests.get(body.requestId);\n\tif (pending) {\n\t\tif (pending.target !== body.target || pending.totalChunks !== body.totalChunks) {\n\t\t\tthrow new Error(\"Chunk metadata does not match the pending request\");\n\t\t}\n\t\tconst existing = pending.chunks.get(body.chunkIndex);\n\t\tif (existing) {\n\t\t\tif (existing.chunk !== body.chunk || existing.sha256 !== body.sha256) {\n\t\t\t\tthrow new Error(`Duplicate chunk index does not match: ${body.chunkIndex}`);\n\t\t\t}\n\t\t\tpending.updatedAtMs = nowMs;\n\t\t\treturn makeAck(body);\n\t\t}\n\t}\n\n\tconst chunkBytes = Buffer.byteLength(body.chunk, \"utf-8\");\n\tcleanupForPendingBytes(chunkBytes, options.maxPendingBytes ?? REQUEST_CHUNK_MAX_PENDING_BYTES, body.requestId);\n\tif (!pending) {\n\t\tpending = {\n\t\t\ttarget: body.target,\n\t\t\ttotalChunks: body.totalChunks,\n\t\t\tchunks: new Map(),\n\t\t\treceivedBytes: 0,\n\t\t\tupdatedAtMs: nowMs,\n\t\t};\n\t\tpendingRequests.set(body.requestId, pending);\n\t}\n\tpending.chunks.set(body.chunkIndex, { chunk: body.chunk, sha256: body.sha256 });\n\tpending.receivedBytes += chunkBytes;\n\tpending.updatedAtMs = nowMs;\n\tpendingRequestBytes += chunkBytes;\n\n\tif (pending.chunks.size !== pending.totalChunks) {\n\t\treturn makeAck(body);\n\t}\n\n\tconst encodedChunks: string[] = [];\n\tfor (let index = 0; index < pending.totalChunks; index++) {\n\t\tconst chunk = pending.chunks.get(index);\n\t\tif (chunk === undefined) throw new Error(`Missing chunk index: ${index}`);\n\t\tencodedChunks.push(chunk.chunk);\n\t}\n\n\tdeletePendingRequest(body.requestId, pending);\n\tconst bodyJson = Buffer.from(encodedChunks.join(\"\"), \"base64\").toString(\"utf-8\");\n\tcompletedRequests.set(body.requestId, {\n\t\ttarget: pending.target,\n\t\ttotalChunks: pending.totalChunks,\n\t\tbodyJson,\n\t\tcompletedChunkIndex: body.chunkIndex,\n\t\tcompletedAtMs: nowMs,\n\t\tchunks: pending.chunks,\n\t});\n\treturn {\n\t\tcomplete: true,\n\t\ttarget: pending.target,\n\t\tbodyJson,\n\t};\n}\n\nexport function clearAllRequestChunks(): void {\n\tpendingRequests.clear();\n\tcompletedRequests.clear();\n\tpendingRequestBytes = 0;\n}\n"]}
|
package/dist/server.d.ts
CHANGED
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
import { type Server as HttpServer } from "node:http";
|
|
2
|
-
import { type Model, type SimpleStreamOptions } from "@earendil-works/pi-ai";
|
|
2
|
+
import { type Context, type Message, type Model, type SimpleStreamOptions } from "@earendil-works/pi-ai";
|
|
3
3
|
import type { ServerConfig } from "./config.ts";
|
|
4
|
-
import { type SessionStaticContext } from "./session-store.ts";
|
|
4
|
+
import { type SessionState, type SessionStaticContext } from "./session-store.ts";
|
|
5
5
|
export { loadConfig, type ServerConfig } from "./config.ts";
|
|
6
6
|
interface StreamRequestBody {
|
|
7
7
|
sessionId: string;
|
|
8
|
+
runId?: string;
|
|
8
9
|
model: Model<any>;
|
|
9
10
|
options?: SimpleStreamOptions;
|
|
10
11
|
staticContext?: SessionStaticContext;
|
|
12
|
+
ephemeralMessages?: Message[];
|
|
13
|
+
contextOverlay?: Message[];
|
|
11
14
|
}
|
|
12
15
|
interface ResolvedStream {
|
|
13
16
|
model: Model<any>;
|
|
14
17
|
options: SimpleStreamOptions;
|
|
15
18
|
}
|
|
16
19
|
export declare function resolveStreamOptions(_config: ServerConfig, model: Model<any>, body: StreamRequestBody): ResolvedStream;
|
|
20
|
+
export declare function buildStreamContext(session: SessionState, body: Pick<StreamRequestBody, "contextOverlay" | "ephemeralMessages">): Context;
|
|
17
21
|
export declare function createPiServer(configOverride?: Partial<ServerConfig>): HttpServer;
|
|
18
22
|
export declare function startServer(configOverride?: Partial<ServerConfig>): HttpServer;
|
|
19
23
|
//# sourceMappingURL=server.d.ts.map
|