@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,362 @@
|
|
|
1
|
+
import { nanoid } from "@fungi.computer/cairn";
|
|
2
|
+
import { parseModelPreference } from "./model-preference.js";
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
import * as Option from "effect/Option";
|
|
5
|
+
import * as Stream from "effect/Stream";
|
|
6
|
+
import { ShiitakeError } from "../errors.js";
|
|
7
|
+
import { decodeClientReadOptions, decodeClientSessionCommand, decodeClientSessionId, decodeClientSubscribeOptions, } from "./client-boundary.js";
|
|
8
|
+
import { mergeObservationSignals, openObservationKernel, publishOwnerObservation, } from "./observation-kernel.js";
|
|
9
|
+
import { checkHostMeta } from "./host-meta.js";
|
|
10
|
+
import { readSessionActivity } from "./session-status.js";
|
|
11
|
+
import { readSessionContext } from "./session-context.js";
|
|
12
|
+
import { abortSession, cancelSession, branchSession, recordOperatorLine, forkSession, readSession, readSessionModelPreference, readSessionReplayPage, readSessionInbox, createSession, deleteSession, deliverSessionGrants, readSessionGrants, setSessionModelPreference, setSessionTitle, } from "./woodstock/index.js";
|
|
13
|
+
/** Generic Agent-scope extension controls over the runtime's one call door. */
|
|
14
|
+
function agentExtensions(runtime) {
|
|
15
|
+
return Object.freeze({
|
|
16
|
+
list: runtime.assertOpen.pipe(Effect.map(() => runtime.extensions.list())),
|
|
17
|
+
resume: (extensionId) => runtime.assertOpen.pipe(Effect.andThen(runtime.extensions.resume(extensionId))),
|
|
18
|
+
call: (extensionId, command, input) => runtime.assertOpen.pipe(Effect.andThen(runtime.extensions.call({ scope: "agent" }, extensionId, command, input))),
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
function currentSessionActivity(options, runtime, sessionId) {
|
|
22
|
+
return runtime
|
|
23
|
+
.readQueueHead(sessionId)
|
|
24
|
+
.pipe(Effect.flatMap((head) => (head.state === "queued"
|
|
25
|
+
? runtime.runningWork(head.jobId)
|
|
26
|
+
: Effect.succeed(null)).pipe(Effect.map((runningWork) => readSessionActivity(options.store, sessionId, head, runningWork)))));
|
|
27
|
+
}
|
|
28
|
+
/** Carry the Session's context use on a baseline, when it has one. */
|
|
29
|
+
function withContext(snapshot, context) {
|
|
30
|
+
return context === undefined
|
|
31
|
+
? snapshot
|
|
32
|
+
: Object.freeze({ ...snapshot, context });
|
|
33
|
+
}
|
|
34
|
+
/** Read the baseline and its context use for one Session. */
|
|
35
|
+
function readSessionWithContext(options, runtime, sessionId, input) {
|
|
36
|
+
return currentSessionActivity(options, runtime, sessionId).pipe(Effect.flatMap((activity) => readSession(options.store, sessionId, activity, input, () => runtime.extensions.project(sessionId))), Effect.flatMap((snapshot) => readSessionContext(options, runtime, sessionId).pipe(Effect.map((context) => withContext(snapshot, context)))));
|
|
37
|
+
}
|
|
38
|
+
function sessionOwnerOperation(runtime, sessionId, operation) {
|
|
39
|
+
return runtime.assertOpen.pipe(Effect.andThen(Effect.try({
|
|
40
|
+
try: () => operation(decodeClientSessionId(sessionId)),
|
|
41
|
+
catch: (error) => error instanceof ShiitakeError
|
|
42
|
+
? error
|
|
43
|
+
: new ShiitakeError("store", "rejected", "Session owner operation failed"),
|
|
44
|
+
})));
|
|
45
|
+
}
|
|
46
|
+
function decodeSessionCommand(sessionId, input) {
|
|
47
|
+
return Effect.try({
|
|
48
|
+
try: () => {
|
|
49
|
+
const decodedCommand = decodeClientSessionCommand(input);
|
|
50
|
+
if (decodedCommand._tag === "Failure")
|
|
51
|
+
throw new ShiitakeError("contract", "invalid", "Session command is invalid");
|
|
52
|
+
return Object.freeze({
|
|
53
|
+
sessionId: decodeClientSessionId(sessionId),
|
|
54
|
+
command: decodedCommand.success,
|
|
55
|
+
});
|
|
56
|
+
},
|
|
57
|
+
catch: (error) => error instanceof ShiitakeError
|
|
58
|
+
? error
|
|
59
|
+
: new ShiitakeError("contract", "invalid", "Session command is invalid"),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function decodeSubscriptionInput(sessionId, input) {
|
|
63
|
+
return Effect.try({
|
|
64
|
+
try: () => Object.freeze({
|
|
65
|
+
sessionId: decodeClientSessionId(sessionId),
|
|
66
|
+
options: decodeClientSubscribeOptions(input),
|
|
67
|
+
}),
|
|
68
|
+
catch: (error) => error instanceof ShiitakeError
|
|
69
|
+
? error
|
|
70
|
+
: new ShiitakeError("contract", "invalid", "Subscribe options are invalid"),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
function resetFrame(options, runtime, sessionId, includeSandboxFailures = false) {
|
|
74
|
+
return runtime.assertOpen.pipe(Effect.andThen(readSessionWithContext(options, runtime, sessionId, {
|
|
75
|
+
includeSandboxFailures,
|
|
76
|
+
})), Effect.map((baseline) => Object.freeze({
|
|
77
|
+
kind: "reset",
|
|
78
|
+
reason: "history_unavailable",
|
|
79
|
+
cursor: baseline.cursor,
|
|
80
|
+
baseline,
|
|
81
|
+
})));
|
|
82
|
+
}
|
|
83
|
+
function continueFromReset(frame) {
|
|
84
|
+
return [
|
|
85
|
+
[frame],
|
|
86
|
+
Option.some({
|
|
87
|
+
after: frame.cursor ?? undefined,
|
|
88
|
+
delivery: frame.baseline.hasMore ? "replay" : "live",
|
|
89
|
+
awaitSignal: !frame.baseline.hasMore,
|
|
90
|
+
}),
|
|
91
|
+
];
|
|
92
|
+
}
|
|
93
|
+
function subscriptionFrames(options, runtime, registration, sessionId, after, delivery, includeSandboxFailures = false) {
|
|
94
|
+
const initial = {
|
|
95
|
+
after,
|
|
96
|
+
delivery,
|
|
97
|
+
awaitSignal: false,
|
|
98
|
+
};
|
|
99
|
+
return Stream.paginate(initial, (state) => (state.awaitSignal
|
|
100
|
+
? registration.awaitSignal.pipe(Effect.flatMap((signal) => registration.pollSignal.pipe(Effect.map((pending) => pending === undefined
|
|
101
|
+
? signal
|
|
102
|
+
: mergeObservationSignals(signal, pending)))))
|
|
103
|
+
: registration.pollSignal).pipe(Effect.flatMap((signal) => {
|
|
104
|
+
if (signal === "closed")
|
|
105
|
+
return Effect.succeed([[], Option.none()]);
|
|
106
|
+
if (signal === "reset")
|
|
107
|
+
return resetFrame(options, runtime, sessionId, includeSandboxFailures).pipe(Effect.map(continueFromReset));
|
|
108
|
+
return runtime.assertOpen.pipe(Effect.andThen(currentSessionActivity(options, runtime, sessionId)), Effect.flatMap((activity) => {
|
|
109
|
+
if (signal === "progress" &&
|
|
110
|
+
state.delivery === "live" &&
|
|
111
|
+
activity.progress !== null) {
|
|
112
|
+
return Effect.succeed([
|
|
113
|
+
[
|
|
114
|
+
{
|
|
115
|
+
kind: "progress",
|
|
116
|
+
progress: activity.progress,
|
|
117
|
+
activity: activity.facts,
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
Option.some({ ...state, awaitSignal: true }),
|
|
121
|
+
]);
|
|
122
|
+
}
|
|
123
|
+
return readSessionReplayPage(options.store, sessionId, state.after, state.delivery, activity, () => runtime.extensions.project(sessionId), includeSandboxFailures).pipe(Effect.flatMap((page) => {
|
|
124
|
+
if (page.kind === "reset")
|
|
125
|
+
return resetFrame(options, runtime, sessionId, includeSandboxFailures).pipe(Effect.map(continueFromReset));
|
|
126
|
+
const latestCursor = page.frames
|
|
127
|
+
.flatMap((frame) => frame.kind === "progress" ? [] : [frame.cursor])
|
|
128
|
+
.at(-1);
|
|
129
|
+
const next = {
|
|
130
|
+
after: latestCursor ?? state.after,
|
|
131
|
+
delivery: page.hasMore ? state.delivery : "live",
|
|
132
|
+
awaitSignal: !page.hasMore,
|
|
133
|
+
};
|
|
134
|
+
// Every update carries the context use it settles on.
|
|
135
|
+
return (page.frames.some((frame) => frame.kind === "update")
|
|
136
|
+
? readSessionContext(options, runtime, sessionId)
|
|
137
|
+
: Effect.succeed(undefined)).pipe(Effect.map((context) => [
|
|
138
|
+
context === undefined
|
|
139
|
+
? page.frames
|
|
140
|
+
: page.frames.map((frame) => frame.kind === "update"
|
|
141
|
+
? Object.freeze({ ...frame, context })
|
|
142
|
+
: frame),
|
|
143
|
+
Option.some(next),
|
|
144
|
+
]));
|
|
145
|
+
}));
|
|
146
|
+
}));
|
|
147
|
+
})));
|
|
148
|
+
}
|
|
149
|
+
function subscribeSession(options, runtime, observation, sessionId, input) {
|
|
150
|
+
return Stream.unwrap(runtime.assertOpen.pipe(Effect.andThen(decodeSubscriptionInput(sessionId, input)), Effect.flatMap((decoded) => Effect.acquireRelease(observation.register(decoded.sessionId), (registration) => Effect.sync(registration.close)).pipe(Effect.map((registration) => subscriptionFrames(options, runtime, registration, decoded.sessionId, decoded.options.after, "replay", decoded.options.includeSandboxFailures === true))))));
|
|
151
|
+
}
|
|
152
|
+
function decodeWatchInput(sessionId, input) {
|
|
153
|
+
return Effect.try({
|
|
154
|
+
try: () => Object.freeze({
|
|
155
|
+
sessionId: decodeClientSessionId(sessionId),
|
|
156
|
+
options: decodeClientReadOptions(input),
|
|
157
|
+
}),
|
|
158
|
+
catch: (error) => error instanceof ShiitakeError
|
|
159
|
+
? error
|
|
160
|
+
: new ShiitakeError("contract", "invalid", "Watch options are invalid"),
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
function watchSession(options, runtime, observation, sessionId, input) {
|
|
164
|
+
return runtime.assertOpen.pipe(Effect.andThen(decodeWatchInput(sessionId, input)), Effect.flatMap((decoded) => Effect.uninterruptibleMask((restore) => Effect.gen(function* () {
|
|
165
|
+
const registration = yield* observation.register(decoded.sessionId);
|
|
166
|
+
return yield* restore(runtime.assertOpen.pipe(Effect.andThen(readSessionWithContext(options, runtime, decoded.sessionId, decoded.options)), Effect.map((snapshot) => Object.freeze({
|
|
167
|
+
snapshot,
|
|
168
|
+
changes: subscriptionFrames(options, runtime, registration, decoded.sessionId, snapshot.cursor ?? undefined, "live", decoded.options.includeSandboxFailures === true).pipe(Stream.ensuring(Effect.sync(registration.close))),
|
|
169
|
+
close: Effect.sync(registration.close),
|
|
170
|
+
})))).pipe(Effect.onError(() => Effect.sync(registration.close)));
|
|
171
|
+
}))));
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Admit one user prompt, mail prompt or compaction request through the
|
|
175
|
+
* accepted-command door.
|
|
176
|
+
*/
|
|
177
|
+
function admitUserWork(runtime, sessionId, input, meta, mail, reply) {
|
|
178
|
+
let request = {
|
|
179
|
+
sessionId,
|
|
180
|
+
command: input,
|
|
181
|
+
when: "now",
|
|
182
|
+
origin: { kind: "user" },
|
|
183
|
+
};
|
|
184
|
+
if (meta !== undefined)
|
|
185
|
+
request = { ...request, meta };
|
|
186
|
+
if (mail !== undefined)
|
|
187
|
+
request = { ...request, mail };
|
|
188
|
+
if (reply !== undefined)
|
|
189
|
+
request = { ...request, reply };
|
|
190
|
+
return runtime.assertOpen.pipe(Effect.andThen(runtime.door.admit(request)), Effect.flatMap((admission) => admission.status === "accepted"
|
|
191
|
+
? Effect.succeed({
|
|
192
|
+
status: "accepted",
|
|
193
|
+
commandId: admission.commandId,
|
|
194
|
+
})
|
|
195
|
+
: Effect.fail(new ShiitakeError("runtime", "unavailable", "Session command was not admitted"))));
|
|
196
|
+
}
|
|
197
|
+
function cancelRequestedSession(options, runtime, sessionId, input, meta) {
|
|
198
|
+
return runtime.assertOpen.pipe(Effect.andThen(cancelSession(options.store, sessionId, input.commandId, input.onlyIfPending, () => runtime.readQueueHeadTransactional(sessionId), runtime.recordAcceptedCancellation, meta)), Effect.flatMap((cancellation) => cancellation === undefined
|
|
199
|
+
? Effect.succeed({
|
|
200
|
+
status: "too_late",
|
|
201
|
+
commandId: input.commandId,
|
|
202
|
+
})
|
|
203
|
+
: (cancellation.active
|
|
204
|
+
? runtime.signalCancellation(cancellation.accepted.commandId, cancellation.accepted.entryId)
|
|
205
|
+
: runtime.requestWake).pipe(Effect.as({
|
|
206
|
+
status: "accepted",
|
|
207
|
+
commandId: cancellation.accepted.commandId,
|
|
208
|
+
}))));
|
|
209
|
+
}
|
|
210
|
+
function abortCurrentSession(options, runtime, sessionId, meta) {
|
|
211
|
+
return runtime.assertOpen.pipe(Effect.andThen(runtime.commit(abortSession(options.store, sessionId, () => runtime.readQueueHeadTransactional(sessionId), runtime.recordAcceptedCancellation, meta))), Effect.flatMap((cancellation) => cancellation === undefined
|
|
212
|
+
? runtime.requestWake
|
|
213
|
+
: runtime.signalCancellation(cancellation.commandId, cancellation.entryId)));
|
|
214
|
+
}
|
|
215
|
+
function selectBranch(options, runtime, sessionId, input) {
|
|
216
|
+
return runtime.assertOpen.pipe(Effect.andThen(branchSession(options.store, {
|
|
217
|
+
sessionId,
|
|
218
|
+
type: "branch",
|
|
219
|
+
entryId: input.entryId,
|
|
220
|
+
assertIdleSession: runtime.assertIdleSession,
|
|
221
|
+
})), Effect.as(undefined));
|
|
222
|
+
}
|
|
223
|
+
function forkBranch(options, runtime, sessionId, input) {
|
|
224
|
+
return runtime.assertOpen.pipe(Effect.andThen(forkSession(options.store, {
|
|
225
|
+
sessionId,
|
|
226
|
+
type: "fork",
|
|
227
|
+
entryId: input.entryId,
|
|
228
|
+
assertIdleSession: runtime.assertIdleSession,
|
|
229
|
+
})));
|
|
230
|
+
}
|
|
231
|
+
function executeSessionCommand(options, runtime, sessionId, meta, input) {
|
|
232
|
+
return runtime.assertOpen.pipe(Effect.andThen(decodeSessionCommand(sessionId, input)),
|
|
233
|
+
// The one path every command's host metadata takes: checked once against
|
|
234
|
+
// the host's schema, then stored on whatever entry the command writes.
|
|
235
|
+
Effect.flatMap((decoded) => checkCommandMeta(options, meta).pipe(Effect.map((checked) => ({ ...decoded, checked })))), Effect.flatMap(({ sessionId: decodedSessionId, command, checked, }) => {
|
|
236
|
+
switch (command.type) {
|
|
237
|
+
case "prompt":
|
|
238
|
+
case "compact":
|
|
239
|
+
return admitUserWork(runtime, decodedSessionId, command, checked);
|
|
240
|
+
case "abort":
|
|
241
|
+
return abortCurrentSession(options, runtime, decodedSessionId, checked).pipe(Effect.as(undefined));
|
|
242
|
+
case "cancel":
|
|
243
|
+
return cancelRequestedSession(options, runtime, decodedSessionId, command, checked);
|
|
244
|
+
case "branch":
|
|
245
|
+
return selectBranch(options, runtime, decodedSessionId, command);
|
|
246
|
+
case "fork":
|
|
247
|
+
return forkBranch(options, runtime, decodedSessionId, command);
|
|
248
|
+
case "operator":
|
|
249
|
+
return runtime.assertOpen.pipe(Effect.andThen(recordOperatorLine(options.store, {
|
|
250
|
+
sessionId: decodedSessionId,
|
|
251
|
+
requestId: command.requestId,
|
|
252
|
+
author: command.author,
|
|
253
|
+
meta: checked,
|
|
254
|
+
text: command.text,
|
|
255
|
+
assertIdleSession: runtime.assertIdleSession,
|
|
256
|
+
})), Effect.as(undefined));
|
|
257
|
+
case "title":
|
|
258
|
+
return runtime
|
|
259
|
+
.commit(sessionOwnerOperation(runtime, decodedSessionId, (decoded) => {
|
|
260
|
+
setSessionTitle(options.store, decoded, command.title);
|
|
261
|
+
}))
|
|
262
|
+
.pipe(Effect.as(undefined));
|
|
263
|
+
}
|
|
264
|
+
}));
|
|
265
|
+
}
|
|
266
|
+
/** Check one command's host metadata against the host's schema, once. */
|
|
267
|
+
function checkCommandMeta(options, meta) {
|
|
268
|
+
return Effect.try({
|
|
269
|
+
try: () => meta === undefined ? undefined : checkHostMeta(options.meta, meta),
|
|
270
|
+
catch: (cause) => cause instanceof ShiitakeError
|
|
271
|
+
? cause
|
|
272
|
+
: new ShiitakeError("contract", "invalid", "Host metadata is invalid"),
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
/** Construct the Effect-native coordinator over the private owner modules. */
|
|
276
|
+
export function openCoordinator(options, runtime) {
|
|
277
|
+
const observation = openObservationKernel(options.store);
|
|
278
|
+
/** One Session handle; `meta` is the host metadata its prompts carry. */
|
|
279
|
+
const sessionFor = (sessionId, meta) => {
|
|
280
|
+
function command(input) {
|
|
281
|
+
return executeSessionCommand(options, runtime, sessionId, meta, input);
|
|
282
|
+
}
|
|
283
|
+
return Object.freeze({
|
|
284
|
+
command,
|
|
285
|
+
abort: command({ type: "abort" }),
|
|
286
|
+
send: (text, input) => Effect.sync(() => input?.requestId ?? nanoid()).pipe(Effect.flatMap((requestId) => {
|
|
287
|
+
const prompt = {
|
|
288
|
+
type: "prompt",
|
|
289
|
+
delivery: input?.delivery ?? "steer",
|
|
290
|
+
requestId,
|
|
291
|
+
text,
|
|
292
|
+
};
|
|
293
|
+
return command(input?.images === undefined
|
|
294
|
+
? prompt
|
|
295
|
+
: { ...prompt, images: input.images }).pipe(Effect.mapError((error) => error.requestId === requestId
|
|
296
|
+
? error
|
|
297
|
+
: new ShiitakeError(error.boundary, error.cause, error.message, requestId)));
|
|
298
|
+
})),
|
|
299
|
+
cancel: (input) => command({ type: "cancel", ...input }),
|
|
300
|
+
modelPreference: Object.freeze({
|
|
301
|
+
read: sessionOwnerOperation(runtime, sessionId, (decoded) => readSessionModelPreference(options.store, decoded)),
|
|
302
|
+
set: (preference) => Effect.try({
|
|
303
|
+
try: () => parseModelPreference(preference),
|
|
304
|
+
catch: () => new ShiitakeError("contract", "invalid", "Model preference is invalid"),
|
|
305
|
+
}).pipe(Effect.flatMap((parsed) => runtime
|
|
306
|
+
.commit(sessionOwnerOperation(runtime, sessionId, (decoded) => {
|
|
307
|
+
setSessionModelPreference(options.store, decoded, parsed);
|
|
308
|
+
return decoded;
|
|
309
|
+
}))
|
|
310
|
+
.pipe(
|
|
311
|
+
// Once committed, observers re-read the Session: the
|
|
312
|
+
// selection's window may have changed.
|
|
313
|
+
Effect.tap((decoded) => Effect.sync(() => {
|
|
314
|
+
publishOwnerObservation(options.store, decoded);
|
|
315
|
+
})), Effect.asVoid))),
|
|
316
|
+
}),
|
|
317
|
+
extension: (extensionId) => Object.freeze({
|
|
318
|
+
call: (command, input) => runtime.assertOpen.pipe(Effect.andThen(runtime.extensions.call({ scope: "session", sessionId }, extensionId, command, input))),
|
|
319
|
+
}),
|
|
320
|
+
read: (input) => runtime.assertOpen.pipe(Effect.andThen(readSessionWithContext(options, runtime, sessionId, input))),
|
|
321
|
+
subscribe: (input) => subscribeSession(options, runtime, observation, sessionId, input),
|
|
322
|
+
watch: (input) => watchSession(options, runtime, observation, sessionId, input),
|
|
323
|
+
});
|
|
324
|
+
};
|
|
325
|
+
const agent = Object.freeze({
|
|
326
|
+
createSession: (requestId) => runtime.assertOpen.pipe(Effect.andThen(createSession(options.store, requestId, runtime.storage.admitNew))),
|
|
327
|
+
deleteSession: (sessionId) => runtime.assertOpen.pipe(Effect.andThen(deleteSession(options.store, {
|
|
328
|
+
sessionId,
|
|
329
|
+
assertIdleSession: runtime.assertIdleSession,
|
|
330
|
+
}))),
|
|
331
|
+
listSessions: (after) => runtime.assertOpen.pipe(Effect.andThen(readSessionInbox(options.store, after))),
|
|
332
|
+
extensions: agentExtensions(runtime),
|
|
333
|
+
session: (sessionId) => sessionFor(sessionId, undefined),
|
|
334
|
+
close: runtime.beginClose.pipe(Effect.andThen(observation.close)),
|
|
335
|
+
});
|
|
336
|
+
return Object.freeze({
|
|
337
|
+
...agent,
|
|
338
|
+
grants: Object.freeze({
|
|
339
|
+
// A synchronous owner write: a delivery lands before any other work
|
|
340
|
+
// can interleave, so the very next tool call reads it.
|
|
341
|
+
deliver: (sessionId, delivery) => sessionOwnerOperation(runtime, sessionId, (decoded) => options.store.transactionSync(() => deliverSessionGrants(options.store, decoded, delivery))),
|
|
342
|
+
read: (sessionId) => sessionOwnerOperation(runtime, sessionId, (decoded) => readSessionGrants(options.store, decoded)),
|
|
343
|
+
}),
|
|
344
|
+
withMeta: (meta) => Object.freeze({
|
|
345
|
+
...agent,
|
|
346
|
+
session: (sessionId) => sessionFor(sessionId, meta),
|
|
347
|
+
}),
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Accept one addressed mail envelope; the door decodes it as a prompt. `reply`
|
|
352
|
+
* is the host's authentication of a delegated Agent's reply; only then does
|
|
353
|
+
* the model see the line inside the verified-reply tag.
|
|
354
|
+
*/
|
|
355
|
+
export function acceptAddressedMail(runtime, mail, meta, reply) {
|
|
356
|
+
return admitUserWork(runtime, mail.to.sessionId, {
|
|
357
|
+
requestId: mail.key,
|
|
358
|
+
delivery: reply === undefined ? "followUp" : "steer",
|
|
359
|
+
type: "prompt",
|
|
360
|
+
text: mail.content,
|
|
361
|
+
}, meta, mail, reply);
|
|
362
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
import type { DocumentsV0, DocumentSpecV0 } from "../session-documents.js";
|
|
3
|
+
/** The declared schema owns typed values; the JSON decoder owns their encoding. */
|
|
4
|
+
export declare function encodeDocument<A, I>(spec: DocumentSpecV0<A, I>, value: A): string;
|
|
5
|
+
/** Validate policy, bound and initial state before admitting any extension. */
|
|
6
|
+
export declare function documentDescriptor(spec: DocumentsV0[string]): {
|
|
7
|
+
schema: Schema.Json;
|
|
8
|
+
initial: string;
|
|
9
|
+
fork: "inherit-at-fork-point" | "current" | "fresh";
|
|
10
|
+
maxBytes: number;
|
|
11
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
import * as SchemaRepresentation from "effect/SchemaRepresentation";
|
|
3
|
+
import { ExtensionStateError } from "../extensions.js";
|
|
4
|
+
import { JsonValueSchema } from "./json-value.js";
|
|
5
|
+
const Declaration = Schema.Struct({
|
|
6
|
+
fork: Schema.Literals(["inherit-at-fork-point", "current", "fresh"]),
|
|
7
|
+
maxBytes: Schema.Int.pipe(Schema.check(Schema.isGreaterThan(0)), Schema.check(Schema.isLessThanOrEqualTo(Number.MAX_SAFE_INTEGER))),
|
|
8
|
+
});
|
|
9
|
+
/** The declared schema owns typed values; the JSON decoder owns their encoding. */
|
|
10
|
+
export function encodeDocument(spec, value) {
|
|
11
|
+
const result = Schema.encodeUnknownResult(spec.schema)(value);
|
|
12
|
+
if (result._tag === "Failure")
|
|
13
|
+
throw new ExtensionStateError({
|
|
14
|
+
reason: "encode",
|
|
15
|
+
message: "Session document does not match its schema",
|
|
16
|
+
});
|
|
17
|
+
const json = Schema.decodeUnknownResult(JsonValueSchema)(result.success);
|
|
18
|
+
if (json._tag === "Failure")
|
|
19
|
+
throw new ExtensionStateError({
|
|
20
|
+
reason: "encode",
|
|
21
|
+
message: "Session document must encode finite JSON",
|
|
22
|
+
});
|
|
23
|
+
const text = JSON.stringify(json.success);
|
|
24
|
+
if (new TextEncoder().encode(text).byteLength > spec.maxBytes)
|
|
25
|
+
throw new ExtensionStateError({
|
|
26
|
+
reason: "bound",
|
|
27
|
+
message: "Session document exceeds maxBytes",
|
|
28
|
+
});
|
|
29
|
+
return text;
|
|
30
|
+
}
|
|
31
|
+
/** Validate policy, bound and initial state before admitting any extension. */
|
|
32
|
+
export function documentDescriptor(spec) {
|
|
33
|
+
const declaration = Schema.decodeUnknownSync(Declaration)(spec);
|
|
34
|
+
return {
|
|
35
|
+
...declaration,
|
|
36
|
+
schema: SchemaRepresentation.toJson(Schema.toRepresentation(spec.schema)),
|
|
37
|
+
initial: encodeDocument(spec, spec.initial),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import type { ShiitakeAgent } from "../client.js";
|
|
3
|
+
import type { ShiitakeEffectAgent, ShiitakeEffectOwner } from "./effect-contracts.js";
|
|
4
|
+
import type { ShiitakeOwnerAgent } from "./contracts.js";
|
|
5
|
+
/** Run one Shiitake Effect through its sole Promise error adaptation. */
|
|
6
|
+
export declare function runShiitakePromise<A, E>(effect: Effect.Effect<A, E>): Promise<A>;
|
|
7
|
+
/** Adapt one canonical Effect agent to Shiitake's ordinary web interface. */
|
|
8
|
+
export declare function toPromiseAgent(agent: ShiitakeEffectAgent, suppliedAddress?: string): ShiitakeAgent;
|
|
9
|
+
/**
|
|
10
|
+
* Adapt the in-process owner, keeping its host-only grants and metadata
|
|
11
|
+
* surfaces; its metadata views share one address.
|
|
12
|
+
*/
|
|
13
|
+
export declare function toPromiseOwner(owner: ShiitakeEffectOwner): ShiitakeOwnerAgent;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { nanoid } from "@fungi.computer/cairn";
|
|
2
|
+
import * as Cause from "effect/Cause";
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
import * as Exit from "effect/Exit";
|
|
5
|
+
import * as Option from "effect/Option";
|
|
6
|
+
import * as Stream from "effect/Stream";
|
|
7
|
+
import { ShiitakeError } from "../errors.js";
|
|
8
|
+
/** Run one Shiitake Effect through its sole Promise error adaptation. */
|
|
9
|
+
export function runShiitakePromise(effect) {
|
|
10
|
+
return Effect.runPromiseExit(effect).then((exit) => {
|
|
11
|
+
if (Exit.isSuccess(exit))
|
|
12
|
+
return exit.value;
|
|
13
|
+
const failure = Option.getOrUndefined(Cause.findErrorOption(exit.cause));
|
|
14
|
+
const error = failure ?? Cause.squash(exit.cause);
|
|
15
|
+
return Promise.reject(error instanceof Error ? error : new Error(String(error)));
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Run one synchronous owner Effect and return or throw its result. Only
|
|
20
|
+
* store operations that never suspend use it.
|
|
21
|
+
*/
|
|
22
|
+
function runShiitakeSync(effect) {
|
|
23
|
+
const exit = Effect.runSyncExit(effect);
|
|
24
|
+
if (Exit.isSuccess(exit))
|
|
25
|
+
return exit.value;
|
|
26
|
+
const failure = Option.getOrUndefined(Cause.findErrorOption(exit.cause));
|
|
27
|
+
const error = failure ?? Cause.squash(exit.cause);
|
|
28
|
+
throw error instanceof Error ? error : new Error(String(error));
|
|
29
|
+
}
|
|
30
|
+
function observationAsyncIterable(stream) {
|
|
31
|
+
const iterable = Stream.toAsyncIterable(stream);
|
|
32
|
+
return Object.freeze({
|
|
33
|
+
[Symbol.asyncIterator]: () => {
|
|
34
|
+
const iterator = iterable[Symbol.asyncIterator]();
|
|
35
|
+
return {
|
|
36
|
+
next: () => iterator.next(),
|
|
37
|
+
return: () => iterator.return?.() ??
|
|
38
|
+
Promise.resolve({ done: true, value: undefined }),
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
function sessionFacade(address, session) {
|
|
44
|
+
function command(input) {
|
|
45
|
+
return runShiitakePromise(session.command(input));
|
|
46
|
+
}
|
|
47
|
+
return Object.freeze({
|
|
48
|
+
address,
|
|
49
|
+
command,
|
|
50
|
+
abort: () => runShiitakePromise(session.abort),
|
|
51
|
+
send: (text, options) => runShiitakePromise(session.send(text, options)),
|
|
52
|
+
cancel: (input) => runShiitakePromise(session.cancel(input)),
|
|
53
|
+
modelPreference: Object.freeze({
|
|
54
|
+
read: () => runShiitakePromise(session.modelPreference.read),
|
|
55
|
+
set: (preference) => runShiitakePromise(session.modelPreference.set(preference)),
|
|
56
|
+
}),
|
|
57
|
+
extension: (extensionId) => {
|
|
58
|
+
const extension = session.extension(extensionId);
|
|
59
|
+
return Object.freeze({
|
|
60
|
+
call: (command, input) => runShiitakePromise(extension.call(command, input)),
|
|
61
|
+
});
|
|
62
|
+
},
|
|
63
|
+
read: (input) => runShiitakePromise(session.read(input)),
|
|
64
|
+
subscribe: (input) => observationAsyncIterable(session.subscribe(input)),
|
|
65
|
+
watch: (input) => runShiitakePromise(session.watch(input)).then((watch) => Object.freeze({
|
|
66
|
+
snapshot: watch.snapshot,
|
|
67
|
+
changes: observationAsyncIterable(watch.changes),
|
|
68
|
+
close: () => runShiitakePromise(watch.close),
|
|
69
|
+
})),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
/** Adapt one canonical Effect agent to Shiitake's ordinary web interface. */
|
|
73
|
+
export function toPromiseAgent(agent, suppliedAddress) {
|
|
74
|
+
const agentAddress = suppliedAddress ?? `local:${nanoid()}`;
|
|
75
|
+
return Object.freeze({
|
|
76
|
+
createSession: (requestId) => runShiitakePromise(agent.createSession(requestId)),
|
|
77
|
+
deleteSession: (sessionId) => runShiitakePromise(agent.deleteSession(sessionId)),
|
|
78
|
+
listSessions: (after) => runShiitakePromise(agent.listSessions(after)),
|
|
79
|
+
extensions: Object.freeze({
|
|
80
|
+
list: () => runShiitakePromise(agent.extensions.list),
|
|
81
|
+
resume: (extensionId) => runShiitakePromise(agent.extensions.resume(extensionId)),
|
|
82
|
+
call: (extensionId, command, input) => runShiitakePromise(agent.extensions.call(extensionId, command, input)),
|
|
83
|
+
}),
|
|
84
|
+
session: (sessionId) => sessionFacade(`${agentAddress}/sessions/${encodeURIComponent(sessionId)}`, agent.session(sessionId)),
|
|
85
|
+
close: () => runShiitakePromise(agent.close),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Adapt the in-process owner, keeping its host-only grants and metadata
|
|
90
|
+
* surfaces; its metadata views share one address.
|
|
91
|
+
*/
|
|
92
|
+
export function toPromiseOwner(owner) {
|
|
93
|
+
const address = `local:${nanoid()}`;
|
|
94
|
+
return Object.freeze({
|
|
95
|
+
...toPromiseAgent(owner, address),
|
|
96
|
+
grants: Object.freeze({
|
|
97
|
+
deliver: (sessionId, delivery) => runShiitakeSync(owner.grants.deliver(sessionId, delivery)),
|
|
98
|
+
read: (sessionId) => runShiitakeSync(owner.grants.read(sessionId)),
|
|
99
|
+
}),
|
|
100
|
+
withMeta: (meta) => toPromiseAgent(owner.withMeta(meta), address),
|
|
101
|
+
});
|
|
102
|
+
}
|