@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.
Files changed (164) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +454 -2
  3. package/dist/agar-extension.d.ts +9 -0
  4. package/dist/agar-extension.js +38 -0
  5. package/dist/agent-control.d.ts +77 -0
  6. package/dist/agent-control.js +434 -0
  7. package/dist/client-entry.d.ts +211 -0
  8. package/dist/client-entry.js +101 -0
  9. package/dist/client-wire.d.ts +199 -0
  10. package/dist/client-wire.js +271 -0
  11. package/dist/client.d.ts +439 -0
  12. package/dist/client.js +4 -0
  13. package/dist/cloudflare.d.ts +220 -0
  14. package/dist/cloudflare.js +531 -0
  15. package/dist/effect.d.ts +42 -0
  16. package/dist/effect.js +20 -0
  17. package/dist/errors.d.ts +50 -0
  18. package/dist/errors.js +81 -0
  19. package/dist/extension-client.d.ts +38 -0
  20. package/dist/extension-client.js +56 -0
  21. package/dist/extension-contract.d.ts +73 -0
  22. package/dist/extension-contract.js +11 -0
  23. package/dist/extensions.d.ts +589 -0
  24. package/dist/extensions.js +263 -0
  25. package/dist/fleet.d.ts +313 -0
  26. package/dist/fleet.js +540 -0
  27. package/dist/hosted.d.ts +133 -0
  28. package/dist/hosted.js +265 -0
  29. package/dist/http-routes.d.ts +772 -0
  30. package/dist/http-routes.js +242 -0
  31. package/dist/http.d.ts +1 -0
  32. package/dist/http.js +1 -0
  33. package/dist/index.d.ts +35 -0
  34. package/dist/index.js +13 -0
  35. package/dist/internal/accepted-run-executor.d.ts +23 -0
  36. package/dist/internal/accepted-run-executor.js +129 -0
  37. package/dist/internal/admission-door.d.ts +75 -0
  38. package/dist/internal/admission-door.js +105 -0
  39. package/dist/internal/agent-close.d.ts +4 -0
  40. package/dist/internal/agent-close.js +42 -0
  41. package/dist/internal/browser-sessions.d.ts +31 -0
  42. package/dist/internal/browser-sessions.js +173 -0
  43. package/dist/internal/client-boundary.d.ts +262 -0
  44. package/dist/internal/client-boundary.js +202 -0
  45. package/dist/internal/compaction/compaction.d.ts +54 -0
  46. package/dist/internal/compaction/compaction.js +291 -0
  47. package/dist/internal/compaction/session-compaction.d.ts +69 -0
  48. package/dist/internal/compaction/session-compaction.js +330 -0
  49. package/dist/internal/compaction/settings.d.ts +11 -0
  50. package/dist/internal/compaction/settings.js +40 -0
  51. package/dist/internal/compaction/utils.d.ts +45 -0
  52. package/dist/internal/compaction/utils.js +319 -0
  53. package/dist/internal/contracts.d.ts +235 -0
  54. package/dist/internal/contracts.js +1 -0
  55. package/dist/internal/coordinator.d.ts +18 -0
  56. package/dist/internal/coordinator.js +362 -0
  57. package/dist/internal/document-codec.d.ts +11 -0
  58. package/dist/internal/document-codec.js +39 -0
  59. package/dist/internal/effect-client.d.ts +13 -0
  60. package/dist/internal/effect-client.js +102 -0
  61. package/dist/internal/effect-contracts.d.ts +236 -0
  62. package/dist/internal/effect-contracts.js +2 -0
  63. package/dist/internal/extension-command-registry.d.ts +11 -0
  64. package/dist/internal/extension-command-registry.js +29 -0
  65. package/dist/internal/extension-command-tools.d.ts +17 -0
  66. package/dist/internal/extension-command-tools.js +50 -0
  67. package/dist/internal/extension-context.d.ts +25 -0
  68. package/dist/internal/extension-context.js +35 -0
  69. package/dist/internal/extension-definitions.d.ts +5 -0
  70. package/dist/internal/extension-definitions.js +6 -0
  71. package/dist/internal/extension-hook.d.ts +25 -0
  72. package/dist/internal/extension-hook.js +46 -0
  73. package/dist/internal/extension-roster.d.ts +23 -0
  74. package/dist/internal/extension-roster.js +228 -0
  75. package/dist/internal/extension-runtime.d.ts +63 -0
  76. package/dist/internal/extension-runtime.js +1004 -0
  77. package/dist/internal/failure.d.ts +2 -0
  78. package/dist/internal/failure.js +9 -0
  79. package/dist/internal/host-meta.d.ts +41 -0
  80. package/dist/internal/host-meta.js +39 -0
  81. package/dist/internal/json-value.d.ts +4 -0
  82. package/dist/internal/json-value.js +3 -0
  83. package/dist/internal/model-acquisition.d.ts +21 -0
  84. package/dist/internal/model-acquisition.js +98 -0
  85. package/dist/internal/model-preference.d.ts +21 -0
  86. package/dist/internal/model-preference.js +20 -0
  87. package/dist/internal/observation-kernel.d.ts +28 -0
  88. package/dist/internal/observation-kernel.js +169 -0
  89. package/dist/internal/promise-facade.d.ts +18 -0
  90. package/dist/internal/promise-facade.js +172 -0
  91. package/dist/internal/run-scope.d.ts +27 -0
  92. package/dist/internal/run-scope.js +214 -0
  93. package/dist/internal/runtime-coordinator.d.ts +34 -0
  94. package/dist/internal/runtime-coordinator.js +233 -0
  95. package/dist/internal/runtime-core.d.ts +19 -0
  96. package/dist/internal/runtime-core.js +39 -0
  97. package/dist/internal/runtime-reconciliation.d.ts +18 -0
  98. package/dist/internal/runtime-reconciliation.js +162 -0
  99. package/dist/internal/session-context.d.ts +11 -0
  100. package/dist/internal/session-context.js +67 -0
  101. package/dist/internal/session-status.d.ts +10 -0
  102. package/dist/internal/session-status.js +52 -0
  103. package/dist/internal/woodstock/agent-reply.d.ts +25 -0
  104. package/dist/internal/woodstock/agent-reply.js +67 -0
  105. package/dist/internal/woodstock/client-projection.d.ts +29 -0
  106. package/dist/internal/woodstock/client-projection.js +512 -0
  107. package/dist/internal/woodstock/compaction-activity.d.ts +17 -0
  108. package/dist/internal/woodstock/compaction-activity.js +32 -0
  109. package/dist/internal/woodstock/cursor.d.ts +9 -0
  110. package/dist/internal/woodstock/cursor.js +44 -0
  111. package/dist/internal/woodstock/extension-store.d.ts +72 -0
  112. package/dist/internal/woodstock/extension-store.js +262 -0
  113. package/dist/internal/woodstock/fleet-store.d.ts +36 -0
  114. package/dist/internal/woodstock/fleet-store.js +100 -0
  115. package/dist/internal/woodstock/identities.d.ts +6 -0
  116. package/dist/internal/woodstock/identities.js +11 -0
  117. package/dist/internal/woodstock/index.d.ts +170 -0
  118. package/dist/internal/woodstock/index.js +546 -0
  119. package/dist/internal/woodstock/progress.d.ts +32 -0
  120. package/dist/internal/woodstock/progress.js +89 -0
  121. package/dist/internal/woodstock/run-recovery.d.ts +8 -0
  122. package/dist/internal/woodstock/run-recovery.js +95 -0
  123. package/dist/internal/woodstock/session-attention.d.ts +16 -0
  124. package/dist/internal/woodstock/session-attention.js +88 -0
  125. package/dist/internal/woodstock/session-documents.d.ts +18 -0
  126. package/dist/internal/woodstock/session-documents.js +51 -0
  127. package/dist/internal/woodstock/session-entry.d.ts +478 -0
  128. package/dist/internal/woodstock/session-entry.js +484 -0
  129. package/dist/internal/woodstock/session-grants.d.ts +24 -0
  130. package/dist/internal/woodstock/session-grants.js +84 -0
  131. package/dist/internal/woodstock/session-inbox.d.ts +56 -0
  132. package/dist/internal/woodstock/session-inbox.js +307 -0
  133. package/dist/internal/woodstock/session-store.d.ts +432 -0
  134. package/dist/internal/woodstock/session-store.js +2006 -0
  135. package/dist/internal/woodstock/storage-admission.d.ts +14 -0
  136. package/dist/internal/woodstock/storage-admission.js +44 -0
  137. package/dist/model-preference.d.ts +20 -0
  138. package/dist/model-preference.js +30 -0
  139. package/dist/node.d.ts +64 -0
  140. package/dist/node.js +256 -0
  141. package/dist/remote-client.d.ts +11 -0
  142. package/dist/remote-client.js +298 -0
  143. package/dist/session-attention.d.ts +35 -0
  144. package/dist/session-attention.js +22 -0
  145. package/dist/session-documents.d.ts +28 -0
  146. package/dist/session-documents.js +1 -0
  147. package/dist/testing.d.ts +54 -0
  148. package/dist/testing.js +215 -0
  149. package/docs/api/README.md +26 -0
  150. package/docs/api/agar-extension.md +40 -0
  151. package/docs/api/agent-control.md +151 -0
  152. package/docs/api/client-entry.md +344 -0
  153. package/docs/api/client.md +1112 -0
  154. package/docs/api/cloudflare.md +500 -0
  155. package/docs/api/effect.md +884 -0
  156. package/docs/api/extensions.md +2003 -0
  157. package/docs/api/fleet.md +734 -0
  158. package/docs/api/hosted.md +382 -0
  159. package/docs/api/http.md +111 -0
  160. package/docs/api/index.md +1116 -0
  161. package/docs/api/model-preference.md +151 -0
  162. package/docs/api/node.md +142 -0
  163. package/docs/api/testing.md +250 -0
  164. 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>;