@hachej/boring-agent 0.4.47 → 0.4.49
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/dist/chat-transport.js +32 -6
- package/package.json +3 -3
package/dist/chat-transport.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { diffRevisions } from '@earendil-works/chord/delta';
|
|
1
2
|
import { RequestGuardError, hasJsonContentType, readJsonBody } from '@hachej/boring-files/request-guard';
|
|
2
3
|
/** Wire protocol shared with the browser client in `@hachej/boring-ui-kit/remote-chat`. */
|
|
3
4
|
export const CHAT_TRANSPORT = Object.freeze({ schema: 'boring.chat-transport', version: 1, maxRequestBytes: 8_388_608, maxContextBytes: 4096, maxHistoryPage: 200, maxPendingFrames: 100, heartbeatMs: 15_000 });
|
|
@@ -60,9 +61,31 @@ function reads(conversation, context, project) {
|
|
|
60
61
|
},
|
|
61
62
|
};
|
|
62
63
|
}
|
|
63
|
-
/**
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
/**
|
|
65
|
+
* Serialize the watch frames of one connection; with `project` a view is projected first (a throw means `projection_failed`).
|
|
66
|
+
* A projected viewer used to get a complete view per update, which grows with the conversation (a 60 KB prompt, then every earlier
|
|
67
|
+
* entry, ten times a second). The first frame is still the complete view; after it, an update is the operation batch between the
|
|
68
|
+
* last view this connection received and the new projected one (`diffRevisions`), which the client already replays like native
|
|
69
|
+
* operations, and the complete view when that is not smaller. Raw operations never leave the host: the batch is computed from projected views.
|
|
70
|
+
*/
|
|
71
|
+
function frameEncoder(project) {
|
|
72
|
+
let sent;
|
|
73
|
+
return frame => {
|
|
74
|
+
if (frame.kind !== 'view' || !project)
|
|
75
|
+
return JSON.stringify(frame);
|
|
76
|
+
// Round-tripped, so the diff is between exactly what the client holds (no `undefined` fields).
|
|
77
|
+
const full = JSON.stringify({ kind: 'view', view: project(frame.view) });
|
|
78
|
+
const next = JSON.parse(full).view;
|
|
79
|
+
if (sent !== undefined) {
|
|
80
|
+
const batch = JSON.stringify({ kind: 'ops', ops: diffRevisions(sent, next) });
|
|
81
|
+
if (batch.length < full.length / 2) {
|
|
82
|
+
sent = next;
|
|
83
|
+
return batch;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
sent = next;
|
|
87
|
+
return full;
|
|
88
|
+
};
|
|
66
89
|
}
|
|
67
90
|
/** Open the conversation watch for an authenticated access, refusing a revoked one. Shared by the HTTP watch and the socket. */
|
|
68
91
|
async function openWatch(access, signal) {
|
|
@@ -76,9 +99,11 @@ async function openWatch(access, signal) {
|
|
|
76
99
|
/**
|
|
77
100
|
* Stream newline-delimited JSON. A trusted viewer receives one view and then the exact native operation batches,
|
|
78
101
|
* replayed in the browser with `@earendil-works/chord/delta`. With `project`, raw operations never leave the host:
|
|
79
|
-
* the viewer receives
|
|
102
|
+
* the viewer receives one projected view, then batches between projected views (`frameEncoder`), and a slow reader gets the
|
|
103
|
+
* batch to the latest one rather than a backlog.
|
|
80
104
|
*/
|
|
81
105
|
function stream(watch, project, signal, heartbeatMs) {
|
|
106
|
+
const encode = frameEncoder(project);
|
|
82
107
|
let pending = [{ kind: 'view', view: watch.value }], wake, end, finished = false;
|
|
83
108
|
// `beat` asks the next pull for a heartbeat. The timer restarts after every frame, so it fires only on an idle stream, and it is
|
|
84
109
|
// cleared when the stream finishes or is cancelled: nothing outlives the response.
|
|
@@ -120,7 +145,7 @@ function stream(watch, project, signal, heartbeatMs) {
|
|
|
120
145
|
if (frame) {
|
|
121
146
|
let line;
|
|
122
147
|
try {
|
|
123
|
-
line =
|
|
148
|
+
line = encode(frame);
|
|
124
149
|
}
|
|
125
150
|
catch {
|
|
126
151
|
finished = true;
|
|
@@ -416,9 +441,10 @@ export function createConversationSocketHandler(options) {
|
|
|
416
441
|
return null;
|
|
417
442
|
}
|
|
418
443
|
const { project } = access;
|
|
444
|
+
const encode = frameEncoder(project);
|
|
419
445
|
const frame = (value) => {
|
|
420
446
|
try {
|
|
421
|
-
send(socket,
|
|
447
|
+
send(socket, encode(value));
|
|
422
448
|
}
|
|
423
449
|
catch {
|
|
424
450
|
void watch.stop();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hachej/boring-agent",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.49",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -188,8 +188,8 @@
|
|
|
188
188
|
"@earendil-works/pi-durable": "~1.4.0",
|
|
189
189
|
"@earendil-works/pi-ai": "~1.4.0",
|
|
190
190
|
"@earendil-works/chord": "~1.4.0",
|
|
191
|
-
"@hachej/boring-files": "^0.4.
|
|
192
|
-
"@hachej/boring-ui-kit": "^0.4.
|
|
191
|
+
"@hachej/boring-files": "^0.4.49",
|
|
192
|
+
"@hachej/boring-ui-kit": "^0.4.49",
|
|
193
193
|
"typebox": "1.3.27",
|
|
194
194
|
"xlsx": "^0.20.3"
|
|
195
195
|
},
|