@fungi.computer/shiitake 0.5.0 → 0.7.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 (88) hide show
  1. package/README.md +59 -21
  2. package/dist/agent-control.d.ts +94 -2
  3. package/dist/agent-control.js +147 -26
  4. package/dist/client-entry.d.ts +20 -0
  5. package/dist/client-entry.js +5 -0
  6. package/dist/client-visitor.d.ts +33 -0
  7. package/dist/client-visitor.js +231 -0
  8. package/dist/client-wire.d.ts +9 -0
  9. package/dist/client-wire.js +24 -3
  10. package/dist/client.d.ts +27 -6
  11. package/dist/client.js +23 -0
  12. package/dist/cloudflare.d.ts +20 -0
  13. package/dist/cloudflare.js +4 -1
  14. package/dist/delegation.d.ts +79 -53
  15. package/dist/delegation.js +26 -21
  16. package/dist/effect.d.ts +26 -1
  17. package/dist/effect.js +14 -1
  18. package/dist/errors.d.ts +2 -2
  19. package/dist/errors.js +2 -0
  20. package/dist/extensions.d.ts +22 -3
  21. package/dist/extensions.js +1 -1
  22. package/dist/fleet.d.ts +25 -2
  23. package/dist/fleet.js +108 -24
  24. package/dist/hosted.js +3 -0
  25. package/dist/http-routes.d.ts +23 -9
  26. package/dist/http.d.ts +1 -0
  27. package/dist/http.js +1 -0
  28. package/dist/index.d.ts +1 -1
  29. package/dist/internal/accepted-run-executor.js +2 -1
  30. package/dist/internal/admission-door.d.ts +6 -1
  31. package/dist/internal/admission-door.js +33 -3
  32. package/dist/internal/contracts.d.ts +19 -1
  33. package/dist/internal/coordinator.js +115 -52
  34. package/dist/internal/effect-client.js +1 -1
  35. package/dist/internal/effect-contracts.d.ts +9 -2
  36. package/dist/internal/extension-command-registry.d.ts +2 -1
  37. package/dist/internal/extension-command-registry.js +3 -1
  38. package/dist/internal/extension-runtime.d.ts +3 -1
  39. package/dist/internal/extension-runtime.js +18 -8
  40. package/dist/internal/model-acquisition.js +2 -0
  41. package/dist/internal/observation-kernel.d.ts +2 -0
  42. package/dist/internal/observation-kernel.js +17 -6
  43. package/dist/internal/promise-facade.js +2 -0
  44. package/dist/internal/run-scope.d.ts +14 -0
  45. package/dist/internal/run-scope.js +58 -15
  46. package/dist/internal/runtime-coordinator.d.ts +14 -0
  47. package/dist/internal/runtime-coordinator.js +60 -11
  48. package/dist/internal/runtime-core.d.ts +3 -0
  49. package/dist/internal/runtime-core.js +1 -0
  50. package/dist/internal/runtime-reconciliation.js +123 -25
  51. package/dist/internal/session-context.d.ts +5 -5
  52. package/dist/internal/session-context.js +80 -83
  53. package/dist/internal/session-status.d.ts +2 -2
  54. package/dist/internal/session-status.js +5 -3
  55. package/dist/internal/woodstock/agent-reply.d.ts +5 -0
  56. package/dist/internal/woodstock/agent-reply.js +3 -0
  57. package/dist/internal/woodstock/client-projection.js +13 -11
  58. package/dist/internal/woodstock/fleet-store.d.ts +3 -2
  59. package/dist/internal/woodstock/fleet-store.js +24 -4
  60. package/dist/internal/woodstock/index.d.ts +7 -3
  61. package/dist/internal/woodstock/index.js +23 -6
  62. package/dist/internal/woodstock/progress.d.ts +11 -1
  63. package/dist/internal/woodstock/progress.js +126 -25
  64. package/dist/internal/woodstock/session-delegator.d.ts +13 -0
  65. package/dist/internal/woodstock/session-delegator.js +44 -0
  66. package/dist/internal/woodstock/session-entry.d.ts +12 -2
  67. package/dist/internal/woodstock/session-store.d.ts +16 -2
  68. package/dist/internal/woodstock/session-store.js +349 -66
  69. package/dist/internal/woodstock/work-senders.d.ts +32 -0
  70. package/dist/internal/woodstock/work-senders.js +70 -0
  71. package/dist/node.js +11 -2
  72. package/dist/remote-client.js +7 -2
  73. package/dist/server-visitor-stream.d.ts +13 -0
  74. package/dist/server-visitor-stream.js +124 -0
  75. package/dist/session-attention.d.ts +30 -0
  76. package/dist/session-attention.js +14 -0
  77. package/docs/api/agent-control.md +212 -15
  78. package/docs/api/client-entry.md +3 -0
  79. package/docs/api/client.md +209 -5
  80. package/docs/api/cloudflare.md +23 -1
  81. package/docs/api/delegation.md +284 -77
  82. package/docs/api/effect.md +100 -12
  83. package/docs/api/extensions.md +73 -12
  84. package/docs/api/fleet.md +138 -25
  85. package/docs/api/http.md +28 -0
  86. package/docs/api/index.md +43 -17
  87. package/docs/api/testing.md +9 -7
  88. package/package.json +10 -6
@@ -1,16 +1,17 @@
1
1
  import { nanoid } from "@fungi.computer/cairn";
2
2
  import { parseModelPreference } from "./model-preference.js";
3
3
  import { acceptedNotice } from "@fungi.computer/bird-dog/effect";
4
+ import { FungiMessageMetaSchema } from "@fungi.computer/agar/agents";
4
5
  import * as Effect from "effect/Effect";
5
6
  import * as Option from "effect/Option";
6
7
  import * as Stream from "effect/Stream";
7
8
  import { ShiitakeError } from "../errors.js";
8
9
  import { decodeClientReadOptions, decodeClientSettlementRequest, requireDecoded, decodeClientSessionCommand, decodeClientSessionId, decodeClientSubscribeOptions, } from "./client-boundary.js";
9
- import { mergeObservationSignals, openObservationKernel, publishOwnerObservation, } from "./observation-kernel.js";
10
+ import { mergeObservationSignals, openObservationKernel, publishAgentExecutionChange, publishOwnerObservation, } from "./observation-kernel.js";
10
11
  import { checkHostMeta } from "./host-meta.js";
11
- import { readSessionActivity } from "./session-status.js";
12
- import { readSessionContext } from "./session-context.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";
12
+ import { readSessionActivity, } from "./session-status.js";
13
+ import { prepareSessionContext } from "./session-context.js";
14
+ import { progressPosition, readProcessProgressDelta, abortSession, cancelSession, branchSession, recordOperatorLine, forkSession, readSession, readCommandSettlement, readSessionModelPreference, readSessionReplayPage, readSessionInbox, readLatestPendingPrompt, createSession, deleteSession, deliverSessionGrants, readSessionGrants, setSessionModelPreference, setSessionTitle, readAdmissionIdentity, assertAdmissionIdentity, } from "./woodstock/index.js";
14
15
  /** Generic Agent-scope extension controls over the runtime's one call door. */
15
16
  function agentExtensions(runtime) {
16
17
  return Object.freeze({
@@ -19,12 +20,15 @@ function agentExtensions(runtime) {
19
20
  call: (extensionId, command, input) => runtime.assertOpen.pipe(Effect.andThen(runtime.extensions.call({ scope: "agent" }, extensionId, command, input))),
20
21
  });
21
22
  }
22
- function currentSessionActivity(options, runtime, sessionId) {
23
+ function currentSessionWork(runtime, sessionId) {
23
24
  return runtime
24
25
  .readQueueHead(sessionId)
25
26
  .pipe(Effect.flatMap((head) => (head.state === "queued"
26
27
  ? runtime.runningWork(head.jobId)
27
- : Effect.succeed(null)).pipe(Effect.map((runningWork) => readSessionActivity(options.store, sessionId, head, runningWork)))));
28
+ : Effect.succeed(null)).pipe(Effect.map((runningWork) => ({ head, runningWork })))));
29
+ }
30
+ function currentSessionActivity(options, runtime, sessionId) {
31
+ return currentSessionWork(runtime, sessionId).pipe(Effect.map(({ head, runningWork }) => readSessionActivity(options.store, sessionId, head, runningWork)));
28
32
  }
29
33
  /** Carry the Session's context use on a baseline, when it has one. */
30
34
  function withContext(snapshot, context) {
@@ -34,7 +38,10 @@ function withContext(snapshot, context) {
34
38
  }
35
39
  /** Read the baseline and its context use for one Session. */
36
40
  function readSessionWithContext(options, runtime, sessionId, input) {
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)))));
41
+ return currentSessionWork(runtime, sessionId).pipe(Effect.flatMap(({ head, runningWork }) => readSession(options.store, sessionId, () => readSessionActivity(options.store, sessionId, head, runningWork), input, () => runtime.extensions.project(sessionId), (snapshot) => ({
42
+ snapshot,
43
+ context: prepareSessionContext(options, sessionId, snapshot.cursor, input?.before === undefined),
44
+ }))), Effect.flatMap(({ snapshot, context }) => context.pipe(Effect.map((value) => withContext(snapshot, value)))));
38
45
  }
39
46
  function sessionOwnerOperation(runtime, sessionId, operation) {
40
47
  return runtime.assertOpen.pipe(Effect.andThen(Effect.try({
@@ -91,7 +98,46 @@ function continueFromReset(frame) {
91
98
  }),
92
99
  ];
93
100
  }
94
- function subscriptionFrames(options, runtime, registration, sessionId, after, delivery, includeSandboxFailures = false) {
101
+ function subscriptionFrames(options, runtime, registration, sessionId, after, delivery, includeSandboxFailures = false, baselineProgress = null) {
102
+ let position = progressPosition(baselineProgress);
103
+ let toolOutput = [];
104
+ const reset = () => resetFrame(options, runtime, sessionId, includeSandboxFailures).pipe(Effect.map((frame) => {
105
+ position = progressPosition(frame.baseline.progress);
106
+ toolOutput = frame.baseline.toolOutput;
107
+ return continueFromReset(frame);
108
+ }));
109
+ const liveFrames = (activity) => {
110
+ const queue = activity.facts.queue;
111
+ const delta = queue.state !== "running"
112
+ ? null
113
+ : readProcessProgressDelta(options.store, sessionId, queue.commandId, position);
114
+ if (delta === undefined)
115
+ return undefined;
116
+ const frames = [];
117
+ if (delta !== null) {
118
+ frames.push({
119
+ kind: "progress",
120
+ progress: delta,
121
+ activity: activity.facts,
122
+ });
123
+ position = {
124
+ messageId: delta.messageId,
125
+ revision: delta.revision,
126
+ text: delta.text.offset + delta.text.append.length,
127
+ thinking: delta.thinking.offset + delta.thinking.append.length,
128
+ };
129
+ }
130
+ if (toolOutput.length !== activity.toolOutput.length ||
131
+ toolOutput.some((slot, index) => slot !== activity.toolOutput[index])) {
132
+ toolOutput = activity.toolOutput;
133
+ frames.push({
134
+ kind: "tool_output",
135
+ toolOutput,
136
+ activity: activity.facts,
137
+ });
138
+ }
139
+ return frames;
140
+ };
95
141
  const initial = {
96
142
  after,
97
143
  delivery,
@@ -105,64 +151,73 @@ function subscriptionFrames(options, runtime, registration, sessionId, after, de
105
151
  if (signal === "closed")
106
152
  return Effect.succeed([[], Option.none()]);
107
153
  if (signal === "reset")
108
- return resetFrame(options, runtime, sessionId, includeSandboxFailures).pipe(Effect.map(continueFromReset));
154
+ return reset();
109
155
  return runtime.assertOpen.pipe(Effect.andThen(currentSessionActivity(options, runtime, sessionId)), Effect.flatMap((activity) => {
110
156
  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
- ]);
116
- if (signal === "progress" && state.delivery === "live") {
157
+ if ((signal === "progress" || signal === "activity") &&
158
+ state.delivery === "live") {
159
+ const frames = liveFrames(activity);
160
+ if (frames === undefined)
161
+ return reset();
117
162
  return Effect.succeed([
118
163
  [
119
- ...(activity.progress === null
120
- ? []
121
- : [
164
+ ...(signal === "activity"
165
+ ? [
122
166
  {
123
- kind: "progress",
124
- progress: activity.progress,
167
+ kind: "activity",
125
168
  activity: activity.facts,
126
169
  },
127
- ]),
128
- {
129
- kind: "tool_output",
130
- toolOutput: activity.toolOutput,
131
- activity: activity.facts,
132
- },
170
+ ]
171
+ : []),
172
+ ...frames,
133
173
  ],
134
174
  Option.some({ ...state, awaitSignal: true }),
135
175
  ]);
136
176
  }
137
- return readSessionReplayPage(options.store, sessionId, state.after, state.delivery, activity, () => runtime.extensions.project(sessionId), includeSandboxFailures).pipe(Effect.flatMap((page) => {
177
+ return readSessionReplayPage(options.store, sessionId, state.after, state.delivery, activity, () => runtime.extensions.project(sessionId), includeSandboxFailures, (page) => {
178
+ const latestCursor = page.kind === "page"
179
+ ? page.frames
180
+ .flatMap((frame) => frame.kind === "progress" ||
181
+ frame.kind === "activity" ||
182
+ frame.kind === "tool_output"
183
+ ? []
184
+ : [frame.cursor])
185
+ .at(-1)
186
+ : undefined;
187
+ // Prepare a figure only for the final moving update,
188
+ // while its cursor and stored context share the read.
189
+ const context = page.kind === "page" &&
190
+ !page.hasMore &&
191
+ page.frames.some((frame) => frame.kind === "update" &&
192
+ frame.cursor !== state.after)
193
+ ? prepareSessionContext(options, sessionId, latestCursor ?? null)
194
+ : Effect.succeed(undefined);
195
+ return { page, latestCursor, context };
196
+ }).pipe(Effect.flatMap(({ page, latestCursor, context, }) => {
138
197
  if (page.kind === "reset")
139
- return resetFrame(options, runtime, sessionId, includeSandboxFailures).pipe(Effect.map(continueFromReset));
140
- const latestCursor = page.frames
141
- .flatMap((frame) => frame.kind === "progress" ||
142
- frame.kind === "activity" ||
143
- frame.kind === "tool_output"
198
+ return reset();
199
+ if (!page.hasMore)
200
+ toolOutput = activity.toolOutput;
201
+ const progressFrames = page.hasMore
144
202
  ? []
145
- : [frame.cursor])
146
- .at(-1);
203
+ : liveFrames(activity);
204
+ if (progressFrames === undefined)
205
+ return reset();
147
206
  const next = {
148
207
  after: latestCursor ?? state.after,
149
208
  delivery: page.hasMore ? state.delivery : "live",
150
209
  awaitSignal: !page.hasMore,
151
210
  };
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)
159
- : Effect.succeed(undefined)).pipe(Effect.map((context) => [
160
- context === undefined
161
- ? page.frames
162
- : page.frames.map((frame) => frame.kind === "update" &&
163
- frame.cursor === latestCursor
164
- ? Object.freeze({ ...frame, context })
165
- : frame),
211
+ return context.pipe(Effect.map((context) => [
212
+ [
213
+ ...(context === undefined
214
+ ? page.frames
215
+ : page.frames.map((frame) => frame.kind === "update" &&
216
+ frame.cursor === latestCursor
217
+ ? Object.freeze({ ...frame, context })
218
+ : frame)),
219
+ ...progressFrames,
220
+ ],
166
221
  Option.some(next),
167
222
  ]));
168
223
  }));
@@ -190,7 +245,7 @@ function watchSession(options, runtime, observation, sessionId, input) {
190
245
  registration.setQueued(snapshot.activity.queue.state === "queued");
191
246
  return Object.freeze({
192
247
  snapshot,
193
- changes: subscriptionFrames(options, runtime, registration, decoded.sessionId, snapshot.cursor ?? undefined, "live", decoded.options.includeSandboxFailures === true).pipe(Stream.ensuring(Effect.sync(registration.close))),
248
+ changes: subscriptionFrames(options, runtime, registration, decoded.sessionId, snapshot.cursor ?? undefined, "live", decoded.options.includeSandboxFailures === true, snapshot.progress).pipe(Stream.ensuring(Effect.sync(registration.close))),
194
249
  close: Effect.sync(registration.close),
195
250
  });
196
251
  }))).pipe(Effect.onError(() => Effect.sync(registration.close)));
@@ -288,7 +343,9 @@ function executeSessionCommand(options, runtime, sessionId, meta, input) {
288
343
  .commit(sessionOwnerOperation(runtime, decodedSessionId, (decoded) => {
289
344
  setSessionTitle(options.store, decoded, command.title);
290
345
  }))
291
- .pipe(Effect.as(undefined));
346
+ .pipe(Effect.tap(() => Effect.sync(() => {
347
+ publishAgentExecutionChange(options.store);
348
+ })), Effect.as(undefined));
292
349
  }
293
350
  }));
294
351
  }
@@ -348,7 +405,7 @@ export function openCoordinator(options, runtime) {
348
405
  })), Effect.asVoid))),
349
406
  }),
350
407
  extension: (extensionId) => Object.freeze({
351
- call: (command, input) => runtime.assertOpen.pipe(Effect.andThen(runtime.extensions.call({ scope: "session", sessionId }, extensionId, command, input))),
408
+ call: (command, input, caller) => runtime.assertOpen.pipe(Effect.andThen(runtime.extensions.call({ scope: "session", sessionId }, extensionId, command, input, caller))),
352
409
  }),
353
410
  read: (input) => runtime.assertOpen.pipe(Effect.andThen(readSessionWithContext(options, runtime, sessionId, input))),
354
411
  readSettlement: (commandId, timeoutMs = 0) => Effect.scoped(Effect.gen(function* () {
@@ -410,6 +467,11 @@ export function openCoordinator(options, runtime) {
410
467
  }),
411
468
  });
412
469
  }
470
+ /** Whether the sender asked this mail to steer the recipient's running turn. */
471
+ function steersRunningTurn(mail) {
472
+ const meta = FungiMessageMetaSchema.safeParse(mail.meta);
473
+ return meta.success && meta.data.delegation?.steer === true;
474
+ }
413
475
  /**
414
476
  * Accept one addressed mail envelope; the door decodes it as a prompt. `reply`
415
477
  * is the host's authentication of a delegated Agent's reply; only then does
@@ -449,6 +511,7 @@ export function acceptAddressedMail(runtime, mail, meta, reply, stageMail, sessi
449
511
  const result = admit(meta === undefined ? request : { ...request, meta });
450
512
  if (result.status !== "accepted")
451
513
  throw new Error("Sessionless addressed mail was not admitted");
514
+ runtime.recordCreatedSessionDelegatorWithinTransaction(sessionId, addressed);
452
515
  return {
453
516
  status: "accepted",
454
517
  commandId: result.commandId,
@@ -458,7 +521,7 @@ export function acceptAddressedMail(runtime, mail, meta, reply, stageMail, sessi
458
521
  }
459
522
  return admitUserWork(runtime, mail.to.sessionId, {
460
523
  requestId: mail.key,
461
- delivery: reply === undefined ? "followUp" : "steer",
524
+ delivery: reply !== undefined || steersRunningTurn(mail) ? "steer" : "followUp",
462
525
  type: "prompt",
463
526
  text: mail.content,
464
527
  }, meta, mail, reply);
@@ -72,7 +72,7 @@ function sessionFacade(address, session) {
72
72
  extension: (extensionId) => {
73
73
  const extension = session.extension(extensionId);
74
74
  return Object.freeze({
75
- call: (command, input) => runShiitakePromise(extension.call(command, input)),
75
+ call: (command, input, caller) => runShiitakePromise(extension.call(command, input, caller)),
76
76
  });
77
77
  },
78
78
  read: (input) => runShiitakePromise(session.read(input)),
@@ -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. */
@@ -99,6 +101,11 @@ export type ShiitakeEffectOptions = Readonly<{
99
101
  store: SqliteOwner;
100
102
  /** Host storage-pressure check; absent never refuses for storage pressure. */
101
103
  storagePressure?: StoragePressureCheck;
104
+ /** Host-owned synchronous new-work admission at the common door. */
105
+ admission?: Readonly<{
106
+ /** Check the host's current flag synchronously inside admission's transaction. */
107
+ assertOpen: () => void;
108
+ }>;
102
109
  /** Compaction budgets; absent uses Pi's defaults for every model. */
103
110
  compaction?: CompactionOptions;
104
111
  /** One host lane in Shiitake's existing Watchdog, opened before recovery. */
@@ -201,7 +208,7 @@ export type ShiitakeEffectSession = Readonly<{
201
208
  /** Effect-native Session-scope extension commands. */
202
209
  export type ShiitakeEffectSessionExtension = Readonly<{
203
210
  /** Run one command with its encoded input; succeeds with the encoded output. */
204
- call: (command: string, input: Json) => Effect.Effect<Json, ShiitakeError>;
211
+ call: (command: string, input: Json, caller?: import("../extensions.js").ExtensionCommandCallerV0) => Effect.Effect<Json, ShiitakeError>;
205
212
  }>;
206
213
  /** Effect-native Agent-scope extension controls. */
207
214
  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
  ? {
@@ -336,9 +340,11 @@ export function openExtensionRuntime(input) {
336
340
  reason: error instanceof ShiitakeError
337
341
  ? error.cause === "conflict"
338
342
  ? "conflict"
339
- : error.cause === "closed"
340
- ? "closed"
341
- : "invalid"
343
+ : error.cause === "archived"
344
+ ? "archived"
345
+ : error.cause === "closed"
346
+ ? "closed"
347
+ : "invalid"
342
348
  : "invalid",
343
349
  message: error instanceof ShiitakeError
344
350
  ? error.message
@@ -567,6 +573,7 @@ export function openExtensionRuntime(input) {
567
573
  send: sendFor(member),
568
574
  timers: timersFor(member),
569
575
  attention: attentionFor(member),
576
+ isObserved: (sessionId) => member.status === "running" && hasOwnerObservers(owner, sessionId),
570
577
  sessionPosition: (sessionId) => requireRead(member.admitted.id).pipe(Effect.andThen(grantGuard(member, "state")), Effect.andThen(storeStep(() => {
571
578
  const id = decodeClientSessionId(sessionId);
572
579
  if (!hasStoredSession(owner, id))
@@ -575,6 +582,7 @@ export function openExtensionRuntime(input) {
575
582
  ?.newestSequence ?? 0);
576
583
  }))),
577
584
  hasPendingSteer: (run) => requireRead(member.admitted.id).pipe(Effect.andThen(grantGuard(member, "run_tools")), Effect.andThen(storeStep(() => hasPendingSessionSteer(owner, decodeClientSessionId(run.sessionId), run.workId)))),
585
+ 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
586
  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
587
  observe: (sessionId) => requireFrame(member.admitted.id).pipe(Effect.map((frame) => {
580
588
  frame.observed.add(sessionId);
@@ -1020,7 +1028,7 @@ export function openExtensionRuntime(input) {
1020
1028
  }
1021
1029
  return Object.freeze(slots);
1022
1030
  },
1023
- call: (target, extensionId, command, raw) => Effect.gen(function* () {
1031
+ call: (target, extensionId, command, raw, caller) => Effect.gen(function* () {
1024
1032
  const member = members.get(extensionId);
1025
1033
  if (member?.admitted.grants.has("commands") !== true)
1026
1034
  return yield* Effect.fail(new ShiitakeError("extensions", "unavailable", "Extension is unavailable"));
@@ -1049,7 +1057,9 @@ export function openExtensionRuntime(input) {
1049
1057
  yield* recordStop(member, `command ${command} has no handler`, undefined, "own-transaction");
1050
1058
  return yield* Effect.fail(new ShiitakeError("extensions", "unavailable", "Extension is stopped"));
1051
1059
  }
1052
- const prepared = handler({ sessionId, input: raw });
1060
+ const prepared = caller === undefined
1061
+ ? handler({ sessionId, input: raw })
1062
+ : handler({ sessionId, input: raw, caller });
1053
1063
  if (prepared._tag === "Failure")
1054
1064
  return yield* Effect.fail(new ShiitakeError("contract", "invalid", "Extension command input is invalid"));
1055
1065
  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)
@@ -139,6 +139,8 @@ function liftOptions(options, runtime) {
139
139
  Object.assign(base, { meta: options.meta });
140
140
  if (options.storagePressure !== undefined)
141
141
  Object.assign(base, { storagePressure: options.storagePressure });
142
+ if (options.admission !== undefined)
143
+ Object.assign(base, { admission: options.admission });
142
144
  if (options.compaction !== undefined)
143
145
  Object.assign(base, { compaction: options.compaction });
144
146
  const toolSource = options.tools;
@@ -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;