@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,2 @@
1
+ /** Retain the original failure without printing Session or provider data. */
2
+ export declare function reportCatchFailure(operation: string, cause: unknown): void;
@@ -0,0 +1,9 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as Redacted from "effect/Redacted";
3
+ /** Retain the original failure without printing Session or provider data. */
4
+ export function reportCatchFailure(operation, cause) {
5
+ Effect.runFork(Effect.logError("shiitake_operation_failed", Redacted.make(cause)).pipe(Effect.annotateLogs({
6
+ operation,
7
+ errorKind: cause instanceof Error ? "error" : "non-error",
8
+ })));
9
+ }
@@ -0,0 +1,41 @@
1
+ import * as Schema from "effect/Schema";
2
+ import type { Json } from "../extensions.js";
3
+ /**
4
+ * Host metadata as stored: any finite JSON value within the byte cap.
5
+ * Shiitake stores and returns it and never branches on it.
6
+ */
7
+ export declare const HostMetaSchema: Schema.Codec<Json>;
8
+ /**
9
+ * Any Standard Schema, such as a Zod 4 schema, whose `validate` settles
10
+ * synchronously. Shiitake only asks whether a value is accepted.
11
+ */
12
+ export type StandardHostMetaSchema = Readonly<{
13
+ "~standard": Readonly<{
14
+ /** Accept or refuse one value; must settle synchronously for Shiitake. */
15
+ validate: (value: Json) => StandardHostMetaResult | PromiseLike<StandardHostMetaResult>;
16
+ }>;
17
+ }>;
18
+ /** One Standard Schema validation outcome; only acceptance matters here. */
19
+ export type StandardHostMetaResult = Readonly<{
20
+ issues?: undefined;
21
+ }> | Readonly<{
22
+ issues: readonly Readonly<{
23
+ message: string;
24
+ }>[];
25
+ }>;
26
+ /**
27
+ * The one schema a host supplies at Agent creation for the metadata it
28
+ * attaches to prompts and operator lines: an Effect schema whose encoded side
29
+ * is JSON, or a synchronous Standard Schema. Shiitake checks each value
30
+ * against it before writing; it never reads the decoded form.
31
+ */
32
+ export type HostMetaOptions = Readonly<{
33
+ schema: Schema.Codec<unknown, Json> | StandardHostMetaSchema;
34
+ }>;
35
+ /**
36
+ * Check one host-attached value: finite JSON within the cap, and accepted by
37
+ * the host's schema when it supplied one. Fails as an invalid contract.
38
+ */
39
+ export declare function checkHostMeta(options: HostMetaOptions | undefined, meta: Json): Json;
40
+ /** Whether two stored metadata values are the same canonical JSON. */
41
+ export declare function sameHostMeta(left: Json | undefined, right: Json | undefined): boolean;
@@ -0,0 +1,39 @@
1
+ import * as Predicate from "effect/Predicate";
2
+ import * as Result from "effect/Result";
3
+ import * as Schema from "effect/Schema";
4
+ import { ShiitakeError } from "../errors.js";
5
+ import { JsonValueSchema } from "./json-value.js";
6
+ /** The most encoded JSON one message's host metadata may occupy. */
7
+ const MAX_HOST_META_BYTES = 4096;
8
+ const encoder = new TextEncoder();
9
+ /**
10
+ * Host metadata as stored: any finite JSON value within the byte cap.
11
+ * Shiitake stores and returns it and never branches on it.
12
+ */
13
+ export const HostMetaSchema = JsonValueSchema.check(Schema.makeFilter((value) => encoder.encode(JSON.stringify(value)).byteLength <= MAX_HOST_META_BYTES));
14
+ const decodeStoredMeta = Schema.decodeUnknownResult(HostMetaSchema);
15
+ /** Whether the host's schema accepts one stored value. */
16
+ function hostAccepts(schema, meta) {
17
+ if (Schema.isSchema(schema))
18
+ return Result.isSuccess(Schema.decodeUnknownResult(schema)(meta));
19
+ const result = schema["~standard"].validate(meta);
20
+ if (Predicate.hasProperty(result, "then"))
21
+ throw new ShiitakeError("contract", "invalid", "Host metadata schema must validate synchronously");
22
+ return result.issues === undefined;
23
+ }
24
+ /**
25
+ * Check one host-attached value: finite JSON within the cap, and accepted by
26
+ * the host's schema when it supplied one. Fails as an invalid contract.
27
+ */
28
+ export function checkHostMeta(options, meta) {
29
+ const stored = decodeStoredMeta(meta);
30
+ if (Result.isFailure(stored))
31
+ throw new ShiitakeError("contract", "invalid", "Host metadata is not JSON within 4096 bytes");
32
+ if (options !== undefined && !hostAccepts(options.schema, stored.success))
33
+ throw new ShiitakeError("contract", "invalid", "Host metadata does not match the host's schema");
34
+ return stored.success;
35
+ }
36
+ /** Whether two stored metadata values are the same canonical JSON. */
37
+ export function sameHostMeta(left, right) {
38
+ return JSON.stringify(left) === JSON.stringify(right);
39
+ }
@@ -0,0 +1,4 @@
1
+ import * as Schema from "effect/Schema";
2
+ import type { Json } from "../extensions.js";
3
+ /** Any finite JSON value; an extension's own schema decodes it further. */
4
+ export declare const JsonValueSchema: Schema.Codec<Json>;
@@ -0,0 +1,3 @@
1
+ import * as Schema from "effect/Schema";
2
+ /** Any finite JSON value; an extension's own schema decodes it further. */
3
+ export const JsonValueSchema = Schema.Json;
@@ -0,0 +1,21 @@
1
+ import * as Effect from "effect/Effect";
2
+ import type * as Scope from "effect/Scope";
3
+ import type { ModelAcquisition, ModelSource } from "./contracts.js";
4
+ import type { ModelAcquisitionEffect, ModelSourceEffect } from "./effect-contracts.js";
5
+ /**
6
+ * Adapt a host Promise while keeping an interrupted late result owned.
7
+ *
8
+ * Effect's interruptible acquisition can stop waiting before a foreign
9
+ * Promise settles. The returned async cleanup therefore waits for that
10
+ * Promise and releases a result which was never transferred to the run
11
+ * scope. A successful result resumes the Effect only after the signal check,
12
+ * so the run scope's finalizer owns it exactly once.
13
+ */
14
+ export declare function acquireModelPromise(acquire: (signal: AbortSignal) => Promise<ModelAcquisition>): ReturnType<ModelSourceEffect["acquire"]>;
15
+ /**
16
+ * Transfer one interruptible acquisition into its run scope without an
17
+ * interruption gap between success and ownership registration.
18
+ */
19
+ export declare function acquireOwnedModel<E, R>(acquire: Effect.Effect<ModelAcquisitionEffect, E, R>): Effect.Effect<ModelAcquisitionEffect, E, R | Scope.Scope>;
20
+ /** Lift one host model source into the Effect runtime boundary. */
21
+ export declare function adaptModelSource(source: ModelSource): ModelSourceEffect;
@@ -0,0 +1,98 @@
1
+ import * as Effect from "effect/Effect";
2
+ import { ShiitakeError } from "../errors.js";
3
+ import { reportCatchFailure } from "./failure.js";
4
+ function acquisitionFailure(cause) {
5
+ if (cause instanceof ShiitakeError)
6
+ return cause;
7
+ return new ShiitakeError("model", "rejected", "Model acquisition failed", undefined, cause);
8
+ }
9
+ function releaseFailure(cause) {
10
+ return new ShiitakeError("model", "rejected", "Model release failed", undefined, cause);
11
+ }
12
+ function asEffectAcquisition(acquisition) {
13
+ const release = Effect.tryPromise({
14
+ try: () => acquisition.release(),
15
+ catch: releaseFailure,
16
+ });
17
+ const shared = { model: acquisition.model, release };
18
+ if (acquisition.stream === undefined)
19
+ return acquisition.retryAfterMs === undefined
20
+ ? Object.freeze(shared)
21
+ : Object.freeze({ ...shared, retryAfterMs: acquisition.retryAfterMs });
22
+ return acquisition.retryAfterMs === undefined
23
+ ? Object.freeze({ ...shared, stream: acquisition.stream })
24
+ : Object.freeze({
25
+ ...shared,
26
+ stream: acquisition.stream,
27
+ retryAfterMs: acquisition.retryAfterMs,
28
+ });
29
+ }
30
+ /**
31
+ * Adapt a host Promise while keeping an interrupted late result owned.
32
+ *
33
+ * Effect's interruptible acquisition can stop waiting before a foreign
34
+ * Promise settles. The returned async cleanup therefore waits for that
35
+ * Promise and releases a result which was never transferred to the run
36
+ * scope. A successful result resumes the Effect only after the signal check,
37
+ * so the run scope's finalizer owns it exactly once.
38
+ */
39
+ export function acquireModelPromise(acquire) {
40
+ return Effect.callback((resume, signal) => {
41
+ if (signal.aborted)
42
+ return;
43
+ let acquisition;
44
+ try {
45
+ acquisition = Promise.resolve(acquire(signal));
46
+ }
47
+ catch (cause) {
48
+ resume(Effect.fail(acquisitionFailure(cause)));
49
+ return;
50
+ }
51
+ const settled = acquisition.then((resource) => {
52
+ if (signal.aborted) {
53
+ // An interrupted foreign Promise may still produce a resource. Keep
54
+ // cancellation pending until that resource has been released.
55
+ return Promise.resolve(resource.release()).then(() => undefined);
56
+ }
57
+ resume(Effect.succeed(asEffectAcquisition(resource)));
58
+ return undefined;
59
+ }, (cause) => {
60
+ if (!signal.aborted)
61
+ resume(Effect.fail(acquisitionFailure(cause)));
62
+ else
63
+ reportCatchFailure("late-model-acquisition", cause);
64
+ });
65
+ // Effect runs this cleanup when its interruptible async wait is stopped.
66
+ // `settled` also observes late rejection, preventing an unhandled Promise.
67
+ return Effect.tryPromise({
68
+ try: () => settled,
69
+ catch: releaseFailure,
70
+ }).pipe(Effect.orDie);
71
+ });
72
+ }
73
+ /**
74
+ * Transfer one interruptible acquisition into its run scope without an
75
+ * interruption gap between success and ownership registration.
76
+ */
77
+ export function acquireOwnedModel(acquire) {
78
+ let owned;
79
+ const transfer = Effect.uninterruptibleMask((restore) => Effect.flatMap(restore(acquire), (acquisition) => Effect.sync(() => {
80
+ owned = acquisition;
81
+ return acquisition;
82
+ })));
83
+ return Effect.acquireRelease(transfer, () => owned === undefined ? Effect.void : owned.release.pipe(Effect.orDie), { interruptible: true });
84
+ }
85
+ /** Lift one host model source into the Effect runtime boundary. */
86
+ export function adaptModelSource(source) {
87
+ const acquire = ({ sessionId, preference }) => acquireModelPromise((signal) => source.acquire({ sessionId, preference, signal }));
88
+ const describe = source.describe;
89
+ return Object.freeze(describe === undefined
90
+ ? { acquire }
91
+ : {
92
+ acquire,
93
+ describe: ({ sessionId, preference, }) => Effect.tryPromise({
94
+ try: (signal) => describe({ sessionId, preference, signal }),
95
+ catch: acquisitionFailure,
96
+ }),
97
+ });
98
+ }
@@ -0,0 +1,21 @@
1
+ import * as Schema from "effect/Schema";
2
+ /** Secret-free request to the host's maintained model catalogue. */
3
+ export declare const ModelPreferenceSchema: Schema.Struct<{
4
+ readonly providerId: Schema.String;
5
+ readonly modelId: Schema.String;
6
+ readonly reasoning: Schema.optional<Schema.Literals<readonly ["minimal", "low", "medium", "high", "xhigh", "max"]>>;
7
+ }>;
8
+ /** Null at the Session boundary means inherit the Agent default. */
9
+ export type ModelPreference = Schema.Schema.Type<typeof ModelPreferenceSchema>;
10
+ /** Decode the bounded client preference; availability remains model-owner policy. */
11
+ export declare const parseModelPreference: (input: unknown, options?: import("effect/SchemaAST").ParseOptions) => {
12
+ readonly providerId: string;
13
+ readonly modelId: string;
14
+ readonly reasoning?: "minimal" | "low" | "medium" | "high" | "xhigh" | "max" | undefined;
15
+ } | null;
16
+ /** Decode the serialized preference with the same maintained schema. */
17
+ export declare const parseModelPreferenceJson: (input: unknown, options?: import("effect/SchemaAST").ParseOptions) => {
18
+ readonly providerId: string;
19
+ readonly modelId: string;
20
+ readonly reasoning?: "minimal" | "low" | "medium" | "high" | "xhigh" | "max" | undefined;
21
+ } | null;
@@ -0,0 +1,20 @@
1
+ import * as Schema from "effect/Schema";
2
+ const identifier = Schema.String.pipe(Schema.check(Schema.isMinLength(1)), Schema.check(Schema.isMaxLength(256)));
3
+ const reasoningLevels = [
4
+ "minimal",
5
+ "low",
6
+ "medium",
7
+ "high",
8
+ "xhigh",
9
+ "max",
10
+ ];
11
+ /** Secret-free request to the host's maintained model catalogue. */
12
+ export const ModelPreferenceSchema = Schema.Struct({
13
+ providerId: identifier,
14
+ modelId: identifier,
15
+ reasoning: Schema.optional(Schema.Literals(reasoningLevels)),
16
+ });
17
+ /** Decode the bounded client preference; availability remains model-owner policy. */
18
+ export const parseModelPreference = Schema.decodeUnknownSync(Schema.NullOr(ModelPreferenceSchema));
19
+ /** Decode the serialized preference with the same maintained schema. */
20
+ export const parseModelPreferenceJson = Schema.decodeUnknownSync(Schema.fromJsonString(Schema.NullOr(ModelPreferenceSchema)));
@@ -0,0 +1,28 @@
1
+ import * as Effect from "effect/Effect";
2
+ import { ShiitakeError } from "../errors.js";
3
+ import type { SqliteOwner } from "./contracts.js";
4
+ /** Finite process-local signal; durable owner state remains authoritative. */
5
+ export type ObservationSignal = "closed" | "reset" | "durable" | "progress";
6
+ export type ObservationReason = "durable" | "progress";
7
+ /** One scoped process-local observation registration. */
8
+ export type ObservationRegistration = Readonly<{
9
+ /** Inspect and consume one pending finite signal without waiting. */
10
+ pollSignal: Effect.Effect<ObservationSignal | undefined>;
11
+ /** Await and consume one finite observation signal. */
12
+ awaitSignal: Effect.Effect<ObservationSignal>;
13
+ /** Release this registration without affecting durable work. */
14
+ close: () => void;
15
+ }>;
16
+ /** Process-local wake coordination; durable facts remain authoritative. */
17
+ export type ObservationKernel = Readonly<{
18
+ /** Register before the caller's first durable read. */
19
+ register: (sessionId: string) => Effect.Effect<ObservationRegistration, ShiitakeError>;
20
+ /** Close every registration and release this agent's owner publisher. */
21
+ close: Effect.Effect<void>;
22
+ }>;
23
+ /** Combine coalesced reasons without allowing a progress hint to hide a commit. */
24
+ export declare function mergeObservationSignals(first: ObservationSignal, second: ObservationSignal): ObservationSignal;
25
+ /** Notify owner observers; null identifies an Agent-wide execution change. */
26
+ export declare function publishOwnerObservation(store: SqliteOwner, sessionId: string | null, reason?: ObservationReason): void;
27
+ /** Open one coalescing process-local observation kernel per agent. */
28
+ export declare function openObservationKernel(store: SqliteOwner): ObservationKernel;
@@ -0,0 +1,169 @@
1
+ import * as Effect from "effect/Effect";
2
+ import { ShiitakeError } from "../errors.js";
3
+ import { reportCatchFailure } from "./failure.js";
4
+ const MAX_OBSERVATIONS = 64;
5
+ const MAX_PENDING_HINTS = 256;
6
+ const ownerPublishers = new WeakMap();
7
+ /** Combine coalesced reasons without allowing a progress hint to hide a commit. */
8
+ export function mergeObservationSignals(first, second) {
9
+ if (first === "closed" || second === "closed")
10
+ return "closed";
11
+ if (first === "reset" || second === "reset")
12
+ return "reset";
13
+ if (first === "durable" || second === "durable")
14
+ return "durable";
15
+ return "progress";
16
+ }
17
+ function queueObservation(listener, reason) {
18
+ if (listener.pendingCount >= MAX_PENDING_HINTS) {
19
+ listener.pendingCount = 0;
20
+ listener.pendingReason = undefined;
21
+ listener.reset = true;
22
+ return;
23
+ }
24
+ listener.pendingCount += 1;
25
+ listener.pendingReason =
26
+ listener.pendingReason === "durable" || reason === "durable"
27
+ ? "durable"
28
+ : "progress";
29
+ }
30
+ function deliverObservation(listener, reason) {
31
+ if (listener.closed || listener.reset)
32
+ return;
33
+ const wake = listener.wake;
34
+ if (wake === undefined) {
35
+ queueObservation(listener, reason);
36
+ return;
37
+ }
38
+ listener.wake = undefined;
39
+ try {
40
+ wake(reason);
41
+ }
42
+ catch (cause) {
43
+ reportCatchFailure("observation-delivery", cause);
44
+ listener.pendingCount = 0;
45
+ listener.pendingReason = undefined;
46
+ listener.reset = true;
47
+ }
48
+ }
49
+ /** Notify owner observers; null identifies an Agent-wide execution change. */
50
+ export function publishOwnerObservation(store, sessionId, reason = "durable") {
51
+ const publishers = ownerPublishers.get(store);
52
+ if (publishers === undefined)
53
+ return;
54
+ for (const publish of publishers) {
55
+ try {
56
+ publish(sessionId, reason);
57
+ }
58
+ catch (cause) {
59
+ reportCatchFailure("owner-observation-publish", cause);
60
+ continue;
61
+ }
62
+ }
63
+ }
64
+ /** Open one coalescing process-local observation kernel per agent. */
65
+ export function openObservationKernel(store) {
66
+ const sessions = new Map();
67
+ const active = new Set();
68
+ const publishers = ownerPublishers.get(store) ?? new Set();
69
+ let released = false;
70
+ const takeSignal = (listener) => {
71
+ if (listener.closed)
72
+ return "closed";
73
+ if (listener.reset) {
74
+ listener.reset = false;
75
+ return "reset";
76
+ }
77
+ if (listener.pendingCount === 0)
78
+ return undefined;
79
+ listener.pendingCount = 0;
80
+ const reason = listener.pendingReason;
81
+ listener.pendingReason = undefined;
82
+ return reason === "durable" ? "durable" : "progress";
83
+ };
84
+ const closeListener = (listener) => {
85
+ if (listener.closed)
86
+ return;
87
+ listener.closed = true;
88
+ listener.pendingCount = 0;
89
+ listener.pendingReason = undefined;
90
+ listener.reset = false;
91
+ const wake = listener.wake;
92
+ listener.wake = undefined;
93
+ active.delete(listener);
94
+ const listeners = sessions.get(listener.sessionId);
95
+ listeners?.delete(listener);
96
+ if (listeners?.size === 0)
97
+ sessions.delete(listener.sessionId);
98
+ if (wake !== undefined) {
99
+ try {
100
+ wake("closed");
101
+ }
102
+ catch (cause) {
103
+ reportCatchFailure("observation-listener-close", cause);
104
+ }
105
+ }
106
+ };
107
+ const publish = (sessionId, reason) => {
108
+ const listeners = sessionId === null ? active : sessions.get(sessionId);
109
+ if (listeners === undefined)
110
+ return;
111
+ for (const listener of listeners)
112
+ deliverObservation(listener, reason);
113
+ };
114
+ publishers.add(publish);
115
+ ownerPublishers.set(store, publishers);
116
+ return Object.freeze({
117
+ register: (sessionId) => Effect.suspend(() => {
118
+ if (released)
119
+ return Effect.fail(new ShiitakeError("runtime", "closed", "Shiitake is closed"));
120
+ if (active.size >= MAX_OBSERVATIONS)
121
+ return Effect.fail(new ShiitakeError("runtime", "rejected", "Observation capacity is exhausted"));
122
+ const listener = {
123
+ sessionId,
124
+ closed: false,
125
+ pendingCount: 0,
126
+ pendingReason: undefined,
127
+ reset: false,
128
+ wake: undefined,
129
+ };
130
+ const listeners = sessions.get(sessionId) ?? new Set();
131
+ listeners.add(listener);
132
+ sessions.set(sessionId, listeners);
133
+ active.add(listener);
134
+ const pollSignal = Effect.sync(() => takeSignal(listener));
135
+ const awaitSignal = Effect.callback((resume) => {
136
+ const signal = takeSignal(listener);
137
+ if (signal !== undefined) {
138
+ resume(Effect.succeed(signal));
139
+ return;
140
+ }
141
+ const wake = (next) => {
142
+ resume(Effect.succeed(next));
143
+ };
144
+ listener.wake = wake;
145
+ return Effect.sync(() => {
146
+ if (listener.wake === wake)
147
+ listener.wake = undefined;
148
+ });
149
+ });
150
+ return Effect.succeed(Object.freeze({
151
+ pollSignal,
152
+ awaitSignal,
153
+ close: () => {
154
+ closeListener(listener);
155
+ },
156
+ }));
157
+ }),
158
+ close: Effect.sync(() => {
159
+ if (released)
160
+ return;
161
+ released = true;
162
+ publishers.delete(publish);
163
+ if (publishers.size === 0)
164
+ ownerPublishers.delete(store);
165
+ for (const listener of active)
166
+ closeListener(listener);
167
+ }),
168
+ });
169
+ }
@@ -0,0 +1,18 @@
1
+ import type { Json } from "../extensions.js";
2
+ import * as Effect from "effect/Effect";
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 type { Accepted } from "../client.js";
7
+ import type { ShiitakeEffectHost } from "../effect.js";
8
+ import { ShiitakeError } from "../errors.js";
9
+ import type { CommandSettlement, ShiitakeOptions, ShiitakeOwnerAgent } from "./contracts.js";
10
+ import type { ShiitakeEffectOwner, ShiitakeEffectOptions } from "./effect-contracts.js";
11
+ /** Open the single Promise adaptation over the canonical Effect agent. */
12
+ export declare function openPromiseAgent(options: ShiitakeOptions, make: (options: ShiitakeEffectOptions) => Effect.Effect<ShiitakeEffectOwner, ShiitakeError>): Promise<ShiitakeOwnerAgent>;
13
+ /** Open the Promise-facing host over the canonical Effect host composition. */
14
+ export declare function openPromiseHost(options: ShiitakeOptions, mailRoom: BirdDogEffect, openHost: (options: ShiitakeEffectOptions, mailRoom: BirdDogEffect) => Effect.Effect<ShiitakeEffectHost, ShiitakeError>): Promise<Readonly<{
15
+ agent: ShiitakeOwnerAgent;
16
+ readSettlement: (sessionId: string, commandId: string) => Promise<CommandSettlement | undefined>;
17
+ acceptMail: (mail: BirdDogMail, meta?: Json, reply?: VerifiedAgentReply) => Promise<Accepted>;
18
+ }>>;
@@ -0,0 +1,172 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as Predicate from "effect/Predicate";
3
+ import * as Schema from "effect/Schema";
4
+ import { fromTypedSandbox } from "@fungi.computer/mycelium/effect";
5
+ import { ShiitakeError } from "../errors.js";
6
+ import { runShiitakePromise, toPromiseOwner } from "./effect-client.js";
7
+ import { adaptModelSource } from "./model-acquisition.js";
8
+ function boundary(boundaryName, message) {
9
+ return new ShiitakeError(boundaryName, "rejected", message);
10
+ }
11
+ const identifier = Schema.String.pipe(Schema.check(Schema.isMinLength(1)), Schema.check(Schema.isMaxLength(256)));
12
+ const instructionText = Schema.String.pipe(Schema.check(Schema.isMinLength(1)), Schema.check(Schema.makeFilter((value) => new TextEncoder().encode(value).byteLength <= 32_000)));
13
+ const instructionSchema = Schema.Struct({
14
+ revision: identifier,
15
+ text: instructionText,
16
+ // The facade preserves opaque registration handles; Mycelium's compiler
17
+ // validates their WeakMap identity when the run scope installs them.
18
+ modules: Schema.optionalKey(Schema.declare((input) => Array.isArray(input) && input.every(Predicate.isObject))),
19
+ });
20
+ const runtimeBindSchema = Schema.declare((input) => Predicate.isFunction(input));
21
+ const runtimeMethodSchema = Schema.declare((input) => Predicate.isFunction(input));
22
+ const runtimeHostSchema = Schema.Struct({ bind: runtimeBindSchema });
23
+ const runtimeCommitSchema = Schema.declare((input) => Predicate.isFunction(input));
24
+ const runtimeHandleSchema = Schema.Struct({
25
+ commit: runtimeCommitSchema,
26
+ requestWake: runtimeMethodSchema,
27
+ close: runtimeMethodSchema,
28
+ });
29
+ const runtimeCloseSchema = Schema.Struct({ close: runtimeMethodSchema });
30
+ const decodeInstructions = Schema.decodeUnknownResult(instructionSchema);
31
+ const decodeRuntimeHost = Schema.decodeUnknownResult(runtimeHostSchema);
32
+ const decodeRuntimeHandle = Schema.decodeUnknownResult(runtimeHandleSchema);
33
+ const decodeRuntimeClose = Schema.decodeUnknownResult(runtimeCloseSchema);
34
+ function runtimeInvalid(cause) {
35
+ return new ShiitakeError("runtime", "invalid", "Runtime host is invalid", undefined, cause);
36
+ }
37
+ function runtimeUnavailable() {
38
+ return new ShiitakeError("runtime", "unavailable", "Runtime host is unavailable");
39
+ }
40
+ function liftRuntimeMethod(receiver, method) {
41
+ return Effect.try({
42
+ try: () => method.call(receiver),
43
+ catch: runtimeUnavailable,
44
+ }).pipe(Effect.flatMap((result) => Effect.tryPromise({
45
+ try: () => Promise.resolve(result),
46
+ catch: runtimeUnavailable,
47
+ })), Effect.flatMap((result) => result === undefined ? Effect.void : Effect.fail(runtimeInvalid())));
48
+ }
49
+ function failInvalidAfterCleanup(receiver, close, cause) {
50
+ const decoded = decodeRuntimeClose({ close });
51
+ if (decoded._tag === "Failure")
52
+ return Effect.fail(runtimeInvalid(cause));
53
+ return Effect.uninterruptible(liftRuntimeMethod(receiver, decoded.success.close)).pipe(Effect.matchCauseEffect({
54
+ onFailure: (cleanup) => Effect.fail(runtimeInvalid({ cause, cleanup })),
55
+ onSuccess: () => Effect.fail(runtimeInvalid(cause)),
56
+ }));
57
+ }
58
+ function liftRuntimeHandle(value) {
59
+ return Effect.suspend(() => {
60
+ if (!Predicate.isObject(value))
61
+ return Effect.fail(runtimeInvalid());
62
+ const receiver = value;
63
+ let close;
64
+ try {
65
+ close = receiver.close;
66
+ }
67
+ catch (cause) {
68
+ return Effect.fail(runtimeInvalid(cause));
69
+ }
70
+ let requestWake;
71
+ try {
72
+ requestWake = receiver.requestWake;
73
+ }
74
+ catch (cause) {
75
+ return failInvalidAfterCleanup(receiver, close, cause);
76
+ }
77
+ const decoded = decodeRuntimeHandle({
78
+ commit: receiver.commit,
79
+ requestWake,
80
+ close,
81
+ });
82
+ if (decoded._tag === "Failure")
83
+ return failInvalidAfterCleanup(receiver, close);
84
+ return Effect.succeed(Object.freeze({
85
+ commit: (mutation) => Effect.tryPromise({
86
+ try: () => receiver.commit(() => runShiitakePromise(mutation)),
87
+ catch: (error) => error instanceof ShiitakeError ? error : runtimeUnavailable(),
88
+ }),
89
+ requestWake: liftRuntimeMethod(receiver, decoded.success.requestWake),
90
+ close: liftRuntimeMethod(receiver, decoded.success.close),
91
+ }));
92
+ });
93
+ }
94
+ function liftRuntime(options) {
95
+ return Effect.try({
96
+ try: () => {
97
+ const runtime = options.runtime;
98
+ if (!Predicate.isObject(runtime))
99
+ throw runtimeInvalid();
100
+ const decoded = decodeRuntimeHost(runtime);
101
+ if (decoded._tag === "Failure")
102
+ throw runtimeInvalid();
103
+ return { receiver: runtime, bind: decoded.success.bind };
104
+ },
105
+ catch: runtimeInvalid,
106
+ }).pipe(Effect.map(({ receiver, bind }) => Object.freeze({
107
+ bind: ({ reconcile, pendingWork }) => Effect.try({
108
+ try: () => bind.call(receiver, {
109
+ reconcile: () => runShiitakePromise(reconcile),
110
+ pendingWork: () => runShiitakePromise(pendingWork),
111
+ }),
112
+ catch: runtimeUnavailable,
113
+ }).pipe(Effect.flatMap((result) => Effect.tryPromise({
114
+ try: () => Promise.resolve(result),
115
+ catch: runtimeUnavailable,
116
+ })), Effect.flatMap(liftRuntimeHandle)),
117
+ })));
118
+ }
119
+ function liftOptions(options, runtime) {
120
+ const base = {
121
+ // Native SQL and transactionSync are synchronous owner capabilities; do
122
+ // not unwrap them into a second Promise/CAS storage abstraction.
123
+ store: options.store,
124
+ runtime,
125
+ extensions: options.extensions,
126
+ instructions: {
127
+ load: ({ sessionId, commandId }) => Effect.tryPromise({
128
+ try: (signal) => options.instructions.load({ sessionId, commandId, signal }),
129
+ catch: () => boundary("instructions", "Instruction acquisition failed"),
130
+ }).pipe(Effect.flatMap((value) => {
131
+ const decoded = decodeInstructions(value);
132
+ return decoded._tag === "Success"
133
+ ? Effect.succeed(decoded.success)
134
+ : Effect.fail(new ShiitakeError("contract", "invalid", "Instruction revision is invalid"));
135
+ })),
136
+ },
137
+ model: adaptModelSource(options.model),
138
+ };
139
+ if (options.meta !== undefined)
140
+ Object.assign(base, { meta: options.meta });
141
+ if (options.storageBudget !== undefined)
142
+ Object.assign(base, { storageBudget: options.storageBudget });
143
+ if (options.compaction !== undefined)
144
+ Object.assign(base, { compaction: options.compaction });
145
+ const toolSource = options.tools;
146
+ if (toolSource === undefined)
147
+ return base;
148
+ const tools = {
149
+ modules: toolSource.modules ?? [],
150
+ sources: toolSource.sources ?? [],
151
+ sandbox: fromTypedSandbox(toolSource.sandbox),
152
+ access: toolSource.access ?? {},
153
+ };
154
+ return {
155
+ ...base,
156
+ tools: toolSource.execution === undefined
157
+ ? tools
158
+ : { ...tools, execution: toolSource.execution },
159
+ };
160
+ }
161
+ /** Open the single Promise adaptation over the canonical Effect agent. */
162
+ export function openPromiseAgent(options, make) {
163
+ return runShiitakePromise(liftRuntime(options).pipe(Effect.flatMap((runtime) => make(liftOptions(options, runtime))))).then(toPromiseOwner);
164
+ }
165
+ /** Open the Promise-facing host over the canonical Effect host composition. */
166
+ export function openPromiseHost(options, mailRoom, openHost) {
167
+ return runShiitakePromise(liftRuntime(options).pipe(Effect.flatMap((runtime) => openHost(liftOptions(options, runtime), mailRoom)))).then(({ agent, acceptMail, readSettlement }) => Object.freeze({
168
+ agent: toPromiseOwner(agent),
169
+ readSettlement: (sessionId, commandId) => runShiitakePromise(readSettlement(sessionId, commandId)),
170
+ acceptMail: (mail, meta, reply) => runShiitakePromise(acceptMail(mail, meta, reply)),
171
+ }));
172
+ }