@fungi.computer/shiitake 0.8.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.
- package/README.md +28 -0
- package/dist/client-wire.js +1 -0
- package/dist/client.d.ts +4 -0
- package/dist/client.js +1 -0
- package/dist/delegation.d.ts +4 -4
- package/dist/http-routes.d.ts +3 -3
- package/dist/http-routes.js +2 -1
- package/dist/internal/coordinator.d.ts +2 -1
- package/dist/internal/coordinator.js +10 -218
- package/dist/internal/observation-kernel.d.ts +21 -0
- package/dist/internal/observation-kernel.js +128 -1
- package/dist/internal/runtime-core.js +2 -1
- package/dist/internal/runtime-reconciliation.js +5 -2
- package/dist/internal/session-observation.d.ts +37 -0
- package/dist/internal/session-observation.js +440 -0
- package/dist/internal/woodstock/client-projection.d.ts +3 -1
- package/dist/internal/woodstock/client-projection.js +12 -2
- package/dist/internal/woodstock/index.d.ts +7 -1
- package/dist/internal/woodstock/index.js +45 -3
- package/dist/internal/woodstock/session-entry.d.ts +2 -2
- package/dist/remote-client.js +5 -1
- package/dist/server-visitor-stream.js +11 -1
- package/dist/session-liveness.d.ts +4 -0
- package/dist/session-liveness.js +4 -0
- package/docs/api/agent-control.md +2 -2
- package/docs/api/client.md +23 -0
- package/docs/api/delegation.md +2 -2
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -22,6 +22,34 @@ To add your own behaviour, start the extension guide at
|
|
|
22
22
|
We don't take VC money. Fungi is built in the open and proudly backed by its
|
|
23
23
|
community.
|
|
24
24
|
|
|
25
|
+
## Observe a Session
|
|
26
|
+
|
|
27
|
+
`session.watch()` returns an owner-atomic snapshot and live `changes`; close the
|
|
28
|
+
watch when the consumer leaves. `session.subscribe({ after })` replays strictly
|
|
29
|
+
after an opaque Woodstock cursor, then joins the same per-Session live feed.
|
|
30
|
+
Woodstock remains the only durable history owner.
|
|
31
|
+
|
|
32
|
+
Watchers share one projection producer and encoded frame per Session. Each
|
|
33
|
+
watcher can retain at most 256 frames or 1 MiB of encoded UTF-8, whichever comes
|
|
34
|
+
first. Overflow drops that backlog and delivers one `reset` with a fresh
|
|
35
|
+
snapshot, even if no later write arrives. There are at most 64 live watchers per
|
|
36
|
+
Agent; closing one releases its capacity. Closing the last watcher stops the
|
|
37
|
+
Session's producer and heartbeat.
|
|
38
|
+
|
|
39
|
+
The byte limit also applies to a single frame: an encoded frame over 1 MiB
|
|
40
|
+
cannot enter even an empty watcher queue. Every watcher whose frame exceeds that
|
|
41
|
+
limit resets, including an otherwise fast watcher. This is a per-watcher check,
|
|
42
|
+
not a reset of other Sessions or of watchers whose filtered frame and backlog
|
|
43
|
+
fit. The fresh Woodstock baseline is the recovery path; silently exceeding the
|
|
44
|
+
queue bound or adding a second chunked wire protocol is not.
|
|
45
|
+
|
|
46
|
+
A typed `{ kind: "heartbeat" }` arrives every 15 seconds. It is not durable and
|
|
47
|
+
never carries or advances a cursor. Durable rows deduplicate by owner sequence;
|
|
48
|
+
message progress uses message identity and revision, not the durable cursor. The
|
|
49
|
+
HTTP client treats 45 seconds without a delivered event as an unavailable,
|
|
50
|
+
possibly busy feed. Silence does not prove the service stopped: Local clients
|
|
51
|
+
must check the service owner's database lease before claiming that.
|
|
52
|
+
|
|
25
53
|
## Recover an uncertain send
|
|
26
54
|
|
|
27
55
|
Both local and HTTP Sessions expose
|
package/dist/client-wire.js
CHANGED
|
@@ -164,6 +164,7 @@ export const SessionSnapshotSchema = Schema.Struct({
|
|
|
164
164
|
context: Schema.optionalKey(SessionContextSchema),
|
|
165
165
|
});
|
|
166
166
|
export const SessionFrameSchema = Schema.Union([
|
|
167
|
+
Schema.Struct({ kind: Schema.Literal("heartbeat") }),
|
|
167
168
|
Schema.Struct({
|
|
168
169
|
kind: Schema.Literal("activity"),
|
|
169
170
|
activity: SessionActivitySchema,
|
package/dist/client.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { SessionAttention } from "./session-attention.js";
|
|
2
|
+
export { sessionHeartbeatIntervalMs, sessionSilenceDeadlineMs, } from "./session-liveness.js";
|
|
2
3
|
import type { CommandSettlement } from "./internal/contracts.js";
|
|
3
4
|
export type { CommandSettlement } from "./internal/contracts.js";
|
|
4
5
|
export { type SessionAttention } from "./session-attention.js";
|
|
@@ -342,6 +343,9 @@ export type SessionSnapshot = Readonly<{
|
|
|
342
343
|
}>;
|
|
343
344
|
/** One bounded replay or live frame. */
|
|
344
345
|
export type SessionFrame = Readonly<{
|
|
346
|
+
/** Process-local liveness only: never a durable cursor or activity fact. */
|
|
347
|
+
kind: "heartbeat";
|
|
348
|
+
}> | Readonly<{
|
|
345
349
|
/** Process-local activity change for the Session's current queue head. */
|
|
346
350
|
kind: "activity";
|
|
347
351
|
activity: SessionActivity;
|
package/dist/client.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { sessionHeartbeatIntervalMs, sessionSilenceDeadlineMs, } from "./session-liveness.js";
|
|
1
2
|
export {} from "./session-attention.js";
|
|
2
3
|
export { createShiitakeClient, } from "./remote-client.js";
|
|
3
4
|
export { bindAgentExtension, bindSessionExtension, } from "./extension-client.js";
|
package/dist/delegation.d.ts
CHANGED
|
@@ -103,11 +103,11 @@ export declare function agentMailRoute(namespace: PostRoute<never, never>["names
|
|
|
103
103
|
};
|
|
104
104
|
} | {
|
|
105
105
|
readonly key: string;
|
|
106
|
+
readonly content: string;
|
|
106
107
|
readonly reply: {
|
|
107
108
|
readonly outcome: "undeliverable";
|
|
108
109
|
readonly reason: "recipient_offline";
|
|
109
110
|
};
|
|
110
|
-
readonly content: string;
|
|
111
111
|
readonly to: {
|
|
112
112
|
readonly agentId: string;
|
|
113
113
|
readonly kind: "agent";
|
|
@@ -162,11 +162,11 @@ export declare function agentMailRoute(namespace: PostRoute<never, never>["names
|
|
|
162
162
|
};
|
|
163
163
|
} | {
|
|
164
164
|
readonly key: string;
|
|
165
|
+
readonly content: string;
|
|
165
166
|
readonly reply: {
|
|
166
167
|
readonly outcome: "undeliverable";
|
|
167
168
|
readonly reason: "recipient_offline";
|
|
168
169
|
};
|
|
169
|
-
readonly content: string;
|
|
170
170
|
readonly to: {
|
|
171
171
|
readonly agentId: string;
|
|
172
172
|
readonly kind: "agent";
|
|
@@ -221,11 +221,11 @@ export declare function agentMailRoute(namespace: PostRoute<never, never>["names
|
|
|
221
221
|
};
|
|
222
222
|
} | {
|
|
223
223
|
readonly key: string;
|
|
224
|
+
readonly content: string;
|
|
224
225
|
readonly reply: {
|
|
225
226
|
readonly outcome: "undeliverable";
|
|
226
227
|
readonly reason: "recipient_offline";
|
|
227
228
|
};
|
|
228
|
-
readonly content: string;
|
|
229
229
|
readonly to: {
|
|
230
230
|
readonly agentId: string;
|
|
231
231
|
readonly kind: "agent";
|
|
@@ -280,11 +280,11 @@ export declare function agentMailRoute(namespace: PostRoute<never, never>["names
|
|
|
280
280
|
};
|
|
281
281
|
} | {
|
|
282
282
|
readonly key: string;
|
|
283
|
+
readonly content: string;
|
|
283
284
|
readonly reply: {
|
|
284
285
|
readonly outcome: "undeliverable";
|
|
285
286
|
readonly reason: "recipient_offline";
|
|
286
287
|
};
|
|
287
|
-
readonly content: string;
|
|
288
288
|
readonly to: {
|
|
289
289
|
readonly agentId: string;
|
|
290
290
|
readonly kind: "agent";
|
package/dist/http-routes.d.ts
CHANGED
|
@@ -513,7 +513,6 @@ export declare function createShiitakeHttpRoutes(resolveSession: ShiitakeSession
|
|
|
513
513
|
readonly content: string;
|
|
514
514
|
readonly usage: readonly {
|
|
515
515
|
model: string;
|
|
516
|
-
provider: import("@fungi.computer/mule/effect").ProviderId;
|
|
517
516
|
usage: {
|
|
518
517
|
input: number;
|
|
519
518
|
output: number;
|
|
@@ -530,6 +529,7 @@ export declare function createShiitakeHttpRoutes(resolveSession: ShiitakeSession
|
|
|
530
529
|
total: number;
|
|
531
530
|
};
|
|
532
531
|
};
|
|
532
|
+
provider: import("@fungi.computer/mule/effect").ProviderId;
|
|
533
533
|
}[];
|
|
534
534
|
readonly toolCalls: number;
|
|
535
535
|
readonly waitingOnYou: readonly {
|
|
@@ -711,7 +711,7 @@ export declare function createShiitakeHttpRoutes(resolveSession: ShiitakeSession
|
|
|
711
711
|
} | {
|
|
712
712
|
toolCallId: string;
|
|
713
713
|
role: "toolResult";
|
|
714
|
-
|
|
714
|
+
toolName: string;
|
|
715
715
|
content: ({
|
|
716
716
|
type: "text";
|
|
717
717
|
text: string;
|
|
@@ -721,7 +721,6 @@ export declare function createShiitakeHttpRoutes(resolveSession: ShiitakeSession
|
|
|
721
721
|
data: string;
|
|
722
722
|
mimeType: string;
|
|
723
723
|
})[];
|
|
724
|
-
toolName: string;
|
|
725
724
|
usage?: {
|
|
726
725
|
input: number;
|
|
727
726
|
output: number;
|
|
@@ -753,6 +752,7 @@ export declare function createShiitakeHttpRoutes(resolveSession: ShiitakeSession
|
|
|
753
752
|
complete: boolean;
|
|
754
753
|
} | undefined;
|
|
755
754
|
isError: boolean;
|
|
755
|
+
timestamp: number;
|
|
756
756
|
durationMs?: number | undefined;
|
|
757
757
|
};
|
|
758
758
|
}[];
|
package/dist/http-routes.js
CHANGED
|
@@ -4,6 +4,7 @@ import { Hono } from "hono";
|
|
|
4
4
|
import { streamSSE, } from "hono/streaming";
|
|
5
5
|
import { validator } from "hono/validator";
|
|
6
6
|
import * as Schema from "effect/Schema";
|
|
7
|
+
import { encodeSessionFrame } from "./internal/observation-kernel.js";
|
|
7
8
|
import { ShiitakeError, toShiitakeErrorWire } from "./errors.js";
|
|
8
9
|
import { JsonValueSchema } from "./internal/json-value.js";
|
|
9
10
|
import { decodeClientIdentity, decodeClientEventsQuery, decodeClientReadQuery, decodeClientSettlementQuery, decodeClientSessionCommandRequest, decodeClientSessionCreationRequest, decodeClientPageCursor, decodeClientSessionId, } from "./internal/client-boundary.js";
|
|
@@ -84,7 +85,7 @@ function frameEvent(frame) {
|
|
|
84
85
|
: frame.kind === "reset"
|
|
85
86
|
? (frame.cursor ?? "")
|
|
86
87
|
: undefined;
|
|
87
|
-
const event = { event: "frame", data:
|
|
88
|
+
const event = { event: "frame", data: encodeSessionFrame(frame).data };
|
|
88
89
|
return id === undefined ? event : { ...event, id };
|
|
89
90
|
}
|
|
90
91
|
async function writeSessionEvents(stream, session, request) {
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import type { BirdDogMail } from "@fungi.computer/bird-dog";
|
|
2
2
|
import type { VerifiedAgentReply } from "./woodstock/index.js";
|
|
3
3
|
import * as Effect from "effect/Effect";
|
|
4
|
+
import type * as Context from "effect/Context";
|
|
4
5
|
import { ShiitakeError } from "../errors.js";
|
|
5
6
|
import type { Json } from "../extensions.js";
|
|
6
7
|
import type { ShiitakeEffectOwner, ShiitakeEffectOptions } from "./effect-contracts.js";
|
|
7
8
|
import type { RuntimeCoordinator } from "./runtime-coordinator.js";
|
|
8
9
|
import { type GrantsDelivery } from "./woodstock/index.js";
|
|
9
10
|
/** Construct the Effect-native coordinator over the private owner modules. */
|
|
10
|
-
export declare function openCoordinator(options: ShiitakeEffectOptions, runtime: RuntimeCoordinator): ShiitakeEffectOwner;
|
|
11
|
+
export declare function openCoordinator(options: ShiitakeEffectOptions, runtime: RuntimeCoordinator, ownerContext: Context.Context<never>): ShiitakeEffectOwner;
|
|
11
12
|
/**
|
|
12
13
|
* Accept one addressed mail envelope; the door decodes it as a prompt. `reply`
|
|
13
14
|
* is the host's authentication of a delegated Agent's reply; only then does
|
|
@@ -4,14 +4,12 @@ import { acceptedNotice } from "@fungi.computer/bird-dog/effect";
|
|
|
4
4
|
import { FungiMessageMetaSchema } from "@fungi.computer/agar/agents";
|
|
5
5
|
import * as Effect from "effect/Effect";
|
|
6
6
|
import * as Option from "effect/Option";
|
|
7
|
-
import * as Stream from "effect/Stream";
|
|
8
7
|
import { ShiitakeError } from "../errors.js";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
8
|
+
import { decodeClientSettlementRequest, requireDecoded, decodeClientSessionCommand, decodeClientSessionId, } from "./client-boundary.js";
|
|
9
|
+
import { openObservationKernel, publishAgentExecutionChange, publishOwnerObservation, } from "./observation-kernel.js";
|
|
10
|
+
import { openSessionObservation } from "./session-observation.js";
|
|
11
11
|
import { checkHostMeta } from "./host-meta.js";
|
|
12
|
-
import {
|
|
13
|
-
import { prepareSessionContext } from "./session-context.js";
|
|
14
|
-
import { progressPosition, readProcessProgressDelta, abortSession, cancelSession, branchSession, recordOperatorLine, forkSession, readSession, readCommandSettlement, readSessionModelPreference, readSessionReplayPage, readSessionInbox, readLatestPendingPrompt, createSession, deleteSession, deliverSessionGrants, readSessionGrants, setSessionModelPreference, setSessionTitle, readAdmissionIdentity, assertAdmissionIdentity, } from "./woodstock/index.js";
|
|
12
|
+
import { abortSession, cancelSession, branchSession, recordOperatorLine, forkSession, readCommandSettlement, readSessionModelPreference, readSessionInbox, readLatestPendingPrompt, createSession, deleteSession, deliverSessionGrants, readSessionGrants, setSessionModelPreference, setSessionTitle, readAdmissionIdentity, assertAdmissionIdentity, } from "./woodstock/index.js";
|
|
15
13
|
/** Generic Agent-scope extension controls over the runtime's one call door. */
|
|
16
14
|
function agentExtensions(runtime) {
|
|
17
15
|
return Object.freeze({
|
|
@@ -20,29 +18,6 @@ function agentExtensions(runtime) {
|
|
|
20
18
|
call: (extensionId, command, input) => runtime.assertOpen.pipe(Effect.andThen(runtime.extensions.call({ scope: "agent" }, extensionId, command, input))),
|
|
21
19
|
});
|
|
22
20
|
}
|
|
23
|
-
function currentSessionWork(runtime, sessionId) {
|
|
24
|
-
return runtime
|
|
25
|
-
.readQueueHead(sessionId)
|
|
26
|
-
.pipe(Effect.flatMap((head) => (head.state === "queued"
|
|
27
|
-
? runtime.runningWork(head.jobId)
|
|
28
|
-
: Effect.succeed(null)).pipe(Effect.map((runningWork) => ({ head, runningWork })))));
|
|
29
|
-
}
|
|
30
|
-
function currentSessionActivity(options, runtime, sessionId) {
|
|
31
|
-
return currentSessionWork(runtime, sessionId).pipe(Effect.map(({ head, runningWork }) => readSessionActivity(options.store, sessionId, head, runningWork)));
|
|
32
|
-
}
|
|
33
|
-
/** Carry the Session's context use on a baseline, when it has one. */
|
|
34
|
-
function withContext(snapshot, context) {
|
|
35
|
-
return context === undefined
|
|
36
|
-
? snapshot
|
|
37
|
-
: Object.freeze({ ...snapshot, context });
|
|
38
|
-
}
|
|
39
|
-
/** Read the baseline and its context use for one Session. */
|
|
40
|
-
function readSessionWithContext(options, runtime, sessionId, input) {
|
|
41
|
-
return currentSessionWork(runtime, sessionId).pipe(Effect.flatMap(({ head, runningWork }) => readSession(options.store, sessionId, () => readSessionActivity(options.store, sessionId, head, runningWork), input, () => runtime.extensions.project(sessionId), (snapshot) => ({
|
|
42
|
-
snapshot,
|
|
43
|
-
context: prepareSessionContext(options, sessionId, snapshot.cursor, input?.before === undefined),
|
|
44
|
-
}))), Effect.flatMap(({ snapshot, context }) => context.pipe(Effect.map((value) => withContext(snapshot, value)))));
|
|
45
|
-
}
|
|
46
21
|
function sessionOwnerOperation(runtime, sessionId, operation) {
|
|
47
22
|
return runtime.assertOpen.pipe(Effect.andThen(Effect.try({
|
|
48
23
|
try: () => operation(decodeClientSessionId(sessionId)),
|
|
@@ -67,190 +42,6 @@ function decodeSessionCommand(sessionId, input) {
|
|
|
67
42
|
: new ShiitakeError("contract", "invalid", "Session command is invalid"),
|
|
68
43
|
});
|
|
69
44
|
}
|
|
70
|
-
function decodeSubscriptionInput(sessionId, input) {
|
|
71
|
-
return Effect.try({
|
|
72
|
-
try: () => Object.freeze({
|
|
73
|
-
sessionId: decodeClientSessionId(sessionId),
|
|
74
|
-
options: decodeClientSubscribeOptions(input),
|
|
75
|
-
}),
|
|
76
|
-
catch: (error) => error instanceof ShiitakeError
|
|
77
|
-
? error
|
|
78
|
-
: new ShiitakeError("contract", "invalid", "Subscribe options are invalid"),
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
function resetFrame(options, runtime, sessionId, includeSandboxFailures = false) {
|
|
82
|
-
return runtime.assertOpen.pipe(Effect.andThen(readSessionWithContext(options, runtime, sessionId, {
|
|
83
|
-
includeSandboxFailures,
|
|
84
|
-
})), Effect.map((baseline) => Object.freeze({
|
|
85
|
-
kind: "reset",
|
|
86
|
-
reason: "history_unavailable",
|
|
87
|
-
cursor: baseline.cursor,
|
|
88
|
-
baseline,
|
|
89
|
-
})));
|
|
90
|
-
}
|
|
91
|
-
function continueFromReset(frame) {
|
|
92
|
-
return [
|
|
93
|
-
[frame],
|
|
94
|
-
Option.some({
|
|
95
|
-
after: frame.cursor ?? undefined,
|
|
96
|
-
delivery: frame.baseline.hasMore ? "replay" : "live",
|
|
97
|
-
awaitSignal: !frame.baseline.hasMore,
|
|
98
|
-
}),
|
|
99
|
-
];
|
|
100
|
-
}
|
|
101
|
-
function subscriptionFrames(options, runtime, registration, sessionId, after, delivery, includeSandboxFailures = false, baselineProgress = null) {
|
|
102
|
-
let position = progressPosition(baselineProgress);
|
|
103
|
-
let toolOutput = [];
|
|
104
|
-
const reset = () => resetFrame(options, runtime, sessionId, includeSandboxFailures).pipe(Effect.map((frame) => {
|
|
105
|
-
position = progressPosition(frame.baseline.progress);
|
|
106
|
-
toolOutput = frame.baseline.toolOutput;
|
|
107
|
-
return continueFromReset(frame);
|
|
108
|
-
}));
|
|
109
|
-
const liveFrames = (activity) => {
|
|
110
|
-
const queue = activity.facts.queue;
|
|
111
|
-
const delta = queue.state !== "running"
|
|
112
|
-
? null
|
|
113
|
-
: readProcessProgressDelta(options.store, sessionId, queue.commandId, position);
|
|
114
|
-
if (delta === undefined)
|
|
115
|
-
return undefined;
|
|
116
|
-
const frames = [];
|
|
117
|
-
if (delta !== null) {
|
|
118
|
-
frames.push({
|
|
119
|
-
kind: "progress",
|
|
120
|
-
progress: delta,
|
|
121
|
-
activity: activity.facts,
|
|
122
|
-
});
|
|
123
|
-
position = {
|
|
124
|
-
messageId: delta.messageId,
|
|
125
|
-
revision: delta.revision,
|
|
126
|
-
text: delta.text.offset + delta.text.append.length,
|
|
127
|
-
thinking: delta.thinking.offset + delta.thinking.append.length,
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
if (toolOutput.length !== activity.toolOutput.length ||
|
|
131
|
-
toolOutput.some((slot, index) => slot !== activity.toolOutput[index])) {
|
|
132
|
-
toolOutput = activity.toolOutput;
|
|
133
|
-
frames.push({
|
|
134
|
-
kind: "tool_output",
|
|
135
|
-
toolOutput,
|
|
136
|
-
activity: activity.facts,
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
return frames;
|
|
140
|
-
};
|
|
141
|
-
const initial = {
|
|
142
|
-
after,
|
|
143
|
-
delivery,
|
|
144
|
-
awaitSignal: false,
|
|
145
|
-
};
|
|
146
|
-
return Stream.paginate(initial, (state) => (state.awaitSignal
|
|
147
|
-
? registration.awaitSignal.pipe(Effect.flatMap((signal) => registration.pollSignal.pipe(Effect.map((pending) => pending === undefined
|
|
148
|
-
? signal
|
|
149
|
-
: mergeObservationSignals(signal, pending)))))
|
|
150
|
-
: registration.pollSignal).pipe(Effect.flatMap((signal) => {
|
|
151
|
-
if (signal === "closed")
|
|
152
|
-
return Effect.succeed([[], Option.none()]);
|
|
153
|
-
if (signal === "reset")
|
|
154
|
-
return reset();
|
|
155
|
-
return runtime.assertOpen.pipe(Effect.andThen(currentSessionActivity(options, runtime, sessionId)), Effect.flatMap((activity) => {
|
|
156
|
-
registration.setQueued(activity.facts.queue.state === "queued");
|
|
157
|
-
if ((signal === "progress" || signal === "activity") &&
|
|
158
|
-
state.delivery === "live") {
|
|
159
|
-
const frames = liveFrames(activity);
|
|
160
|
-
if (frames === undefined)
|
|
161
|
-
return reset();
|
|
162
|
-
return Effect.succeed([
|
|
163
|
-
[
|
|
164
|
-
...(signal === "activity"
|
|
165
|
-
? [
|
|
166
|
-
{
|
|
167
|
-
kind: "activity",
|
|
168
|
-
activity: activity.facts,
|
|
169
|
-
},
|
|
170
|
-
]
|
|
171
|
-
: []),
|
|
172
|
-
...frames,
|
|
173
|
-
],
|
|
174
|
-
Option.some({ ...state, awaitSignal: true }),
|
|
175
|
-
]);
|
|
176
|
-
}
|
|
177
|
-
return readSessionReplayPage(options.store, sessionId, state.after, state.delivery, activity, () => runtime.extensions.project(sessionId), includeSandboxFailures, (page) => {
|
|
178
|
-
const latestCursor = page.kind === "page"
|
|
179
|
-
? page.frames
|
|
180
|
-
.flatMap((frame) => frame.kind === "progress" ||
|
|
181
|
-
frame.kind === "activity" ||
|
|
182
|
-
frame.kind === "tool_output"
|
|
183
|
-
? []
|
|
184
|
-
: [frame.cursor])
|
|
185
|
-
.at(-1)
|
|
186
|
-
: undefined;
|
|
187
|
-
// Prepare a figure only for the final moving update,
|
|
188
|
-
// while its cursor and stored context share the read.
|
|
189
|
-
const context = page.kind === "page" &&
|
|
190
|
-
!page.hasMore &&
|
|
191
|
-
page.frames.some((frame) => frame.kind === "update" &&
|
|
192
|
-
frame.cursor !== state.after)
|
|
193
|
-
? prepareSessionContext(options, sessionId, latestCursor ?? null)
|
|
194
|
-
: Effect.succeed(undefined);
|
|
195
|
-
return { page, latestCursor, context };
|
|
196
|
-
}).pipe(Effect.flatMap(({ page, latestCursor, context, }) => {
|
|
197
|
-
if (page.kind === "reset")
|
|
198
|
-
return reset();
|
|
199
|
-
if (!page.hasMore)
|
|
200
|
-
toolOutput = activity.toolOutput;
|
|
201
|
-
const progressFrames = page.hasMore
|
|
202
|
-
? []
|
|
203
|
-
: liveFrames(activity);
|
|
204
|
-
if (progressFrames === undefined)
|
|
205
|
-
return reset();
|
|
206
|
-
const next = {
|
|
207
|
-
after: latestCursor ?? state.after,
|
|
208
|
-
delivery: page.hasMore ? state.delivery : "live",
|
|
209
|
-
awaitSignal: !page.hasMore,
|
|
210
|
-
};
|
|
211
|
-
return context.pipe(Effect.map((context) => [
|
|
212
|
-
[
|
|
213
|
-
...(context === undefined
|
|
214
|
-
? page.frames
|
|
215
|
-
: page.frames.map((frame) => frame.kind === "update" &&
|
|
216
|
-
frame.cursor === latestCursor
|
|
217
|
-
? Object.freeze({ ...frame, context })
|
|
218
|
-
: frame)),
|
|
219
|
-
...progressFrames,
|
|
220
|
-
],
|
|
221
|
-
Option.some(next),
|
|
222
|
-
]));
|
|
223
|
-
}));
|
|
224
|
-
}));
|
|
225
|
-
})));
|
|
226
|
-
}
|
|
227
|
-
function subscribeSession(options, runtime, observation, sessionId, input) {
|
|
228
|
-
return Stream.unwrap(runtime.assertOpen.pipe(Effect.andThen(decodeSubscriptionInput(sessionId, input)), Effect.flatMap((decoded) => Effect.acquireRelease(observation.register(decoded.sessionId), (registration) => Effect.sync(registration.close)).pipe(Effect.map((registration) => subscriptionFrames(options, runtime, registration, decoded.sessionId, decoded.options.after, "replay", decoded.options.includeSandboxFailures === true))))));
|
|
229
|
-
}
|
|
230
|
-
function decodeWatchInput(sessionId, input) {
|
|
231
|
-
return Effect.try({
|
|
232
|
-
try: () => Object.freeze({
|
|
233
|
-
sessionId: decodeClientSessionId(sessionId),
|
|
234
|
-
options: decodeClientReadOptions(input),
|
|
235
|
-
}),
|
|
236
|
-
catch: (error) => error instanceof ShiitakeError
|
|
237
|
-
? error
|
|
238
|
-
: new ShiitakeError("contract", "invalid", "Watch options are invalid"),
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
function watchSession(options, runtime, observation, sessionId, input) {
|
|
242
|
-
return runtime.assertOpen.pipe(Effect.andThen(decodeWatchInput(sessionId, input)), Effect.flatMap((decoded) => Effect.uninterruptibleMask((restore) => Effect.gen(function* () {
|
|
243
|
-
const registration = yield* observation.register(decoded.sessionId);
|
|
244
|
-
return yield* restore(runtime.assertOpen.pipe(Effect.andThen(readSessionWithContext(options, runtime, decoded.sessionId, decoded.options)), Effect.map((snapshot) => {
|
|
245
|
-
registration.setQueued(snapshot.activity.queue.state === "queued");
|
|
246
|
-
return Object.freeze({
|
|
247
|
-
snapshot,
|
|
248
|
-
changes: subscriptionFrames(options, runtime, registration, decoded.sessionId, snapshot.cursor ?? undefined, "live", decoded.options.includeSandboxFailures === true, snapshot.progress).pipe(Stream.ensuring(Effect.sync(registration.close))),
|
|
249
|
-
close: Effect.sync(registration.close),
|
|
250
|
-
});
|
|
251
|
-
}))).pipe(Effect.onError(() => Effect.sync(registration.close)));
|
|
252
|
-
}))));
|
|
253
|
-
}
|
|
254
45
|
/**
|
|
255
46
|
* Admit one user prompt, mail prompt or compaction request through the
|
|
256
47
|
* accepted-command door.
|
|
@@ -359,8 +150,9 @@ function checkCommandMeta(options, meta) {
|
|
|
359
150
|
});
|
|
360
151
|
}
|
|
361
152
|
/** Construct the Effect-native coordinator over the private owner modules. */
|
|
362
|
-
export function openCoordinator(options, runtime) {
|
|
153
|
+
export function openCoordinator(options, runtime, ownerContext) {
|
|
363
154
|
const observation = openObservationKernel(options.store);
|
|
155
|
+
const sessionObservation = openSessionObservation(options, runtime, observation, ownerContext);
|
|
364
156
|
/** One Session handle; `meta` is the host metadata its prompts carry. */
|
|
365
157
|
const sessionFor = (sessionId, meta) => {
|
|
366
158
|
function command(input) {
|
|
@@ -407,7 +199,7 @@ export function openCoordinator(options, runtime) {
|
|
|
407
199
|
extension: (extensionId) => Object.freeze({
|
|
408
200
|
call: (command, input, caller) => runtime.assertOpen.pipe(Effect.andThen(runtime.extensions.call({ scope: "session", sessionId }, extensionId, command, input, caller))),
|
|
409
201
|
}),
|
|
410
|
-
read: (input) => runtime.assertOpen.pipe(Effect.andThen(
|
|
202
|
+
read: (input) => runtime.assertOpen.pipe(Effect.andThen(sessionObservation.read(sessionId, input))),
|
|
411
203
|
readSettlement: (commandId, timeoutMs = 0) => Effect.scoped(Effect.gen(function* () {
|
|
412
204
|
yield* runtime.assertOpen;
|
|
413
205
|
const decoded = yield* Effect.try({
|
|
@@ -437,8 +229,8 @@ export function openCoordinator(options, runtime) {
|
|
|
437
229
|
}
|
|
438
230
|
}).pipe(Effect.timeoutOption(decoded.timeoutMs), Effect.map(Option.getOrUndefined));
|
|
439
231
|
})),
|
|
440
|
-
subscribe: (input) =>
|
|
441
|
-
watch: (input) =>
|
|
232
|
+
subscribe: (input) => sessionObservation.subscribe(sessionId, input),
|
|
233
|
+
watch: (input) => sessionObservation.watch(sessionId, input),
|
|
442
234
|
});
|
|
443
235
|
};
|
|
444
236
|
const agent = Object.freeze({
|
|
@@ -450,7 +242,7 @@ export function openCoordinator(options, runtime) {
|
|
|
450
242
|
listSessions: (after) => runtime.assertOpen.pipe(Effect.andThen(readSessionInbox(options.store, after))),
|
|
451
243
|
extensions: agentExtensions(runtime),
|
|
452
244
|
session: (sessionId) => sessionFor(sessionId, undefined),
|
|
453
|
-
close: runtime.beginClose.pipe(Effect.andThen(observation.close)),
|
|
245
|
+
close: runtime.beginClose.pipe(Effect.andThen(sessionObservation.close), Effect.andThen(observation.close)),
|
|
454
246
|
});
|
|
455
247
|
return Object.freeze({
|
|
456
248
|
...agent,
|
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
import * as Effect from "effect/Effect";
|
|
2
|
+
import type { SessionFrame } from "../client.js";
|
|
2
3
|
import { ShiitakeError } from "../errors.js";
|
|
3
4
|
import type { SqliteOwner } from "./contracts.js";
|
|
5
|
+
/** The feed shares one frame's JSON and byte accounting across its watchers. */
|
|
6
|
+
export declare function encodeSessionFrame(frame: SessionFrame): Readonly<{
|
|
7
|
+
data: string;
|
|
8
|
+
bytes: number;
|
|
9
|
+
}>;
|
|
10
|
+
/** One watcher has its own bounded backlog, not its own projection producer. */
|
|
11
|
+
export type SessionFrameQueue = Readonly<{
|
|
12
|
+
take: Effect.Effect<SessionFrame | "reset" | "closed", ShiitakeError>;
|
|
13
|
+
/** Record the join before reading; retain later frames for cursor deduplication. */
|
|
14
|
+
checkpoint: (revision?: number) => void;
|
|
15
|
+
close: () => void;
|
|
16
|
+
}>;
|
|
17
|
+
/** Bounded dropping fan-out; Woodstock, never this queue, owns replay. */
|
|
18
|
+
export declare function openSessionFrameFeed(onIdle: () => void): {
|
|
19
|
+
register(includeSandboxFailures?: boolean): SessionFrameQueue;
|
|
20
|
+
publish(frame: SessionFrame, revision?: number): void;
|
|
21
|
+
end(error?: ShiitakeError): void;
|
|
22
|
+
};
|
|
4
23
|
/** Finite process-local signal; durable owner state remains authoritative. */
|
|
5
24
|
export type ObservationSignal = "closed" | "reset" | "durable" | "activity" | "progress";
|
|
6
25
|
export type ObservationReason = "durable" | "activity" | "progress" | "settlement";
|
|
@@ -8,6 +27,8 @@ export type ObservationReason = "durable" | "activity" | "progress" | "settlemen
|
|
|
8
27
|
export declare function hasOwnerObservers(store: SqliteOwner, sessionId: string): boolean;
|
|
9
28
|
/** One scoped process-local observation registration. */
|
|
10
29
|
export type ObservationRegistration = Readonly<{
|
|
30
|
+
/** Owner-hint revision for the snapshot/live join, never a wire cursor. */
|
|
31
|
+
revision: number;
|
|
11
32
|
/** Inspect and consume one pending finite signal without waiting. */
|
|
12
33
|
pollSignal: Effect.Effect<ObservationSignal | undefined>;
|
|
13
34
|
/** Await and consume one finite observation signal. */
|
|
@@ -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
|
|
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) {
|
|
@@ -169,6 +292,7 @@ export function openObservationKernel(store) {
|
|
|
169
292
|
if (active.size >= MAX_OBSERVATIONS)
|
|
170
293
|
return Effect.fail(new ShiitakeError("runtime", "rejected", "Observation capacity is exhausted"));
|
|
171
294
|
const listener = {
|
|
295
|
+
revision: 0,
|
|
172
296
|
sessionId,
|
|
173
297
|
settlementOnly: interest === "settlement",
|
|
174
298
|
closed: false,
|
|
@@ -199,6 +323,9 @@ export function openObservationKernel(store) {
|
|
|
199
323
|
});
|
|
200
324
|
});
|
|
201
325
|
return Effect.succeed(Object.freeze({
|
|
326
|
+
get revision() {
|
|
327
|
+
return listener.revision;
|
|
328
|
+
},
|
|
202
329
|
pollSignal,
|
|
203
330
|
awaitSignal,
|
|
204
331
|
close: () => {
|