hyperbrane 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -3
- package/dist/agent/agentService.d.ts +11 -3
- package/dist/agent/agentService.js +63 -19
- package/dist/agent/agentStorage.d.ts +2 -0
- package/dist/agent/agentStorage.js +4 -2
- package/dist/agent/api.d.ts +83 -32
- package/dist/agent/api.js +7 -0
- package/dist/agent/client.d.ts +3 -0
- package/dist/agent/client.js +5 -0
- package/dist/agent/hostTranscript.d.ts +13 -0
- package/dist/agent/hostTranscript.js +190 -0
- package/dist/agent/session/backend.d.ts +82 -0
- package/dist/agent/session/backend.js +1 -0
- package/dist/agent/session/claude/home.d.ts +2 -0
- package/dist/agent/session/claude/home.js +4 -0
- package/dist/agent/session/claude/transcript.d.ts +13 -0
- package/dist/agent/session/claude/transcript.js +29 -0
- package/dist/agent/session/client.d.ts +5 -1
- package/dist/agent/session/codex/home.d.ts +2 -0
- package/dist/agent/session/codex/home.js +4 -0
- package/dist/agent/session/codex/messages.d.ts +30 -0
- package/dist/agent/session/codex/messages.js +135 -0
- package/dist/agent/session/codex/transcript.d.ts +12 -0
- package/dist/agent/session/codex/transcript.js +22 -0
- package/dist/agent/session/fileTranscript.d.ts +92 -0
- package/dist/agent/session/fileTranscript.js +454 -0
- package/dist/agent/session/sdkMessages.d.ts +15 -0
- package/dist/agent/session/sdkMessages.js +28 -0
- package/dist/agent/session/sessionStorage.d.ts +4 -0
- package/dist/agent/session/sessionStorage.js +13 -2
- package/dist/agent/session/sharedConnection.d.ts +1 -0
- package/dist/agent/session/storedTranscripts.d.ts +13 -0
- package/dist/agent/session/storedTranscripts.js +8 -0
- package/dist/agent/session/worker/protocol.d.ts +5 -1
- package/dist/agent/session/worker/protocol.js +6 -1
- package/dist/agent/session/worker/workerParams.d.ts +11 -0
- package/dist/agent/session/worker/workerParams.js +163 -0
- package/dist/agent/session/workerSseConnection.d.ts +20 -4
- package/dist/agent/session/workerSseConnection.js +57 -7
- package/dist/agent/vendor-types/codex/app-server/AbsolutePathBuf.d.ts +10 -0
- package/dist/agent/vendor-types/codex/app-server/AbsolutePathBuf.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/FunctionCallOutputBody.d.ts +2 -0
- package/dist/agent/vendor-types/codex/app-server/FunctionCallOutputBody.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/FunctionCallOutputContentItem.d.ts +22 -0
- package/dist/agent/vendor-types/codex/app-server/FunctionCallOutputContentItem.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/ImageDetail.d.ts +1 -0
- package/dist/agent/vendor-types/codex/app-server/ImageDetail.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/ImageGenerationFailure.d.ts +5 -0
- package/dist/agent/vendor-types/codex/app-server/ImageGenerationFailure.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/ImageGenerationItem.d.ts +11 -0
- package/dist/agent/vendor-types/codex/app-server/ImageGenerationItem.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/LegacyAppPathString.d.ts +23 -0
- package/dist/agent/vendor-types/codex/app-server/LegacyAppPathString.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/MessagePhase.d.ts +7 -0
- package/dist/agent/vendor-types/codex/app-server/MessagePhase.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/ReasoningEffort.d.ts +4 -0
- package/dist/agent/vendor-types/codex/app-server/ReasoningEffort.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/SleepItem.d.ts +7 -0
- package/dist/agent/vendor-types/codex/app-server/SleepItem.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/WebSearchItem.d.ts +14 -0
- package/dist/agent/vendor-types/codex/app-server/WebSearchItem.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/serde_json/JsonValue.d.ts +3 -0
- package/dist/agent/vendor-types/codex/app-server/serde_json/JsonValue.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/v2/AgentMessageDelivery.d.ts +1 -0
- package/dist/agent/vendor-types/codex/app-server/v2/AgentMessageDelivery.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/v2/AsyncUserInputQuestion.d.ts +4 -0
- package/dist/agent/vendor-types/codex/app-server/v2/AsyncUserInputQuestion.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/v2/ByteRange.d.ts +4 -0
- package/dist/agent/vendor-types/codex/app-server/v2/ByteRange.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/v2/CollabAgentState.d.ts +5 -0
- package/dist/agent/vendor-types/codex/app-server/v2/CollabAgentState.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/v2/CollabAgentStatus.d.ts +1 -0
- package/dist/agent/vendor-types/codex/app-server/v2/CollabAgentStatus.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/v2/CollabAgentTool.d.ts +1 -0
- package/dist/agent/vendor-types/codex/app-server/v2/CollabAgentTool.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/v2/CollabAgentToolCallStatus.d.ts +1 -0
- package/dist/agent/vendor-types/codex/app-server/v2/CollabAgentToolCallStatus.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/v2/CommandAction.d.ts +19 -0
- package/dist/agent/vendor-types/codex/app-server/v2/CommandAction.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/v2/CommandExecutionSource.d.ts +1 -0
- package/dist/agent/vendor-types/codex/app-server/v2/CommandExecutionSource.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/v2/CommandExecutionStatus.d.ts +1 -0
- package/dist/agent/vendor-types/codex/app-server/v2/CommandExecutionStatus.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/v2/DynamicToolCallOutputContentItem.d.ts +10 -0
- package/dist/agent/vendor-types/codex/app-server/v2/DynamicToolCallOutputContentItem.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/v2/DynamicToolCallStatus.d.ts +1 -0
- package/dist/agent/vendor-types/codex/app-server/v2/DynamicToolCallStatus.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/v2/FileUpdateChange.d.ts +6 -0
- package/dist/agent/vendor-types/codex/app-server/v2/FileUpdateChange.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/v2/HookPromptFragment.d.ts +4 -0
- package/dist/agent/vendor-types/codex/app-server/v2/HookPromptFragment.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/v2/McpAppDisplayMode.d.ts +1 -0
- package/dist/agent/vendor-types/codex/app-server/v2/McpAppDisplayMode.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/v2/McpAppUi.d.ts +8 -0
- package/dist/agent/vendor-types/codex/app-server/v2/McpAppUi.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/v2/McpToolCallAppContext.d.ts +7 -0
- package/dist/agent/vendor-types/codex/app-server/v2/McpToolCallAppContext.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/v2/McpToolCallError.d.ts +3 -0
- package/dist/agent/vendor-types/codex/app-server/v2/McpToolCallError.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/v2/McpToolCallResult.d.ts +6 -0
- package/dist/agent/vendor-types/codex/app-server/v2/McpToolCallResult.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/v2/McpToolCallStatus.d.ts +1 -0
- package/dist/agent/vendor-types/codex/app-server/v2/McpToolCallStatus.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/v2/MemoryCitation.d.ts +5 -0
- package/dist/agent/vendor-types/codex/app-server/v2/MemoryCitation.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/v2/MemoryCitationEntry.d.ts +6 -0
- package/dist/agent/vendor-types/codex/app-server/v2/MemoryCitationEntry.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/v2/PatchApplyStatus.d.ts +1 -0
- package/dist/agent/vendor-types/codex/app-server/v2/PatchApplyStatus.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/v2/PatchChangeKind.d.ts +8 -0
- package/dist/agent/vendor-types/codex/app-server/v2/PatchChangeKind.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/v2/SubAgentActivityKind.d.ts +1 -0
- package/dist/agent/vendor-types/codex/app-server/v2/SubAgentActivityKind.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/v2/TextElement.d.ts +11 -0
- package/dist/agent/vendor-types/codex/app-server/v2/TextElement.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/v2/ThreadItem.d.ts +215 -0
- package/dist/agent/vendor-types/codex/app-server/v2/ThreadItem.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/v2/UserInput.d.ts +35 -0
- package/dist/agent/vendor-types/codex/app-server/v2/UserInput.js +2 -0
- package/dist/agent/vendor-types/codex/app-server/v2/WebSearchAction.d.ts +14 -0
- package/dist/agent/vendor-types/codex/app-server/v2/WebSearchAction.js +2 -0
- package/dist/agent/workerBundle.generated.js +1 -1
- package/dist/cli/agent.js +30 -4
- package/dist/cli/services.d.ts +6 -5
- package/dist/cli/services.js +4 -3
- package/dist/dimension/api.d.ts +5 -4
- package/dist/dimension/paths.d.ts +4 -0
- package/dist/dimension/paths.js +10 -2
- package/dist/dimension/service.d.ts +5 -1
- package/dist/dimension/service.js +5 -1
- package/dist/dimension/storage.js +3 -2
- package/dist/index.d.ts +2 -3
- package/dist/index.js +7 -5
- package/dist/lib/docker.d.ts +1 -0
- package/dist/portal-gateway/api.d.ts +6 -5
- package/dist/portal-gateway/gatewayService.d.ts +5 -1
- package/dist/portal-gateway/gatewayService.js +25 -13
- package/dist/portal-gateway/image.d.ts +1 -1
- package/dist/portal-gateway/image.js +1 -1
- package/dist/portal-gateway/imageFiles.generated.js +1 -1
- package/dist/portal-gateway/portalService.d.ts +2 -0
- package/dist/portal-gateway/portalService.js +61 -29
- package/dist/portal-gateway/upstream.d.ts +22 -11
- package/dist/sandbox/api.d.ts +60 -23
- package/dist/sandbox/boatService.js +11 -3
- package/dist/sandbox/containerService.js +10 -3
- package/dist/sandbox/endpointProxy.js +16 -1
- package/dist/sandbox/localService.js +20 -2
- package/dist/sandbox/sshService.js +37 -4
- package/package.json +25 -8
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
import { SandboxNotFoundError } from "../../sandbox/api.js";
|
|
2
|
+
import { AgentInvalidError } from "../api.js";
|
|
3
|
+
export function sandboxFiles(sandbox) {
|
|
4
|
+
return {
|
|
5
|
+
size: async (path) => (await sandbox.stat(path)).size,
|
|
6
|
+
readFileRange: (path, start, end) => sandbox.readFileRange(path, start, end),
|
|
7
|
+
listDirectory: (path) => sandbox.listDirectory(path),
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
/** The file of conversation `nativeId` laid out as `layout` under `home`; null when there's none yet. */
|
|
11
|
+
export function locateTranscript(files, home, layout, nativeId) {
|
|
12
|
+
const escape = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
13
|
+
const [before = "", after = ""] = layout.name.split("{id}").map((part) => part.split("*").map(escape).join(".*"));
|
|
14
|
+
const name = new RegExp(`^${before}${escape(nativeId)}${after}$`);
|
|
15
|
+
return findFile(files, `${home}/${layout.dir}`, layout.depth, (entry) => name.test(entry));
|
|
16
|
+
}
|
|
17
|
+
/** How much of the file is read at a time. */
|
|
18
|
+
export const CHUNK_BYTES = 64 * 1024;
|
|
19
|
+
/**
|
|
20
|
+
* How far out of time order lines get, nearly always (parallel branches completing; in long claude
|
|
21
|
+
* sessions half are within 2s, 99% within a minute): searches allow for this much.
|
|
22
|
+
*/
|
|
23
|
+
export const TIME_SLACK_MS = 60_000;
|
|
24
|
+
/** How many timed lines a probe of the bisection looks at: the earliest wins, so one odd line can't mislead it. */
|
|
25
|
+
export const PROBE_LINES = 3;
|
|
26
|
+
/** How much a probe reads, at most, before it gives up on finding a timed line (huge tool outputs). */
|
|
27
|
+
export const PROBE_BYTES = 4 * CHUNK_BYTES;
|
|
28
|
+
const NEWLINE = 0x0a;
|
|
29
|
+
/** A line's time (epoch ms) from its `field`, null without a valid one. */
|
|
30
|
+
function lineTime(entry, field) {
|
|
31
|
+
const value = entry?.[field];
|
|
32
|
+
const time = typeof value === "string" ? Date.parse(value) : NaN;
|
|
33
|
+
return Number.isNaN(time) ? null : time;
|
|
34
|
+
}
|
|
35
|
+
/** How much is read at a time when searching the whole file for where stretches were written again. */
|
|
36
|
+
const SCAN_BYTES = 1024 * 1024;
|
|
37
|
+
/**
|
|
38
|
+
* The stretches written again in the files searched last, by path and size: a search reads the whole
|
|
39
|
+
* file, and a file the backend only appends to that hasn't grown has the same ones.
|
|
40
|
+
*/
|
|
41
|
+
const searchedCopies = new Map();
|
|
42
|
+
const SEARCHED_FILES = 16;
|
|
43
|
+
/** `locate` finds the file; null when there's none yet (nothing stored). */
|
|
44
|
+
export function createFileTranscript(files, locate, lineMessages, layout) {
|
|
45
|
+
function parse(bytes, start) {
|
|
46
|
+
try {
|
|
47
|
+
const entry = JSON.parse(new TextDecoder().decode(bytes));
|
|
48
|
+
return { start, time: lineTime(entry, layout.time), messages: lineMessages(entry) };
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// A line still being written (the last one), or one this reader doesn't know: nothing in it.
|
|
52
|
+
return { start, time: null, messages: [] };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** The stretches between `from` and `to` that aren't skipped, in order. */
|
|
56
|
+
function kept(from, to, skip) {
|
|
57
|
+
const parts = [];
|
|
58
|
+
let start = from;
|
|
59
|
+
for (const stretch of [...skip].sort((a, b) => a.start - b.start)) {
|
|
60
|
+
if (stretch.end <= start || stretch.start >= to)
|
|
61
|
+
continue;
|
|
62
|
+
if (stretch.start > start)
|
|
63
|
+
parts.push({ start, end: stretch.start });
|
|
64
|
+
start = Math.max(start, stretch.end);
|
|
65
|
+
}
|
|
66
|
+
if (start < to)
|
|
67
|
+
parts.push({ start, end: to });
|
|
68
|
+
return parts;
|
|
69
|
+
}
|
|
70
|
+
/** The file's lines before byte `end`, last first, read a chunk at a time, passing over `skip`. */
|
|
71
|
+
async function* backward(path, end, skip = []) {
|
|
72
|
+
for (const part of kept(0, end, skip).reverse())
|
|
73
|
+
yield* backwardIn(path, part.start, part.end);
|
|
74
|
+
}
|
|
75
|
+
async function* backwardIn(path, floor, end) {
|
|
76
|
+
let position = end;
|
|
77
|
+
// Bytes of a line that runs on into what was read before (later in the file).
|
|
78
|
+
let carry = new Uint8Array(0);
|
|
79
|
+
while (position > floor) {
|
|
80
|
+
const from = Math.max(floor, position - CHUNK_BYTES);
|
|
81
|
+
const chunk = await files.readFileRange(path, from, position);
|
|
82
|
+
const bytes = joined(chunk, carry);
|
|
83
|
+
let lineEnd = bytes.length;
|
|
84
|
+
for (let index = bytes.length - 1; index >= 0; index--) {
|
|
85
|
+
if (bytes[index] !== NEWLINE)
|
|
86
|
+
continue;
|
|
87
|
+
if (lineEnd > index + 1)
|
|
88
|
+
yield parse(bytes.subarray(index + 1, lineEnd), from + index + 1);
|
|
89
|
+
lineEnd = index;
|
|
90
|
+
}
|
|
91
|
+
carry = bytes.slice(0, lineEnd);
|
|
92
|
+
position = from;
|
|
93
|
+
}
|
|
94
|
+
if (carry.length > 0)
|
|
95
|
+
yield parse(carry, floor);
|
|
96
|
+
}
|
|
97
|
+
/** The file's lines from byte `start` to byte `end`, in order, read a chunk at a time, passing over `skip`. */
|
|
98
|
+
async function* forward(path, start, end, skip = []) {
|
|
99
|
+
for (const part of kept(start, end, skip))
|
|
100
|
+
yield* forwardIn(path, part.start, part.end);
|
|
101
|
+
}
|
|
102
|
+
async function* forwardIn(path, start, end) {
|
|
103
|
+
let position = start;
|
|
104
|
+
let carry = new Uint8Array(0);
|
|
105
|
+
let carryStart = start;
|
|
106
|
+
while (position < end) {
|
|
107
|
+
const to = Math.min(end, position + CHUNK_BYTES);
|
|
108
|
+
const bytes = joined(carry, await files.readFileRange(path, position, to));
|
|
109
|
+
let lineStart = 0;
|
|
110
|
+
for (let index = 0; index < bytes.length; index++) {
|
|
111
|
+
if (bytes[index] !== NEWLINE)
|
|
112
|
+
continue;
|
|
113
|
+
if (index > lineStart)
|
|
114
|
+
yield parse(bytes.subarray(lineStart, index), carryStart + lineStart);
|
|
115
|
+
lineStart = index + 1;
|
|
116
|
+
}
|
|
117
|
+
carry = bytes.slice(lineStart);
|
|
118
|
+
carryStart += lineStart;
|
|
119
|
+
position = to;
|
|
120
|
+
}
|
|
121
|
+
if (carry.length > 0)
|
|
122
|
+
yield parse(carry, carryStart);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Where the lines from time `target` on start, about, between `low` and `high`: the start of a line
|
|
126
|
+
* (or `high`) with only lines from before it, allowing for the slack, ahead of it. Bisects, each
|
|
127
|
+
* probe reading the times of the first few lines after its middle, passing over `skip`.
|
|
128
|
+
*/
|
|
129
|
+
async function seek(path, low, high, target, skip) {
|
|
130
|
+
return (await narrow(path, low, high, target, skip)).high;
|
|
131
|
+
}
|
|
132
|
+
/** seek's bisection: where it got, from both sides. */
|
|
133
|
+
async function narrow(path, low, high, target, skip) {
|
|
134
|
+
while (high - low > CHUNK_BYTES) {
|
|
135
|
+
let middle = Math.floor((low + high) / 2);
|
|
136
|
+
const inside = skip.find((stretch) => stretch.start <= middle && middle < stretch.end);
|
|
137
|
+
if (inside) {
|
|
138
|
+
if (inside.end >= high) {
|
|
139
|
+
high = Math.max(low, inside.start);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
middle = inside.end;
|
|
143
|
+
}
|
|
144
|
+
const probe = await probeTime(path, middle, high, skip);
|
|
145
|
+
if (!probe)
|
|
146
|
+
break; // one line spans it all: start from where the search got
|
|
147
|
+
if (probe.time >= target)
|
|
148
|
+
high = probe.start;
|
|
149
|
+
else
|
|
150
|
+
low = probe.start;
|
|
151
|
+
}
|
|
152
|
+
return { low, high };
|
|
153
|
+
}
|
|
154
|
+
/** The first line starting after `from` (before `limit`), with the earliest time of the next few timed lines. */
|
|
155
|
+
async function probeTime(path, from, limit, skip) {
|
|
156
|
+
let start = -1;
|
|
157
|
+
let time = Infinity;
|
|
158
|
+
let timed = 0;
|
|
159
|
+
for await (const line of forward(path, from, Math.min(limit, from + PROBE_BYTES), skip)) {
|
|
160
|
+
if (line.start === from && from > 0)
|
|
161
|
+
continue; // the middle of a line: only its end was read
|
|
162
|
+
if (line.start >= limit)
|
|
163
|
+
break;
|
|
164
|
+
if (start < 0)
|
|
165
|
+
start = line.start;
|
|
166
|
+
if (line.time === null)
|
|
167
|
+
continue;
|
|
168
|
+
time = Math.min(time, line.time);
|
|
169
|
+
if (++timed === PROBE_LINES)
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
return start < 0 || timed === 0 ? null : { start, time };
|
|
173
|
+
}
|
|
174
|
+
/** The time of the first timed line from `from` on (a line start), if there's one in reach. */
|
|
175
|
+
async function timeAt(path, from, size, skip) {
|
|
176
|
+
for await (const line of forward(path, from, Math.min(size, from + PROBE_BYTES), skip))
|
|
177
|
+
if (line.time !== null)
|
|
178
|
+
return line.time;
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
/** Where the line holding byte `at` starts. */
|
|
182
|
+
async function lineStart(path, at) {
|
|
183
|
+
for await (const line of backward(path, at + 1))
|
|
184
|
+
return line.start;
|
|
185
|
+
return 0;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* The stretches the backend wrote again (TranscriptLayout.copies): each starts at a line with the
|
|
189
|
+
* marker whose time goes back from the timed line before it, and runs until a line is newer than
|
|
190
|
+
* that one. Found by searching the whole file for the marker; the rest only
|
|
191
|
+
* reads around what's found.
|
|
192
|
+
*/
|
|
193
|
+
function copiesOf(path, size) {
|
|
194
|
+
if (!layout.copies)
|
|
195
|
+
return Promise.resolve([]);
|
|
196
|
+
const key = `${path}\0${size}`;
|
|
197
|
+
let copies = searchedCopies.get(key);
|
|
198
|
+
if (!copies) {
|
|
199
|
+
copies = findCopies(path, size, layout.copies);
|
|
200
|
+
copies.catch(() => searchedCopies.delete(key));
|
|
201
|
+
searchedCopies.set(key, copies);
|
|
202
|
+
// A few files' worth: a worker serves an agent's sessions.
|
|
203
|
+
if (searchedCopies.size > SEARCHED_FILES)
|
|
204
|
+
searchedCopies.delete(searchedCopies.keys().next().value);
|
|
205
|
+
}
|
|
206
|
+
return copies;
|
|
207
|
+
}
|
|
208
|
+
async function findCopies(path, size, marker) {
|
|
209
|
+
const needle = Buffer.from(marker);
|
|
210
|
+
const found = [];
|
|
211
|
+
for (let position = 0; position < size; position += SCAN_BYTES) {
|
|
212
|
+
// Overlapping by the marker's length, so one across two reads is found.
|
|
213
|
+
const from = Math.max(0, position - needle.length + 1);
|
|
214
|
+
const bytes = Buffer.from(await files.readFileRange(path, from, Math.min(size, position + SCAN_BYTES)));
|
|
215
|
+
for (let index = bytes.indexOf(needle); index >= 0; index = bytes.indexOf(needle, index + 1)) {
|
|
216
|
+
if (from + index >= position || position === 0)
|
|
217
|
+
found.push(from + index);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
const copies = [];
|
|
221
|
+
for (const [index, at] of found.entries()) {
|
|
222
|
+
// The marker's line, and the timed line before it.
|
|
223
|
+
const start = await lineStart(path, at);
|
|
224
|
+
let earlier = null;
|
|
225
|
+
for await (const before of backward(path, start)) {
|
|
226
|
+
if (before.time === null)
|
|
227
|
+
continue;
|
|
228
|
+
earlier = before.time;
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
if (earlier === null || copies.some((copy) => copy.start <= start && start < copy.end))
|
|
232
|
+
continue;
|
|
233
|
+
let time = null;
|
|
234
|
+
for await (const line of forward(path, start, size)) {
|
|
235
|
+
time = line.time;
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
// A compaction as it happened comes after what's before it (compacting takes a while: 25s and
|
|
239
|
+
// more); a copy starts back at its time.
|
|
240
|
+
if (time === null || time >= earlier)
|
|
241
|
+
continue;
|
|
242
|
+
// It runs up to what was written before it, and ends with the first line newer than that. It
|
|
243
|
+
// holds what was said since a compaction, so it ends by the next marker: the rest of the file
|
|
244
|
+
// (other copies) can't mislead the search.
|
|
245
|
+
const next = found[index + 1];
|
|
246
|
+
const near = await narrow(path, start + 1, next === undefined ? size : await lineStart(path, next), earlier + 1, []);
|
|
247
|
+
let end = near.high;
|
|
248
|
+
for await (const line of forward(path, near.low, near.high)) {
|
|
249
|
+
if (line.start <= start || line.time === null || line.time <= earlier)
|
|
250
|
+
continue;
|
|
251
|
+
end = line.start;
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
copies.push({ start, end });
|
|
255
|
+
}
|
|
256
|
+
return copies;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* The file's lines before `end`, last first, each with the time it's paged by: its own, unless it's
|
|
260
|
+
* odd, more than the slack away from both the timed lines around it in the same direction (a
|
|
261
|
+
* background task's result written minutes after it began, a note dated ahead), when it's the one
|
|
262
|
+
* before it's; a line without a time goes with the timed line after it. These times follow the file
|
|
263
|
+
* order to within the slack, so the late result is paged where it is in the conversation.
|
|
264
|
+
* `after`: the time of the first timed line from `end` on.
|
|
265
|
+
*/
|
|
266
|
+
async function* pagedBackward(path, end, after, skip) {
|
|
267
|
+
let next = after; // the time of the timed line after the one waiting
|
|
268
|
+
let waiting = []; // a timed line and the untimed ones before it, until the one before it is read
|
|
269
|
+
const settle = function* (before) {
|
|
270
|
+
const [timed, ...untimed] = waiting;
|
|
271
|
+
if (!timed)
|
|
272
|
+
return;
|
|
273
|
+
const own = timed.time;
|
|
274
|
+
const late = before !== null && own < before - TIME_SLACK_MS && (next === null || own < next - TIME_SLACK_MS);
|
|
275
|
+
const ahead = before !== null && own > before + TIME_SLACK_MS && (next === null || own > next + TIME_SLACK_MS);
|
|
276
|
+
const time = late || ahead ? before : own;
|
|
277
|
+
yield { ...timed, time };
|
|
278
|
+
for (const line of untimed)
|
|
279
|
+
yield { ...line, time };
|
|
280
|
+
next = own;
|
|
281
|
+
waiting = [];
|
|
282
|
+
};
|
|
283
|
+
for await (const line of backward(path, end, skip)) {
|
|
284
|
+
if (line.time === null) {
|
|
285
|
+
// Before any timed line: it goes with the line after the end, or is the newest.
|
|
286
|
+
if (waiting.length === 0)
|
|
287
|
+
yield { ...line, time: next ?? Infinity };
|
|
288
|
+
else
|
|
289
|
+
waiting.push(line);
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
yield* settle(line.time);
|
|
293
|
+
waiting = [line];
|
|
294
|
+
}
|
|
295
|
+
yield* settle(null);
|
|
296
|
+
}
|
|
297
|
+
async function page(before, limit, found) {
|
|
298
|
+
const path = await locate();
|
|
299
|
+
if (!path)
|
|
300
|
+
return { messages: [], nextBefore: null };
|
|
301
|
+
const size = await files.size(path);
|
|
302
|
+
const copies = found?.copies ?? (await copiesOf(path, size));
|
|
303
|
+
const from = Math.min(found?.end ?? (before === null ? size : await seek(path, 0, size, before + TIME_SLACK_MS, copies)), size);
|
|
304
|
+
const after = found ? found.after : from < size ? await timeAt(path, from, size, copies) : null;
|
|
305
|
+
// Read back until the page is full and a line is older than its oldest by more than the slack:
|
|
306
|
+
// nothing before it can belong on the page.
|
|
307
|
+
const taken = [];
|
|
308
|
+
let count = 0;
|
|
309
|
+
let oldest = Infinity; // the time of the page's limit-th newest message, once it's full
|
|
310
|
+
let older = false;
|
|
311
|
+
for await (const line of pagedBackward(path, from, after, copies)) {
|
|
312
|
+
if (count >= limit && line.time < oldest - TIME_SLACK_MS) {
|
|
313
|
+
older = true;
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
if (line.messages.length === 0 || (before !== null && !(line.time < before)))
|
|
317
|
+
continue;
|
|
318
|
+
taken.push(line);
|
|
319
|
+
count += line.messages.length;
|
|
320
|
+
if (count >= limit)
|
|
321
|
+
oldest = oldestOnPage(taken, limit);
|
|
322
|
+
}
|
|
323
|
+
// The page: its newest messages by time, ties with the oldest of them included. A line's messages
|
|
324
|
+
// stay together, so a page can hold a message or so more than the limit.
|
|
325
|
+
const kept = count >= limit ? taken.filter((line) => line.time >= oldest) : taken;
|
|
326
|
+
if (kept.length < taken.length)
|
|
327
|
+
older = true;
|
|
328
|
+
return {
|
|
329
|
+
messages: kept.sort((a, b) => a.start - b.start).flatMap((line) => line.messages),
|
|
330
|
+
nextBefore: older && Number.isFinite(oldest) ? new Date(oldest).toISOString() : null,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
// Passing over the copies too: a resumed session's stretch written again isn't replayed twice.
|
|
334
|
+
async function replay(afterId, emit) {
|
|
335
|
+
const path = await locate();
|
|
336
|
+
if (!path)
|
|
337
|
+
return afterId === undefined;
|
|
338
|
+
const end = await files.size(path);
|
|
339
|
+
const copies = await copiesOf(path, end);
|
|
340
|
+
let start = 0;
|
|
341
|
+
if (afterId !== undefined) {
|
|
342
|
+
let found = null;
|
|
343
|
+
for await (const line of backward(path, end, copies)) {
|
|
344
|
+
if (line.messages.some((message) => message.uuid === afterId)) {
|
|
345
|
+
found = line;
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (!found)
|
|
350
|
+
return false;
|
|
351
|
+
start = found.start;
|
|
352
|
+
}
|
|
353
|
+
let passed = afterId === undefined;
|
|
354
|
+
for await (const line of forward(path, start, end, copies)) {
|
|
355
|
+
for (const message of line.messages) {
|
|
356
|
+
if (passed)
|
|
357
|
+
emit(message);
|
|
358
|
+
else if (message.uuid === afterId)
|
|
359
|
+
passed = true;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return true;
|
|
363
|
+
}
|
|
364
|
+
return { page, replay };
|
|
365
|
+
}
|
|
366
|
+
/** The time of the `limit`-th newest message of `lines`. */
|
|
367
|
+
function oldestOnPage(lines, limit) {
|
|
368
|
+
const newestFirst = [...lines].sort((a, b) => b.time - a.time);
|
|
369
|
+
let count = 0;
|
|
370
|
+
for (const line of newestFirst) {
|
|
371
|
+
count += line.messages.length;
|
|
372
|
+
if (count >= limit)
|
|
373
|
+
return line.time;
|
|
374
|
+
}
|
|
375
|
+
return newestFirst[newestFirst.length - 1]?.time ?? Infinity;
|
|
376
|
+
}
|
|
377
|
+
function joined(first, second) {
|
|
378
|
+
if (first.length === 0)
|
|
379
|
+
return second;
|
|
380
|
+
if (second.length === 0)
|
|
381
|
+
return first;
|
|
382
|
+
const bytes = new Uint8Array(first.length + second.length);
|
|
383
|
+
bytes.set(first);
|
|
384
|
+
bytes.set(second, first.length);
|
|
385
|
+
return bytes;
|
|
386
|
+
}
|
|
387
|
+
const DEFAULT_PAGE_SIZE = 50;
|
|
388
|
+
const MAX_PAGE_SIZE = 500;
|
|
389
|
+
/** A page of the API's transcript (AgentTranscript.getTranscript); none stored for a session that never ran. */
|
|
390
|
+
export async function transcriptPage(transcript, { limit, before, cursor } = {}, found) {
|
|
391
|
+
const time = transcriptTime(before ?? cursor);
|
|
392
|
+
if (!transcript)
|
|
393
|
+
return { messages: [], nextBefore: null, nextCursor: null };
|
|
394
|
+
const size = Math.min(Math.max(Math.floor(limit ?? DEFAULT_PAGE_SIZE), 1), MAX_PAGE_SIZE);
|
|
395
|
+
const page = await transcript.page(time, size, found);
|
|
396
|
+
return { ...page, nextCursor: page.nextBefore };
|
|
397
|
+
}
|
|
398
|
+
/** AgentTranscriptRequest.before as epoch ms; null for none. */
|
|
399
|
+
export function transcriptTime(before) {
|
|
400
|
+
if (before === undefined)
|
|
401
|
+
return null;
|
|
402
|
+
const time = Date.parse(before);
|
|
403
|
+
if (Number.isNaN(time))
|
|
404
|
+
throw new AgentInvalidError(`invalid time ${before}: give an ISO 8601 time`);
|
|
405
|
+
return time;
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* The first file `depth` levels under `dir` whose name `matches` (the backends' own layouts, like
|
|
409
|
+
* projects/<project>/<id>.jsonl), each level listed at once; null when there's none, or no `dir`.
|
|
410
|
+
*/
|
|
411
|
+
async function findFile(files, dir, depth, matches) {
|
|
412
|
+
let names;
|
|
413
|
+
try {
|
|
414
|
+
names = await files.listDirectory(dir);
|
|
415
|
+
}
|
|
416
|
+
catch (error) {
|
|
417
|
+
if (error instanceof SandboxNotFoundError)
|
|
418
|
+
return null;
|
|
419
|
+
throw error;
|
|
420
|
+
}
|
|
421
|
+
if (depth <= 1) {
|
|
422
|
+
const name = names.find(matches);
|
|
423
|
+
return name === undefined ? null : `${dir}/${name}`;
|
|
424
|
+
}
|
|
425
|
+
// Newest first, where names sort by date (codex's YYYY/MM/DD): a recent conversation is the likely one.
|
|
426
|
+
const found = await Promise.all(names.sort().reverse().map((name) =>
|
|
427
|
+
// One that isn't a directory has nothing below it.
|
|
428
|
+
findFile(files, `${dir}/${name}`, depth - 1, matches).catch(() => null)));
|
|
429
|
+
return found.find((path) => path !== null) ?? null;
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* `files` with what was already fetched of the file at `path` in the same round trip: its size, and
|
|
433
|
+
* pieces of it (what a page reads, mostly). Reading back beyond them (a page across huge tool
|
|
434
|
+
* outputs) fetches twice as much as the time before, so it takes a few more reads, not one per chunk.
|
|
435
|
+
*/
|
|
436
|
+
export function prefetchedFiles(files, path, size, pieces) {
|
|
437
|
+
let more = Math.max(CHUNK_BYTES, ...pieces.map(({ bytes }) => bytes.length));
|
|
438
|
+
return {
|
|
439
|
+
size: async (at) => (at === path ? size : files.size(at)),
|
|
440
|
+
readFileRange: async (at, from, to) => {
|
|
441
|
+
if (at !== path)
|
|
442
|
+
return files.readFileRange(at, from, to);
|
|
443
|
+
let piece = pieces.find(({ start, bytes }) => from >= start && to <= start + bytes.length);
|
|
444
|
+
if (!piece) {
|
|
445
|
+
more *= 2;
|
|
446
|
+
const start = Math.max(0, Math.min(from, to - more));
|
|
447
|
+
piece = { start, bytes: await files.readFileRange(at, start, to) };
|
|
448
|
+
pieces.push(piece);
|
|
449
|
+
}
|
|
450
|
+
return piece.bytes.subarray(from - piece.start, to - piece.start);
|
|
451
|
+
},
|
|
452
|
+
listDirectory: (at) => files.listDirectory(at),
|
|
453
|
+
};
|
|
454
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { SDKAssistantMessage, SDKResultMessage } from "../../vendor-types/claude/agent-sdk/sdk.js";
|
|
2
|
+
import type { UserMessage } from "../api.ts";
|
|
3
|
+
/** claude's tool for asking the user; other backends' questions appear in the stream under its name. */
|
|
4
|
+
export declare const ASK_USER_QUESTION = "AskUserQuestion";
|
|
5
|
+
export type AssistantContentBlock = SDKAssistantMessage["message"]["content"][number];
|
|
6
|
+
export type AssistantUsage = SDKAssistantMessage["message"]["usage"];
|
|
7
|
+
/** Token counts in the SDK's shape. */
|
|
8
|
+
export declare function resultUsage({ input, output, cacheRead, cacheWrite }?: {
|
|
9
|
+
input?: number | undefined;
|
|
10
|
+
output?: number | undefined;
|
|
11
|
+
cacheRead?: number | undefined;
|
|
12
|
+
cacheWrite?: number | undefined;
|
|
13
|
+
}): SDKResultMessage["usage"];
|
|
14
|
+
/** The text of a user message, whichever shape its content has. */
|
|
15
|
+
export declare function userMessageText(message: UserMessage): string;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// The stream speaks the Claude Agent SDK's messages (api.ts), so a backend that isn't claude makes
|
|
2
|
+
// them up: these are the pieces it needs.
|
|
3
|
+
/** claude's tool for asking the user; other backends' questions appear in the stream under its name. */
|
|
4
|
+
export const ASK_USER_QUESTION = "AskUserQuestion";
|
|
5
|
+
/** Token counts in the SDK's shape. */
|
|
6
|
+
export function resultUsage({ input = 0, output = 0, cacheRead = 0, cacheWrite = 0 } = {}) {
|
|
7
|
+
return {
|
|
8
|
+
input_tokens: input,
|
|
9
|
+
output_tokens: output,
|
|
10
|
+
cache_read_input_tokens: cacheRead,
|
|
11
|
+
cache_creation_input_tokens: cacheWrite,
|
|
12
|
+
cache_creation: { ephemeral_1h_input_tokens: 0, ephemeral_5m_input_tokens: 0 },
|
|
13
|
+
server_tool_use: { web_fetch_requests: 0, web_search_requests: 0 },
|
|
14
|
+
service_tier: "standard",
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/** The text of a user message, whichever shape its content has. */
|
|
18
|
+
export function userMessageText(message) {
|
|
19
|
+
const content = message.message.content;
|
|
20
|
+
if (typeof content === "string")
|
|
21
|
+
return content;
|
|
22
|
+
if (!Array.isArray(content))
|
|
23
|
+
return "";
|
|
24
|
+
return content
|
|
25
|
+
.map((block) => (block.type === "text" && typeof block.text === "string" ? block.text : ""))
|
|
26
|
+
.filter(Boolean)
|
|
27
|
+
.join("\n");
|
|
28
|
+
}
|
|
@@ -53,6 +53,10 @@ export interface SessionStorage {
|
|
|
53
53
|
* host side of that mount, so the host reaches the worker through it.
|
|
54
54
|
*/
|
|
55
55
|
export declare function workerSocketPath(runDirectory: string, agentName: string): string;
|
|
56
|
+
/** Where the session's record is in a dimension's state directory (see createSessionStorage). */
|
|
57
|
+
export declare function sessionRecordPath(stateDirectory: string, agentName: string, sessionName: string): string;
|
|
58
|
+
/** Where the backend CLI's home is (SessionStorage.backendHome) in a dimension's state directory. */
|
|
59
|
+
export declare function backendHomePath(stateDirectory: string, backend: AgentBackend): string;
|
|
56
60
|
/**
|
|
57
61
|
* `stateDirectory` is a sandbox path: the dimension's state directory (STATE_PATH, mounted from the
|
|
58
62
|
* host's hyperbrane storage, so everything here lives there with the rest). Layout:
|
|
@@ -7,6 +7,17 @@ import { checkName, createJsonDirectory } from "../../lib/jsonDirectory.js";
|
|
|
7
7
|
export function workerSocketPath(runDirectory, agentName) {
|
|
8
8
|
return `${runDirectory}/${agentName}.sock`;
|
|
9
9
|
}
|
|
10
|
+
function sessionsDirectory(stateDirectory, agentName) {
|
|
11
|
+
return `${stateDirectory}/agents/${agentName}/sessions`;
|
|
12
|
+
}
|
|
13
|
+
/** Where the session's record is in a dimension's state directory (see createSessionStorage). */
|
|
14
|
+
export function sessionRecordPath(stateDirectory, agentName, sessionName) {
|
|
15
|
+
return `${sessionsDirectory(stateDirectory, agentName)}/${checkName(sessionName)}.json`;
|
|
16
|
+
}
|
|
17
|
+
/** Where the backend CLI's home is (SessionStorage.backendHome) in a dimension's state directory. */
|
|
18
|
+
export function backendHomePath(stateDirectory, backend) {
|
|
19
|
+
return `${stateDirectory}/backends/${backend}`;
|
|
20
|
+
}
|
|
10
21
|
/**
|
|
11
22
|
* `stateDirectory` is a sandbox path: the dimension's state directory (STATE_PATH, mounted from the
|
|
12
23
|
* host's hyperbrane storage, so everything here lives there with the rest). Layout:
|
|
@@ -15,7 +26,7 @@ export function workerSocketPath(runDirectory, agentName) {
|
|
|
15
26
|
* (the CLIs' homes). The socket goes in `runDirectory`, the dimension's run directory.
|
|
16
27
|
*/
|
|
17
28
|
export function createSessionStorage(sandbox, agentName, { stateDirectory = STATE_PATH, runDirectory = RUN_PATH } = {}) {
|
|
18
|
-
const sessionsDir =
|
|
29
|
+
const sessionsDir = sessionsDirectory(stateDirectory, agentName);
|
|
19
30
|
const sessions = createJsonDirectory(sandbox, async () => sessionsDir);
|
|
20
31
|
const sessionDir = (sessionName) => `${sessionsDir}/${checkName(sessionName)}`;
|
|
21
32
|
const permissionRequests = (sessionName) => createJsonDirectory(sandbox, async () => `${sessionDir(sessionName)}/permission-requests`);
|
|
@@ -66,7 +77,7 @@ export function createSessionStorage(sandbox, agentName, { stateDirectory = STAT
|
|
|
66
77
|
return workerSocketPath(runDirectory, agentName);
|
|
67
78
|
}
|
|
68
79
|
async function backendHome(backend) {
|
|
69
|
-
const path =
|
|
80
|
+
const path = backendHomePath(stateDirectory, backend);
|
|
70
81
|
await sandbox.createDirectory(path);
|
|
71
82
|
return path;
|
|
72
83
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AgentBackend } from "../api.ts";
|
|
2
|
+
import type { SessionBackend } from "./backend.ts";
|
|
3
|
+
import type { LineMessages, TranscriptLayout } from "./fileTranscript.ts";
|
|
4
|
+
/**
|
|
5
|
+
* Each backend's conversations as it stores them: where (under the home homeEnvironment points it
|
|
6
|
+
* at) and how a line reads, for reading a transcript with nothing running, from the host. None of
|
|
7
|
+
* the backends' other code, which runs only in the dimension.
|
|
8
|
+
*/
|
|
9
|
+
export type StoredTranscripts = Pick<SessionBackend, "homeEnvironment"> & {
|
|
10
|
+
layout: TranscriptLayout;
|
|
11
|
+
lineMessages(nativeId: string): LineMessages;
|
|
12
|
+
};
|
|
13
|
+
export declare const STORED_TRANSCRIPTS: Record<AgentBackend, StoredTranscripts>;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { claudeHomeEnvironment } from "./claude/home.js";
|
|
2
|
+
import { CLAUDE_TRANSCRIPT_LAYOUT, claudeLineMessages } from "./claude/transcript.js";
|
|
3
|
+
import { codexHomeEnvironment } from "./codex/home.js";
|
|
4
|
+
import { CODEX_TRANSCRIPT_LAYOUT, codexLineMessages } from "./codex/transcript.js";
|
|
5
|
+
export const STORED_TRANSCRIPTS = {
|
|
6
|
+
claude: { homeEnvironment: claudeHomeEnvironment, layout: CLAUDE_TRANSCRIPT_LAYOUT, lineMessages: () => claudeLineMessages },
|
|
7
|
+
codex: { homeEnvironment: codexHomeEnvironment, layout: CODEX_TRANSCRIPT_LAYOUT, lineMessages: codexLineMessages },
|
|
8
|
+
};
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { type AgentAuthStatus, type AgentBackend, type AgentBackgroundTask, type AgentLastUserMessage, type AgentModelInfo, type AgentPermissionRequest, type AgentPermissionResult, type AgentQuestionRequest, type AgentQuestionResponse, type AgentSession, type AgentSessionInfo, type AgentSessionLimits, type AgentSessionState, type AgentStreamingMessage, type AgentTranscriptPage, type UserMessage } from "../../api.ts";
|
|
1
|
+
import { type AgentAuthStatus, type AgentBackend, type AgentBackendInfo, type AgentBackgroundTask, type AgentLastUserMessage, type AgentModelInfo, type AgentPermissionRequest, type AgentPermissionResult, type AgentQuestionRequest, type AgentQuestionResponse, type AgentSession, type AgentSessionInfo, type AgentSessionLimits, type AgentSessionState, type AgentStreamingMessage, type AgentTranscriptPage, type UserMessage } from "../../api.ts";
|
|
2
2
|
import type { JsonRpcErrorObject } from "./jsonRpc.ts";
|
|
3
3
|
/**
|
|
4
4
|
* The worker's JSON-RPC protocol, spoken over its unix socket (see createWorkerServer): method ->
|
|
5
5
|
* [params, result]. Sessions are named by `session`; everything answers `null` unless it says.
|
|
6
6
|
* worker/health and worker/shutdown are how the host tells a worker's build and replaces it: they
|
|
7
7
|
* never change.
|
|
8
|
+
*
|
|
9
|
+
* @sealed
|
|
8
10
|
*/
|
|
9
11
|
export type WorkerRequests = {
|
|
10
12
|
"worker/health": [Record<string, never>, WorkerHealth];
|
|
@@ -13,6 +15,7 @@ export type WorkerRequests = {
|
|
|
13
15
|
"backend/install": [{
|
|
14
16
|
backend: AgentBackend;
|
|
15
17
|
}, null];
|
|
18
|
+
"backend/list": [Record<string, never>, AgentBackendInfo[]];
|
|
16
19
|
"session/create": [{
|
|
17
20
|
session: AgentSession;
|
|
18
21
|
}, AgentSessionInfo];
|
|
@@ -54,6 +57,7 @@ export type WorkerRequests = {
|
|
|
54
57
|
"session/transcript": [{
|
|
55
58
|
session: string;
|
|
56
59
|
limit?: number;
|
|
60
|
+
before?: string;
|
|
57
61
|
cursor?: string;
|
|
58
62
|
}, AgentTranscriptPage];
|
|
59
63
|
"permission/list": [{
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { AgentConflictError, AgentInvalidError, AgentNotFoundError, } from "../../api.js";
|
|
1
|
+
import { AgentBackendNotInstalledError, AgentConflictError, AgentInvalidError, AgentNotFoundError, } from "../../api.js";
|
|
2
2
|
const METHOD_NOT_FOUND = -32601;
|
|
3
3
|
const INVALID_PARAMS = -32602;
|
|
4
4
|
const INTERNAL_ERROR = -32603;
|
|
5
5
|
// Server-defined codes, for the Agent*Errors that aren't bad params.
|
|
6
6
|
const NOT_FOUND = -32004;
|
|
7
7
|
const CONFLICT = -32009;
|
|
8
|
+
const BACKEND_NOT_INSTALLED = -32010;
|
|
8
9
|
export class WorkerMethodNotFoundError extends Error {
|
|
9
10
|
name = "WorkerMethodNotFoundError";
|
|
10
11
|
}
|
|
@@ -15,6 +16,8 @@ export function encodeWorkerError(error) {
|
|
|
15
16
|
return { code: INVALID_PARAMS, message };
|
|
16
17
|
if (error instanceof AgentNotFoundError)
|
|
17
18
|
return { code: NOT_FOUND, message };
|
|
19
|
+
if (error instanceof AgentBackendNotInstalledError)
|
|
20
|
+
return { code: BACKEND_NOT_INSTALLED, message };
|
|
18
21
|
if (error instanceof AgentConflictError)
|
|
19
22
|
return { code: CONFLICT, message };
|
|
20
23
|
if (error instanceof WorkerMethodNotFoundError)
|
|
@@ -28,5 +31,7 @@ export function decodeWorkerError({ code, message }) {
|
|
|
28
31
|
return new AgentNotFoundError(message);
|
|
29
32
|
if (code === CONFLICT)
|
|
30
33
|
return new AgentConflictError(message);
|
|
34
|
+
if (code === BACKEND_NOT_INSTALLED)
|
|
35
|
+
return new AgentBackendNotInstalledError(message);
|
|
31
36
|
return new Error(`worker: ${message}`);
|
|
32
37
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { WorkerMethod, WorkerRequests } from "./protocol.ts";
|
|
2
|
+
type Fields = Record<string, unknown>;
|
|
3
|
+
/**
|
|
4
|
+
* Turns each method's params, as they arrived (JSON), into the protocol's types, or throws an
|
|
5
|
+
* AgentInvalidError saying what's wrong. Keeps only the fields the types have, so nothing unknown
|
|
6
|
+
* reaches the service (or its storage).
|
|
7
|
+
*/
|
|
8
|
+
export declare const WORKER_PARAMS: {
|
|
9
|
+
[M in WorkerMethod]: (params: Fields) => WorkerRequests[M][0];
|
|
10
|
+
};
|
|
11
|
+
export {};
|