@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.
Files changed (63) hide show
  1. package/README.md +19 -18
  2. package/dist/agent-control.d.ts +24 -19
  3. package/dist/agent-control.js +69 -128
  4. package/dist/client-wire.d.ts +12 -3
  5. package/dist/client-wire.js +33 -8
  6. package/dist/client.d.ts +34 -3
  7. package/dist/client.js +1 -1
  8. package/dist/cloudflare.d.ts +3 -1
  9. package/dist/cloudflare.js +2 -2
  10. package/dist/delegation.d.ts +151 -17
  11. package/dist/delegation.js +174 -41
  12. package/dist/effect.d.ts +1 -1
  13. package/dist/fleet.d.ts +37 -10
  14. package/dist/fleet.js +217 -48
  15. package/dist/http-routes.d.ts +84 -12
  16. package/dist/http-routes.js +15 -1
  17. package/dist/index.d.ts +2 -2
  18. package/dist/internal/admission-door.d.ts +2 -0
  19. package/dist/internal/admission-door.js +9 -2
  20. package/dist/internal/client-boundary.d.ts +10 -0
  21. package/dist/internal/client-boundary.js +10 -0
  22. package/dist/internal/compaction/compaction.js +1 -1
  23. package/dist/internal/contracts.d.ts +2 -0
  24. package/dist/internal/coordinator.d.ts +3 -1
  25. package/dist/internal/coordinator.js +98 -13
  26. package/dist/internal/effect-client.d.ts +3 -1
  27. package/dist/internal/effect-client.js +3 -2
  28. package/dist/internal/effect-contracts.d.ts +6 -2
  29. package/dist/internal/model-acquisition.js +2 -0
  30. package/dist/internal/observation-kernel.d.ts +8 -4
  31. package/dist/internal/observation-kernel.js +44 -9
  32. package/dist/internal/promise-facade.d.ts +1 -1
  33. package/dist/internal/promise-facade.js +1 -1
  34. package/dist/internal/run-scope.js +37 -8
  35. package/dist/internal/runtime-coordinator.d.ts +5 -0
  36. package/dist/internal/runtime-coordinator.js +8 -4
  37. package/dist/internal/runtime-core.d.ts +2 -1
  38. package/dist/internal/runtime-core.js +6 -1
  39. package/dist/internal/session-context.d.ts +1 -1
  40. package/dist/internal/session-context.js +84 -13
  41. package/dist/internal/woodstock/agent-reply.d.ts +7 -2
  42. package/dist/internal/woodstock/agent-reply.js +22 -13
  43. package/dist/internal/woodstock/fleet-store.d.ts +4 -2
  44. package/dist/internal/woodstock/fleet-store.js +70 -6
  45. package/dist/internal/woodstock/index.d.ts +2 -0
  46. package/dist/internal/woodstock/index.js +11 -5
  47. package/dist/internal/woodstock/run-recovery.js +4 -1
  48. package/dist/internal/woodstock/session-entry.d.ts +13 -3
  49. package/dist/internal/woodstock/session-entry.js +1 -0
  50. package/dist/internal/woodstock/session-store.d.ts +4 -0
  51. package/dist/internal/woodstock/session-store.js +26 -19
  52. package/dist/remote-client.js +5 -1
  53. package/dist/testing.d.ts +5 -2
  54. package/dist/testing.js +4 -2
  55. package/docs/api/agent-control.md +18 -12
  56. package/docs/api/client.md +78 -0
  57. package/docs/api/delegation.md +72 -55
  58. package/docs/api/effect.md +5 -0
  59. package/docs/api/fleet.md +23 -23
  60. package/docs/api/http.md +2 -1
  61. package/docs/api/index.md +13 -0
  62. package/docs/api/testing.md +3 -3
  63. 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
- mail.from.agentId !== reply.agentId ||
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 is a model call: its bounded provider message is shown. */
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
- // Every update carries the context use it settles on.
145
- return (page.frames.some((frame) => frame.kind === "update")
146
- ? readSessionContext(options, sessionId)
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) => Object.freeze({
177
- snapshot,
178
- changes: subscriptionFrames(options, runtime, registration, decoded.sessionId, snapshot.cursor ?? undefined, "live", decoded.options.includeSandboxFailures === true).pipe(Stream.ensuring(Effect.sync(registration.close))),
179
- close: Effect.sync(registration.close),
180
- })))).pipe(Effect.onError(() => Effect.sync(registration.close)));
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>): Promise<A>;
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 means `piHttpIdleTimeoutMs` (Pi coding-agent's own default).
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 owner observers; null identifies an Agent-wide execution change. */
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
- : "progress";
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 owner observers; null identifies an Agent-wide execution change. */
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 === "durable" ? "durable" : "progress";
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 = (sessionId, reason) => {
108
- const listeners = sessionId === null ? active : sessions.get(sessionId);
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
- deliverObservation(listener, reason);
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
  }