@fungi.computer/shiitake 0.3.1 → 0.5.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 +19 -18
- package/dist/agent-control.d.ts +24 -19
- package/dist/agent-control.js +69 -128
- package/dist/client-wire.d.ts +12 -3
- package/dist/client-wire.js +33 -8
- package/dist/client.d.ts +34 -3
- package/dist/client.js +1 -1
- package/dist/cloudflare.d.ts +3 -1
- package/dist/cloudflare.js +2 -2
- package/dist/delegation.d.ts +151 -17
- package/dist/delegation.js +174 -41
- package/dist/effect.d.ts +1 -1
- package/dist/fleet.d.ts +37 -10
- package/dist/fleet.js +217 -48
- package/dist/http-routes.d.ts +84 -12
- package/dist/http-routes.js +15 -1
- package/dist/index.d.ts +2 -2
- package/dist/internal/admission-door.d.ts +2 -0
- package/dist/internal/admission-door.js +9 -2
- package/dist/internal/client-boundary.d.ts +10 -0
- package/dist/internal/client-boundary.js +10 -0
- package/dist/internal/compaction/compaction.js +1 -1
- package/dist/internal/contracts.d.ts +2 -0
- package/dist/internal/coordinator.d.ts +3 -1
- package/dist/internal/coordinator.js +98 -13
- package/dist/internal/effect-client.d.ts +3 -1
- package/dist/internal/effect-client.js +3 -2
- package/dist/internal/effect-contracts.d.ts +6 -2
- package/dist/internal/model-acquisition.js +2 -0
- package/dist/internal/observation-kernel.d.ts +8 -4
- package/dist/internal/observation-kernel.js +44 -9
- package/dist/internal/promise-facade.d.ts +1 -1
- package/dist/internal/promise-facade.js +1 -1
- package/dist/internal/run-scope.js +37 -8
- package/dist/internal/runtime-coordinator.d.ts +5 -0
- package/dist/internal/runtime-coordinator.js +8 -4
- package/dist/internal/runtime-core.d.ts +2 -1
- package/dist/internal/runtime-core.js +6 -1
- package/dist/internal/session-context.d.ts +1 -1
- package/dist/internal/session-context.js +84 -13
- package/dist/internal/woodstock/agent-reply.d.ts +7 -2
- package/dist/internal/woodstock/agent-reply.js +22 -13
- package/dist/internal/woodstock/fleet-store.d.ts +4 -2
- package/dist/internal/woodstock/fleet-store.js +70 -6
- package/dist/internal/woodstock/index.d.ts +2 -0
- package/dist/internal/woodstock/index.js +11 -5
- package/dist/internal/woodstock/run-recovery.js +4 -1
- package/dist/internal/woodstock/session-entry.d.ts +13 -3
- package/dist/internal/woodstock/session-entry.js +1 -0
- package/dist/internal/woodstock/session-store.d.ts +4 -0
- package/dist/internal/woodstock/session-store.js +26 -19
- package/dist/remote-client.js +5 -1
- package/dist/testing.d.ts +5 -2
- package/dist/testing.js +4 -2
- package/docs/api/agent-control.md +18 -12
- package/docs/api/client.md +78 -0
- package/docs/api/delegation.md +72 -55
- package/docs/api/effect.md +5 -0
- package/docs/api/fleet.md +23 -23
- package/docs/api/http.md +2 -1
- package/docs/api/index.md +13 -0
- package/docs/api/testing.md +3 -3
- package/package.json +9 -8
package/dist/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { VerifiedAgentReply } from "./internal/woodstock/index.js";
|
|
|
5
5
|
import type { BirdDogEffect } from "@fungi.computer/bird-dog/effect";
|
|
6
6
|
import type { Accepted } from "./client.js";
|
|
7
7
|
import type { ShiitakeOptions, ShiitakeOwnerAgent } from "./internal/contracts.js";
|
|
8
|
-
export type { AbortCommand, AgentExtensionControls, BranchCommand, CompactCommand, CancelCommand, CancelResult, Accepted, ForkCommand, ForkedSession, OperatorCommand, TitleCommand, PromptCommand, PromptDelivery, SendOptions, ReadOptions, SessionCommand, SessionCommandHandler, SessionExtensionControls, SessionExtensionSlots, SessionFrame, SessionMessage, SessionProgress, SessionToolOutput, SessionContext, SessionSnapshot, SessionInboxState, SessionSummary, SessionSummaryPage, SessionSummaryRow, SessionActivity, AgentRunningWork, SessionTerminal, SessionCompactionMarker, SessionCompactionNotice, SessionCompactionTrigger, ShiitakeEventSourceConstructor, SessionWatch, ShiitakeAgent, ShiitakeSession, SubscribeOptions, TooLate, UnknownRequest, WatchOptions, } from "./client.js";
|
|
8
|
+
export type { AbortCommand, AgentExtensionControls, BranchCommand, CompactCommand, CancelCommand, CancelResult, Accepted, DelegationReceipt, Held, ForkCommand, ForkedSession, OperatorCommand, TitleCommand, PromptCommand, PromptDelivery, SendOptions, ReadOptions, SessionCommand, SessionCommandHandler, SessionExtensionControls, SessionExtensionSlots, SessionFrame, SessionMessage, SessionProgress, SessionToolOutput, SessionContext, SessionSnapshot, SessionInboxState, SessionSummary, SessionSummaryPage, SessionSummaryRow, SessionActivity, AgentRunningWork, SessionTerminal, SessionCompactionMarker, SessionCompactionNotice, SessionCompactionTrigger, ShiitakeEventSourceConstructor, SessionWatch, ShiitakeAgent, ShiitakeSession, SubscribeOptions, TooLate, UnknownRequest, WatchOptions, } from "./client.js";
|
|
9
9
|
export type { ColdSessionPage, ColdSessionPageRequest, ColdSessionRow, } from "./cold-sessions.js";
|
|
10
10
|
export { parseForkedSession, parseSessionFrame, parseSessionSnapshot, } from "./client.js";
|
|
11
11
|
export type { InstructionSnapshot, InstructionSource, ModelAcquisition, ModelDescription, ModelSource, RuntimeHost, RuntimePendingWork, SqliteCursor, SqliteOwner, SqliteValue, SqliteRow, StoragePressureCheck, CompactionBudget, CompactionOptions, GrantsDelivery, GrantsDeliveryResult, ShiitakeOptions, ShiitakeOwnerAgent, ToolAccess, HostMetaOptions, StandardHostMetaResult, StandardHostMetaSchema, ToolSource, } from "./internal/contracts.js";
|
|
@@ -22,7 +22,7 @@ export type ShiitakeHost = Readonly<{
|
|
|
22
22
|
readSettlement: (sessionId: string, commandId: string) => Promise<CommandSettlement | undefined>;
|
|
23
23
|
agent: ShiitakeOwnerAgent;
|
|
24
24
|
/** Durably admit one addressed mail item through the host owner. */
|
|
25
|
-
acceptMail: (mail: BirdDogMail, meta?: HostJson, reply?: VerifiedAgentReply) => Promise<Accepted>;
|
|
25
|
+
acceptMail: (mail: BirdDogMail, meta?: HostJson, reply?: VerifiedAgentReply, stageAcceptedNotice?: (mail: BirdDogMail) => void) => Promise<Accepted>;
|
|
26
26
|
}>;
|
|
27
27
|
/** Construct the host-only Promise facade over the canonical Effect program. */
|
|
28
28
|
export declare function makeHost(options: ShiitakeOptions, mailRoom: BirdDogEffect): Promise<ShiitakeHost>;
|
|
@@ -32,6 +32,8 @@ export type AdmitRequest = Readonly<{
|
|
|
32
32
|
* host passes it, after checking the reply against the mail it sent.
|
|
33
33
|
*/
|
|
34
34
|
reply?: VerifiedAgentReply;
|
|
35
|
+
/** Synchronous owner-local side effect committed with a mail admission. */
|
|
36
|
+
onAccepted?: (sessionId: string, commandId: string) => void;
|
|
35
37
|
}>;
|
|
36
38
|
/** Outcome of one door admission. */
|
|
37
39
|
export type AdmitResult = Readonly<{
|
|
@@ -16,9 +16,14 @@ function decodeReply(request) {
|
|
|
16
16
|
return undefined;
|
|
17
17
|
const reply = requireDecoded(decodeVerifiedReply(request.reply), "Verified Agent reply is invalid");
|
|
18
18
|
const mail = request.mail;
|
|
19
|
+
const agentAuthored = mail?.from.kind === "agent" &&
|
|
20
|
+
mail.from.agentId === reply.agentId &&
|
|
21
|
+
mail.from.sessionId === reply.sessionId;
|
|
22
|
+
const systemUndeliverable = mail?.from.kind === "system" &&
|
|
23
|
+
reply.outcome === "undeliverable" &&
|
|
24
|
+
reply.reason === "recipient_offline";
|
|
19
25
|
if (mail?.replyTo.length !== 0 ||
|
|
20
|
-
|
|
21
|
-
mail.from.sessionId !== reply.sessionId ||
|
|
26
|
+
(!agentAuthored && !systemUndeliverable) ||
|
|
22
27
|
request.command.type !== "prompt" ||
|
|
23
28
|
request.command.delivery !== "steer")
|
|
24
29
|
throw new ShiitakeError("contract", "invalid", "Verified Agent reply does not match its mail");
|
|
@@ -35,6 +40,7 @@ export function openAdmissionDoor(input) {
|
|
|
35
40
|
when: request.when,
|
|
36
41
|
mail: request.mail,
|
|
37
42
|
reply: decodeReply(request),
|
|
43
|
+
onAccepted: request.onAccepted,
|
|
38
44
|
meta: request.meta === undefined
|
|
39
45
|
? undefined
|
|
40
46
|
: checkHostMeta(input.hostMeta, request.meta),
|
|
@@ -69,6 +75,7 @@ export function openAdmissionDoor(input) {
|
|
|
69
75
|
input.enqueue(work, request.mail);
|
|
70
76
|
}
|
|
71
77
|
: undefined, targetWorkId, request.reply);
|
|
78
|
+
request.onAccepted?.(sessionId, accepted.accepted.commandId);
|
|
72
79
|
touched.set(sessionId, (touched.get(sessionId) ?? false) || accepted.delivery === "new");
|
|
73
80
|
return Object.freeze({
|
|
74
81
|
status: "accepted",
|
|
@@ -8,6 +8,16 @@ export declare const PromptImageSchema: Schema.Struct<{
|
|
|
8
8
|
}>;
|
|
9
9
|
/** Delivery policy for input accepted while a Session run is active. */
|
|
10
10
|
export type PromptDelivery = "steer" | "followUp";
|
|
11
|
+
/** One bounded exact-command read, shared by the in-process and HTTP doors. */
|
|
12
|
+
export declare const decodeClientSettlementRequest: (input: unknown, options?: import("effect/SchemaAST").ParseOptions) => Result.Result<{
|
|
13
|
+
readonly commandId: string;
|
|
14
|
+
readonly timeoutMs: number;
|
|
15
|
+
}, Schema.SchemaError>;
|
|
16
|
+
/** Query encoding of the same bounded exact-command read. */
|
|
17
|
+
export declare const decodeClientSettlementQuery: (input: unknown, options?: import("effect/SchemaAST").ParseOptions) => Result.Result<{
|
|
18
|
+
readonly commandId: string;
|
|
19
|
+
readonly timeoutMs: number;
|
|
20
|
+
}, Schema.SchemaError>;
|
|
11
21
|
declare const SessionCreationRequestSchema: Schema.Struct<{
|
|
12
22
|
readonly requestId: Schema.String;
|
|
13
23
|
}>;
|
|
@@ -27,6 +27,16 @@ export const PromptImageSchema = Schema.Struct({
|
|
|
27
27
|
const PromptImagesSchema = Schema.Array(PromptImageSchema).check(Schema.isMinLength(1), Schema.isMaxLength(8));
|
|
28
28
|
const PromptDeliverySchema = Schema.Literals(["steer", "followUp"]);
|
|
29
29
|
const SessionIdSchema = boundedId;
|
|
30
|
+
/** One bounded exact-command read, shared by the in-process and HTTP doors. */
|
|
31
|
+
export const decodeClientSettlementRequest = Schema.decodeUnknownResult(Schema.Struct({
|
|
32
|
+
commandId: boundedId,
|
|
33
|
+
timeoutMs: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0), Schema.isLessThanOrEqualTo(30_000)),
|
|
34
|
+
}), { onExcessProperty: "error" });
|
|
35
|
+
/** Query encoding of the same bounded exact-command read. */
|
|
36
|
+
export const decodeClientSettlementQuery = Schema.decodeUnknownResult(Schema.Struct({
|
|
37
|
+
commandId: boundedId,
|
|
38
|
+
timeoutMs: Schema.NumberFromString.check(Schema.isInt(), Schema.isBetween({ minimum: 0, maximum: 30_000 })),
|
|
39
|
+
}), { onExcessProperty: "error" });
|
|
30
40
|
const decodeSessionPageCursorValue = Schema.decodeUnknownResult(boundedText(512));
|
|
31
41
|
const SessionCreationRequestSchema = Schema.Struct({ requestId: boundedId });
|
|
32
42
|
const PromptCommandSchema = Schema.Struct({
|
|
@@ -8,7 +8,7 @@ import { uuidv7 } from "@earendil-works/pi-ai/utils/uuid";
|
|
|
8
8
|
import * as Effect from "effect/Effect";
|
|
9
9
|
import { ShiitakeError } from "../../errors.js";
|
|
10
10
|
import { computeFileLists, createFileOps, extractFileOpsFromMessage, formatFileOperations, serializeConversation, SUMMARIZATION_PROMPT, SUMMARIZATION_SYSTEM_PROMPT, TURN_PREFIX_SUMMARIZATION_PROMPT, UPDATE_SUMMARIZATION_PROMPT, compactionSummaryText, summaryFailure, } from "./utils.js";
|
|
11
|
-
/** A summary
|
|
11
|
+
/** A summary uses the stream failure diagnostic, never failed-turn provider prose. */
|
|
12
12
|
function failure(message) {
|
|
13
13
|
return new ShiitakeError("model", "rejected", message);
|
|
14
14
|
}
|
|
@@ -47,6 +47,8 @@ export type ModelAcquisition = Readonly<{
|
|
|
47
47
|
* Unknown messages must return undefined.
|
|
48
48
|
*/
|
|
49
49
|
failureReason?: (message: AssistantMessage) => string | undefined;
|
|
50
|
+
/** Provider prose for this door's exact failed turn; never a diagnostic. */
|
|
51
|
+
providerError?: (message: AssistantMessage) => string | undefined;
|
|
50
52
|
/** Release the acquired model resources. */
|
|
51
53
|
release: () => Promise<void>;
|
|
52
54
|
}>;
|
|
@@ -5,6 +5,7 @@ import { ShiitakeError } from "../errors.js";
|
|
|
5
5
|
import type { Json } from "../extensions.js";
|
|
6
6
|
import type { ShiitakeEffectOwner, ShiitakeEffectOptions } from "./effect-contracts.js";
|
|
7
7
|
import type { RuntimeCoordinator } from "./runtime-coordinator.js";
|
|
8
|
+
import { type GrantsDelivery } from "./woodstock/index.js";
|
|
8
9
|
/** Construct the Effect-native coordinator over the private owner modules. */
|
|
9
10
|
export declare function openCoordinator(options: ShiitakeEffectOptions, runtime: RuntimeCoordinator): ShiitakeEffectOwner;
|
|
10
11
|
/**
|
|
@@ -12,7 +13,8 @@ export declare function openCoordinator(options: ShiitakeEffectOptions, runtime:
|
|
|
12
13
|
* is the host's authentication of a delegated Agent's reply; only then does
|
|
13
14
|
* the model see the line inside the verified-reply tag.
|
|
14
15
|
*/
|
|
15
|
-
export declare function acceptAddressedMail(runtime: RuntimeCoordinator, mail: BirdDogMail, meta?: Json, reply?: VerifiedAgentReply): Effect.Effect<Readonly<{
|
|
16
|
+
export declare function acceptAddressedMail(runtime: RuntimeCoordinator, mail: BirdDogMail, meta?: Json, reply?: VerifiedAgentReply, stageMail?: (mail: BirdDogMail) => void, sessionGrantDelivery?: (sessionId: string) => GrantsDelivery): Effect.Effect<Readonly<{
|
|
16
17
|
status: "accepted";
|
|
17
18
|
commandId: string;
|
|
19
|
+
sessionId?: string;
|
|
18
20
|
}>, ShiitakeError, never>;
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { nanoid } from "@fungi.computer/cairn";
|
|
2
2
|
import { parseModelPreference } from "./model-preference.js";
|
|
3
|
+
import { acceptedNotice } from "@fungi.computer/bird-dog/effect";
|
|
3
4
|
import * as Effect from "effect/Effect";
|
|
4
5
|
import * as Option from "effect/Option";
|
|
5
6
|
import * as Stream from "effect/Stream";
|
|
6
7
|
import { ShiitakeError } from "../errors.js";
|
|
7
|
-
import { decodeClientReadOptions, decodeClientSessionCommand, decodeClientSessionId, decodeClientSubscribeOptions, } from "./client-boundary.js";
|
|
8
|
+
import { decodeClientReadOptions, decodeClientSettlementRequest, requireDecoded, decodeClientSessionCommand, decodeClientSessionId, decodeClientSubscribeOptions, } from "./client-boundary.js";
|
|
8
9
|
import { mergeObservationSignals, openObservationKernel, publishOwnerObservation, } from "./observation-kernel.js";
|
|
9
10
|
import { checkHostMeta } from "./host-meta.js";
|
|
10
11
|
import { readSessionActivity } from "./session-status.js";
|
|
11
12
|
import { readSessionContext } from "./session-context.js";
|
|
12
|
-
import { abortSession, cancelSession, branchSession, recordOperatorLine, forkSession, readSession, readSessionModelPreference, readSessionReplayPage, readSessionInbox, readLatestPendingPrompt, createSession, deleteSession, deliverSessionGrants, readSessionGrants, setSessionModelPreference, setSessionTitle, readAdmissionIdentity, assertAdmissionIdentity, } from "./woodstock/index.js";
|
|
13
|
+
import { abortSession, cancelSession, branchSession, recordOperatorLine, forkSession, readSession, readCommandSettlement, readSessionModelPreference, readSessionReplayPage, readSessionInbox, readLatestPendingPrompt, createSession, deleteSession, deliverSessionGrants, readSessionGrants, setSessionModelPreference, setSessionTitle, readAdmissionIdentity, assertAdmissionIdentity, } from "./woodstock/index.js";
|
|
13
14
|
/** Generic Agent-scope extension controls over the runtime's one call door. */
|
|
14
15
|
function agentExtensions(runtime) {
|
|
15
16
|
return Object.freeze({
|
|
@@ -33,7 +34,7 @@ function withContext(snapshot, context) {
|
|
|
33
34
|
}
|
|
34
35
|
/** Read the baseline and its context use for one Session. */
|
|
35
36
|
function readSessionWithContext(options, runtime, sessionId, input) {
|
|
36
|
-
return currentSessionActivity(options, runtime, sessionId).pipe(Effect.flatMap((activity) => readSession(options.store, sessionId, activity, input, () => runtime.extensions.project(sessionId))), Effect.flatMap((snapshot) => readSessionContext(options, sessionId).pipe(Effect.map((context) => withContext(snapshot, context)))));
|
|
37
|
+
return currentSessionActivity(options, runtime, sessionId).pipe(Effect.flatMap((activity) => readSession(options.store, sessionId, activity, input, () => runtime.extensions.project(sessionId))), Effect.flatMap((snapshot) => readSessionContext(options, sessionId, snapshot.cursor, input?.before === undefined).pipe(Effect.map((context) => withContext(snapshot, context)))));
|
|
37
38
|
}
|
|
38
39
|
function sessionOwnerOperation(runtime, sessionId, operation) {
|
|
39
40
|
return runtime.assertOpen.pipe(Effect.andThen(Effect.try({
|
|
@@ -106,6 +107,12 @@ function subscriptionFrames(options, runtime, registration, sessionId, after, de
|
|
|
106
107
|
if (signal === "reset")
|
|
107
108
|
return resetFrame(options, runtime, sessionId, includeSandboxFailures).pipe(Effect.map(continueFromReset));
|
|
108
109
|
return runtime.assertOpen.pipe(Effect.andThen(currentSessionActivity(options, runtime, sessionId)), Effect.flatMap((activity) => {
|
|
110
|
+
registration.setQueued(activity.facts.queue.state === "queued");
|
|
111
|
+
if (signal === "activity" && state.delivery === "live")
|
|
112
|
+
return Effect.succeed([
|
|
113
|
+
[{ kind: "activity", activity: activity.facts }],
|
|
114
|
+
Option.some({ ...state, awaitSignal: true }),
|
|
115
|
+
]);
|
|
109
116
|
if (signal === "progress" && state.delivery === "live") {
|
|
110
117
|
return Effect.succeed([
|
|
111
118
|
[
|
|
@@ -132,6 +139,7 @@ function subscriptionFrames(options, runtime, registration, sessionId, after, de
|
|
|
132
139
|
return resetFrame(options, runtime, sessionId, includeSandboxFailures).pipe(Effect.map(continueFromReset));
|
|
133
140
|
const latestCursor = page.frames
|
|
134
141
|
.flatMap((frame) => frame.kind === "progress" ||
|
|
142
|
+
frame.kind === "activity" ||
|
|
135
143
|
frame.kind === "tool_output"
|
|
136
144
|
? []
|
|
137
145
|
: [frame.cursor])
|
|
@@ -141,13 +149,18 @@ function subscriptionFrames(options, runtime, registration, sessionId, after, de
|
|
|
141
149
|
delivery: page.hasMore ? state.delivery : "live",
|
|
142
150
|
awaitSignal: !page.hasMore,
|
|
143
151
|
};
|
|
144
|
-
//
|
|
145
|
-
|
|
146
|
-
|
|
152
|
+
// Only a final update that moves the cursor gets a
|
|
153
|
+
// figure. A wake that moved nothing never re-reads the
|
|
154
|
+
// whole kept window.
|
|
155
|
+
const movedHistory = page.frames.some((frame) => frame.kind === "update" &&
|
|
156
|
+
frame.cursor !== state.after);
|
|
157
|
+
return (movedHistory && !page.hasMore
|
|
158
|
+
? readSessionContext(options, sessionId, latestCursor ?? null)
|
|
147
159
|
: Effect.succeed(undefined)).pipe(Effect.map((context) => [
|
|
148
160
|
context === undefined
|
|
149
161
|
? page.frames
|
|
150
|
-
: page.frames.map((frame) => frame.kind === "update"
|
|
162
|
+
: page.frames.map((frame) => frame.kind === "update" &&
|
|
163
|
+
frame.cursor === latestCursor
|
|
151
164
|
? Object.freeze({ ...frame, context })
|
|
152
165
|
: frame),
|
|
153
166
|
Option.some(next),
|
|
@@ -173,11 +186,14 @@ function decodeWatchInput(sessionId, input) {
|
|
|
173
186
|
function watchSession(options, runtime, observation, sessionId, input) {
|
|
174
187
|
return runtime.assertOpen.pipe(Effect.andThen(decodeWatchInput(sessionId, input)), Effect.flatMap((decoded) => Effect.uninterruptibleMask((restore) => Effect.gen(function* () {
|
|
175
188
|
const registration = yield* observation.register(decoded.sessionId);
|
|
176
|
-
return yield* restore(runtime.assertOpen.pipe(Effect.andThen(readSessionWithContext(options, runtime, decoded.sessionId, decoded.options)), Effect.map((snapshot) =>
|
|
177
|
-
snapshot
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
189
|
+
return yield* restore(runtime.assertOpen.pipe(Effect.andThen(readSessionWithContext(options, runtime, decoded.sessionId, decoded.options)), Effect.map((snapshot) => {
|
|
190
|
+
registration.setQueued(snapshot.activity.queue.state === "queued");
|
|
191
|
+
return Object.freeze({
|
|
192
|
+
snapshot,
|
|
193
|
+
changes: subscriptionFrames(options, runtime, registration, decoded.sessionId, snapshot.cursor ?? undefined, "live", decoded.options.includeSandboxFailures === true).pipe(Stream.ensuring(Effect.sync(registration.close))),
|
|
194
|
+
close: Effect.sync(registration.close),
|
|
195
|
+
});
|
|
196
|
+
}))).pipe(Effect.onError(() => Effect.sync(registration.close)));
|
|
181
197
|
}))));
|
|
182
198
|
}
|
|
183
199
|
/**
|
|
@@ -335,6 +351,35 @@ export function openCoordinator(options, runtime) {
|
|
|
335
351
|
call: (command, input) => runtime.assertOpen.pipe(Effect.andThen(runtime.extensions.call({ scope: "session", sessionId }, extensionId, command, input))),
|
|
336
352
|
}),
|
|
337
353
|
read: (input) => runtime.assertOpen.pipe(Effect.andThen(readSessionWithContext(options, runtime, sessionId, input))),
|
|
354
|
+
readSettlement: (commandId, timeoutMs = 0) => Effect.scoped(Effect.gen(function* () {
|
|
355
|
+
yield* runtime.assertOpen;
|
|
356
|
+
const decoded = yield* Effect.try({
|
|
357
|
+
try: () => ({
|
|
358
|
+
sessionId: decodeClientSessionId(sessionId),
|
|
359
|
+
...requireDecoded(decodeClientSettlementRequest({ commandId, timeoutMs }), "Settlement request is invalid"),
|
|
360
|
+
}),
|
|
361
|
+
catch: (cause) => cause instanceof ShiitakeError
|
|
362
|
+
? cause
|
|
363
|
+
: new ShiitakeError("contract", "invalid", "Settlement request is invalid"),
|
|
364
|
+
});
|
|
365
|
+
// Subscribe before reading: a commit racing the first read cannot
|
|
366
|
+
// be missed. Only settlement hints reach this registration.
|
|
367
|
+
const registration = yield* Effect.acquireRelease(observation.register(decoded.sessionId, "settlement"), (value) => Effect.sync(value.close));
|
|
368
|
+
const read = readCommandSettlement(options.store, decoded.sessionId, decoded.commandId);
|
|
369
|
+
const initial = yield* read;
|
|
370
|
+
if (initial !== undefined || decoded.timeoutMs === 0)
|
|
371
|
+
return initial;
|
|
372
|
+
return yield* Effect.gen(function* () {
|
|
373
|
+
for (;;) {
|
|
374
|
+
const hint = yield* registration.awaitSignal;
|
|
375
|
+
if (hint === "closed")
|
|
376
|
+
return yield* Effect.fail(new ShiitakeError("runtime", "closed", "Shiitake is closed"));
|
|
377
|
+
const settled = yield* read;
|
|
378
|
+
if (settled !== undefined)
|
|
379
|
+
return settled;
|
|
380
|
+
}
|
|
381
|
+
}).pipe(Effect.timeoutOption(decoded.timeoutMs), Effect.map(Option.getOrUndefined));
|
|
382
|
+
})),
|
|
338
383
|
subscribe: (input) => subscribeSession(options, runtime, observation, sessionId, input),
|
|
339
384
|
watch: (input) => watchSession(options, runtime, observation, sessionId, input),
|
|
340
385
|
});
|
|
@@ -370,7 +415,47 @@ export function openCoordinator(options, runtime) {
|
|
|
370
415
|
* is the host's authentication of a delegated Agent's reply; only then does
|
|
371
416
|
* the model see the line inside the verified-reply tag.
|
|
372
417
|
*/
|
|
373
|
-
export function acceptAddressedMail(runtime, mail, meta, reply) {
|
|
418
|
+
export function acceptAddressedMail(runtime, mail, meta, reply, stageMail, sessionGrantDelivery) {
|
|
419
|
+
if (mail.to.sessionId === undefined) {
|
|
420
|
+
if (reply !== undefined || mail.replyTo.length === 0)
|
|
421
|
+
throw new Error("Only delegated work can create a recipient Session");
|
|
422
|
+
return runtime.door.transact((admit) => {
|
|
423
|
+
const sessionId = runtime.createSessionWithinTransaction(mail.key);
|
|
424
|
+
const grants = sessionGrantDelivery?.(sessionId);
|
|
425
|
+
if (grants !== undefined)
|
|
426
|
+
runtime.stageSessionGrantsWithinTransaction(sessionId, grants);
|
|
427
|
+
const addressed = {
|
|
428
|
+
...mail,
|
|
429
|
+
to: { ...mail.to, sessionId },
|
|
430
|
+
};
|
|
431
|
+
const request = {
|
|
432
|
+
sessionId,
|
|
433
|
+
requireExistingSession: true,
|
|
434
|
+
command: {
|
|
435
|
+
requestId: mail.key,
|
|
436
|
+
delivery: "followUp",
|
|
437
|
+
type: "prompt",
|
|
438
|
+
text: mail.content,
|
|
439
|
+
},
|
|
440
|
+
when: "now",
|
|
441
|
+
origin: { kind: "user" },
|
|
442
|
+
mail: addressed,
|
|
443
|
+
onAccepted: (_acceptedSessionId, commandId) => {
|
|
444
|
+
if (stageMail === undefined)
|
|
445
|
+
throw new Error("Recipient Agent mail outbox is unavailable");
|
|
446
|
+
stageMail(acceptedNotice(mail, sessionId, commandId, Date.now()));
|
|
447
|
+
},
|
|
448
|
+
};
|
|
449
|
+
const result = admit(meta === undefined ? request : { ...request, meta });
|
|
450
|
+
if (result.status !== "accepted")
|
|
451
|
+
throw new Error("Sessionless addressed mail was not admitted");
|
|
452
|
+
return {
|
|
453
|
+
status: "accepted",
|
|
454
|
+
commandId: result.commandId,
|
|
455
|
+
sessionId,
|
|
456
|
+
};
|
|
457
|
+
});
|
|
458
|
+
}
|
|
374
459
|
return admitUserWork(runtime, mail.to.sessionId, {
|
|
375
460
|
requestId: mail.key,
|
|
376
461
|
delivery: reply === undefined ? "followUp" : "steer",
|
|
@@ -5,7 +5,9 @@ import type { ShiitakeOwnerAgent, ShiitakeOptions } from "./contracts.js";
|
|
|
5
5
|
/** Adapt a Promise tool configuration for both initial open and reload. */
|
|
6
6
|
export declare function liftTools(toolSource: NonNullable<ShiitakeOptions["tools"]>): NonNullable<ShiitakeEffectOptions["tools"]>;
|
|
7
7
|
/** Run one Shiitake Effect through its sole Promise error adaptation. */
|
|
8
|
-
export declare function runShiitakePromise<A, E>(effect: Effect.Effect<A, E
|
|
8
|
+
export declare function runShiitakePromise<A, E>(effect: Effect.Effect<A, E>, options?: Readonly<{
|
|
9
|
+
signal?: AbortSignal;
|
|
10
|
+
}>): Promise<A>;
|
|
9
11
|
/** Adapt one canonical Effect agent to Shiitake's ordinary web interface. */
|
|
10
12
|
export declare function toPromiseAgent(agent: ShiitakeEffectAgent, suppliedAddress?: string): ShiitakeAgent;
|
|
11
13
|
/**
|
|
@@ -19,8 +19,8 @@ export function liftTools(toolSource) {
|
|
|
19
19
|
: { ...tools, execution: toolSource.execution };
|
|
20
20
|
}
|
|
21
21
|
/** Run one Shiitake Effect through its sole Promise error adaptation. */
|
|
22
|
-
export function runShiitakePromise(effect) {
|
|
23
|
-
return Effect.runPromiseExit(effect).then((exit) => {
|
|
22
|
+
export function runShiitakePromise(effect, options) {
|
|
23
|
+
return Effect.runPromiseExit(effect, options).then((exit) => {
|
|
24
24
|
if (Exit.isSuccess(exit))
|
|
25
25
|
return exit.value;
|
|
26
26
|
const failure = Option.getOrUndefined(Cause.findErrorOption(exit.cause));
|
|
@@ -76,6 +76,7 @@ function sessionFacade(address, session) {
|
|
|
76
76
|
});
|
|
77
77
|
},
|
|
78
78
|
read: (input) => runShiitakePromise(session.read(input)),
|
|
79
|
+
readSettlement: (commandId, options) => runShiitakePromise(session.readSettlement(commandId, options?.timeoutMs), options),
|
|
79
80
|
subscribe: (input) => observationAsyncIterable(session.subscribe(input)),
|
|
80
81
|
watch: (input) => runShiitakePromise(session.watch(input)).then((watch) => Object.freeze({
|
|
81
82
|
snapshot: watch.snapshot,
|
|
@@ -2,7 +2,7 @@ import type { HostMetaOptions } from "./host-meta.js";
|
|
|
2
2
|
import type { ExtensionRosterV0 } from "../extensions.js";
|
|
3
3
|
import type { Api, AssistantMessage, Model, MuleStreamFunction, RetryPolicy } from "@fungi.computer/mule/effect";
|
|
4
4
|
import type { CapabilitySource, ExecutionOptions, ModuleRegistration } from "@fungi.computer/mycelium";
|
|
5
|
-
import type { GrantsDelivery, GrantsDeliveryResult, ToolAccess } from "./contracts.js";
|
|
5
|
+
import type { CommandSettlement, GrantsDelivery, GrantsDeliveryResult, ToolAccess } from "./contracts.js";
|
|
6
6
|
import type { Sandbox } from "@fungi.computer/mycelium/effect";
|
|
7
7
|
import * as Effect from "effect/Effect";
|
|
8
8
|
import * as Stream from "effect/Stream";
|
|
@@ -26,6 +26,8 @@ export type ModelAcquisitionEffect = Readonly<{
|
|
|
26
26
|
retryable: (message: AssistantMessage) => boolean;
|
|
27
27
|
/** Owner-authored reason for this door's failure message, never SDK prose. */
|
|
28
28
|
failureReason?: (message: AssistantMessage) => string | undefined;
|
|
29
|
+
/** Provider prose for this door's exact failed turn; never a diagnostic. */
|
|
30
|
+
providerError?: (message: AssistantMessage) => string | undefined;
|
|
29
31
|
/** Effect finalizer for acquired model resources. */
|
|
30
32
|
release: Effect.Effect<void, ShiitakeError>;
|
|
31
33
|
}>;
|
|
@@ -128,7 +130,7 @@ export type ShiitakeEffectOptions = Readonly<{
|
|
|
128
130
|
retry?: RetryPolicy;
|
|
129
131
|
/**
|
|
130
132
|
* Maximum silence between live provider stream events, in milliseconds.
|
|
131
|
-
* Omitted
|
|
133
|
+
* Omitted uses Mule's model stream limits.
|
|
132
134
|
*/
|
|
133
135
|
modelStreamIdleTimeoutMs?: number;
|
|
134
136
|
}>;
|
|
@@ -189,6 +191,8 @@ export type ShiitakeEffectSession = Readonly<{
|
|
|
189
191
|
}>;
|
|
190
192
|
/** Read a bounded durable baseline. */
|
|
191
193
|
read: (input?: ReadOptions) => Effect.Effect<SessionSnapshot, ShiitakeError>;
|
|
194
|
+
/** Exact command facts; timeout releases observation without cancelling work. */
|
|
195
|
+
readSettlement: (commandId: string, timeoutMs?: number) => Effect.Effect<CommandSettlement | undefined, ShiitakeError>;
|
|
192
196
|
/** Subscribe to strict-after replay and live frames. */
|
|
193
197
|
subscribe: (input: SubscribeOptions) => Stream.Stream<SessionFrame, ShiitakeError>;
|
|
194
198
|
/** Open a race-safe baseline plus live changes resource. */
|
|
@@ -21,6 +21,8 @@ function asEffectAcquisition(acquisition) {
|
|
|
21
21
|
};
|
|
22
22
|
if (acquisition.failureReason !== undefined)
|
|
23
23
|
shared = { ...shared, failureReason: acquisition.failureReason };
|
|
24
|
+
if (acquisition.providerError !== undefined)
|
|
25
|
+
shared = { ...shared, providerError: acquisition.providerError };
|
|
24
26
|
if (acquisition.stream === undefined)
|
|
25
27
|
return acquisition.retryAfterMs === undefined
|
|
26
28
|
? Object.freeze(shared)
|
|
@@ -2,8 +2,8 @@ import * as Effect from "effect/Effect";
|
|
|
2
2
|
import { ShiitakeError } from "../errors.js";
|
|
3
3
|
import type { SqliteOwner } from "./contracts.js";
|
|
4
4
|
/** Finite process-local signal; durable owner state remains authoritative. */
|
|
5
|
-
export type ObservationSignal = "closed" | "reset" | "durable" | "progress";
|
|
6
|
-
export type ObservationReason = "durable" | "progress";
|
|
5
|
+
export type ObservationSignal = "closed" | "reset" | "durable" | "activity" | "progress";
|
|
6
|
+
export type ObservationReason = "durable" | "activity" | "progress" | "settlement";
|
|
7
7
|
/** One scoped process-local observation registration. */
|
|
8
8
|
export type ObservationRegistration = Readonly<{
|
|
9
9
|
/** Inspect and consume one pending finite signal without waiting. */
|
|
@@ -12,17 +12,21 @@ export type ObservationRegistration = Readonly<{
|
|
|
12
12
|
awaitSignal: Effect.Effect<ObservationSignal>;
|
|
13
13
|
/** Release this registration without affecting durable work. */
|
|
14
14
|
close: () => void;
|
|
15
|
+
/** Track whether this Session's current queue head is waiting on the Agent. */
|
|
16
|
+
setQueued: (queued: boolean) => void;
|
|
15
17
|
}>;
|
|
16
18
|
/** Process-local wake coordination; durable facts remain authoritative. */
|
|
17
19
|
export type ObservationKernel = Readonly<{
|
|
18
20
|
/** Register before the caller's first durable read. */
|
|
19
|
-
register: (sessionId: string) => Effect.Effect<ObservationRegistration, ShiitakeError>;
|
|
21
|
+
register: (sessionId: string, interest?: "settlement") => Effect.Effect<ObservationRegistration, ShiitakeError>;
|
|
20
22
|
/** Close every registration and release this agent's owner publisher. */
|
|
21
23
|
close: Effect.Effect<void>;
|
|
22
24
|
}>;
|
|
23
25
|
/** Combine coalesced reasons without allowing a progress hint to hide a commit. */
|
|
24
26
|
export declare function mergeObservationSignals(first: ObservationSignal, second: ObservationSignal): ObservationSignal;
|
|
25
|
-
/** Notify
|
|
27
|
+
/** Notify observers of one Session's durable or process-local change. */
|
|
26
28
|
export declare function publishOwnerObservation(store: SqliteOwner, sessionId: string | null, reason?: ObservationReason): void;
|
|
29
|
+
/** Wake queued Sessions when this Agent's execution slot changes. */
|
|
30
|
+
export declare function publishAgentExecutionChange(store: SqliteOwner): void;
|
|
27
31
|
/** Open one coalescing process-local observation kernel per agent. */
|
|
28
32
|
export declare function openObservationKernel(store: SqliteOwner): ObservationKernel;
|
|
@@ -12,6 +12,8 @@ export function mergeObservationSignals(first, second) {
|
|
|
12
12
|
return "reset";
|
|
13
13
|
if (first === "durable" || second === "durable")
|
|
14
14
|
return "durable";
|
|
15
|
+
if (first === "activity" || second === "activity")
|
|
16
|
+
return "activity";
|
|
15
17
|
return "progress";
|
|
16
18
|
}
|
|
17
19
|
function queueObservation(listener, reason) {
|
|
@@ -25,7 +27,9 @@ function queueObservation(listener, reason) {
|
|
|
25
27
|
listener.pendingReason =
|
|
26
28
|
listener.pendingReason === "durable" || reason === "durable"
|
|
27
29
|
? "durable"
|
|
28
|
-
: "
|
|
30
|
+
: listener.pendingReason === "activity" || reason === "activity"
|
|
31
|
+
? "activity"
|
|
32
|
+
: "progress";
|
|
29
33
|
}
|
|
30
34
|
function deliverObservation(listener, reason) {
|
|
31
35
|
if (listener.closed || listener.reset)
|
|
@@ -46,14 +50,14 @@ function deliverObservation(listener, reason) {
|
|
|
46
50
|
listener.reset = true;
|
|
47
51
|
}
|
|
48
52
|
}
|
|
49
|
-
/** Notify
|
|
53
|
+
/** Notify observers of one Session's durable or process-local change. */
|
|
50
54
|
export function publishOwnerObservation(store, sessionId, reason = "durable") {
|
|
51
55
|
const publishers = ownerPublishers.get(store);
|
|
52
56
|
if (publishers === undefined)
|
|
53
57
|
return;
|
|
54
58
|
for (const publish of publishers) {
|
|
55
59
|
try {
|
|
56
|
-
publish(sessionId, reason);
|
|
60
|
+
publish({ kind: "session", sessionId, reason });
|
|
57
61
|
}
|
|
58
62
|
catch (cause) {
|
|
59
63
|
reportCatchFailure("owner-observation-publish", cause);
|
|
@@ -61,6 +65,20 @@ export function publishOwnerObservation(store, sessionId, reason = "durable") {
|
|
|
61
65
|
}
|
|
62
66
|
}
|
|
63
67
|
}
|
|
68
|
+
/** Wake queued Sessions when this Agent's execution slot changes. */
|
|
69
|
+
export function publishAgentExecutionChange(store) {
|
|
70
|
+
const publishers = ownerPublishers.get(store);
|
|
71
|
+
if (publishers === undefined)
|
|
72
|
+
return;
|
|
73
|
+
for (const publish of publishers) {
|
|
74
|
+
try {
|
|
75
|
+
publish({ kind: "agent-execution" });
|
|
76
|
+
}
|
|
77
|
+
catch (cause) {
|
|
78
|
+
reportCatchFailure("owner-execution-change-publish", cause);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
64
82
|
/** Open one coalescing process-local observation kernel per agent. */
|
|
65
83
|
export function openObservationKernel(store) {
|
|
66
84
|
const sessions = new Map();
|
|
@@ -79,7 +97,7 @@ export function openObservationKernel(store) {
|
|
|
79
97
|
listener.pendingCount = 0;
|
|
80
98
|
const reason = listener.pendingReason;
|
|
81
99
|
listener.pendingReason = undefined;
|
|
82
|
-
return reason
|
|
100
|
+
return reason ?? "progress";
|
|
83
101
|
};
|
|
84
102
|
const closeListener = (listener) => {
|
|
85
103
|
if (listener.closed)
|
|
@@ -104,26 +122,39 @@ export function openObservationKernel(store) {
|
|
|
104
122
|
}
|
|
105
123
|
}
|
|
106
124
|
};
|
|
107
|
-
const publish = (
|
|
108
|
-
|
|
125
|
+
const publish = (publication) => {
|
|
126
|
+
if (publication.kind === "agent-execution") {
|
|
127
|
+
for (const listener of active)
|
|
128
|
+
if (listener.queued && !listener.settlementOnly)
|
|
129
|
+
deliverObservation(listener, "activity");
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const listeners = publication.sessionId === null
|
|
133
|
+
? active
|
|
134
|
+
: sessions.get(publication.sessionId);
|
|
109
135
|
if (listeners === undefined)
|
|
110
136
|
return;
|
|
111
|
-
for (const listener of listeners)
|
|
112
|
-
|
|
137
|
+
for (const listener of listeners) {
|
|
138
|
+
if (listener.settlementOnly && publication.reason !== "settlement")
|
|
139
|
+
continue;
|
|
140
|
+
deliverObservation(listener, publication.reason === "settlement" ? "durable" : publication.reason);
|
|
141
|
+
}
|
|
113
142
|
};
|
|
114
143
|
publishers.add(publish);
|
|
115
144
|
ownerPublishers.set(store, publishers);
|
|
116
145
|
return Object.freeze({
|
|
117
|
-
register: (sessionId) => Effect.suspend(() => {
|
|
146
|
+
register: (sessionId, interest) => Effect.suspend(() => {
|
|
118
147
|
if (released)
|
|
119
148
|
return Effect.fail(new ShiitakeError("runtime", "closed", "Shiitake is closed"));
|
|
120
149
|
if (active.size >= MAX_OBSERVATIONS)
|
|
121
150
|
return Effect.fail(new ShiitakeError("runtime", "rejected", "Observation capacity is exhausted"));
|
|
122
151
|
const listener = {
|
|
123
152
|
sessionId,
|
|
153
|
+
settlementOnly: interest === "settlement",
|
|
124
154
|
closed: false,
|
|
125
155
|
pendingCount: 0,
|
|
126
156
|
pendingReason: undefined,
|
|
157
|
+
queued: true,
|
|
127
158
|
reset: false,
|
|
128
159
|
wake: undefined,
|
|
129
160
|
};
|
|
@@ -153,6 +184,10 @@ export function openObservationKernel(store) {
|
|
|
153
184
|
close: () => {
|
|
154
185
|
closeListener(listener);
|
|
155
186
|
},
|
|
187
|
+
setQueued: (queued) => {
|
|
188
|
+
if (!listener.closed)
|
|
189
|
+
listener.queued = queued;
|
|
190
|
+
},
|
|
156
191
|
}));
|
|
157
192
|
}),
|
|
158
193
|
close: Effect.sync(() => {
|
|
@@ -14,5 +14,5 @@ export declare function openPromiseAgent(options: ShiitakeOptions, make: (option
|
|
|
14
14
|
export declare function openPromiseHost(options: ShiitakeOptions, mailRoom: BirdDogEffect, openHost: (options: ShiitakeEffectOptions, mailRoom: BirdDogEffect) => Effect.Effect<ShiitakeEffectHost, ShiitakeError>): Promise<Readonly<{
|
|
15
15
|
agent: ShiitakeOwnerAgent;
|
|
16
16
|
readSettlement: (sessionId: string, commandId: string) => Promise<CommandSettlement | undefined>;
|
|
17
|
-
acceptMail: (mail: BirdDogMail, meta?: Json, reply?: VerifiedAgentReply) => Promise<Accepted>;
|
|
17
|
+
acceptMail: (mail: BirdDogMail, meta?: Json, reply?: VerifiedAgentReply, stageAcceptedNotice?: (mail: BirdDogMail) => void) => Promise<Accepted>;
|
|
18
18
|
}>>;
|
|
@@ -155,6 +155,6 @@ export function openPromiseHost(options, mailRoom, openHost) {
|
|
|
155
155
|
return runShiitakePromise(liftRuntime(options).pipe(Effect.flatMap((runtime) => openHost(liftOptions(options, runtime), mailRoom)))).then(({ agent, acceptMail, readSettlement }) => Object.freeze({
|
|
156
156
|
agent: toPromiseOwner(agent),
|
|
157
157
|
readSettlement: (sessionId, commandId) => runShiitakePromise(readSettlement(sessionId, commandId)),
|
|
158
|
-
acceptMail: (mail, meta, reply) => runShiitakePromise(acceptMail(mail, meta, reply)),
|
|
158
|
+
acceptMail: (mail, meta, reply, stageAcceptedNotice) => runShiitakePromise(acceptMail(mail, meta, reply, stageAcceptedNotice)),
|
|
159
159
|
}));
|
|
160
160
|
}
|