@fungi.computer/shiitake 0.4.0 → 0.6.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 (68) hide show
  1. package/README.md +1 -10
  2. package/dist/agent-control.d.ts +98 -5
  3. package/dist/agent-control.js +111 -8
  4. package/dist/client-entry.d.ts +20 -0
  5. package/dist/client-entry.js +5 -0
  6. package/dist/client-visitor.d.ts +31 -0
  7. package/dist/client-visitor.js +226 -0
  8. package/dist/client-wire.d.ts +5 -0
  9. package/dist/client-wire.js +6 -0
  10. package/dist/client.d.ts +4 -3
  11. package/dist/client.js +1 -0
  12. package/dist/delegation.d.ts +100 -77
  13. package/dist/delegation.js +73 -23
  14. package/dist/effect.d.ts +25 -0
  15. package/dist/effect.js +14 -1
  16. package/dist/extensions.d.ts +21 -2
  17. package/dist/extensions.js +1 -1
  18. package/dist/fleet.d.ts +22 -2
  19. package/dist/fleet.js +127 -36
  20. package/dist/hosted.js +3 -0
  21. package/dist/http-routes.d.ts +11 -0
  22. package/dist/internal/accepted-run-executor.js +2 -1
  23. package/dist/internal/admission-door.d.ts +5 -0
  24. package/dist/internal/admission-door.js +34 -3
  25. package/dist/internal/contracts.d.ts +14 -1
  26. package/dist/internal/coordinator.js +8 -2
  27. package/dist/internal/effect-client.js +1 -1
  28. package/dist/internal/effect-contracts.d.ts +4 -2
  29. package/dist/internal/extension-command-registry.d.ts +2 -1
  30. package/dist/internal/extension-command-registry.js +3 -1
  31. package/dist/internal/extension-runtime.d.ts +3 -1
  32. package/dist/internal/extension-runtime.js +13 -5
  33. package/dist/internal/model-acquisition.js +2 -0
  34. package/dist/internal/observation-kernel.d.ts +2 -0
  35. package/dist/internal/observation-kernel.js +17 -6
  36. package/dist/internal/run-scope.d.ts +14 -0
  37. package/dist/internal/run-scope.js +32 -9
  38. package/dist/internal/runtime-coordinator.js +8 -1
  39. package/dist/internal/runtime-reconciliation.js +119 -24
  40. package/dist/internal/woodstock/agent-reply.d.ts +5 -0
  41. package/dist/internal/woodstock/agent-reply.js +3 -0
  42. package/dist/internal/woodstock/client-projection.js +9 -1
  43. package/dist/internal/woodstock/fleet-store.d.ts +1 -0
  44. package/dist/internal/woodstock/fleet-store.js +28 -1
  45. package/dist/internal/woodstock/index.d.ts +4 -0
  46. package/dist/internal/woodstock/index.js +7 -0
  47. package/dist/internal/woodstock/session-delegator.d.ts +12 -0
  48. package/dist/internal/woodstock/session-delegator.js +44 -0
  49. package/dist/internal/woodstock/session-entry.d.ts +12 -2
  50. package/dist/internal/woodstock/session-store.d.ts +2 -0
  51. package/dist/internal/woodstock/session-store.js +20 -0
  52. package/dist/internal/woodstock/work-senders.d.ts +32 -0
  53. package/dist/internal/woodstock/work-senders.js +70 -0
  54. package/dist/remote-client.js +7 -2
  55. package/dist/server-visitor-stream.d.ts +13 -0
  56. package/dist/server-visitor-stream.js +115 -0
  57. package/dist/session-attention.d.ts +30 -0
  58. package/dist/session-attention.js +14 -0
  59. package/docs/api/agent-control.md +209 -13
  60. package/docs/api/client-entry.md +3 -0
  61. package/docs/api/client.md +136 -1
  62. package/docs/api/cloudflare.md +7 -1
  63. package/docs/api/delegation.md +234 -61
  64. package/docs/api/effect.md +16 -6
  65. package/docs/api/extensions.md +65 -4
  66. package/docs/api/fleet.md +69 -16
  67. package/docs/api/index.md +6 -4
  68. package/package.json +10 -6
@@ -1,6 +1,6 @@
1
1
  import type { HostMetaOptions } from "./host-meta.js";
2
2
  import type { ExtensionRosterV0 } from "../extensions.js";
3
- import type { Api, AssistantMessage, Model, MuleStreamFunction, RetryPolicy } from "@fungi.computer/mule/effect";
3
+ import type { Api, AssistantMessage, Model, MuleStreamFunction, ProviderRejection, RetryPolicy } from "@fungi.computer/mule/effect";
4
4
  import type { CapabilitySource, ExecutionOptions, ModuleRegistration } from "@fungi.computer/mycelium";
5
5
  import type { CommandSettlement, GrantsDelivery, GrantsDeliveryResult, ToolAccess } from "./contracts.js";
6
6
  import type { Sandbox } from "@fungi.computer/mycelium/effect";
@@ -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
+ /** Bounded provider facts for this door's exact failure, never SDK prose. */
30
+ rejection?: (message: AssistantMessage) => ProviderRejection | undefined;
29
31
  /** Provider prose for this door's exact failed turn; never a diagnostic. */
30
32
  providerError?: (message: AssistantMessage) => string | undefined;
31
33
  /** Effect finalizer for acquired model resources. */
@@ -201,7 +203,7 @@ export type ShiitakeEffectSession = Readonly<{
201
203
  /** Effect-native Session-scope extension commands. */
202
204
  export type ShiitakeEffectSessionExtension = Readonly<{
203
205
  /** Run one command with its encoded input; succeeds with the encoded output. */
204
- call: (command: string, input: Json) => Effect.Effect<Json, ShiitakeError>;
206
+ call: (command: string, input: Json, caller?: import("../extensions.js").ExtensionCommandCallerV0) => Effect.Effect<Json, ShiitakeError>;
205
207
  }>;
206
208
  /** Effect-native Agent-scope extension controls. */
207
209
  export type ShiitakeEffectAgentExtensions = Readonly<{
@@ -1,11 +1,12 @@
1
1
  import * as Effect from "effect/Effect";
2
2
  import * as Result from "effect/Result";
3
3
  import * as Schema from "effect/Schema";
4
- import type { AdmissionError, CommandHandlersV0, CommandsV0, ExtensionCommandError, ExtensionRead, ExtensionStateError, ExtensionTx, Json } from "../extensions.js";
4
+ import type { AdmissionError, CommandHandlersV0, CommandsV0, ExtensionCommandError, ExtensionCommandCallerV0, ExtensionRead, ExtensionStateError, ExtensionTx, Json } from "../extensions.js";
5
5
  /** A parsed invocation retains its typed handler until the transaction runs. */
6
6
  export type RegisteredExtensionCommandV0 = (frame: Readonly<{
7
7
  sessionId: string | null;
8
8
  input: Json;
9
+ caller?: ExtensionCommandCallerV0;
9
10
  }>) => Result.Result<() => Effect.Effect<Json, ExtensionCommandError | ExtensionStateError | AdmissionError, ExtensionTx | ExtensionRead>, Schema.SchemaError>;
10
11
  /** Capture each declared command's schemas and matching handler at activation. */
11
12
  export declare function registerExtensionCommands<C extends CommandsV0>(specs: C, handlers: CommandHandlersV0<C>): ReadonlyMap<string, RegisteredExtensionCommandV0>;
@@ -13,7 +13,9 @@ function bindCommand(spec, handler) {
13
13
  const decodeInput = Schema.decodeUnknownResult(spec.input, {
14
14
  onExcessProperty: "error",
15
15
  });
16
- return (frame) => Result.flatMap(decodeSession(frame.sessionId), (sessionId) => Result.map(decodeInput(frame.input), (input) => () => invokeTransactionHook(() => handler({ sessionId, input })).pipe(Effect.flatMap((output) => Schema.encodeEffect(spec.output)(output).pipe(Effect.flatMap(Schema.decodeUnknownEffect(JsonValueSchema)), Effect.orDie)))));
16
+ return (frame) => Result.flatMap(decodeSession(frame.sessionId), (sessionId) => Result.map(decodeInput(frame.input), (input) => () => invokeTransactionHook(() => handler(frame.caller === undefined
17
+ ? { sessionId, input }
18
+ : { sessionId, input, caller: frame.caller })).pipe(Effect.flatMap((output) => Schema.encodeEffect(spec.output)(output).pipe(Effect.flatMap(Schema.decodeUnknownEffect(JsonValueSchema)), Effect.orDie)))));
17
19
  }
18
20
  /** Capture each declared command's schemas and matching handler at activation. */
19
21
  export function registerExtensionCommands(specs, handlers) {
@@ -9,6 +9,8 @@ import type { SqliteOwner } from "./contracts.js";
9
9
  import type { AdmittedExtensionV0 } from "./extension-roster.js";
10
10
  /** The settled consumer name core itself uses for mail and control repair. */
11
11
  export declare const coreConsumer = "core";
12
+ /** Core reports to delegators after the extensions have seen a settlement. */
13
+ export declare const reportConsumer = "core-report";
12
14
  /** Core's generic view of rostered v1 extensions. */
13
15
  export type ExtensionRuntime = Readonly<{
14
16
  /** Prepare additions without replacing any live member; apply only between turns. */
@@ -53,7 +55,7 @@ export type ExtensionRuntime = Readonly<{
53
55
  sessionId: string;
54
56
  } | {
55
57
  scope: "agent";
56
- }>, extensionId: string, command: string, input: Json) => Effect.Effect<Json, ShiitakeError>;
58
+ }>, extensionId: string, command: string, input: Json, caller?: import("../extensions.js").ExtensionCommandCallerV0) => Effect.Effect<Json, ShiitakeError>;
57
59
  list: () => readonly ExtensionStatusV0[];
58
60
  resume: (id: string) => Effect.Effect<ExtensionStatusV0, ShiitakeError>;
59
61
  close: Effect.Effect<void, ShiitakeError>;
@@ -12,11 +12,13 @@ import { ShiitakeError } from "../errors.js";
12
12
  import { AdmissionError, ExtensionCommandError, ExtensionRead, ExtensionStateError, ExtensionStop, ExtensionTx, } from "../extensions.js";
13
13
  import { decodeClientSessionId } from "./client-boundary.js";
14
14
  import { AsyncExtensionTransactionError, invokeExtensionHook, invokeSyncHandler, } from "./extension-hook.js";
15
- import { setSessionAttention, clearSessionAttention, projectSessionAttention, readAttentionSessionPage, replaceAttentionGrants, hasStoredSession, readSessionLineage, hasPendingSessionSteer, hasSuccessfulToolResult, advanceCursor, declareSessionDocument, readSessionDocument, writeSessionDocument, deleteAllRecords, deleteRecord, deleteTimer, deleteExtensionTimers, readAllRecords, readCursor, readDueTimers, readRecord, readRecordPage, readStateLedger, readStatus, readTimer, readTimerWake, writeRecord, writeStateLedger, writeStatus, writeTimer, } from "./woodstock/index.js";
15
+ import { hasSessionAttention, setSessionAttention, clearSessionAttention, projectSessionAttention, readAttentionSessionPage, replaceAttentionGrants, hasStoredSession, readSessionLineage, hasPendingSessionSteer, readWorkSender, hasSuccessfulToolResult, advanceCursor, declareSessionDocument, readSessionDocument, writeSessionDocument, deleteAllRecords, deleteRecord, deleteTimer, deleteExtensionTimers, readAllRecords, readCursor, readDueTimers, readRecord, readRecordPage, readStateLedger, readStatus, readTimer, readTimerWake, writeRecord, writeStateLedger, writeStatus, writeTimer, } from "./woodstock/index.js";
16
16
  import { JsonValueSchema } from "./json-value.js";
17
- import { publishOwnerObservation } from "./observation-kernel.js";
17
+ import { hasOwnerObservers, publishOwnerObservation, } from "./observation-kernel.js";
18
18
  /** The settled consumer name core itself uses for mail and control repair. */
19
19
  export const coreConsumer = "core";
20
+ /** Core reports to delegators after the extensions have seen a settlement. */
21
+ export const reportConsumer = "core-report";
20
22
  const maximumKeyLength = 256;
21
23
  const maximumRecordBytes = 1024 * 1024;
22
24
  const maximumReasonLength = 512;
@@ -288,7 +290,7 @@ export function openExtensionRuntime(input) {
288
290
  set: (sessionId, item) => {
289
291
  const decoded = Schema.decodeUnknownResult(AttentionItemSchema, {
290
292
  onExcessProperty: "error",
291
- })(item);
293
+ })(item.waitingOn === undefined ? item : { ...item, inbox: false });
292
294
  return decoded._tag === "Failure"
293
295
  ? Effect.fail(stateError("bound", "Attention item is invalid"))
294
296
  : mutate(sessionId, () => {
@@ -297,6 +299,7 @@ export function openExtensionRuntime(input) {
297
299
  setSessionAttention(owner, sessionId, id, decoded.success);
298
300
  });
299
301
  },
302
+ awaitsPerson: (sessionId) => requireRead(id).pipe(Effect.tap(() => grantGuard(member, "attention")), Effect.andThen(storeStep(() => hasSessionAttention(owner, sessionId)))),
300
303
  clear: (sessionId, itemId) => boundedKey(itemId, "An attention id").pipe(Effect.andThen(mutate(sessionId, () => {
301
304
  clearSessionAttention(owner, sessionId, id, itemId);
302
305
  }))),
@@ -323,6 +326,7 @@ export function openExtensionRuntime(input) {
323
326
  extensionId: id,
324
327
  correlation: request.correlation ?? null,
325
328
  },
329
+ continuesWorkId: request.continuesWorkId,
326
330
  });
327
331
  return result.status === "accepted"
328
332
  ? {
@@ -567,6 +571,7 @@ export function openExtensionRuntime(input) {
567
571
  send: sendFor(member),
568
572
  timers: timersFor(member),
569
573
  attention: attentionFor(member),
574
+ isObserved: (sessionId) => member.status === "running" && hasOwnerObservers(owner, sessionId),
570
575
  sessionPosition: (sessionId) => requireRead(member.admitted.id).pipe(Effect.andThen(grantGuard(member, "state")), Effect.andThen(storeStep(() => {
571
576
  const id = decodeClientSessionId(sessionId);
572
577
  if (!hasStoredSession(owner, id))
@@ -575,6 +580,7 @@ export function openExtensionRuntime(input) {
575
580
  ?.newestSequence ?? 0);
576
581
  }))),
577
582
  hasPendingSteer: (run) => requireRead(member.admitted.id).pipe(Effect.andThen(grantGuard(member, "run_tools")), Effect.andThen(storeStep(() => hasPendingSessionSteer(owner, decodeClientSessionId(run.sessionId), run.workId)))),
583
+ workSender: (run) => requireRead(member.admitted.id).pipe(Effect.andThen(grantGuard(member, "run_tools")), Effect.andThen(storeStep(() => readWorkSender(owner, decodeClientSessionId(run.sessionId), run.workId) ?? null))),
578
584
  hasSuccessfulToolResult: (run, toolCallId) => requireRead(member.admitted.id).pipe(Effect.andThen(grantGuard(member, "run_tools")), Effect.andThen(storeStep(() => hasSuccessfulToolResult(owner, decodeClientSessionId(run.sessionId), run.workId, toolCallId)))),
579
585
  observe: (sessionId) => requireFrame(member.admitted.id).pipe(Effect.map((frame) => {
580
586
  frame.observed.add(sessionId);
@@ -1020,7 +1026,7 @@ export function openExtensionRuntime(input) {
1020
1026
  }
1021
1027
  return Object.freeze(slots);
1022
1028
  },
1023
- call: (target, extensionId, command, raw) => Effect.gen(function* () {
1029
+ call: (target, extensionId, command, raw, caller) => Effect.gen(function* () {
1024
1030
  const member = members.get(extensionId);
1025
1031
  if (member?.admitted.grants.has("commands") !== true)
1026
1032
  return yield* Effect.fail(new ShiitakeError("extensions", "unavailable", "Extension is unavailable"));
@@ -1049,7 +1055,9 @@ export function openExtensionRuntime(input) {
1049
1055
  yield* recordStop(member, `command ${command} has no handler`, undefined, "own-transaction");
1050
1056
  return yield* Effect.fail(new ShiitakeError("extensions", "unavailable", "Extension is stopped"));
1051
1057
  }
1052
- const prepared = handler({ sessionId, input: raw });
1058
+ const prepared = caller === undefined
1059
+ ? handler({ sessionId, input: raw })
1060
+ : handler({ sessionId, input: raw, caller });
1053
1061
  if (prepared._tag === "Failure")
1054
1062
  return yield* Effect.fail(new ShiitakeError("contract", "invalid", "Extension command input is invalid"));
1055
1063
  const body = prepared.success();
@@ -23,6 +23,8 @@ function asEffectAcquisition(acquisition) {
23
23
  shared = { ...shared, failureReason: acquisition.failureReason };
24
24
  if (acquisition.providerError !== undefined)
25
25
  shared = { ...shared, providerError: acquisition.providerError };
26
+ if (acquisition.rejection !== undefined)
27
+ shared = { ...shared, rejection: acquisition.rejection };
26
28
  if (acquisition.stream === undefined)
27
29
  return acquisition.retryAfterMs === undefined
28
30
  ? Object.freeze(shared)
@@ -4,6 +4,8 @@ import type { SqliteOwner } from "./contracts.js";
4
4
  /** Finite process-local signal; durable owner state remains authoritative. */
5
5
  export type ObservationSignal = "closed" | "reset" | "durable" | "activity" | "progress";
6
6
  export type ObservationReason = "durable" | "activity" | "progress" | "settlement";
7
+ /** Actual live-view demand, excluding exact-command settlement waiters. */
8
+ export declare function hasOwnerObservers(store: SqliteOwner, sessionId: string): boolean;
7
9
  /** One scoped process-local observation registration. */
8
10
  export type ObservationRegistration = Readonly<{
9
11
  /** Inspect and consume one pending finite signal without waiting. */
@@ -4,6 +4,13 @@ import { reportCatchFailure } from "./failure.js";
4
4
  const MAX_OBSERVATIONS = 64;
5
5
  const MAX_PENDING_HINTS = 256;
6
6
  const ownerPublishers = new WeakMap();
7
+ /** Actual live-view demand, excluding exact-command settlement waiters. */
8
+ export function hasOwnerObservers(store, sessionId) {
9
+ for (const publisher of ownerPublishers.get(store) ?? [])
10
+ if (publisher.observes(sessionId))
11
+ return true;
12
+ return false;
13
+ }
7
14
  /** Combine coalesced reasons without allowing a progress hint to hide a commit. */
8
15
  export function mergeObservationSignals(first, second) {
9
16
  if (first === "closed" || second === "closed")
@@ -55,9 +62,9 @@ export function publishOwnerObservation(store, sessionId, reason = "durable") {
55
62
  const publishers = ownerPublishers.get(store);
56
63
  if (publishers === undefined)
57
64
  return;
58
- for (const publish of publishers) {
65
+ for (const publisher of publishers) {
59
66
  try {
60
- publish({ kind: "session", sessionId, reason });
67
+ publisher.publish({ kind: "session", sessionId, reason });
61
68
  }
62
69
  catch (cause) {
63
70
  reportCatchFailure("owner-observation-publish", cause);
@@ -70,9 +77,9 @@ export function publishAgentExecutionChange(store) {
70
77
  const publishers = ownerPublishers.get(store);
71
78
  if (publishers === undefined)
72
79
  return;
73
- for (const publish of publishers) {
80
+ for (const publisher of publishers) {
74
81
  try {
75
- publish({ kind: "agent-execution" });
82
+ publisher.publish({ kind: "agent-execution" });
76
83
  }
77
84
  catch (cause) {
78
85
  reportCatchFailure("owner-execution-change-publish", cause);
@@ -140,7 +147,11 @@ export function openObservationKernel(store) {
140
147
  deliverObservation(listener, publication.reason === "settlement" ? "durable" : publication.reason);
141
148
  }
142
149
  };
143
- publishers.add(publish);
150
+ const publisher = {
151
+ publish,
152
+ observes: (sessionId) => [...(sessions.get(sessionId) ?? [])].some((listener) => !listener.closed && !listener.settlementOnly),
153
+ };
154
+ publishers.add(publisher);
144
155
  ownerPublishers.set(store, publishers);
145
156
  return Object.freeze({
146
157
  register: (sessionId, interest) => Effect.suspend(() => {
@@ -194,7 +205,7 @@ export function openObservationKernel(store) {
194
205
  if (released)
195
206
  return;
196
207
  released = true;
197
- publishers.delete(publish);
208
+ publishers.delete(publisher);
198
209
  if (publishers.size === 0)
199
210
  ownerPublishers.delete(store);
200
211
  for (const listener of active)
@@ -1,4 +1,5 @@
1
1
  import type { FileTool } from "@fungi.computer/knapsack";
2
+ import { type MuleError } from "@fungi.computer/mule/effect";
2
3
  import type { ModuleRegistration } from "@fungi.computer/mycelium";
3
4
  import * as Effect from "effect/Effect";
4
5
  import { ShiitakeError } from "../errors.js";
@@ -17,6 +18,19 @@ export declare class ToolSetupError extends ShiitakeError {
17
18
  constructor(phase: keyof typeof toolSetupMessages);
18
19
  get diagnostic(): string;
19
20
  }
21
+ /** Closed reasons a rejected run names, never a foreign error's own text. */
22
+ declare const runRejectedDiagnostics: {
23
+ readonly model: "model-failed";
24
+ readonly tool: "tool-failed";
25
+ readonly control: "control-failed";
26
+ readonly listener: "listener-failed";
27
+ };
28
+ /** A run Mule ended with a non-model error; it names a closed reason. */
29
+ export declare class RunRejectedError extends ShiitakeError {
30
+ private readonly reason;
31
+ constructor(reason: keyof typeof runRejectedDiagnostics, underlying: MuleError);
32
+ get diagnostic(): string;
33
+ }
20
34
  /** Run one accepted command with Pi compaction explicitly composed around Mule. */
21
35
  export declare function runAcceptedCommand(options: ShiitakeEffectOptions, prepared: PreparedSessionRun, executionContract: RunContract, extensionModules: readonly ModuleRegistration[], extensionFileTools: readonly FileTool[], compactionHandoff: CompactionHandoffV0 | undefined, promptSections?: () => Effect.Effect<readonly PromptSectionV0[], ShiitakeError>): Effect.Effect<{
22
36
  run: import("@fungi.computer/mule/effect").MuleRunOutcome;
@@ -10,9 +10,6 @@ import { openCompactionSettings } from "./compaction/settings.js";
10
10
  import { acquireOwnedModel } from "./model-acquisition.js";
11
11
  import { returnedFailureReason, unavailableRunReason, } from "./run-failure-reason.js";
12
12
  import { isCompactCommand, openMuleRecorder, readSessionGrants, recordLiveRetryEvent, recordSandboxFailure, takeSessionControls, } from "./woodstock/index.js";
13
- function runtimeFailure(message) {
14
- return new ShiitakeError("runtime", "rejected", message);
15
- }
16
13
  const toolSetupMessages = {
17
14
  missing: "Instruction capabilities have no tool runtime",
18
15
  runtime: "Tool runtime failed",
@@ -29,6 +26,28 @@ export class ToolSetupError extends ShiitakeError {
29
26
  return toolSetupMessages[this.phase];
30
27
  }
31
28
  }
29
+ /** Closed reasons a rejected run names, never a foreign error's own text. */
30
+ const runRejectedDiagnostics = {
31
+ model: "model-failed",
32
+ tool: "tool-failed",
33
+ control: "control-failed",
34
+ listener: "listener-failed",
35
+ };
36
+ /** A run Mule ended with a non-model error; it names a closed reason. */
37
+ export class RunRejectedError extends ShiitakeError {
38
+ reason;
39
+ constructor(reason, underlying) {
40
+ super("runtime", "rejected", "Mule rejected the durable command run", undefined, underlying);
41
+ this.reason = reason;
42
+ }
43
+ get diagnostic() {
44
+ return runRejectedDiagnostics[this.reason];
45
+ }
46
+ }
47
+ /** Name a non-model Mule error by its closed reason. */
48
+ function rejectedRun(error) {
49
+ return new RunRejectedError(error.phase, error);
50
+ }
32
51
  /**
33
52
  * Map Mule's retry transitions onto Woodstock's retained `live_retry` facts.
34
53
  * A started attempt ends the backoff countdown before streaming resumes.
@@ -229,22 +248,26 @@ export function runAcceptedCommand(options, prepared, executionContract, extensi
229
248
  acquired.failureReason?.(exit.value.message) ??
230
249
  returnedFailureReason(exit.value);
231
250
  failureReason = (failedModelAttempts > 1 &&
232
- exit.value.modelLimitReason === undefined
251
+ exit.value.modelLimitReason === undefined &&
252
+ acquired.rejection?.(exit.value.message)?.code !==
253
+ "usage-limit-reached"
233
254
  ? `The model failed ${String(failedModelAttempts)} times on this conversation: ${cause}`
234
255
  : cause).slice(0, 200);
235
256
  return recorder(failedEnd, failureReason);
236
257
  }
237
258
  const error = Cause.findErrorOption(exit.cause);
238
- return recorder(failedEnd, Option.isSome(error) && error.value.phase === "model"
239
- ? `model:rejected: ${error.value.message}`.slice(0, 200)
240
- : unavailableRunReason({
259
+ return recorder(failedEnd, Option.isNone(error)
260
+ ? unavailableRunReason({
241
261
  runId: failedEnd.run.value,
242
262
  provider: acquired.model.provider,
243
263
  model: acquired.model.id,
244
- }));
264
+ })
265
+ : error.value.phase === "model"
266
+ ? `model:rejected: ${error.value.message}`.slice(0, 200)
267
+ : `runtime:rejected: ${rejectedRun(error.value).diagnostic}`);
245
268
  }), Effect.mapError((error) => error.phase === "model"
246
269
  ? new ShiitakeError("model", "rejected", error.message, undefined, error)
247
- : runtimeFailure("Mule rejected the durable command run")));
270
+ : rejectedRun(error)));
248
271
  },
249
272
  });
250
273
  return { run, failureReason };
@@ -9,7 +9,7 @@ import { agentTurnRecovery, executeAcceptedRun, } from "./accepted-run-executor.
9
9
  import { openAdmissionDoor } from "./admission-door.js";
10
10
  import { openCompactionSettings } from "./compaction/settings.js";
11
11
  import { admitExtensionRosterV0, } from "./extension-roster.js";
12
- import { coreConsumer, openExtensionRuntime, } from "./extension-runtime.js";
12
+ import { coreConsumer, reportConsumer, openExtensionRuntime, } from "./extension-runtime.js";
13
13
  import { reconcileRuntime } from "./runtime-reconciliation.js";
14
14
  import { advanceCursor, createSessionWithinTransaction, deliverSessionGrants, prepareExtensionStore, projectSessionRun, openStorageAdmission, readCursor, } from "./woodstock/index.js";
15
15
  function runtimeFailure(message) {
@@ -141,6 +141,12 @@ export function openRuntimeCoordinator(options, coreDigest, admitted, mailRoom)
141
141
  options.store.transactionSync(() => {
142
142
  advanceCursor(options.store, coreConsumer, 0);
143
143
  });
144
+ // A store that predates reports starts them at the core cursor
145
+ // instead of reporting its history.
146
+ if (readCursor(options.store, reportConsumer) === undefined)
147
+ options.store.transactionSync(() => {
148
+ advanceCursor(options.store, reportConsumer, readCursor(options.store, coreConsumer) ?? 0);
149
+ });
144
150
  },
145
151
  catch: () => new ShiitakeError("store", "unavailable", "Extension store is unavailable"),
146
152
  });
@@ -165,6 +171,7 @@ export function openRuntimeCoordinator(options, coreDigest, admitted, mailRoom)
165
171
  const cursors = yield* Effect.try({
166
172
  try: () => [
167
173
  readCursor(options.store, coreConsumer) ?? 0,
174
+ readCursor(options.store, reportConsumer) ?? 0,
168
175
  ...extensions.settledConsumers().map(({ cursor }) => cursor),
169
176
  ],
170
177
  catch: () => runtimeFailure("Settled cursor is unavailable"),
@@ -5,21 +5,19 @@ import * as Schema from "effect/Schema";
5
5
  import { agentTurnRecovery } from "./accepted-run-executor.js";
6
6
  import { ShiitakeError } from "../errors.js";
7
7
  import { ExtensionId } from "../extension-contract.js";
8
- import { coreConsumer } from "./extension-runtime.js";
8
+ import { coreConsumer, reportConsumer, } from "./extension-runtime.js";
9
9
  import { publishOwnerObservation } from "./observation-kernel.js";
10
- import { advanceCursor, readCursor, readExtensionAdmissionOrigin, readSettledRunFacts, readSettlementContent, readSettlementDiagnostic, readSettlementMeta, recordWorkSettlement, sessionLineageRefused, settleControls, } from "./woodstock/index.js";
10
+ import { advanceCursor, readCursor, readExtensionAdmissionOrigin, readSettledRunFacts, readSettlementContent, readSettlementDiagnostic, readSessionDelegator, readSettlementMeta, readWaitingOn, readWaitingOnSender, recordWorkSettlement, sessionLineageRefused, settleControls, } from "./woodstock/index.js";
11
11
  const pageSize = 64;
12
12
  function runtimeFailure(message) {
13
13
  return new ShiitakeError("runtime", "rejected", message);
14
14
  }
15
- function fanOutSettlement(store, mailRoom, job) {
16
- if (mailRoom === undefined)
17
- return Effect.void;
15
+ /** BirdDog's settlement facts for one settled job, without its correspondents. */
16
+ function settlementFacts(store, job,
17
+ /** The open items this settlement names as waiting on its recipient. */
18
+ waitingOnYou) {
18
19
  return Effect.gen(function* () {
19
- const payload = yield* Effect.try({
20
- try: () => decodeAgentJob(job),
21
- catch: () => runtimeFailure("Agent work payload is invalid"),
22
- });
20
+ const payload = yield* decodeJob(job);
23
21
  const content = job.outcome === "cancelled"
24
22
  ? ""
25
23
  : yield* readSettlementContent(store, payload.work);
@@ -27,14 +25,103 @@ function fanOutSettlement(store, mailRoom, job) {
27
25
  const diagnostic = job.outcome === "completed" || job.outcome === "cancelled"
28
26
  ? undefined
29
27
  : yield* readSettlementDiagnostic(store, payload.work);
30
- const settled = diagnostic === undefined
31
- ? { job, content }
32
- : { job, content, diagnostic };
33
- yield* mailRoom
34
- .fanOut(meta === undefined ? settled : { ...settled, meta })
35
- .pipe(Effect.mapError(() => runtimeFailure("Agent reply fan-out is unavailable")));
28
+ let settled = { job, content };
29
+ if (diagnostic !== undefined)
30
+ settled = { ...settled, diagnostic };
31
+ // A cancelled run names nothing; what it left open is the canceller's to see.
32
+ const waiting = job.outcome === "cancelled" ? [] : yield* waitingOnYou;
33
+ if (waiting.length > 0)
34
+ settled = { ...settled, waitingOnYou: waiting };
35
+ return meta === undefined ? settled : { ...settled, meta };
36
36
  });
37
37
  }
38
+ function decodeJob(job) {
39
+ return Effect.try({
40
+ try: () => decodeAgentJob(job),
41
+ catch: () => runtimeFailure("Agent work payload is invalid"),
42
+ });
43
+ }
44
+ function fanOut(mailRoom, settled) {
45
+ return mailRoom
46
+ .fanOut(settled)
47
+ .pipe(Effect.mapError(() => runtimeFailure("Agent reply fan-out is unavailable")));
48
+ }
49
+ /** Reply to the delegating mail that admitted this work, if it asked for one. */
50
+ function replyToMail(store, mailRoom, job) {
51
+ if (mailRoom === undefined)
52
+ return Effect.void;
53
+ return Effect.gen(function* () {
54
+ const payload = yield* decodeJob(job);
55
+ if ((payload.mail?.replyTo.length ?? 0) === 0)
56
+ return;
57
+ // The reply goes to the mail's sender: name what waits on that sender.
58
+ const waiting = Effect.try({
59
+ try: () => readWaitingOnSender(store, payload.work.sessionId, payload.work.commandFactId),
60
+ catch: () => runtimeFailure("Session attention is unavailable"),
61
+ });
62
+ yield* fanOut(mailRoom, yield* settlementFacts(store, job, waiting));
63
+ });
64
+ }
65
+ /**
66
+ * Report work no delegating mail answered to the Session's delegator of
67
+ * record. It runs after the extensions have seen the settlement, so a goal
68
+ * or loop that admitted the next turn keeps the Session busy and the stretch
69
+ * of turns reports once, when the Session finally rests. Another Agent's
70
+ * original mail is that Agent's correspondence and never reported here.
71
+ */
72
+ function reportToDelegator(store, watchdog, mailRoom, job) {
73
+ if (mailRoom === undefined)
74
+ return Effect.void;
75
+ return Effect.gen(function* () {
76
+ const payload = yield* decodeJob(job);
77
+ const mail = payload.mail;
78
+ if (mail !== undefined && mail.replyTo.length > 0)
79
+ return;
80
+ const head = yield* watchdog
81
+ .readQueueHead(job.queue)
82
+ .pipe(Effect.mapError(() => runtimeFailure("Session queue head is unavailable")));
83
+ if (head.state !== "idle")
84
+ return;
85
+ const delegator = yield* Effect.try({
86
+ try: () => readSessionDelegator(store, payload.work.sessionId),
87
+ catch: () => runtimeFailure("Session delegator is unavailable"),
88
+ });
89
+ if (delegator === undefined)
90
+ return;
91
+ const from = mail?.reply === undefined ? mail?.from : undefined;
92
+ const anotherAgentsMail = from !== undefined &&
93
+ !(from.kind === "agent" &&
94
+ delegator.replyTo.some(({ agentId }) => agentId === from.agentId));
95
+ if (anotherAgentsMail)
96
+ return;
97
+ // The report goes to the delegator: name what waits on it.
98
+ const waiting = Effect.try({
99
+ try: () => readWaitingOn(store, payload.work.sessionId, delegator.replyTo),
100
+ catch: () => runtimeFailure("Session attention is unavailable"),
101
+ });
102
+ const settled = yield* settlementFacts(store, job, waiting);
103
+ yield* fanOut(mailRoom, { ...settled, delegator });
104
+ });
105
+ }
106
+ /**
107
+ * The `core-report` settled consumer: after the extensions' turn, report each
108
+ * Session's last settled job in the page, one record at a time.
109
+ */
110
+ function reportSettled(store, watchdog, mailRoom, records) {
111
+ return Effect.forEach(records, (record, index) => Effect.gen(function* () {
112
+ const later = records
113
+ .slice(index + 1)
114
+ .some(({ job }) => job.queue === record.job.queue);
115
+ if (!later && !(yield* settledInRefusedSession(store, record.job)))
116
+ yield* reportToDelegator(store, watchdog, mailRoom, record.job);
117
+ yield* Effect.try({
118
+ try: () => {
119
+ advanceCursor(store, reportConsumer, record.sequence);
120
+ },
121
+ catch: () => runtimeFailure("Settled cursor is unavailable"),
122
+ });
123
+ }), { discard: true });
124
+ }
38
125
  function repairWorkSettlement(store, job) {
39
126
  const outcome = job.outcome;
40
127
  if (outcome !== "interrupted" &&
@@ -148,7 +235,7 @@ function settleCore(store, watchdog, mailRoom, records) {
148
235
  if (!(yield* settledInRefusedSession(store, record.job))) {
149
236
  yield* repairWorkSettlement(store, record.job);
150
237
  yield* settleTargetedControls(store, watchdog, record.job);
151
- yield* fanOutSettlement(store, mailRoom, record.job);
238
+ yield* replyToMail(store, mailRoom, record.job);
152
239
  }
153
240
  yield* Effect.try({
154
241
  try: () => {
@@ -172,26 +259,34 @@ export function reconcileRuntime(input) {
172
259
  try: () => readCursor(input.store, coreConsumer) ?? 0,
173
260
  catch: () => runtimeFailure("Settled cursor is unavailable"),
174
261
  });
262
+ const reportCursor = yield* Effect.try({
263
+ try: () => readCursor(input.store, reportConsumer) ?? coreCursor,
264
+ catch: () => runtimeFailure("Settled cursor is unavailable"),
265
+ });
175
266
  const consumers = input.extensions.settledConsumers();
176
- const after = Math.min(coreCursor, ...consumers.map(({ cursor }) => cursor));
267
+ const after = Math.min(coreCursor, reportCursor, ...consumers.map(({ cursor }) => cursor));
177
268
  const page = yield* input.watchdog.readSettled(pageSize, after);
178
269
  const core = yield* Effect.exit(settleCore(input.store, input.watchdog, input.mailRoom, page.filter(({ sequence }) => sequence > coreCursor)));
270
+ const processed = yield* Effect.try({
271
+ try: () => readCursor(input.store, coreConsumer) ?? 0,
272
+ catch: () => runtimeFailure("Settled cursor is unavailable"),
273
+ });
179
274
  const runs = yield* Effect.try({
180
- try: () => {
181
- const processed = readCursor(input.store, coreConsumer) ?? 0;
182
- return page
183
- .filter(({ sequence }) => sequence <= processed)
184
- .map((record) => settledRun(input.store, record));
185
- },
275
+ try: () => page
276
+ .filter(({ sequence }) => sequence <= processed)
277
+ .map((record) => settledRun(input.store, record)),
186
278
  catch: () => runtimeFailure("Settled run facts are unavailable"),
187
279
  });
188
- yield* input.extensions.deliverSettled(runs, readCursor(input.store, coreConsumer) ?? 0);
280
+ yield* input.extensions.deliverSettled(runs, processed);
281
+ const reports = yield* Effect.exit(reportSettled(input.store, input.watchdog, input.mailRoom, page.filter(({ sequence }) => sequence > reportCursor && sequence <= processed)));
189
282
  const moreTimers = yield* input.extensions.fireDueTimers(Date.now());
190
283
  if (page.length === pageSize || moreTimers)
191
284
  yield* input.requestWake;
192
285
  yield* drainMail(input.mailRoom);
193
286
  if (Exit.isFailure(core))
194
287
  return yield* Effect.failCause(core.cause);
288
+ if (Exit.isFailure(reports))
289
+ return yield* Effect.failCause(reports.cause);
195
290
  }).pipe(Effect.mapError((failure) => failure instanceof ShiitakeError
196
291
  ? failure
197
292
  : runtimeFailure("Runtime reconciliation failed")));
@@ -9,6 +9,11 @@ export declare const VerifiedAgentReplySchema: Schema.Union<readonly [Schema.Str
9
9
  readonly commandId: Schema.String;
10
10
  readonly outcome: Schema.Literals<readonly ["completed", "failed", "interrupted", "cancelled", "outcome_unknown"]>;
11
11
  readonly diagnostic: Schema.optionalKey<Schema.String>;
12
+ readonly waitingOnYou: Schema.optionalKey<Schema.$Array<Schema.Struct<{
13
+ readonly itemId: Schema.String;
14
+ readonly kind: Schema.String;
15
+ readonly summary: Schema.String;
16
+ }>>>;
12
17
  readonly agentId: Schema.String;
13
18
  readonly sessionId: Schema.String;
14
19
  }>, Schema.Struct<{
@@ -42,6 +42,9 @@ function opening(reply) {
42
42
  attributes.push(["outcome", reply.outcome]);
43
43
  if (reply.outcome !== "undeliverable" && reply.diagnostic !== undefined)
44
44
  attributes.push(["diagnostic", reply.diagnostic]);
45
+ if (reply.outcome !== "undeliverable" && reply.waitingOnYou !== undefined)
46
+ // A host fact, outside the child's text: the items the child left waiting on the recipient.
47
+ attributes.push(["waiting_on_you", JSON.stringify(reply.waitingOnYou)]);
45
48
  if (reply.outcome === "undeliverable")
46
49
  attributes.push(["reason", reply.reason]);
47
50
  const rendered = attributes
@@ -1,9 +1,11 @@
1
1
  import { messageAuthor } from "@fungi.computer/agar/agents";
2
2
  import { readSessionAttention } from "./session-attention.js";
3
+ import { readWaitingOnSender } from "./work-senders.js";
4
+ import { readSessionDelegator } from "./session-delegator.js";
3
5
  import { ShiitakeError } from "../../errors.js";
4
6
  import { decodeWoodstockCursor, encodeWoodstockCursor } from "./cursor.js";
5
7
  import { operatorClientMessage, promptCommand } from "./session-entry.js";
6
- import { lineageEntrySequence, lineageHasSequence, lineageRowLimit, readLineageWindow, readLineageWorkFacts, readSessionEntriesForWork, readSessionLineage, readPendingSessionCommands, readSessionCommandAdmission, readNewestPendingSessionCommand, } from "./session-store.js";
8
+ import { lineageEntrySequence, lineageHasSequence, lineageRowLimit, readLineageWindow, readLineageWorkFacts, hasSessionWorkTerminal, readSessionEntriesForWork, readSessionLineage, readPendingSessionCommands, readSessionCommandAdmission, readNewestPendingSessionCommand, } from "./session-store.js";
7
9
  const maximumPagePayloadBytes = 2_097_152;
8
10
  const replayPageLimit = 200;
9
11
  const maximumRowsPerPage = lineageRowLimit;
@@ -503,6 +505,10 @@ export function projectCommandSettlement(owner, sessionId, commandId) {
503
505
  const admission = readSessionCommandAdmission(owner, sessionId, commandId);
504
506
  if (admission === undefined)
505
507
  throw unavailable("Accepted Session command is unavailable");
508
+ // A live work has no settlement to project. The owner's terminal index
509
+ // answers this without decoding the work again on every polling tick.
510
+ if (!hasSessionWorkTerminal(owner, sessionId, admission.entry.workId))
511
+ return undefined;
506
512
  const rows = readSessionEntriesForWork(owner, sessionId, admission.entry.workId);
507
513
  const work = workFacts(owner, sessionId, rows);
508
514
  const terminal = rows
@@ -521,6 +527,7 @@ export function projectCommandSettlement(owner, sessionId, commandId) {
521
527
  const sender = messageAuthor(admission.entry.meta);
522
528
  return {
523
529
  acceptedSender: sender?.kind === "agent" ? sender.id : null,
530
+ delegator: readSessionDelegator(owner, sessionId)?.replyTo[0]?.agentId ?? null,
524
531
  terminal: meta === undefined ? terminal : { ...terminal, meta },
525
532
  content: terminal.outcome === "cancelled"
526
533
  ? ""
@@ -534,5 +541,6 @@ export function projectCommandSettlement(owner, sessionId, commandId) {
534
541
  usage,
535
542
  })),
536
543
  toolCalls: new Set(rows.flatMap(({ entry }) => entry.kind === "tool_intent" ? [entry.call.id] : [])).size,
544
+ waitingOnYou: readWaitingOnSender(owner, sessionId, admission.entry.workId),
537
545
  };
538
546
  }
@@ -34,4 +34,5 @@ export declare function createFleetDelegationStore(storage: WatchdogSqliteOwner)
34
34
  reserve(requestId: string, input: string, mail: string): void;
35
35
  recordReceipt(requestId: string, receipt: string, expectedMail: string, acceptedMail: string): void;
36
36
  recordHeld(requestId: string): void;
37
+ recordUndeliverable(requestId: string): boolean;
37
38
  };