@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.
- package/README.md +28 -0
- package/dist/client-wire.js +10 -13
- package/dist/client.d.ts +6 -10
- package/dist/client.js +1 -0
- package/dist/delegation.d.ts +4 -4
- package/dist/fleet.d.ts +5 -1
- package/dist/fleet.js +5 -1
- package/dist/http-routes.d.ts +4 -11
- 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 +140 -4
- package/dist/internal/run-scope.d.ts +1 -0
- package/dist/internal/run-scope.js +5 -3
- package/dist/internal/runtime-coordinator.js +12 -7
- 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/session-status.d.ts +2 -0
- package/dist/internal/session-status.js +17 -1
- 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 +47 -4
- package/dist/internal/woodstock/progress.d.ts +2 -0
- package/dist/internal/woodstock/progress.js +5 -0
- 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/dist/testing.d.ts +2 -0
- package/dist/testing.js +2 -0
- package/docs/api/agent-control.md +2 -2
- package/docs/api/client.md +28 -14
- package/docs/api/delegation.md +2 -2
- package/docs/api/fleet.md +33 -4
- package/docs/api/testing.md +11 -0
- package/package.json +5 -5
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
|
@@ -28,19 +28,15 @@ const SessionActivitySchema = Schema.Struct({
|
|
|
28
28
|
Schema.Struct({
|
|
29
29
|
state: Schema.Literal("queued"),
|
|
30
30
|
commandId: Schema.NonEmptyString,
|
|
31
|
-
runningWork: Schema.NullOr(Schema.
|
|
32
|
-
Schema.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
lane: Schema.NonEmptyString,
|
|
41
|
-
jobId: Schema.NonEmptyString,
|
|
42
|
-
}),
|
|
43
|
-
])),
|
|
31
|
+
runningWork: Schema.NullOr(Schema.Struct({
|
|
32
|
+
phase: Schema.Literals([
|
|
33
|
+
"thinking",
|
|
34
|
+
"streaming",
|
|
35
|
+
"running_tool",
|
|
36
|
+
"waiting_on_you",
|
|
37
|
+
"cancelling",
|
|
38
|
+
]),
|
|
39
|
+
})),
|
|
44
40
|
}),
|
|
45
41
|
Schema.Struct({
|
|
46
42
|
state: Schema.Literal("running"),
|
|
@@ -168,6 +164,7 @@ export const SessionSnapshotSchema = Schema.Struct({
|
|
|
168
164
|
context: Schema.optionalKey(SessionContextSchema),
|
|
169
165
|
});
|
|
170
166
|
export const SessionFrameSchema = Schema.Union([
|
|
167
|
+
Schema.Struct({ kind: Schema.Literal("heartbeat") }),
|
|
171
168
|
Schema.Struct({
|
|
172
169
|
kind: Schema.Literal("activity"),
|
|
173
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";
|
|
@@ -132,17 +133,9 @@ export type SessionTerminal = Readonly<{
|
|
|
132
133
|
*/
|
|
133
134
|
meta?: Json;
|
|
134
135
|
}>;
|
|
135
|
-
/**
|
|
136
|
+
/** The occupying work's current phase; never its identity or transcript. */
|
|
136
137
|
export type AgentRunningWork = Readonly<{
|
|
137
|
-
|
|
138
|
-
sessionId: string;
|
|
139
|
-
commandId: string;
|
|
140
|
-
/** Owner-issued display title; empty when no title is known. */
|
|
141
|
-
title: string;
|
|
142
|
-
}> | Readonly<{
|
|
143
|
-
kind: "host";
|
|
144
|
-
lane: string;
|
|
145
|
-
jobId: string;
|
|
138
|
+
phase: "thinking" | "streaming" | "running_tool" | "waiting_on_you" | "cancelling";
|
|
146
139
|
}>;
|
|
147
140
|
/**
|
|
148
141
|
* Raw facts about what a Session is doing now. Shiitake publishes facts only;
|
|
@@ -350,6 +343,9 @@ export type SessionSnapshot = Readonly<{
|
|
|
350
343
|
}>;
|
|
351
344
|
/** One bounded replay or live frame. */
|
|
352
345
|
export type SessionFrame = Readonly<{
|
|
346
|
+
/** Process-local liveness only: never a durable cursor or activity fact. */
|
|
347
|
+
kind: "heartbeat";
|
|
348
|
+
}> | Readonly<{
|
|
353
349
|
/** Process-local activity change for the Session's current queue head. */
|
|
354
350
|
kind: "activity";
|
|
355
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/fleet.d.ts
CHANGED
|
@@ -272,7 +272,11 @@ export declare function delegationSendMeta(sender: Readonly<{
|
|
|
272
272
|
* while the verified reply retains the failed recipient's identity.
|
|
273
273
|
*/
|
|
274
274
|
export declare function delegationReplyMeta(delegation: AuthenticatedDelegationReply, replierName: string): DelegationReplyMeta;
|
|
275
|
-
/**
|
|
275
|
+
/**
|
|
276
|
+
* Shape check for a one-way ancestor notice, not sender provenance. The Local
|
|
277
|
+
* mail receiver checks mesh membership before calling it. This addressed mail
|
|
278
|
+
* is still work and may steer the parent's running turn when admitted.
|
|
279
|
+
*/
|
|
276
280
|
export declare function isOneWayAncestorNotice(mail: BirdDogMail): boolean;
|
|
277
281
|
/**
|
|
278
282
|
* Delegated work an owner still waits on; `unreadable` says why its state
|
package/dist/fleet.js
CHANGED
|
@@ -361,7 +361,11 @@ export function delegationReplyMeta(delegation, replierName) {
|
|
|
361
361
|
const delegationMetadata = z.object({
|
|
362
362
|
delegation: FungiMessageMetaSchema.shape.delegation,
|
|
363
363
|
});
|
|
364
|
-
/**
|
|
364
|
+
/**
|
|
365
|
+
* Shape check for a one-way ancestor notice, not sender provenance. The Local
|
|
366
|
+
* mail receiver checks mesh membership before calling it. This addressed mail
|
|
367
|
+
* is still work and may steer the parent's running turn when admitted.
|
|
368
|
+
*/
|
|
365
369
|
export function isOneWayAncestorNotice(mail) {
|
|
366
370
|
if (mail.reply !== undefined ||
|
|
367
371
|
mail.replyTo.length !== 0 ||
|
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
|
}[];
|
|
@@ -818,14 +818,7 @@ export declare function createShiitakeHttpRoutes(resolveSession: ShiitakeSession
|
|
|
818
818
|
readonly state: "queued";
|
|
819
819
|
readonly commandId: string;
|
|
820
820
|
readonly runningWork: {
|
|
821
|
-
readonly
|
|
822
|
-
readonly sessionId: string;
|
|
823
|
-
readonly commandId: string;
|
|
824
|
-
readonly title: string;
|
|
825
|
-
} | {
|
|
826
|
-
readonly kind: "host";
|
|
827
|
-
readonly lane: string;
|
|
828
|
-
readonly jobId: string;
|
|
821
|
+
readonly phase: "thinking" | "streaming" | "running_tool" | "waiting_on_you" | "cancelling";
|
|
829
822
|
} | null;
|
|
830
823
|
} | {
|
|
831
824
|
readonly state: "running";
|
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. */
|