@fungi.computer/shiitake 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +454 -2
- package/dist/agar-extension.d.ts +9 -0
- package/dist/agar-extension.js +38 -0
- package/dist/agent-control.d.ts +77 -0
- package/dist/agent-control.js +434 -0
- package/dist/client-entry.d.ts +211 -0
- package/dist/client-entry.js +101 -0
- package/dist/client-wire.d.ts +199 -0
- package/dist/client-wire.js +271 -0
- package/dist/client.d.ts +439 -0
- package/dist/client.js +4 -0
- package/dist/cloudflare.d.ts +220 -0
- package/dist/cloudflare.js +531 -0
- package/dist/effect.d.ts +42 -0
- package/dist/effect.js +20 -0
- package/dist/errors.d.ts +50 -0
- package/dist/errors.js +81 -0
- package/dist/extension-client.d.ts +38 -0
- package/dist/extension-client.js +56 -0
- package/dist/extension-contract.d.ts +73 -0
- package/dist/extension-contract.js +11 -0
- package/dist/extensions.d.ts +589 -0
- package/dist/extensions.js +263 -0
- package/dist/fleet.d.ts +313 -0
- package/dist/fleet.js +540 -0
- package/dist/hosted.d.ts +133 -0
- package/dist/hosted.js +265 -0
- package/dist/http-routes.d.ts +772 -0
- package/dist/http-routes.js +242 -0
- package/dist/http.d.ts +1 -0
- package/dist/http.js +1 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +13 -0
- package/dist/internal/accepted-run-executor.d.ts +23 -0
- package/dist/internal/accepted-run-executor.js +129 -0
- package/dist/internal/admission-door.d.ts +75 -0
- package/dist/internal/admission-door.js +105 -0
- package/dist/internal/agent-close.d.ts +4 -0
- package/dist/internal/agent-close.js +42 -0
- package/dist/internal/browser-sessions.d.ts +31 -0
- package/dist/internal/browser-sessions.js +173 -0
- package/dist/internal/client-boundary.d.ts +262 -0
- package/dist/internal/client-boundary.js +202 -0
- package/dist/internal/compaction/compaction.d.ts +54 -0
- package/dist/internal/compaction/compaction.js +291 -0
- package/dist/internal/compaction/session-compaction.d.ts +69 -0
- package/dist/internal/compaction/session-compaction.js +330 -0
- package/dist/internal/compaction/settings.d.ts +11 -0
- package/dist/internal/compaction/settings.js +40 -0
- package/dist/internal/compaction/utils.d.ts +45 -0
- package/dist/internal/compaction/utils.js +319 -0
- package/dist/internal/contracts.d.ts +235 -0
- package/dist/internal/contracts.js +1 -0
- package/dist/internal/coordinator.d.ts +18 -0
- package/dist/internal/coordinator.js +362 -0
- package/dist/internal/document-codec.d.ts +11 -0
- package/dist/internal/document-codec.js +39 -0
- package/dist/internal/effect-client.d.ts +13 -0
- package/dist/internal/effect-client.js +102 -0
- package/dist/internal/effect-contracts.d.ts +236 -0
- package/dist/internal/effect-contracts.js +2 -0
- package/dist/internal/extension-command-registry.d.ts +11 -0
- package/dist/internal/extension-command-registry.js +29 -0
- package/dist/internal/extension-command-tools.d.ts +17 -0
- package/dist/internal/extension-command-tools.js +50 -0
- package/dist/internal/extension-context.d.ts +25 -0
- package/dist/internal/extension-context.js +35 -0
- package/dist/internal/extension-definitions.d.ts +5 -0
- package/dist/internal/extension-definitions.js +6 -0
- package/dist/internal/extension-hook.d.ts +25 -0
- package/dist/internal/extension-hook.js +46 -0
- package/dist/internal/extension-roster.d.ts +23 -0
- package/dist/internal/extension-roster.js +228 -0
- package/dist/internal/extension-runtime.d.ts +63 -0
- package/dist/internal/extension-runtime.js +1004 -0
- package/dist/internal/failure.d.ts +2 -0
- package/dist/internal/failure.js +9 -0
- package/dist/internal/host-meta.d.ts +41 -0
- package/dist/internal/host-meta.js +39 -0
- package/dist/internal/json-value.d.ts +4 -0
- package/dist/internal/json-value.js +3 -0
- package/dist/internal/model-acquisition.d.ts +21 -0
- package/dist/internal/model-acquisition.js +98 -0
- package/dist/internal/model-preference.d.ts +21 -0
- package/dist/internal/model-preference.js +20 -0
- package/dist/internal/observation-kernel.d.ts +28 -0
- package/dist/internal/observation-kernel.js +169 -0
- package/dist/internal/promise-facade.d.ts +18 -0
- package/dist/internal/promise-facade.js +172 -0
- package/dist/internal/run-scope.d.ts +27 -0
- package/dist/internal/run-scope.js +214 -0
- package/dist/internal/runtime-coordinator.d.ts +34 -0
- package/dist/internal/runtime-coordinator.js +233 -0
- package/dist/internal/runtime-core.d.ts +19 -0
- package/dist/internal/runtime-core.js +39 -0
- package/dist/internal/runtime-reconciliation.d.ts +18 -0
- package/dist/internal/runtime-reconciliation.js +162 -0
- package/dist/internal/session-context.d.ts +11 -0
- package/dist/internal/session-context.js +67 -0
- package/dist/internal/session-status.d.ts +10 -0
- package/dist/internal/session-status.js +52 -0
- package/dist/internal/woodstock/agent-reply.d.ts +25 -0
- package/dist/internal/woodstock/agent-reply.js +67 -0
- package/dist/internal/woodstock/client-projection.d.ts +29 -0
- package/dist/internal/woodstock/client-projection.js +512 -0
- package/dist/internal/woodstock/compaction-activity.d.ts +17 -0
- package/dist/internal/woodstock/compaction-activity.js +32 -0
- package/dist/internal/woodstock/cursor.d.ts +9 -0
- package/dist/internal/woodstock/cursor.js +44 -0
- package/dist/internal/woodstock/extension-store.d.ts +72 -0
- package/dist/internal/woodstock/extension-store.js +262 -0
- package/dist/internal/woodstock/fleet-store.d.ts +36 -0
- package/dist/internal/woodstock/fleet-store.js +100 -0
- package/dist/internal/woodstock/identities.d.ts +6 -0
- package/dist/internal/woodstock/identities.js +11 -0
- package/dist/internal/woodstock/index.d.ts +170 -0
- package/dist/internal/woodstock/index.js +546 -0
- package/dist/internal/woodstock/progress.d.ts +32 -0
- package/dist/internal/woodstock/progress.js +89 -0
- package/dist/internal/woodstock/run-recovery.d.ts +8 -0
- package/dist/internal/woodstock/run-recovery.js +95 -0
- package/dist/internal/woodstock/session-attention.d.ts +16 -0
- package/dist/internal/woodstock/session-attention.js +88 -0
- package/dist/internal/woodstock/session-documents.d.ts +18 -0
- package/dist/internal/woodstock/session-documents.js +51 -0
- package/dist/internal/woodstock/session-entry.d.ts +478 -0
- package/dist/internal/woodstock/session-entry.js +484 -0
- package/dist/internal/woodstock/session-grants.d.ts +24 -0
- package/dist/internal/woodstock/session-grants.js +84 -0
- package/dist/internal/woodstock/session-inbox.d.ts +56 -0
- package/dist/internal/woodstock/session-inbox.js +307 -0
- package/dist/internal/woodstock/session-store.d.ts +432 -0
- package/dist/internal/woodstock/session-store.js +2006 -0
- package/dist/internal/woodstock/storage-admission.d.ts +14 -0
- package/dist/internal/woodstock/storage-admission.js +44 -0
- package/dist/model-preference.d.ts +20 -0
- package/dist/model-preference.js +30 -0
- package/dist/node.d.ts +64 -0
- package/dist/node.js +256 -0
- package/dist/remote-client.d.ts +11 -0
- package/dist/remote-client.js +298 -0
- package/dist/session-attention.d.ts +35 -0
- package/dist/session-attention.js +22 -0
- package/dist/session-documents.d.ts +28 -0
- package/dist/session-documents.js +1 -0
- package/dist/testing.d.ts +54 -0
- package/dist/testing.js +215 -0
- package/docs/api/README.md +26 -0
- package/docs/api/agar-extension.md +40 -0
- package/docs/api/agent-control.md +151 -0
- package/docs/api/client-entry.md +344 -0
- package/docs/api/client.md +1112 -0
- package/docs/api/cloudflare.md +500 -0
- package/docs/api/effect.md +884 -0
- package/docs/api/extensions.md +2003 -0
- package/docs/api/fleet.md +734 -0
- package/docs/api/hosted.md +382 -0
- package/docs/api/http.md +111 -0
- package/docs/api/index.md +1116 -0
- package/docs/api/model-preference.md +151 -0
- package/docs/api/node.md +142 -0
- package/docs/api/testing.md +250 -0
- package/package.json +130 -3
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { nanoid } from "@fungi.computer/cairn";
|
|
2
|
+
import { parseModelPreferenceJson } from "./internal/model-preference.js";
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
import * as Cause from "effect/Cause";
|
|
5
|
+
import * as Latch from "effect/Latch";
|
|
6
|
+
import * as Exit from "effect/Exit";
|
|
7
|
+
import * as Option from "effect/Option";
|
|
8
|
+
import * as Queue from "effect/Queue";
|
|
9
|
+
import * as Schema from "effect/Schema";
|
|
10
|
+
import * as Scope from "effect/Scope";
|
|
11
|
+
import * as Sink from "effect/Sink";
|
|
12
|
+
import * as Stream from "effect/Stream";
|
|
13
|
+
import { parseAccepted, parseCancelResult, parseForkedSession, parseCreatedSession, parseDeletedSession, parseExtensionOutput, parseExtensionStatus, parseExtensionStatuses, parseSessionFrame, parseSessionSnapshot, parseSessionSummaryPage, parseShiitakeError, } from "./client-wire.js";
|
|
14
|
+
import { ShiitakeError } from "./errors.js";
|
|
15
|
+
import { decodeClientReadOptions, decodeClientSessionCreationRequestId, decodeClientPageCursor, decodeClientSessionId, decodeClientSubscribeOptions, } from "./internal/client-boundary.js";
|
|
16
|
+
import { toPromiseAgent } from "./internal/effect-client.js";
|
|
17
|
+
function unavailable(message = "Remote Shiitake operation is unavailable") {
|
|
18
|
+
return new ShiitakeError("runtime", "unavailable", message);
|
|
19
|
+
}
|
|
20
|
+
function invalid(message) {
|
|
21
|
+
return new ShiitakeError("runtime", "invalid", message);
|
|
22
|
+
}
|
|
23
|
+
function clientClosedError() {
|
|
24
|
+
return new ShiitakeError("runtime", "closed", "Shiitake client is closed");
|
|
25
|
+
}
|
|
26
|
+
const closedEventSourceReadyState = 2;
|
|
27
|
+
const decodeEventData = Schema.decodeUnknownSync(Schema.String);
|
|
28
|
+
function eventData(event) {
|
|
29
|
+
if (!(event instanceof MessageEvent))
|
|
30
|
+
throw invalid("Session stream event is invalid");
|
|
31
|
+
return decodeEventData(event.data);
|
|
32
|
+
}
|
|
33
|
+
function decodeRemoteEvent(input) {
|
|
34
|
+
if (input.kind === "source-error")
|
|
35
|
+
return Effect.fail(unavailable("Session stream is unavailable"));
|
|
36
|
+
if (input.kind === "snapshot")
|
|
37
|
+
return Effect.try({
|
|
38
|
+
try: () => ({
|
|
39
|
+
kind: "snapshot",
|
|
40
|
+
value: parseSessionSnapshot(eventData(input.event)),
|
|
41
|
+
}),
|
|
42
|
+
catch: () => invalid("Session snapshot is invalid"),
|
|
43
|
+
});
|
|
44
|
+
if (input.kind === "frame")
|
|
45
|
+
return Effect.try({
|
|
46
|
+
try: () => ({
|
|
47
|
+
kind: "frame",
|
|
48
|
+
value: parseSessionFrame(eventData(input.event)),
|
|
49
|
+
}),
|
|
50
|
+
catch: () => invalid("Session frame is invalid"),
|
|
51
|
+
});
|
|
52
|
+
return Effect.try({
|
|
53
|
+
try: () => parseShiitakeError(eventData(input.event)),
|
|
54
|
+
catch: () => invalid("Session runtime error is invalid"),
|
|
55
|
+
}).pipe(Effect.flatMap(Effect.fail));
|
|
56
|
+
}
|
|
57
|
+
function closeRemoteSource(active, sources) {
|
|
58
|
+
return Effect.sync(() => {
|
|
59
|
+
if (active.closed)
|
|
60
|
+
return;
|
|
61
|
+
active.closed = true;
|
|
62
|
+
sources.delete(active);
|
|
63
|
+
active.source.close();
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
function remoteEvents(url, clientClosed, sources, eventSource) {
|
|
67
|
+
return Stream.callback((queue) => Effect.acquireRelease(Effect.try({
|
|
68
|
+
try: () => {
|
|
69
|
+
const constructor = eventSource ?? EventSource;
|
|
70
|
+
const active = {
|
|
71
|
+
source: new constructor(url),
|
|
72
|
+
closed: false,
|
|
73
|
+
};
|
|
74
|
+
const emit = (kind, event) => {
|
|
75
|
+
Queue.offerUnsafe(queue, { kind, event });
|
|
76
|
+
};
|
|
77
|
+
active.source.addEventListener("snapshot", (event) => {
|
|
78
|
+
emit("snapshot", event);
|
|
79
|
+
});
|
|
80
|
+
active.source.addEventListener("frame", (event) => {
|
|
81
|
+
emit("frame", event);
|
|
82
|
+
});
|
|
83
|
+
active.source.addEventListener("runtime-error", (event) => {
|
|
84
|
+
emit("runtime-error", event);
|
|
85
|
+
});
|
|
86
|
+
active.source.addEventListener("error", (event) => {
|
|
87
|
+
if (active.source.readyState === closedEventSourceReadyState)
|
|
88
|
+
emit("source-error", event);
|
|
89
|
+
});
|
|
90
|
+
sources.add(active);
|
|
91
|
+
return active;
|
|
92
|
+
},
|
|
93
|
+
catch: () => unavailable("Session stream could not open"),
|
|
94
|
+
}), (active) => closeRemoteSource(active, sources))).pipe(Stream.mapEffect(decodeRemoteEvent), Stream.interruptWhen(clientClosed.await));
|
|
95
|
+
}
|
|
96
|
+
function sessionUrl(baseUrl, sessionId) {
|
|
97
|
+
return `${baseUrl}/sessions/${encodeURIComponent(sessionId)}`;
|
|
98
|
+
}
|
|
99
|
+
function subscriptionEventsPath(eventsUrl, after, includeSandboxFailures = false) {
|
|
100
|
+
const query = new URLSearchParams({ mode: "subscribe" });
|
|
101
|
+
if (after !== undefined)
|
|
102
|
+
query.set("after", after);
|
|
103
|
+
if (includeSandboxFailures)
|
|
104
|
+
query.set("includeSandboxFailures", "true");
|
|
105
|
+
return `${eventsUrl}?${query.toString()}`;
|
|
106
|
+
}
|
|
107
|
+
function remoteFrames(url, clientClosed, sources, eventSource) {
|
|
108
|
+
return remoteEvents(url, clientClosed, sources, eventSource).pipe(Stream.mapEffect((event) => event.kind === "frame"
|
|
109
|
+
? Effect.succeed(event.value)
|
|
110
|
+
: Effect.fail(invalid("Session subscription received a snapshot"))));
|
|
111
|
+
}
|
|
112
|
+
function openRemoteWatch(url, scopes, ensureOpen, clientClosed, sources, eventSource) {
|
|
113
|
+
return Effect.gen(function* () {
|
|
114
|
+
yield* ensureOpen;
|
|
115
|
+
const watchClosed = yield* Latch.make(false);
|
|
116
|
+
const scope = yield* Scope.make();
|
|
117
|
+
yield* Effect.sync(() => scopes.add(scope));
|
|
118
|
+
yield* Scope.addFinalizer(scope, Effect.sync(() => {
|
|
119
|
+
scopes.delete(scope);
|
|
120
|
+
}));
|
|
121
|
+
const opened = yield* Effect.exit(remoteEvents(url, clientClosed, sources, eventSource).pipe(Stream.peel(Sink.head()), Scope.provide(scope)));
|
|
122
|
+
if (Exit.isFailure(opened)) {
|
|
123
|
+
yield* Scope.close(scope, opened);
|
|
124
|
+
return yield* Effect.failCause(opened.cause);
|
|
125
|
+
}
|
|
126
|
+
const [first, remaining] = opened.value;
|
|
127
|
+
if (Option.isNone(first) || first.value.kind !== "snapshot") {
|
|
128
|
+
const error = invalid("Session stream did not begin with a snapshot");
|
|
129
|
+
yield* Scope.close(scope, Exit.fail(error));
|
|
130
|
+
return yield* Effect.fail(error);
|
|
131
|
+
}
|
|
132
|
+
const changes = remaining.pipe(Stream.mapEffect((event) => event.kind === "frame"
|
|
133
|
+
? Effect.succeed(event.value)
|
|
134
|
+
: Effect.succeed({
|
|
135
|
+
kind: "reset",
|
|
136
|
+
reason: "history_unavailable",
|
|
137
|
+
cursor: event.value.cursor,
|
|
138
|
+
baseline: event.value,
|
|
139
|
+
})), Stream.interruptWhen(watchClosed.await), Stream.catchCause((cause) => watchClosed.isOpen() && Cause.hasInterruptsOnly(cause)
|
|
140
|
+
? Stream.empty
|
|
141
|
+
: Stream.failCause(cause)));
|
|
142
|
+
return Object.freeze({
|
|
143
|
+
snapshot: first.value.value,
|
|
144
|
+
changes,
|
|
145
|
+
close: watchClosed.open.pipe(Effect.andThen(Scope.close(scope, Exit.void))),
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
function parseResponse(send, parse, description) {
|
|
150
|
+
return Effect.tryPromise({
|
|
151
|
+
try: async (signal) => {
|
|
152
|
+
const response = await send(signal);
|
|
153
|
+
return { response, body: await response.text() };
|
|
154
|
+
},
|
|
155
|
+
catch: () => unavailable(`${description} request failed`),
|
|
156
|
+
}).pipe(Effect.flatMap(({ response, body }) => {
|
|
157
|
+
if (!response.ok)
|
|
158
|
+
return Effect.try({
|
|
159
|
+
try: () => parseShiitakeError(body),
|
|
160
|
+
catch: () => invalid(`${description} error response is invalid`),
|
|
161
|
+
}).pipe(Effect.flatMap(Effect.fail));
|
|
162
|
+
return Effect.try({
|
|
163
|
+
try: () => parse(body),
|
|
164
|
+
catch: () => invalid(`${description} response is invalid`),
|
|
165
|
+
});
|
|
166
|
+
}));
|
|
167
|
+
}
|
|
168
|
+
function makeRemoteEffectAgent(options) {
|
|
169
|
+
const baseUrl = options.baseUrl.replace(/\/+$/u, "");
|
|
170
|
+
const fetch = options.fetch ?? globalThis.fetch;
|
|
171
|
+
const scopes = new Set();
|
|
172
|
+
const sources = new Set();
|
|
173
|
+
const clientClosed = Latch.makeUnsafe(false);
|
|
174
|
+
let closed = false;
|
|
175
|
+
const ensureOpen = Effect.suspend(() => closed ? Effect.fail(clientClosedError()) : Effect.void);
|
|
176
|
+
const request = (send, parse, description) => Effect.raceFirst(parseResponse(send, parse, description), clientClosed.await.pipe(Effect.andThen(Effect.fail(clientClosedError()))));
|
|
177
|
+
/** The one generic extension command fetcher (session or agent scope). */
|
|
178
|
+
const callExtension = (url, input) => ensureOpen.pipe(Effect.andThen(request((signal) => fetch(url, {
|
|
179
|
+
method: "POST",
|
|
180
|
+
headers: { "Content-Type": "application/json" },
|
|
181
|
+
body: JSON.stringify(input),
|
|
182
|
+
signal,
|
|
183
|
+
}), parseExtensionOutput, "Extension command")));
|
|
184
|
+
const session = (sessionIdInput) => {
|
|
185
|
+
const sessionId = decodeClientSessionId(sessionIdInput);
|
|
186
|
+
const addressedUrl = sessionUrl(baseUrl, sessionId);
|
|
187
|
+
const eventsUrl = `${addressedUrl}/events`;
|
|
188
|
+
function command(input) {
|
|
189
|
+
return ensureOpen.pipe(Effect.andThen(request((signal) => fetch(`${addressedUrl}/command`, {
|
|
190
|
+
method: "POST",
|
|
191
|
+
headers: { "Content-Type": "application/json" },
|
|
192
|
+
body: JSON.stringify({ command: input }),
|
|
193
|
+
signal,
|
|
194
|
+
}), input.type === "prompt" || input.type === "compact"
|
|
195
|
+
? parseAccepted
|
|
196
|
+
: input.type === "cancel"
|
|
197
|
+
? parseCancelResult
|
|
198
|
+
: input.type === "fork"
|
|
199
|
+
? parseForkedSession
|
|
200
|
+
: () => undefined, "Command")));
|
|
201
|
+
}
|
|
202
|
+
return Object.freeze({
|
|
203
|
+
command,
|
|
204
|
+
abort: command({ type: "abort" }),
|
|
205
|
+
send: (text, sendOptions) => Effect.sync(() => sendOptions?.requestId ?? nanoid()).pipe(Effect.flatMap((requestId) => {
|
|
206
|
+
const prompt = {
|
|
207
|
+
type: "prompt",
|
|
208
|
+
delivery: sendOptions?.delivery ?? "steer",
|
|
209
|
+
requestId,
|
|
210
|
+
text,
|
|
211
|
+
};
|
|
212
|
+
return command(sendOptions?.images === undefined
|
|
213
|
+
? prompt
|
|
214
|
+
: { ...prompt, images: sendOptions.images }).pipe(Effect.mapError((error) => error.requestId === requestId
|
|
215
|
+
? error
|
|
216
|
+
: new ShiitakeError(error.boundary, error.cause, error.message, requestId)));
|
|
217
|
+
})),
|
|
218
|
+
cancel: (input) => command({ type: "cancel", ...input }),
|
|
219
|
+
modelPreference: Object.freeze({
|
|
220
|
+
read: ensureOpen.pipe(Effect.andThen(request((signal) => fetch(`${addressedUrl}/model-preference`, { signal }), parseModelPreferenceJson, "Model preference read"))),
|
|
221
|
+
set: (preference) => ensureOpen.pipe(Effect.andThen(request((signal) => fetch(`${addressedUrl}/model-preference`, {
|
|
222
|
+
signal,
|
|
223
|
+
method: "PUT",
|
|
224
|
+
headers: { "Content-Type": "application/json" },
|
|
225
|
+
body: JSON.stringify(preference),
|
|
226
|
+
}), () => undefined, "Model preference set"))),
|
|
227
|
+
}),
|
|
228
|
+
extension: (extensionId) => Object.freeze({
|
|
229
|
+
call: (command, input) => callExtension(`${addressedUrl}/extensions/${encodeURIComponent(extensionId)}/commands/${encodeURIComponent(command)}`, input),
|
|
230
|
+
}),
|
|
231
|
+
read: (input) => ensureOpen.pipe(Effect.andThen(Effect.try({
|
|
232
|
+
try: () => decodeClientReadOptions(input),
|
|
233
|
+
catch: (error) => error instanceof ShiitakeError
|
|
234
|
+
? error
|
|
235
|
+
: invalid("Read options are invalid"),
|
|
236
|
+
})), Effect.flatMap(({ limit, before, includeSandboxFailures }) => {
|
|
237
|
+
const params = new URLSearchParams({ limit: String(limit) });
|
|
238
|
+
if (before !== undefined)
|
|
239
|
+
params.set("before", before);
|
|
240
|
+
if (includeSandboxFailures === true)
|
|
241
|
+
params.set("includeSandboxFailures", "true");
|
|
242
|
+
return request((signal) => fetch(`${addressedUrl}?${params.toString()}`, { signal }), parseSessionSnapshot, "Session read");
|
|
243
|
+
})),
|
|
244
|
+
subscribe: (input) => Stream.unwrap(ensureOpen.pipe(Effect.andThen(Effect.try({
|
|
245
|
+
try: () => decodeClientSubscribeOptions(input),
|
|
246
|
+
catch: (error) => error instanceof ShiitakeError
|
|
247
|
+
? error
|
|
248
|
+
: invalid("Subscribe options are invalid"),
|
|
249
|
+
})), Effect.map(({ after, includeSandboxFailures }) => remoteFrames(subscriptionEventsPath(eventsUrl, after, includeSandboxFailures === true), clientClosed, sources, options.eventSource)))),
|
|
250
|
+
watch: (input) => openRemoteWatch(input?.includeSandboxFailures === true
|
|
251
|
+
? `${eventsUrl}?includeSandboxFailures=true`
|
|
252
|
+
: eventsUrl, scopes, ensureOpen, clientClosed, sources, options.eventSource),
|
|
253
|
+
});
|
|
254
|
+
};
|
|
255
|
+
return Object.freeze({
|
|
256
|
+
createSession: (requestIdInput) => ensureOpen.pipe(Effect.flatMap(() => {
|
|
257
|
+
const requestId = decodeClientSessionCreationRequestId({
|
|
258
|
+
requestId: requestIdInput,
|
|
259
|
+
});
|
|
260
|
+
return request((signal) => fetch(`${baseUrl}/sessions`, {
|
|
261
|
+
method: "POST",
|
|
262
|
+
headers: { "Content-Type": "application/json" },
|
|
263
|
+
body: JSON.stringify({ requestId }),
|
|
264
|
+
signal,
|
|
265
|
+
}), parseCreatedSession, "Session creation");
|
|
266
|
+
})),
|
|
267
|
+
deleteSession: (sessionIdInput) => ensureOpen.pipe(Effect.flatMap(() => {
|
|
268
|
+
const sessionId = decodeClientSessionId(sessionIdInput);
|
|
269
|
+
return request((signal) => fetch(`${baseUrl}/sessions/${encodeURIComponent(sessionId)}`, {
|
|
270
|
+
method: "DELETE",
|
|
271
|
+
signal,
|
|
272
|
+
}), parseDeletedSession, "Session deletion");
|
|
273
|
+
})),
|
|
274
|
+
listSessions: (after) => ensureOpen.pipe(Effect.flatMap(() => {
|
|
275
|
+
const query = new URLSearchParams();
|
|
276
|
+
if (after !== undefined)
|
|
277
|
+
query.set("after", decodeClientPageCursor(after));
|
|
278
|
+
const suffix = query.size === 0 ? "" : `?${query.toString()}`;
|
|
279
|
+
return request((signal) => fetch(`${baseUrl}/sessions${suffix}`, { signal }), parseSessionSummaryPage, "Session discovery");
|
|
280
|
+
})),
|
|
281
|
+
extensions: Object.freeze({
|
|
282
|
+
list: ensureOpen.pipe(Effect.andThen(request((signal) => fetch(`${baseUrl}/extensions`, { signal }), parseExtensionStatuses, "Extension list"))),
|
|
283
|
+
resume: (extensionId) => ensureOpen.pipe(Effect.andThen(request((signal) => fetch(`${baseUrl}/extensions/${encodeURIComponent(extensionId)}/resume`, { method: "POST", signal }), parseExtensionStatus, "Extension resume"))),
|
|
284
|
+
call: (extensionId, command, input) => callExtension(`${baseUrl}/extensions/${encodeURIComponent(extensionId)}/commands/${encodeURIComponent(command)}`, input),
|
|
285
|
+
}),
|
|
286
|
+
session,
|
|
287
|
+
close: Effect.suspend(() => {
|
|
288
|
+
if (closed)
|
|
289
|
+
return Effect.void;
|
|
290
|
+
closed = true;
|
|
291
|
+
return clientClosed.open.pipe(Effect.andThen(Effect.forEach([...scopes], (scope) => Scope.close(scope, Exit.void), { discard: true })), Effect.andThen(Effect.forEach([...sources], (active) => closeRemoteSource(active, sources), { discard: true })));
|
|
292
|
+
}),
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
/** Connect to a remotely hosted Shiitake agent. */
|
|
296
|
+
export function createShiitakeClient(options) {
|
|
297
|
+
return toPromiseAgent(makeRemoteEffectAgent(options), options.baseUrl.replace(/\/+$/u, ""));
|
|
298
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
/** An extension-owned request for human attention; expiry is owned by its timer. */
|
|
3
|
+
export declare const AttentionItemSchema: Schema.Struct<{
|
|
4
|
+
readonly id: Schema.NonEmptyString;
|
|
5
|
+
readonly kind: Schema.NonEmptyString;
|
|
6
|
+
readonly summary: Schema.NonEmptyString;
|
|
7
|
+
readonly since: Schema.Int;
|
|
8
|
+
readonly expiresAt: Schema.optional<Schema.Int>;
|
|
9
|
+
}>;
|
|
10
|
+
/** Input to the granted attention door, scoped to the calling extension. */
|
|
11
|
+
export type AttentionItem = typeof AttentionItemSchema.Type;
|
|
12
|
+
/** Public identity includes the owner so two extensions can use the same id. */
|
|
13
|
+
export declare const SessionAttentionSchema: Schema.Struct<{
|
|
14
|
+
readonly extensionId: Schema.NonEmptyString;
|
|
15
|
+
readonly id: Schema.NonEmptyString;
|
|
16
|
+
readonly kind: Schema.NonEmptyString;
|
|
17
|
+
readonly summary: Schema.NonEmptyString;
|
|
18
|
+
readonly since: Schema.Int;
|
|
19
|
+
readonly expiresAt: Schema.optional<Schema.Int>;
|
|
20
|
+
}>;
|
|
21
|
+
/** One open attention item in a Session observation. */
|
|
22
|
+
export type SessionAttention = typeof SessionAttentionSchema.Type;
|
|
23
|
+
/** Private resource ceiling for a complete, bounded Session observation. */
|
|
24
|
+
export declare const maximumSessionAttention = 64;
|
|
25
|
+
/** Private outstanding-resource ceiling per extension, released when items clear. */
|
|
26
|
+
export declare const maximumExtensionAttention = 4096;
|
|
27
|
+
/** All open items, ordered by extension and id; cleared only by their owner. */
|
|
28
|
+
export declare const SessionAttentionListSchema: Schema.$Array<Schema.Struct<{
|
|
29
|
+
readonly extensionId: Schema.NonEmptyString;
|
|
30
|
+
readonly id: Schema.NonEmptyString;
|
|
31
|
+
readonly kind: Schema.NonEmptyString;
|
|
32
|
+
readonly summary: Schema.NonEmptyString;
|
|
33
|
+
readonly since: Schema.Int;
|
|
34
|
+
readonly expiresAt: Schema.optional<Schema.Int>;
|
|
35
|
+
}>>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
const identity = Schema.NonEmptyString.check(Schema.isMaxLength(256));
|
|
3
|
+
const instant = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0));
|
|
4
|
+
/** An extension-owned request for human attention; expiry is owned by its timer. */
|
|
5
|
+
export const AttentionItemSchema = Schema.Struct({
|
|
6
|
+
id: identity,
|
|
7
|
+
kind: Schema.NonEmptyString.check(Schema.isMaxLength(64)),
|
|
8
|
+
summary: Schema.NonEmptyString.check(Schema.isMaxLength(200)),
|
|
9
|
+
since: instant,
|
|
10
|
+
expiresAt: Schema.optional(instant),
|
|
11
|
+
});
|
|
12
|
+
/** Public identity includes the owner so two extensions can use the same id. */
|
|
13
|
+
export const SessionAttentionSchema = AttentionItemSchema.mapFields((fields) => ({
|
|
14
|
+
...fields,
|
|
15
|
+
extensionId: identity,
|
|
16
|
+
}));
|
|
17
|
+
/** Private resource ceiling for a complete, bounded Session observation. */
|
|
18
|
+
export const maximumSessionAttention = 64;
|
|
19
|
+
/** Private outstanding-resource ceiling per extension, released when items clear. */
|
|
20
|
+
export const maximumExtensionAttention = 4096;
|
|
21
|
+
/** All open items, ordered by extension and id; cleared only by their owner. */
|
|
22
|
+
export const SessionAttentionListSchema = Schema.Array(SessionAttentionSchema).check(Schema.isMaxLength(maximumSessionAttention));
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type * as Schema from "effect/Schema";
|
|
2
|
+
/** Which source value a new Session starts with. */
|
|
3
|
+
export type DocumentForkPolicyV0 = "inherit-at-fork-point" | "current" | "fresh";
|
|
4
|
+
/** A typed JSON document, bounded by its encoded UTF-8 size. */
|
|
5
|
+
export type DocumentSpecV0<A, I = A> = Readonly<{
|
|
6
|
+
schema: Schema.Codec<A, I>;
|
|
7
|
+
initial: A;
|
|
8
|
+
fork: DocumentForkPolicyV0;
|
|
9
|
+
maxBytes: number;
|
|
10
|
+
}>;
|
|
11
|
+
/** Existential declarations; each initial value is decoded by its own schema. */
|
|
12
|
+
export type DocumentsV0 = Readonly<Record<string, Readonly<{
|
|
13
|
+
schema: Schema.Codec<unknown>;
|
|
14
|
+
initial: unknown;
|
|
15
|
+
fork: DocumentForkPolicyV0;
|
|
16
|
+
maxBytes: number;
|
|
17
|
+
}>>>;
|
|
18
|
+
/** Both operations require the declaring extension's active core transaction. */
|
|
19
|
+
export type DocumentHandleV0<A> = Readonly<{
|
|
20
|
+
/** Read the value at the Session’s selected transcript leaf, or its initial value. */
|
|
21
|
+
read: (sessionId: string) => A;
|
|
22
|
+
/** Validate and append a new snapshot atomically with the enclosing transaction. */
|
|
23
|
+
update: (sessionId: string, change: (value: A) => A) => void;
|
|
24
|
+
}>;
|
|
25
|
+
/** Handles preserve the decoded type of each declared document. */
|
|
26
|
+
export type DocumentHandlesV0<D extends DocumentsV0> = {
|
|
27
|
+
readonly [K in keyof D]: DocumentHandleV0<D[K]["schema"]["Type"]>;
|
|
28
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { ExtensionRosterV0, Json, RosteredExtensionV0 } from "./extensions.js";
|
|
2
|
+
import { type FauxResponseStep } from "@earendil-works/pi-ai/providers/faux";
|
|
3
|
+
import { type ModelSource, type SessionSnapshot, type ShiitakeOwnerAgent, type ShiitakeOptions, type SqliteOwner } from "./index.js";
|
|
4
|
+
/** A deterministic model source for trusted tests; no provider account or I/O. */
|
|
5
|
+
export declare function fauxModelSource(replies: readonly FauxResponseStep[]): ModelSource;
|
|
6
|
+
/** One scripted model tool call; the scripted sandbox runs exactly this call. */
|
|
7
|
+
export declare class ScriptedToolCall {
|
|
8
|
+
/** `<alias>.<operation>` as the model would call it, e.g. `bookmarks.add`. */
|
|
9
|
+
readonly path: string;
|
|
10
|
+
readonly input: Json;
|
|
11
|
+
constructor(
|
|
12
|
+
/** `<alias>.<operation>` as the model would call it, e.g. `bookmarks.add`. */
|
|
13
|
+
path: string, input: Json);
|
|
14
|
+
}
|
|
15
|
+
/** One model turn in a test script: a reply, a tool call, or a faux step. */
|
|
16
|
+
export type ScriptStep = string | ScriptedToolCall | FauxResponseStep;
|
|
17
|
+
/** Script one model turn that calls an operation with this input. */
|
|
18
|
+
export declare function callTool(path: string, input?: Json): ScriptedToolCall;
|
|
19
|
+
/** Script one model turn that replies with the system prompt it received. */
|
|
20
|
+
export declare function echoSystemPrompt(): FauxResponseStep;
|
|
21
|
+
/** Optional canonical Agent inputs and a borrowed owner; defaults are in-memory. */
|
|
22
|
+
export type MemoryHostOptions = Partial<Omit<ShiitakeOptions, "store" | "runtime" | "extensions">> & Readonly<{
|
|
23
|
+
/** Borrow this owner without closing it; otherwise own a Node memory database. */
|
|
24
|
+
owner?: SqliteOwner;
|
|
25
|
+
/**
|
|
26
|
+
* An explicit roster and grants, or a plain list of extensions. A list is
|
|
27
|
+
* granted exactly what each extension requests: test hosts only;
|
|
28
|
+
* `Shiitake.make` always takes explicit grants.
|
|
29
|
+
*/
|
|
30
|
+
extensions?: ExtensionRosterV0 | readonly RosteredExtensionV0[];
|
|
31
|
+
/**
|
|
32
|
+
* Script the model turns. Tool calls run through a sandbox that only runs
|
|
33
|
+
* these calls. Use instead of `model` and `tools.sandbox`.
|
|
34
|
+
*/
|
|
35
|
+
script?: readonly ScriptStep[];
|
|
36
|
+
}>;
|
|
37
|
+
/** Explicitly driven in-process host for trusted tests and runnable examples. */
|
|
38
|
+
export type MemoryHost = Readonly<{
|
|
39
|
+
/** The canonical public Shiitake Agent. */
|
|
40
|
+
agent: ShiitakeOwnerAgent;
|
|
41
|
+
/** Invoke one canonical reconciliation tick; no timer runs in the background. */
|
|
42
|
+
reconcile: () => Promise<void>;
|
|
43
|
+
/** Drain pending Agent work with a 1–1,000 tick budget (default 1,000). */
|
|
44
|
+
settle: (sessionId: string, maxTicks?: number) => Promise<SessionSnapshot>;
|
|
45
|
+
/** Close the Agent and only the database owned by this helper. */
|
|
46
|
+
close: () => Promise<void>;
|
|
47
|
+
}>;
|
|
48
|
+
/**
|
|
49
|
+
* Compose the real Shiitake runtime over an optional borrowed SQLite owner.
|
|
50
|
+
* Call reconcile/settle explicitly after acceptance. Defaults use a faux model
|
|
51
|
+
* and rejecting sandbox; custom callbacks are NOT isolation and retain host
|
|
52
|
+
* privileges. No submitted JavaScript is evaluated by this helper.
|
|
53
|
+
*/
|
|
54
|
+
export declare function memoryHost(options?: MemoryHostOptions): Promise<MemoryHost>;
|
package/dist/testing.js
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { DatabaseSync } from "node:sqlite";
|
|
2
|
+
import { fromNodeSqlite } from "@fungi.computer/watchdog/node-sqlite";
|
|
3
|
+
import { fauxModel } from "@fungi.computer/mule/testing";
|
|
4
|
+
import { rejectingSandbox } from "@fungi.computer/mycelium/testing";
|
|
5
|
+
import * as Schema from "effect/Schema";
|
|
6
|
+
import * as z from "zod";
|
|
7
|
+
import { fauxAssistantMessage, fauxToolCall, } from "@earendil-works/pi-ai/providers/faux";
|
|
8
|
+
import { GuestThrew } from "@fungi.computer/mycelium";
|
|
9
|
+
import { Shiitake, } from "./index.js";
|
|
10
|
+
const settlementBudgetSchema = z.int().min(1).max(1_000).default(1_000);
|
|
11
|
+
/** A deterministic model source for trusted tests; no provider account or I/O. */
|
|
12
|
+
export function fauxModelSource(replies) {
|
|
13
|
+
return {
|
|
14
|
+
acquire: ({ signal }) => {
|
|
15
|
+
signal.throwIfAborted();
|
|
16
|
+
const faux = fauxModel(replies);
|
|
17
|
+
return Promise.resolve({
|
|
18
|
+
model: faux.model,
|
|
19
|
+
stream: faux.stream,
|
|
20
|
+
release: () => Promise.resolve(),
|
|
21
|
+
});
|
|
22
|
+
},
|
|
23
|
+
describe: ({ signal }) => {
|
|
24
|
+
signal.throwIfAborted();
|
|
25
|
+
return Promise.resolve({
|
|
26
|
+
contextWindow: fauxModel(replies).model.contextWindow,
|
|
27
|
+
});
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/** One scripted model tool call; the scripted sandbox runs exactly this call. */
|
|
32
|
+
export class ScriptedToolCall {
|
|
33
|
+
path;
|
|
34
|
+
input;
|
|
35
|
+
constructor(
|
|
36
|
+
/** `<alias>.<operation>` as the model would call it, e.g. `bookmarks.add`. */
|
|
37
|
+
path, input) {
|
|
38
|
+
this.path = path;
|
|
39
|
+
this.input = input;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** Script one model turn that calls an operation with this input. */
|
|
43
|
+
export function callTool(path, input = {}) {
|
|
44
|
+
return new ScriptedToolCall(path, input);
|
|
45
|
+
}
|
|
46
|
+
const systemMessage = z.object({
|
|
47
|
+
role: z.literal("system"),
|
|
48
|
+
content: z.string(),
|
|
49
|
+
});
|
|
50
|
+
/** Script one model turn that replies with the system prompt it received. */
|
|
51
|
+
export function echoSystemPrompt() {
|
|
52
|
+
return (context) => {
|
|
53
|
+
const system = systemMessage.safeParse(context.messages[0]);
|
|
54
|
+
return fauxAssistantMessage(system.success ? system.data.content : "(no system prompt)");
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function scriptedCode(call) {
|
|
58
|
+
return `return await ${call.path}(${JSON.stringify(call.input)});`;
|
|
59
|
+
}
|
|
60
|
+
const isReplyText = Schema.is(Schema.String);
|
|
61
|
+
const thrownMessage = z.object({ message: z.string() });
|
|
62
|
+
function scriptedRun(steps) {
|
|
63
|
+
const calls = new Map();
|
|
64
|
+
const replies = steps.map((step) => {
|
|
65
|
+
if (isReplyText(step))
|
|
66
|
+
return fauxAssistantMessage(step);
|
|
67
|
+
if (step instanceof ScriptedToolCall) {
|
|
68
|
+
const code = scriptedCode(step);
|
|
69
|
+
calls.set(code, step);
|
|
70
|
+
return fauxAssistantMessage(fauxToolCall("execute", { code }), {
|
|
71
|
+
stopReason: "toolUse",
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return step;
|
|
75
|
+
});
|
|
76
|
+
return {
|
|
77
|
+
model: fauxModelSource(replies),
|
|
78
|
+
sandbox: {
|
|
79
|
+
execute: async (request, signal) => {
|
|
80
|
+
signal.throwIfAborted();
|
|
81
|
+
const call = calls.get(request.code);
|
|
82
|
+
if (call === undefined)
|
|
83
|
+
throw new Error("The scripted sandbox runs only scripted calls");
|
|
84
|
+
const dot = call.path.indexOf(".");
|
|
85
|
+
const operation = request.bindings[call.path.slice(0, dot)]?.[call.path.slice(dot + 1)];
|
|
86
|
+
if (dot < 1 || operation === undefined)
|
|
87
|
+
// Model code calling an operation this run lacks fails in the guest.
|
|
88
|
+
throw new GuestThrew({
|
|
89
|
+
name: "ReferenceError",
|
|
90
|
+
message: `${call.path} is not available to this run`,
|
|
91
|
+
});
|
|
92
|
+
try {
|
|
93
|
+
return await operation(call.input);
|
|
94
|
+
}
|
|
95
|
+
catch (cause) {
|
|
96
|
+
// An uncaught rejection in model code is a guest exception.
|
|
97
|
+
throw new GuestThrew({
|
|
98
|
+
name: "Error",
|
|
99
|
+
message: thrownMessage.safeParse(cause).data?.message ?? String(cause),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/** Grant each listed extension exactly the capabilities it requests. */
|
|
107
|
+
function requestedRoster(extensions) {
|
|
108
|
+
if ("roster" in extensions)
|
|
109
|
+
return extensions;
|
|
110
|
+
return {
|
|
111
|
+
roster: extensions,
|
|
112
|
+
grants: extensions.map((extension) => ({
|
|
113
|
+
id: extension.manifest.id,
|
|
114
|
+
capabilities: extension.manifest.requests,
|
|
115
|
+
})),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Compose the real Shiitake runtime over an optional borrowed SQLite owner.
|
|
120
|
+
* Call reconcile/settle explicitly after acceptance. Defaults use a faux model
|
|
121
|
+
* and rejecting sandbox; custom callbacks are NOT isolation and retain host
|
|
122
|
+
* privileges. No submitted JavaScript is evaluated by this helper.
|
|
123
|
+
*/
|
|
124
|
+
export async function memoryHost(options = {}) {
|
|
125
|
+
const { owner, script, extensions, ...agentOptions } = options;
|
|
126
|
+
if (script !== undefined &&
|
|
127
|
+
(options.model !== undefined || options.tools !== undefined))
|
|
128
|
+
throw new Error("Use script, or model and tools, not both");
|
|
129
|
+
const scripted = script === undefined ? undefined : scriptedRun(script);
|
|
130
|
+
const storage = owner === undefined ? openMemoryOwner() : { owner, close: () => undefined };
|
|
131
|
+
let released = false;
|
|
132
|
+
let bound;
|
|
133
|
+
let closed = false;
|
|
134
|
+
const runtime = {
|
|
135
|
+
bind: (input) => {
|
|
136
|
+
bound = input;
|
|
137
|
+
return Promise.resolve({
|
|
138
|
+
commit: (mutation) => mutation(),
|
|
139
|
+
requestWake: () => Promise.resolve(),
|
|
140
|
+
close: () => {
|
|
141
|
+
closed = true;
|
|
142
|
+
return Promise.resolve();
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
const requireBound = () => {
|
|
148
|
+
if (closed || bound === undefined)
|
|
149
|
+
throw new Error("Memory host is closed or unbound");
|
|
150
|
+
return bound;
|
|
151
|
+
};
|
|
152
|
+
try {
|
|
153
|
+
const agent = await Shiitake.make({
|
|
154
|
+
...agentOptions,
|
|
155
|
+
store: storage.owner,
|
|
156
|
+
runtime,
|
|
157
|
+
model: scripted?.model ??
|
|
158
|
+
options.model ??
|
|
159
|
+
fauxModelSource([fauxAssistantMessage("complete")]),
|
|
160
|
+
instructions: options.instructions ?? {
|
|
161
|
+
load: () => Promise.resolve({
|
|
162
|
+
revision: "memory-host-v1",
|
|
163
|
+
text: "Complete this test run.",
|
|
164
|
+
}),
|
|
165
|
+
},
|
|
166
|
+
extensions: extensions === undefined
|
|
167
|
+
? { roster: [], grants: [] }
|
|
168
|
+
: requestedRoster(extensions),
|
|
169
|
+
tools: scripted === undefined
|
|
170
|
+
? (options.tools ?? {
|
|
171
|
+
sandbox: rejectingSandbox("Memory host has no direct tools"),
|
|
172
|
+
})
|
|
173
|
+
: { sandbox: scripted.sandbox },
|
|
174
|
+
});
|
|
175
|
+
return {
|
|
176
|
+
agent,
|
|
177
|
+
reconcile: async () => requireBound().reconcile(),
|
|
178
|
+
settle: async (sessionId, maxTicks) => {
|
|
179
|
+
const budget = settlementBudgetSchema.parse(maxTicks);
|
|
180
|
+
for (let tick = 0; tick < budget; tick += 1) {
|
|
181
|
+
const host = requireBound();
|
|
182
|
+
if (!(await host.pendingWork()).pending)
|
|
183
|
+
return agent.session(sessionId).read();
|
|
184
|
+
await host.reconcile();
|
|
185
|
+
}
|
|
186
|
+
throw new Error(`Memory host did not settle within ${String(budget)} reconciliation ticks`);
|
|
187
|
+
},
|
|
188
|
+
close: async () => {
|
|
189
|
+
if (released)
|
|
190
|
+
return;
|
|
191
|
+
released = true;
|
|
192
|
+
try {
|
|
193
|
+
await agent.close();
|
|
194
|
+
}
|
|
195
|
+
finally {
|
|
196
|
+
storage.close();
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
catch (cause) {
|
|
202
|
+
storage.close();
|
|
203
|
+
throw cause;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function openMemoryOwner() {
|
|
207
|
+
const database = new DatabaseSync(":memory:");
|
|
208
|
+
database.exec("PRAGMA foreign_keys = ON");
|
|
209
|
+
return {
|
|
210
|
+
owner: fromNodeSqlite(database),
|
|
211
|
+
close: () => {
|
|
212
|
+
database.close();
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
}
|