@fungi.computer/shiitake 0.0.0-stage → 0.0.1
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 +72 -0
- package/dist/agent-control.js +431 -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 +297 -0
- package/dist/fleet.js +515 -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 +292 -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 +32 -0
- package/dist/internal/woodstock/fleet-store.js +93 -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 +904 -0
- package/docs/api/extensions.md +2003 -0
- package/docs/api/fleet.md +706 -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,27 @@
|
|
|
1
|
+
import type { FileTool } from "@fungi.computer/knapsack";
|
|
2
|
+
import type { ModuleRegistration } from "@fungi.computer/mycelium";
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
import { ShiitakeError } from "../errors.js";
|
|
5
|
+
import type { ShiitakeEffectOptions } from "./effect-contracts.js";
|
|
6
|
+
import type { CompactionHandoffV0, PromptSectionV0 } from "../extensions.js";
|
|
7
|
+
import { type ManualCompactionOutcome } from "./compaction/session-compaction.js";
|
|
8
|
+
import { type PreparedSessionRun, type RunContract } from "./woodstock/index.js";
|
|
9
|
+
declare const toolSetupMessages: {
|
|
10
|
+
readonly missing: "Instruction capabilities have no tool runtime";
|
|
11
|
+
readonly runtime: "Tool runtime failed";
|
|
12
|
+
readonly lease: "Tool lease failed";
|
|
13
|
+
};
|
|
14
|
+
/** Closed setup phases owned here; foreign error messages never define them. */
|
|
15
|
+
export declare class ToolSetupError extends ShiitakeError {
|
|
16
|
+
private readonly phase;
|
|
17
|
+
constructor(phase: keyof typeof toolSetupMessages);
|
|
18
|
+
get diagnostic(): string;
|
|
19
|
+
}
|
|
20
|
+
/** Run one accepted command with Pi compaction explicitly composed around Mule. */
|
|
21
|
+
export declare function runAcceptedCommand(options: ShiitakeEffectOptions, prepared: PreparedSessionRun, executionContract: RunContract, extensionModules: readonly ModuleRegistration[], extensionFileTools: readonly FileTool[], compactionHandoff: CompactionHandoffV0 | undefined, promptSections?: () => Effect.Effect<readonly PromptSectionV0[], ShiitakeError>): Effect.Effect<import("@fungi.computer/mule/effect").MuleRunOutcome, ShiitakeError, never>;
|
|
22
|
+
/**
|
|
23
|
+
* Run one accepted compaction request: the run's model acquisition and the
|
|
24
|
+
* Session's one compaction commit, with no model turn and no tools.
|
|
25
|
+
*/
|
|
26
|
+
export declare function runAcceptedCompaction(options: ShiitakeEffectOptions, prepared: PreparedSessionRun, compactionHandoff: CompactionHandoffV0 | undefined): Effect.Effect<ManualCompactionOutcome, ShiitakeError>;
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { muleEffect, piCodingAgentRetryDefaults, piHttpIdleTimeoutMs, } from "@fungi.computer/mule/effect";
|
|
2
|
+
import { make as makeMycelium } from "@fungi.computer/mycelium/effect";
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
import { ShiitakeError } from "../errors.js";
|
|
5
|
+
import { compactSession, runWithCompaction, } from "./compaction/session-compaction.js";
|
|
6
|
+
import { openCompactionSettings } from "./compaction/settings.js";
|
|
7
|
+
import { acquireOwnedModel } from "./model-acquisition.js";
|
|
8
|
+
import { isCompactCommand, openMuleRecorder, readSessionGrants, recordLiveRetryEvent, recordSandboxFailure, takeSessionControls, } from "./woodstock/index.js";
|
|
9
|
+
function runtimeFailure(message) {
|
|
10
|
+
return new ShiitakeError("runtime", "rejected", message);
|
|
11
|
+
}
|
|
12
|
+
const toolSetupMessages = {
|
|
13
|
+
missing: "Instruction capabilities have no tool runtime",
|
|
14
|
+
runtime: "Tool runtime failed",
|
|
15
|
+
lease: "Tool lease failed",
|
|
16
|
+
};
|
|
17
|
+
/** Closed setup phases owned here; foreign error messages never define them. */
|
|
18
|
+
export class ToolSetupError extends ShiitakeError {
|
|
19
|
+
phase;
|
|
20
|
+
constructor(phase) {
|
|
21
|
+
super("runtime", "rejected", toolSetupMessages[phase]);
|
|
22
|
+
this.phase = phase;
|
|
23
|
+
}
|
|
24
|
+
get diagnostic() {
|
|
25
|
+
return toolSetupMessages[this.phase];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Map Mule's retry transitions onto Woodstock's retained `live_retry` facts.
|
|
30
|
+
* A started attempt ends the backoff countdown before streaming resumes.
|
|
31
|
+
*/
|
|
32
|
+
function toLiveRetryEvent(event) {
|
|
33
|
+
if (event.type === "retry_attempt_started")
|
|
34
|
+
return {
|
|
35
|
+
type: "auto_retry_attempt",
|
|
36
|
+
run: event.run,
|
|
37
|
+
attempt: event.attempt,
|
|
38
|
+
};
|
|
39
|
+
return event.type === "retry_scheduled"
|
|
40
|
+
? {
|
|
41
|
+
type: "auto_retry_start",
|
|
42
|
+
run: event.run,
|
|
43
|
+
attempt: event.attempt,
|
|
44
|
+
maxAttempts: event.maxAttempts,
|
|
45
|
+
delayMs: event.delayMs,
|
|
46
|
+
errorMessage: event.errorMessage,
|
|
47
|
+
}
|
|
48
|
+
: event.finalError === undefined
|
|
49
|
+
? {
|
|
50
|
+
type: "auto_retry_end",
|
|
51
|
+
run: event.run,
|
|
52
|
+
attempt: event.attempt,
|
|
53
|
+
success: event.success,
|
|
54
|
+
}
|
|
55
|
+
: {
|
|
56
|
+
type: "auto_retry_end",
|
|
57
|
+
run: event.run,
|
|
58
|
+
attempt: event.attempt,
|
|
59
|
+
success: event.success,
|
|
60
|
+
finalError: event.finalError,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/** Acquire the run's tools for exactly one Session, so hosts can project per Session. */
|
|
64
|
+
function acquiredTools(options, sessionId, workId, meta, instructions, extensionModules, extensionFileTools) {
|
|
65
|
+
const toolSource = options.tools;
|
|
66
|
+
const instructionModules = instructions.modules ?? [];
|
|
67
|
+
const modules = [...instructionModules, ...extensionModules];
|
|
68
|
+
if (toolSource === undefined) {
|
|
69
|
+
if (instructionModules.length > 0 || extensionFileTools.length > 0)
|
|
70
|
+
return Effect.fail(new ToolSetupError("missing"));
|
|
71
|
+
return Effect.succeed([]);
|
|
72
|
+
}
|
|
73
|
+
return Effect.gen(function* () {
|
|
74
|
+
let session = { sessionId, commandId: workId };
|
|
75
|
+
if (meta !== undefined)
|
|
76
|
+
session = { ...session, meta };
|
|
77
|
+
const runtimeOptions = {
|
|
78
|
+
sources: toolSource.sources,
|
|
79
|
+
session,
|
|
80
|
+
sandbox: toolSource.sandbox,
|
|
81
|
+
onSandboxFailure: (event) => recordSandboxFailure(options.store, { sessionId, commandId: workId }, event).pipe(Effect.orDie),
|
|
82
|
+
modules: [...toolSource.modules, ...modules],
|
|
83
|
+
tools: extensionFileTools,
|
|
84
|
+
access: {
|
|
85
|
+
...toolSource.access,
|
|
86
|
+
// Mycelium reads the stored grants at every call, so a grant the
|
|
87
|
+
// host changes mid-run applies to the very next call.
|
|
88
|
+
sessionGrants: () => readSessionGrants(options.store, sessionId)?.grants ?? {},
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
const runtime = yield* makeMycelium(toolSource.execution === undefined
|
|
92
|
+
? runtimeOptions
|
|
93
|
+
: { ...runtimeOptions, execution: toolSource.execution }).pipe(Effect.mapError(() => new ToolSetupError("runtime")));
|
|
94
|
+
const lease = yield* runtime
|
|
95
|
+
.acquire()
|
|
96
|
+
.pipe(Effect.mapError(() => new ToolSetupError("lease")));
|
|
97
|
+
return [lease.executeTool, ...lease.tools];
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
/** Run one accepted command with Pi compaction explicitly composed around Mule. */
|
|
101
|
+
export function runAcceptedCommand(options, prepared, executionContract, extensionModules, extensionFileTools, compactionHandoff, promptSections) {
|
|
102
|
+
return Effect.scoped(Effect.gen(function* () {
|
|
103
|
+
const instructions = yield* options.instructions.load({
|
|
104
|
+
sessionId: prepared.command.sessionId,
|
|
105
|
+
commandId: prepared.command.entry.workId,
|
|
106
|
+
});
|
|
107
|
+
const releaseInstructions = instructions.release;
|
|
108
|
+
if (releaseInstructions !== undefined)
|
|
109
|
+
yield* Effect.addFinalizer(() => Effect.sync(releaseInstructions));
|
|
110
|
+
const acquired = yield* acquireOwnedModel(options.model.acquire({
|
|
111
|
+
sessionId: prepared.command.sessionId,
|
|
112
|
+
preference: prepared.modelPreference,
|
|
113
|
+
}));
|
|
114
|
+
const tools = yield* acquiredTools(options, prepared.command.sessionId, prepared.command.entry.workId, prepared.command.entry.meta, instructions, extensionModules, extensionFileTools);
|
|
115
|
+
const accepted = Object.freeze({
|
|
116
|
+
sessionId: prepared.command.sessionId,
|
|
117
|
+
requestId: prepared.command.entry.requestId,
|
|
118
|
+
commandId: prepared.command.entry.workId,
|
|
119
|
+
});
|
|
120
|
+
const recorder = yield* openMuleRecorder(options.store, accepted, executionContract);
|
|
121
|
+
const controlMessages = (delivery) => takeSessionControls(options.store, {
|
|
122
|
+
sessionId: accepted.sessionId,
|
|
123
|
+
workId: accepted.commandId,
|
|
124
|
+
delivery,
|
|
125
|
+
}).pipe(Effect.map((controls) => controls.map(({ message }) => message)), Effect.mapError((error) => ({
|
|
126
|
+
_tag: "MuleError",
|
|
127
|
+
// Control failures are terminal-worthy; listener failures
|
|
128
|
+
// suppress the terminal notification because the listener
|
|
129
|
+
// cannot store it.
|
|
130
|
+
phase: "control",
|
|
131
|
+
message: error instanceof ShiitakeError
|
|
132
|
+
? error.message
|
|
133
|
+
: "Session control delivery failed",
|
|
134
|
+
})));
|
|
135
|
+
let resume = prepared.resume;
|
|
136
|
+
const settings = openCompactionSettings(options.compaction)(acquired.model);
|
|
137
|
+
return yield* runWithCompaction({
|
|
138
|
+
store: options.store,
|
|
139
|
+
sessionId: prepared.command.sessionId,
|
|
140
|
+
workId: prepared.command.entry.workId,
|
|
141
|
+
model: acquired.model,
|
|
142
|
+
stream: acquired.stream,
|
|
143
|
+
handoff: compactionHandoff,
|
|
144
|
+
settings,
|
|
145
|
+
path: prepared.path,
|
|
146
|
+
messages: prepared.messages,
|
|
147
|
+
resuming: resume !== undefined,
|
|
148
|
+
run: (workingMessages, betweenTurns) => {
|
|
149
|
+
const continuation = resume;
|
|
150
|
+
resume = undefined;
|
|
151
|
+
const listener = (event) => recorder(event);
|
|
152
|
+
const input = {
|
|
153
|
+
model: acquired.model,
|
|
154
|
+
modelStreamIdleTimeoutMs: options.modelStreamIdleTimeoutMs ?? piHttpIdleTimeoutMs,
|
|
155
|
+
retry: options.retry ?? piCodingAgentRetryDefaults,
|
|
156
|
+
retryAfterMs: acquired.retryAfterMs,
|
|
157
|
+
systemPrompt: instructions.text,
|
|
158
|
+
prepareSystemPrompt: () => (promptSections === undefined
|
|
159
|
+
? Effect.succeed([])
|
|
160
|
+
: promptSections()).pipe(Effect.map((sections) => instructions.text +
|
|
161
|
+
sections
|
|
162
|
+
.map(({ name, text }) => `\n\n[extension:${name}]\n${text}`)
|
|
163
|
+
.join("")), Effect.mapError((cause) => ({
|
|
164
|
+
_tag: "MuleError",
|
|
165
|
+
phase: "model",
|
|
166
|
+
message: "Extension prompt is unavailable",
|
|
167
|
+
cause,
|
|
168
|
+
}))),
|
|
169
|
+
messages: workingMessages,
|
|
170
|
+
resume: continuation,
|
|
171
|
+
tools,
|
|
172
|
+
getSteeringMessages: () => controlMessages("steer"),
|
|
173
|
+
prepareNextTurn: (turn) => betweenTurns(turn.context, turn.newMessages.length).pipe(Effect.mapError((error) => ({
|
|
174
|
+
_tag: "MuleError",
|
|
175
|
+
phase: "control",
|
|
176
|
+
message: error.message,
|
|
177
|
+
}))),
|
|
178
|
+
onRetry: (event) => recordLiveRetryEvent(options.store, accepted, toLiveRetryEvent(event), event.turn),
|
|
179
|
+
};
|
|
180
|
+
return muleEffect
|
|
181
|
+
.run(acquired.stream === undefined
|
|
182
|
+
? input
|
|
183
|
+
: { ...input, stream: acquired.stream }, listener)
|
|
184
|
+
.pipe(Effect.mapError((error) => runtimeFailure(error.phase === "model"
|
|
185
|
+
? error.message
|
|
186
|
+
: "Mule rejected the durable command run")));
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
}));
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Run one accepted compaction request: the run's model acquisition and the
|
|
193
|
+
* Session's one compaction commit, with no model turn and no tools.
|
|
194
|
+
*/
|
|
195
|
+
export function runAcceptedCompaction(options, prepared, compactionHandoff) {
|
|
196
|
+
return Effect.scoped(Effect.gen(function* () {
|
|
197
|
+
const acquired = yield* acquireOwnedModel(options.model.acquire({
|
|
198
|
+
sessionId: prepared.command.sessionId,
|
|
199
|
+
preference: prepared.modelPreference,
|
|
200
|
+
}));
|
|
201
|
+
const entry = prepared.command.entry;
|
|
202
|
+
return yield* compactSession({
|
|
203
|
+
store: options.store,
|
|
204
|
+
sessionId: prepared.command.sessionId,
|
|
205
|
+
workId: entry.workId,
|
|
206
|
+
model: acquired.model,
|
|
207
|
+
stream: acquired.stream,
|
|
208
|
+
handoff: compactionHandoff,
|
|
209
|
+
settings: openCompactionSettings(options.compaction)(acquired.model),
|
|
210
|
+
path: prepared.path,
|
|
211
|
+
instructions: isCompactCommand(entry) ? entry.instructions : undefined,
|
|
212
|
+
});
|
|
213
|
+
}));
|
|
214
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type BirdDogEffect } from "@fungi.computer/bird-dog/effect";
|
|
2
|
+
import { type WatchdogQueueHead } from "@fungi.computer/watchdog/effect";
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
import type { AgentRunningWork } from "../client.js";
|
|
5
|
+
import { ShiitakeError } from "../errors.js";
|
|
6
|
+
import { type AdmissionDoor } from "./admission-door.js";
|
|
7
|
+
import type { ShiitakeEffectOptions } from "./effect-contracts.js";
|
|
8
|
+
import type { AdmittedExtensionV0 } from "./extension-roster.js";
|
|
9
|
+
import { type ExtensionRuntime } from "./extension-runtime.js";
|
|
10
|
+
import { type RecordAcceptedCancellation, type StorageAdmission } from "./woodstock/index.js";
|
|
11
|
+
/** Bound lifecycle and owner-transaction doors used by the public coordinator. */
|
|
12
|
+
export type RuntimeCoordinator = Readonly<{
|
|
13
|
+
activeSessions: Effect.Effect<readonly string[]>;
|
|
14
|
+
runningWork: (jobId: string) => Effect.Effect<AgentRunningWork | null, ShiitakeError>;
|
|
15
|
+
assertOpen: Effect.Effect<void, ShiitakeError>;
|
|
16
|
+
/** Rostered v1 extensions bound to core's doors and tables. */
|
|
17
|
+
extensions: ExtensionRuntime;
|
|
18
|
+
beginClose: Effect.Effect<void>;
|
|
19
|
+
/** The one accepted-command door every prompt producer admits through. */
|
|
20
|
+
door: AdmissionDoor;
|
|
21
|
+
/** Storage-pressure check for new Sessions and new prompts. */
|
|
22
|
+
storage: StorageAdmission;
|
|
23
|
+
recordAcceptedCancellation: RecordAcceptedCancellation;
|
|
24
|
+
assertIdleSession: (queue: string) => void;
|
|
25
|
+
signalCancellation: (jobId: string, cancellationId: string) => Effect.Effect<void, ShiitakeError>;
|
|
26
|
+
commit: <A>(mutation: Effect.Effect<A, ShiitakeError>) => Effect.Effect<A, ShiitakeError>;
|
|
27
|
+
requestWake: Effect.Effect<void, ShiitakeError>;
|
|
28
|
+
readQueueHead: (queue: string) => Effect.Effect<WatchdogQueueHead, ShiitakeError>;
|
|
29
|
+
/** Transactional queue-head projection used by owner-atomic cancellation. */
|
|
30
|
+
readQueueHeadTransactional: (queue: string) => WatchdogQueueHead;
|
|
31
|
+
close: Effect.Effect<void, ShiitakeError>;
|
|
32
|
+
}>;
|
|
33
|
+
/** Construct Watchdog once and bind its deep tick to the host wake door. */
|
|
34
|
+
export declare function openRuntimeCoordinator(options: ShiitakeEffectOptions, coreDigest: string, admitted: readonly AdmittedExtensionV0[], mailRoom?: BirdDogEffect): Effect.Effect<RuntimeCoordinator, ShiitakeError>;
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { agentJobFromInput, } from "@fungi.computer/bird-dog/effect";
|
|
2
|
+
import { WatchdogError } from "@fungi.computer/watchdog";
|
|
3
|
+
import { makeRuntime as makeWatchdogRuntime, } from "@fungi.computer/watchdog/effect";
|
|
4
|
+
import * as Effect from "effect/Effect";
|
|
5
|
+
import * as Latch from "effect/Latch";
|
|
6
|
+
import { publishOwnerObservation } from "./observation-kernel.js";
|
|
7
|
+
import { ShiitakeError } from "../errors.js";
|
|
8
|
+
import { agentTurnRecovery, executeAcceptedRun, } from "./accepted-run-executor.js";
|
|
9
|
+
import { openAdmissionDoor } from "./admission-door.js";
|
|
10
|
+
import { openCompactionSettings } from "./compaction/settings.js";
|
|
11
|
+
import { coreConsumer, openExtensionRuntime, } from "./extension-runtime.js";
|
|
12
|
+
import { reconcileRuntime } from "./runtime-reconciliation.js";
|
|
13
|
+
import { advanceCursor, prepareExtensionStore, projectSessionRun, openStorageAdmission, readCursor, } from "./woodstock/index.js";
|
|
14
|
+
function runtimeFailure(message) {
|
|
15
|
+
return new ShiitakeError("runtime", "rejected", message);
|
|
16
|
+
}
|
|
17
|
+
/** Construct Watchdog once and bind its deep tick to the host wake door. */
|
|
18
|
+
export function openRuntimeCoordinator(options, coreDigest, admitted, mailRoom) {
|
|
19
|
+
return Effect.gen(function* () {
|
|
20
|
+
if (options.hostJobs?.lane === "default")
|
|
21
|
+
return yield* Effect.fail(runtimeFailure("Host lane conflicts with Agent work"));
|
|
22
|
+
let lifecycleState = "open";
|
|
23
|
+
let retainedFailure;
|
|
24
|
+
let retainedBeforeClose;
|
|
25
|
+
const closing = Latch.makeUnsafe(false);
|
|
26
|
+
const retainFailure = (failure) => Effect.sync(() => {
|
|
27
|
+
if (lifecycleState === "open")
|
|
28
|
+
retainedFailure ??= failure;
|
|
29
|
+
});
|
|
30
|
+
const beginClose = Effect.suspend(() => {
|
|
31
|
+
if (lifecycleState !== "open")
|
|
32
|
+
return Effect.void;
|
|
33
|
+
lifecycleState = "closing";
|
|
34
|
+
retainedBeforeClose = retainedFailure;
|
|
35
|
+
return closing.open;
|
|
36
|
+
}).pipe(Effect.uninterruptible);
|
|
37
|
+
const boundRuntime = {};
|
|
38
|
+
const extensionsRef = { current: undefined };
|
|
39
|
+
const requestBoundWake = Effect.suspend(() => boundRuntime.requestWake ??
|
|
40
|
+
Effect.fail(runtimeFailure("Runtime host is unavailable")));
|
|
41
|
+
const hostExecutor = { current: undefined };
|
|
42
|
+
const activeSessions = new Set();
|
|
43
|
+
const runningSlots = new Map();
|
|
44
|
+
const watchdogOptions = {
|
|
45
|
+
owner: options.store,
|
|
46
|
+
// Each Watchdog slot serializes ticks and inspects only its partition. A claim
|
|
47
|
+
// found on a new owner has no surviving execution (including DO eviction).
|
|
48
|
+
// Hosts must keep one runtime per SQLite owner, never two live coordinators.
|
|
49
|
+
isAlive: () => Effect.succeed(false),
|
|
50
|
+
executor: {
|
|
51
|
+
execute: (job) => Effect.acquireUseRelease(Effect.sync(() => {
|
|
52
|
+
const runningWork = job.lane === options.hostJobs?.lane
|
|
53
|
+
? { kind: "host", lane: job.lane, jobId: job.jobId }
|
|
54
|
+
: {
|
|
55
|
+
kind: "session",
|
|
56
|
+
sessionId: job.queue,
|
|
57
|
+
commandId: job.jobId,
|
|
58
|
+
};
|
|
59
|
+
runningSlots.set(runningWork.kind === "host" ? "host" : "serial", runningWork);
|
|
60
|
+
if (runningWork.kind === "session")
|
|
61
|
+
activeSessions.add(job.queue);
|
|
62
|
+
// A different Session's wait changes when this executor changes.
|
|
63
|
+
publishOwnerObservation(options.store, null);
|
|
64
|
+
}), () => job.lane === options.hostJobs?.lane
|
|
65
|
+
? (hostExecutor.current?.execute(job) ??
|
|
66
|
+
Effect.fail(new WatchdogError("execution_failed")))
|
|
67
|
+
: executeAcceptedRun(options, coreDigest, job, extensionsRef.current), () => Effect.sync(() => {
|
|
68
|
+
if (job.lane !== options.hostJobs?.lane)
|
|
69
|
+
activeSessions.delete(job.queue);
|
|
70
|
+
runningSlots.delete(job.lane === options.hostJobs?.lane ? "host" : "serial");
|
|
71
|
+
publishOwnerObservation(options.store, null);
|
|
72
|
+
})),
|
|
73
|
+
},
|
|
74
|
+
wake: {
|
|
75
|
+
recompute: () => requestBoundWake.pipe(Effect.mapError(() => new WatchdogError("wake_unavailable"))),
|
|
76
|
+
},
|
|
77
|
+
// Inbox rows change in the same transaction as the queue they project.
|
|
78
|
+
project: (transition) => {
|
|
79
|
+
projectSessionRun(options.store, transition, Date.now());
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
const openWatchdog = options.hostJobs === undefined
|
|
83
|
+
? makeWatchdogRuntime(watchdogOptions)
|
|
84
|
+
: makeWatchdogRuntime({
|
|
85
|
+
...watchdogOptions,
|
|
86
|
+
concurrentLane: options.hostJobs.lane,
|
|
87
|
+
}).pipe(Effect.tap((runtime) => Effect.sync(() => {
|
|
88
|
+
hostExecutor.current = options.hostJobs?.open(runtime);
|
|
89
|
+
})));
|
|
90
|
+
const watchdog = yield* openWatchdog.pipe(Effect.mapError(() => runtimeFailure("Watchdog is unavailable")));
|
|
91
|
+
const contract = () => ({
|
|
92
|
+
core: coreDigest,
|
|
93
|
+
extensions: [...(extensionsRef.current?.runParticipants() ?? [])],
|
|
94
|
+
});
|
|
95
|
+
// Refuse invalid compaction budgets when the owner opens, not mid-run.
|
|
96
|
+
yield* Effect.try({
|
|
97
|
+
try: () => openCompactionSettings(options.compaction),
|
|
98
|
+
catch: (error) => error instanceof ShiitakeError
|
|
99
|
+
? error
|
|
100
|
+
: runtimeFailure("Compaction settings are unavailable"),
|
|
101
|
+
});
|
|
102
|
+
const storage = yield* Effect.try({
|
|
103
|
+
try: () => openStorageAdmission(options.storageBudget),
|
|
104
|
+
catch: (error) => error instanceof ShiitakeError
|
|
105
|
+
? error
|
|
106
|
+
: runtimeFailure("Storage budget is unavailable"),
|
|
107
|
+
});
|
|
108
|
+
const door = openAdmissionDoor({
|
|
109
|
+
owner: options.store,
|
|
110
|
+
admitNew: storage.admitNew,
|
|
111
|
+
hostMeta: options.meta,
|
|
112
|
+
contract,
|
|
113
|
+
enqueue: (work, mail) => {
|
|
114
|
+
const input = { work: { ...work, recovery: agentTurnRecovery } };
|
|
115
|
+
watchdog.transactional.enqueueAcceptedJob(agentJobFromInput(mail === undefined ? input : { ...input, mail }));
|
|
116
|
+
},
|
|
117
|
+
readQueueHead: (queue) => watchdog.transactional.readQueueHead(queue),
|
|
118
|
+
commit: (mutation) => Effect.suspend(() => boundRuntime.commit === undefined
|
|
119
|
+
? Effect.fail(runtimeFailure("Runtime host is unavailable"))
|
|
120
|
+
: boundRuntime.commit(mutation)),
|
|
121
|
+
requestWake: requestBoundWake,
|
|
122
|
+
});
|
|
123
|
+
yield* Effect.try({
|
|
124
|
+
try: () => {
|
|
125
|
+
prepareExtensionStore(options.store);
|
|
126
|
+
if (readCursor(options.store, coreConsumer) === undefined)
|
|
127
|
+
options.store.transactionSync(() => {
|
|
128
|
+
advanceCursor(options.store, coreConsumer, 0);
|
|
129
|
+
});
|
|
130
|
+
},
|
|
131
|
+
catch: () => new ShiitakeError("store", "unavailable", "Extension store is unavailable"),
|
|
132
|
+
});
|
|
133
|
+
const extensions = yield* openExtensionRuntime({
|
|
134
|
+
owner: options.store,
|
|
135
|
+
readQueueHead: (sessionId) => watchdog.transactional.readQueueHead(sessionId),
|
|
136
|
+
door,
|
|
137
|
+
admitted,
|
|
138
|
+
requestWake: requestBoundWake,
|
|
139
|
+
});
|
|
140
|
+
extensionsRef.current = extensions;
|
|
141
|
+
const reconcile = reconcileRuntime({
|
|
142
|
+
watchdog,
|
|
143
|
+
store: options.store,
|
|
144
|
+
extensions,
|
|
145
|
+
requestWake: requestBoundWake,
|
|
146
|
+
mailRoom,
|
|
147
|
+
}).pipe(Effect.tapError(retainFailure));
|
|
148
|
+
const pendingWork = Effect.gen(function* () {
|
|
149
|
+
const watchdogPending = yield* watchdog.hasPendingWork;
|
|
150
|
+
const now = Date.now();
|
|
151
|
+
const cursors = yield* Effect.try({
|
|
152
|
+
try: () => [
|
|
153
|
+
readCursor(options.store, coreConsumer) ?? 0,
|
|
154
|
+
...extensions.settledConsumers().map(({ cursor }) => cursor),
|
|
155
|
+
],
|
|
156
|
+
catch: () => runtimeFailure("Settled cursor is unavailable"),
|
|
157
|
+
});
|
|
158
|
+
const settledLag = (yield* watchdog.readSettled(1, Math.min(...cursors))).length > 0;
|
|
159
|
+
const timers = yield* Effect.try({
|
|
160
|
+
try: () => extensions.timerWake(now),
|
|
161
|
+
catch: () => runtimeFailure("Extension timers are unavailable"),
|
|
162
|
+
});
|
|
163
|
+
return {
|
|
164
|
+
pending: watchdogPending || settledLag || timers.due,
|
|
165
|
+
nextWakeAt: timers.nextWakeAt,
|
|
166
|
+
};
|
|
167
|
+
});
|
|
168
|
+
const bound = yield* options.runtime
|
|
169
|
+
.bind({
|
|
170
|
+
reconcile,
|
|
171
|
+
pendingWork: pendingWork.pipe(Effect.mapError(() => runtimeFailure("Pending work is unavailable"))),
|
|
172
|
+
})
|
|
173
|
+
.pipe(Effect.onError(() => extensions.close.pipe(Effect.orDie)));
|
|
174
|
+
const requestWake = Effect.suspend(() => lifecycleState === "open"
|
|
175
|
+
? Effect.raceFirst(bound.requestWake.pipe(Effect.catch(retainFailure)), closing.await)
|
|
176
|
+
: Effect.void);
|
|
177
|
+
boundRuntime.requestWake = requestWake;
|
|
178
|
+
boundRuntime.commit = bound.commit;
|
|
179
|
+
// Every host must get one post-bind tick to recover durable work after a cold reopen.
|
|
180
|
+
yield* bound.requestWake.pipe(Effect.onError(() => Effect.all([bound.close, extensions.close]).pipe(Effect.orDie)));
|
|
181
|
+
return Object.freeze({
|
|
182
|
+
activeSessions: Effect.sync(() => [...activeSessions]),
|
|
183
|
+
runningWork: (jobId) => watchdog.read(jobId).pipe(Effect.map((result) => result.status === "found"
|
|
184
|
+
? (runningSlots.get(result.job.lane === options.hostJobs?.lane
|
|
185
|
+
? "host"
|
|
186
|
+
: "serial") ?? null)
|
|
187
|
+
: null), Effect.mapError((cause) => new ShiitakeError("runtime", "rejected", "Running work is unavailable", undefined, cause))),
|
|
188
|
+
extensions,
|
|
189
|
+
commit: bound.commit,
|
|
190
|
+
assertOpen: Effect.suspend(() => lifecycleState === "open"
|
|
191
|
+
? Effect.void
|
|
192
|
+
: Effect.fail(new ShiitakeError("runtime", "closed", "Shiitake is closed"))),
|
|
193
|
+
beginClose,
|
|
194
|
+
door,
|
|
195
|
+
storage,
|
|
196
|
+
recordAcceptedCancellation: ({ jobId, cancellationId, onlyIfPending }) => watchdog.transactional.recordCancellationIntent({
|
|
197
|
+
jobId,
|
|
198
|
+
cancellationId,
|
|
199
|
+
onlyIfQueued: onlyIfPending,
|
|
200
|
+
}),
|
|
201
|
+
assertIdleSession: (queue) => {
|
|
202
|
+
try {
|
|
203
|
+
const head = watchdog.transactional.readQueueHead(queue);
|
|
204
|
+
if (head.state !== "idle")
|
|
205
|
+
throw new ShiitakeError("runtime", "conflict", "Session branch or fork requires an idle Session");
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
if (error instanceof ShiitakeError)
|
|
209
|
+
throw error;
|
|
210
|
+
throw runtimeFailure("Session status is unavailable");
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
signalCancellation: (jobId, cancellationId) => watchdog.cancel({ jobId, cancellationId }).pipe(Effect.flatMap((result) => result.status === "recorded" || result.status === "replayed"
|
|
214
|
+
? Effect.void
|
|
215
|
+
: Effect.fail(runtimeFailure("Cancellation is unavailable"))), Effect.mapError(() => runtimeFailure("Cancellation is unavailable"))),
|
|
216
|
+
readQueueHead: (queue) => watchdog
|
|
217
|
+
.readQueueHead(queue)
|
|
218
|
+
.pipe(Effect.mapError(() => runtimeFailure("Session status is unavailable"))),
|
|
219
|
+
readQueueHeadTransactional: (queue) => watchdog.transactional.readQueueHead(queue),
|
|
220
|
+
requestWake,
|
|
221
|
+
close: beginClose.pipe(Effect.andThen(Effect.matchCauseEffect(bound.close, {
|
|
222
|
+
onFailure: (cause) => extensions.close.pipe(Effect.andThen(retainedBeforeClose === undefined
|
|
223
|
+
? Effect.failCause(cause)
|
|
224
|
+
: Effect.fail(retainedBeforeClose))),
|
|
225
|
+
onSuccess: () => extensions.close.pipe(Effect.andThen(retainedBeforeClose === undefined
|
|
226
|
+
? Effect.void
|
|
227
|
+
: Effect.fail(retainedBeforeClose))),
|
|
228
|
+
})), Effect.ensuring(Effect.sync(() => {
|
|
229
|
+
lifecycleState = "closed";
|
|
230
|
+
}))),
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { CommandSettlement } from "./contracts.js";
|
|
2
|
+
import type { Json } from "../extensions.js";
|
|
3
|
+
import type { BirdDogMail } from "@fungi.computer/bird-dog";
|
|
4
|
+
import type { VerifiedAgentReply } from "./woodstock/index.js";
|
|
5
|
+
import type { BirdDogEffect } from "@fungi.computer/bird-dog/effect";
|
|
6
|
+
import * as Effect from "effect/Effect";
|
|
7
|
+
import type { Accepted } from "../client.js";
|
|
8
|
+
import { ShiitakeError } from "../errors.js";
|
|
9
|
+
import type { ShiitakeEffectOptions, ShiitakeEffectOwner } from "./effect-contracts.js";
|
|
10
|
+
/** Private host composition with the addressed-mail ingress kept off Session. */
|
|
11
|
+
export type RuntimeCore = Readonly<{
|
|
12
|
+
agent: ShiitakeEffectOwner;
|
|
13
|
+
readSettlement: (sessionId: string, commandId: string) => Effect.Effect<CommandSettlement | undefined, ShiitakeError>;
|
|
14
|
+
/** Owner-local execution projection; never served by the remote client. */
|
|
15
|
+
activeSessions: Effect.Effect<readonly string[]>;
|
|
16
|
+
acceptMail: (mail: BirdDogMail, meta?: Json, reply?: VerifiedAgentReply) => Effect.Effect<Accepted, ShiitakeError>;
|
|
17
|
+
}>;
|
|
18
|
+
/** Open Runtime Core, optionally joining its owner-local BirdDog mail room. */
|
|
19
|
+
export declare function openRuntimeCore(options: ShiitakeEffectOptions, mailRoom?: BirdDogEffect): Effect.Effect<RuntimeCore, ShiitakeError>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import { ShiitakeError } from "../errors.js";
|
|
3
|
+
import { composeAgentClose } from "./agent-close.js";
|
|
4
|
+
import { acceptAddressedMail, openCoordinator } from "./coordinator.js";
|
|
5
|
+
import { admitExtensionRosterV0, coreProtocolDigest, } from "./extension-roster.js";
|
|
6
|
+
import { openRuntimeCoordinator } from "./runtime-coordinator.js";
|
|
7
|
+
import { readCommandSettlement, prepareWoodstockSchema, } from "./woodstock/index.js";
|
|
8
|
+
/** Open Runtime Core, optionally joining its owner-local BirdDog mail room. */
|
|
9
|
+
export function openRuntimeCore(options, mailRoom) {
|
|
10
|
+
return Effect.gen(function* () {
|
|
11
|
+
const admitted = yield* admitExtensionRosterV0(options.extensions);
|
|
12
|
+
const coreDigest = yield* coreProtocolDigest([]);
|
|
13
|
+
yield* prepareWoodstockSchema(options.store);
|
|
14
|
+
const runtime = yield* openRuntimeCoordinator(options, coreDigest, admitted, mailRoom);
|
|
15
|
+
const coordinator = openCoordinator(options, runtime);
|
|
16
|
+
const agent = Object.freeze({
|
|
17
|
+
session: coordinator.session,
|
|
18
|
+
createSession: coordinator.createSession,
|
|
19
|
+
deleteSession: coordinator.deleteSession,
|
|
20
|
+
listSessions: coordinator.listSessions,
|
|
21
|
+
extensions: coordinator.extensions,
|
|
22
|
+
close: composeAgentClose(coordinator.close, runtime.close),
|
|
23
|
+
});
|
|
24
|
+
const owner = Object.freeze({
|
|
25
|
+
...agent,
|
|
26
|
+
grants: coordinator.grants,
|
|
27
|
+
withMeta: (meta) => Object.freeze({
|
|
28
|
+
...agent,
|
|
29
|
+
session: coordinator.withMeta(meta).session,
|
|
30
|
+
}),
|
|
31
|
+
});
|
|
32
|
+
return Object.freeze({
|
|
33
|
+
agent: owner,
|
|
34
|
+
readSettlement: (sessionId, commandId) => runtime.assertOpen.pipe(Effect.andThen(readCommandSettlement(options.store, sessionId, commandId))),
|
|
35
|
+
activeSessions: runtime.activeSessions,
|
|
36
|
+
acceptMail: (mail, meta, reply) => acceptAddressedMail(runtime, mail, meta, reply),
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type BirdDogEffect } from "@fungi.computer/bird-dog/effect";
|
|
2
|
+
import type { WatchdogRuntimeEffect } from "@fungi.computer/watchdog/effect";
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
import { ShiitakeError } from "../errors.js";
|
|
5
|
+
import type { SqliteOwner } from "./contracts.js";
|
|
6
|
+
import { type ExtensionRuntime } from "./extension-runtime.js";
|
|
7
|
+
/**
|
|
8
|
+
* One reconcile tick. Each step is its own Exit boundary: a failed extension
|
|
9
|
+
* stops only itself, and mail always drains. Only a core failure fails the
|
|
10
|
+
* tick, so the host backs off only for core.
|
|
11
|
+
*/
|
|
12
|
+
export declare function reconcileRuntime(input: Readonly<{
|
|
13
|
+
watchdog: WatchdogRuntimeEffect;
|
|
14
|
+
store: SqliteOwner;
|
|
15
|
+
mailRoom: BirdDogEffect | undefined;
|
|
16
|
+
extensions: ExtensionRuntime;
|
|
17
|
+
requestWake: Effect.Effect<void, ShiitakeError>;
|
|
18
|
+
}>): Effect.Effect<void, ShiitakeError>;
|