@fungi.computer/shiitake 0.7.0 → 0.9.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 (41) hide show
  1. package/README.md +28 -0
  2. package/dist/client-wire.js +10 -13
  3. package/dist/client.d.ts +6 -10
  4. package/dist/client.js +1 -0
  5. package/dist/delegation.d.ts +4 -4
  6. package/dist/fleet.d.ts +5 -1
  7. package/dist/fleet.js +5 -1
  8. package/dist/http-routes.d.ts +4 -11
  9. package/dist/http-routes.js +2 -1
  10. package/dist/internal/coordinator.d.ts +2 -1
  11. package/dist/internal/coordinator.js +10 -218
  12. package/dist/internal/observation-kernel.d.ts +21 -0
  13. package/dist/internal/observation-kernel.js +140 -4
  14. package/dist/internal/run-scope.d.ts +1 -0
  15. package/dist/internal/run-scope.js +5 -3
  16. package/dist/internal/runtime-coordinator.js +12 -7
  17. package/dist/internal/runtime-core.js +2 -1
  18. package/dist/internal/runtime-reconciliation.js +5 -2
  19. package/dist/internal/session-observation.d.ts +37 -0
  20. package/dist/internal/session-observation.js +440 -0
  21. package/dist/internal/session-status.d.ts +2 -0
  22. package/dist/internal/session-status.js +17 -1
  23. package/dist/internal/woodstock/client-projection.d.ts +3 -1
  24. package/dist/internal/woodstock/client-projection.js +12 -2
  25. package/dist/internal/woodstock/index.d.ts +7 -1
  26. package/dist/internal/woodstock/index.js +47 -4
  27. package/dist/internal/woodstock/progress.d.ts +2 -0
  28. package/dist/internal/woodstock/progress.js +5 -0
  29. package/dist/internal/woodstock/session-entry.d.ts +2 -2
  30. package/dist/remote-client.js +5 -1
  31. package/dist/server-visitor-stream.js +11 -1
  32. package/dist/session-liveness.d.ts +4 -0
  33. package/dist/session-liveness.js +4 -0
  34. package/dist/testing.d.ts +2 -0
  35. package/dist/testing.js +2 -0
  36. package/docs/api/agent-control.md +2 -2
  37. package/docs/api/client.md +28 -14
  38. package/docs/api/delegation.md +2 -2
  39. package/docs/api/fleet.md +33 -4
  40. package/docs/api/testing.md +11 -0
  41. package/package.json +5 -5
@@ -1,8 +1,128 @@
1
1
  import * as Effect from "effect/Effect";
2
+ import * as Pull from "effect/Pull";
3
+ import * as Queue from "effect/Queue";
2
4
  import { ShiitakeError } from "../errors.js";
3
5
  import { reportCatchFailure } from "./failure.js";
4
6
  const MAX_OBSERVATIONS = 64;
5
7
  const MAX_PENDING_HINTS = 256;
8
+ const encodedFrames = new WeakMap();
9
+ const frameEncoder = new TextEncoder();
10
+ /** The feed shares one frame's JSON and byte accounting across its watchers. */
11
+ export function encodeSessionFrame(frame) {
12
+ const cached = encodedFrames.get(frame);
13
+ if (cached !== undefined)
14
+ return cached;
15
+ const data = JSON.stringify(frame);
16
+ const encoded = Object.freeze({
17
+ data,
18
+ bytes: frameEncoder.encode(data).byteLength,
19
+ });
20
+ encodedFrames.set(frame, encoded);
21
+ return encoded;
22
+ }
23
+ function withoutSandboxFailures(frame) {
24
+ if (frame.kind === "update" && frame.sandboxFailures !== undefined) {
25
+ const value = { ...frame };
26
+ delete value.sandboxFailures;
27
+ return value;
28
+ }
29
+ if (frame.kind === "reset" && frame.baseline.sandboxFailures !== undefined) {
30
+ const baseline = { ...frame.baseline };
31
+ delete baseline.sandboxFailures;
32
+ return { ...frame, baseline };
33
+ }
34
+ return frame;
35
+ }
36
+ /** Bounded dropping fan-out; Woodstock, never this queue, owns replay. */
37
+ export function openSessionFrameFeed(onIdle) {
38
+ const listeners = new Set();
39
+ let ended = false;
40
+ let failure;
41
+ const clear = (listener) => {
42
+ Effect.runSync(Queue.clear(listener.frames));
43
+ listener.bytes = 0;
44
+ };
45
+ return {
46
+ register(includeSandboxFailures = false) {
47
+ const listener = {
48
+ frames: Effect.runSync(Queue.dropping(256)),
49
+ bytes: 0,
50
+ lagged: false,
51
+ closed: false,
52
+ observed: -1,
53
+ includeSandboxFailures,
54
+ };
55
+ listeners.add(listener);
56
+ const take = Effect.suspend(() => {
57
+ if (listener.closed)
58
+ return Effect.succeed("closed");
59
+ if (failure !== undefined)
60
+ return Effect.fail(failure);
61
+ if (listener.lagged)
62
+ return Effect.succeed("reset");
63
+ if (ended)
64
+ return Effect.succeed("closed");
65
+ return Queue.take(listener.frames).pipe(Pull.catchDone(() => Effect.succeed("closed")), Effect.flatMap((frame) => {
66
+ if (failure !== undefined)
67
+ return Effect.fail(failure);
68
+ if (frame !== "reset" && frame !== "closed")
69
+ listener.bytes -= encodeSessionFrame(frame).bytes;
70
+ return Effect.succeed(frame);
71
+ }));
72
+ });
73
+ return Object.freeze({
74
+ take,
75
+ checkpoint: (revision = -1) => {
76
+ clear(listener);
77
+ listener.lagged = false;
78
+ listener.observed = revision;
79
+ },
80
+ close: () => {
81
+ if (listener.closed)
82
+ return;
83
+ listener.closed = true;
84
+ clear(listener);
85
+ listeners.delete(listener);
86
+ Queue.endUnsafe(listener.frames);
87
+ if (listeners.size === 0)
88
+ onIdle();
89
+ },
90
+ });
91
+ },
92
+ publish(frame, revision = Infinity) {
93
+ if (ended)
94
+ return;
95
+ const publicFrame = withoutSandboxFailures(frame);
96
+ for (const listener of listeners) {
97
+ if (listener.closed || listener.lagged || revision <= listener.observed)
98
+ continue;
99
+ const value = listener.includeSandboxFailures ? frame : publicFrame;
100
+ const { bytes } = encodeSessionFrame(value);
101
+ if (listener.bytes + bytes > 1_048_576 ||
102
+ !Queue.offerUnsafe(listener.frames, value)) {
103
+ clear(listener);
104
+ // Keep exactly one reset obligation; no incoming backlog can grow it.
105
+ listener.lagged = true;
106
+ Queue.offerUnsafe(listener.frames, "reset");
107
+ }
108
+ else {
109
+ listener.bytes += bytes;
110
+ }
111
+ }
112
+ },
113
+ end(error) {
114
+ if (ended)
115
+ return;
116
+ ended = true;
117
+ failure = error;
118
+ for (const listener of listeners) {
119
+ clear(listener);
120
+ listener.lagged = false;
121
+ Queue.endUnsafe(listener.frames);
122
+ }
123
+ },
124
+ };
125
+ }
6
126
  const ownerPublishers = new WeakMap();
7
127
  /** Actual live-view demand, excluding exact-command settlement waiters. */
8
128
  export function hasOwnerObservers(store, sessionId) {
@@ -39,7 +159,10 @@ function queueObservation(listener, reason) {
39
159
  : "progress";
40
160
  }
41
161
  function deliverObservation(listener, reason) {
42
- if (listener.closed || listener.reset)
162
+ if (listener.closed)
163
+ return;
164
+ listener.revision += 1;
165
+ if (listener.reset)
43
166
  return;
44
167
  const wake = listener.wake;
45
168
  if (wake === undefined) {
@@ -57,6 +180,14 @@ function deliverObservation(listener, reason) {
57
180
  listener.reset = true;
58
181
  }
59
182
  }
183
+ /** Queued views reconcile occupancy without replaying another Session's facts. */
184
+ function notifyQueuedViews(listeners, sourceSessionId) {
185
+ for (const listener of listeners)
186
+ if (listener.queued &&
187
+ !listener.settlementOnly &&
188
+ listener.sessionId !== sourceSessionId)
189
+ deliverObservation(listener, "activity");
190
+ }
60
191
  /** Notify observers of one Session's durable or process-local change. */
61
192
  export function publishOwnerObservation(store, sessionId, reason = "durable") {
62
193
  const publishers = ownerPublishers.get(store);
@@ -131,11 +262,12 @@ export function openObservationKernel(store) {
131
262
  };
132
263
  const publish = (publication) => {
133
264
  if (publication.kind === "agent-execution") {
134
- for (const listener of active)
135
- if (listener.queued && !listener.settlementOnly)
136
- deliverObservation(listener, "activity");
265
+ notifyQueuedViews(active, null);
137
266
  return;
138
267
  }
268
+ // Reuse bounded activity hints for the occupant's changing phase.
269
+ if (publication.sessionId !== null && publication.reason !== "settlement")
270
+ notifyQueuedViews(active, publication.sessionId);
139
271
  const listeners = publication.sessionId === null
140
272
  ? active
141
273
  : sessions.get(publication.sessionId);
@@ -160,6 +292,7 @@ export function openObservationKernel(store) {
160
292
  if (active.size >= MAX_OBSERVATIONS)
161
293
  return Effect.fail(new ShiitakeError("runtime", "rejected", "Observation capacity is exhausted"));
162
294
  const listener = {
295
+ revision: 0,
163
296
  sessionId,
164
297
  settlementOnly: interest === "settlement",
165
298
  closed: false,
@@ -190,6 +323,9 @@ export function openObservationKernel(store) {
190
323
  });
191
324
  });
192
325
  return Effect.succeed(Object.freeze({
326
+ get revision() {
327
+ return listener.revision;
328
+ },
193
329
  pollSignal,
194
330
  awaitSignal,
195
331
  close: () => {
@@ -7,6 +7,7 @@ import type { ShiitakeEffectOptions } from "./effect-contracts.js";
7
7
  import type { CompactionHandoffV0, PromptSectionV0 } from "../extensions.js";
8
8
  import { type ManualCompactionOutcome } from "./compaction/session-compaction.js";
9
9
  import { type PreparedSessionRun, type RunContract } from "./woodstock/index.js";
10
+ export declare const runFinalizerGraceMs = 1000;
10
11
  declare const toolSetupMessages: {
11
12
  readonly missing: "Instruction capabilities have no tool runtime";
12
13
  readonly runtime: "Tool runtime failed";
@@ -12,9 +12,11 @@ import { openCompactionSettings } from "./compaction/settings.js";
12
12
  import { acquireOwnedModel } from "./model-acquisition.js";
13
13
  import { returnedFailureReason, unavailableRunReason, } from "./run-failure-reason.js";
14
14
  import { isCompactCommand, openMuleRecorder, readSessionGrants, recordLiveRetryEvent, recordSandboxFailure, takeSessionControls, } from "./woodstock/index.js";
15
- // A run writes its terminal before scope release. Give each foreign release
16
- // one second to clean up without blocking the next Watchdog job indefinitely.
17
- const runFinalizerGraceMs = 1_000;
15
+ // A run writes its terminal before scope release. Allow one second per foreign
16
+ // release before detaching it: the held-model regression exercises the full
17
+ // grace and proves Watchdog can settle and claim the next priority-1 steer.
18
+ // Keep the test's eventual queue-idle budget tied to this product bound.
19
+ export const runFinalizerGraceMs = 1_000;
18
20
  function boundedRunFinalizer(name, workId, release) {
19
21
  return Effect.gen(function* () {
20
22
  // A detached release may finish later; interrupting a finalizer itself
@@ -3,6 +3,7 @@ import { WatchdogError } from "@fungi.computer/watchdog";
3
3
  import { makeRuntime as makeWatchdogRuntime, } from "@fungi.computer/watchdog/effect";
4
4
  import * as Effect from "effect/Effect";
5
5
  import * as Latch from "effect/Latch";
6
+ import { readSessionPhase } from "./session-status.js";
6
7
  import { readOutstandingDelegationSync } from "../fleet.js";
7
8
  import { publishAgentExecutionChange } from "./observation-kernel.js";
8
9
  import { ShiitakeError } from "../errors.js";
@@ -12,7 +13,7 @@ import { openCompactionSettings } from "./compaction/settings.js";
12
13
  import { admitExtensionRosterV0, } from "./extension-roster.js";
13
14
  import { coreConsumer, reportConsumer, openExtensionRuntime, } from "./extension-runtime.js";
14
15
  import { reconcileRuntime } from "./runtime-reconciliation.js";
15
- import { advanceCursor, createSessionWithinTransaction, deliverSessionGrants, prepareExtensionStore, recordSessionDelegator, projectSessionRun, openStorageAdmission, readCursor, readSessionInboxPage, readSessionSummary, } from "./woodstock/index.js";
16
+ import { advanceCursor, createSessionWithinTransaction, deliverSessionGrants, prepareExtensionStore, recordSessionDelegator, projectSessionRun, openStorageAdmission, readCursor, readSessionInboxPage, } from "./woodstock/index.js";
16
17
  function runtimeFailure(message) {
17
18
  return new ShiitakeError("runtime", "rejected", message);
18
19
  }
@@ -285,12 +286,16 @@ export function openRuntimeCoordinator(options, coreDigest, admitted, mailRoom)
285
286
  ? "host"
286
287
  : "serial")
287
288
  : undefined;
288
- if (work === undefined || work.kind === "host")
289
- return Effect.succeed(work ?? null);
290
- return readSessionSummary(options.store, work.sessionId).pipe(Effect.map((summary) => ({
291
- ...work,
292
- title: summary?.title ?? "",
293
- })));
289
+ if (work === undefined)
290
+ return Effect.succeed(null);
291
+ if (work.kind === "host")
292
+ return Effect.succeed({ phase: "thinking" });
293
+ return Effect.try({
294
+ try: () => ({
295
+ phase: readSessionPhase(options.store, work.sessionId, watchdog.transactional.readQueueHead(work.sessionId)),
296
+ }),
297
+ catch: (cause) => new ShiitakeError("runtime", "rejected", "Running phase is unavailable", undefined, cause),
298
+ });
294
299
  }), Effect.mapError((cause) => new ShiitakeError("runtime", "rejected", "Running work is unavailable", undefined, cause))),
295
300
  extensions,
296
301
  commit: bound.commit,
@@ -12,7 +12,8 @@ export function openRuntimeCore(options, mailRoom) {
12
12
  const coreDigest = yield* coreProtocolDigest([]);
13
13
  yield* prepareWoodstockSchema(options.store);
14
14
  const runtime = yield* openRuntimeCoordinator(options, coreDigest, admitted, mailRoom);
15
- const coordinator = openCoordinator(options, runtime);
15
+ const ownerContext = yield* Effect.context();
16
+ const coordinator = openCoordinator(options, runtime, ownerContext);
16
17
  const agent = Object.freeze({
17
18
  session: coordinator.session,
18
19
  createSession: coordinator.createSession,
@@ -7,7 +7,7 @@ import { ShiitakeError } from "../errors.js";
7
7
  import { ExtensionId } from "../extension-contract.js";
8
8
  import { coreConsumer, reportConsumer, } from "./extension-runtime.js";
9
9
  import { publishOwnerObservation } from "./observation-kernel.js";
10
- import { advanceCursor, readCursor, readExtensionAdmissionOrigin, readSettledRunFacts, readSettlementContent, readSettlementDiagnostic, readSessionDelegator, readSettlementMeta, readWaitingOn, readWaitingOnSender, recordWorkSettlement, sessionLineageRefused, settleControls, } from "./woodstock/index.js";
10
+ import { advanceCursor, readCursor, readExtensionAdmissionOrigin, readSettledRunFacts, readSettlementContent, readSettlementStretchContent, readSettlementDiagnostic, readSessionDelegator, readSettlementMeta, readWaitingOn, readWaitingOnSender, recordWorkSettlement, sessionLineageRefused, settleControls, } from "./woodstock/index.js";
11
11
  const pageSize = 64;
12
12
  function runtimeFailure(message) {
13
13
  return new ShiitakeError("runtime", "rejected", message);
@@ -100,7 +100,10 @@ function reportToDelegator(store, watchdog, mailRoom, job) {
100
100
  catch: () => runtimeFailure("Session attention is unavailable"),
101
101
  });
102
102
  const settled = yield* settlementFacts(store, job, waiting);
103
- yield* fanOut(mailRoom, { ...settled, delegator });
103
+ const content = job.outcome === "cancelled"
104
+ ? ""
105
+ : yield* readSettlementStretchContent(store, payload.work);
106
+ yield* fanOut(mailRoom, { ...settled, content, delegator });
104
107
  });
105
108
  }
106
109
  /**
@@ -0,0 +1,37 @@
1
+ import * as Effect from "effect/Effect";
2
+ import type * as Context from "effect/Context";
3
+ import * as Stream from "effect/Stream";
4
+ import type { SessionContext, SessionFrame, SessionSnapshot, SubscribeOptions, WatchOptions } from "../client.js";
5
+ import { ShiitakeError } from "../errors.js";
6
+ import type { ShiitakeEffectOptions } from "./effect-contracts.js";
7
+ import { type ObservationKernel } from "./observation-kernel.js";
8
+ import type { RuntimeCoordinator } from "./runtime-coordinator.js";
9
+ import { readSession } from "./woodstock/index.js";
10
+ /** Shared live observation; Woodstock remains the bounded baseline and replay owner. */
11
+ export declare function openSessionObservation(options: ShiitakeEffectOptions, runtime: RuntimeCoordinator, observation: ObservationKernel, ownerContext: Context.Context<never>): Readonly<{
12
+ read: (sessionId: string, input: Parameters<typeof readSession>[3]) => Effect.Effect<Readonly<{
13
+ sessionId: string;
14
+ cursor: string | null;
15
+ hasMore: boolean;
16
+ oldestCursor?: string | null;
17
+ messages: readonly import("../client.js").SessionMessage[];
18
+ terminals: readonly import("../client.js").SessionTerminal[];
19
+ compactions?: readonly import("../client.js").SessionCompactionMarker[];
20
+ compactionNotices?: readonly import("../client.js").SessionCompactionNotice[];
21
+ sandboxFailures?: readonly import("@fungi.computer/mycelium").SandboxFailureEvent[];
22
+ pending: import("../client.js").SessionPendingPrompts;
23
+ activity: import("../client.js").SessionActivity;
24
+ progress: import("../client.js").SessionProgress | null;
25
+ toolOutput: readonly import("../client.js").SessionToolOutput[];
26
+ extensions: import("../client.js").SessionExtensionSlots;
27
+ attention: readonly import("../session-attention.js").SessionAttention[];
28
+ context?: SessionContext;
29
+ }>, ShiitakeError, never>;
30
+ subscribe: (sessionId: string, input: SubscribeOptions) => Stream.Stream<SessionFrame, ShiitakeError, never>;
31
+ watch: (sessionId: string, input: WatchOptions | undefined) => Effect.Effect<Readonly<{
32
+ snapshot: SessionSnapshot;
33
+ changes: Stream.Stream<SessionFrame, ShiitakeError>;
34
+ close: Effect.Effect<void, ShiitakeError>;
35
+ }>, ShiitakeError, never>;
36
+ close: Effect.Effect<void, never, never>;
37
+ }>;