@fungi.computer/shiitake 0.5.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 (65) hide show
  1. package/dist/agent-control.d.ts +93 -1
  2. package/dist/agent-control.js +108 -6
  3. package/dist/client-entry.d.ts +20 -0
  4. package/dist/client-entry.js +5 -0
  5. package/dist/client-visitor.d.ts +31 -0
  6. package/dist/client-visitor.js +226 -0
  7. package/dist/client-wire.d.ts +5 -0
  8. package/dist/client-wire.js +6 -0
  9. package/dist/client.d.ts +4 -3
  10. package/dist/client.js +1 -0
  11. package/dist/delegation.d.ts +74 -53
  12. package/dist/delegation.js +4 -15
  13. package/dist/effect.d.ts +25 -0
  14. package/dist/effect.js +14 -1
  15. package/dist/extensions.d.ts +21 -2
  16. package/dist/extensions.js +1 -1
  17. package/dist/fleet.d.ts +15 -0
  18. package/dist/fleet.js +32 -6
  19. package/dist/hosted.js +3 -0
  20. package/dist/http-routes.d.ts +11 -0
  21. package/dist/internal/accepted-run-executor.js +2 -1
  22. package/dist/internal/admission-door.d.ts +5 -0
  23. package/dist/internal/admission-door.js +27 -1
  24. package/dist/internal/contracts.d.ts +14 -1
  25. package/dist/internal/coordinator.js +8 -2
  26. package/dist/internal/effect-client.js +1 -1
  27. package/dist/internal/effect-contracts.d.ts +4 -2
  28. package/dist/internal/extension-command-registry.d.ts +2 -1
  29. package/dist/internal/extension-command-registry.js +3 -1
  30. package/dist/internal/extension-runtime.d.ts +3 -1
  31. package/dist/internal/extension-runtime.js +13 -5
  32. package/dist/internal/model-acquisition.js +2 -0
  33. package/dist/internal/observation-kernel.d.ts +2 -0
  34. package/dist/internal/observation-kernel.js +17 -6
  35. package/dist/internal/run-scope.d.ts +14 -0
  36. package/dist/internal/run-scope.js +32 -9
  37. package/dist/internal/runtime-coordinator.js +8 -1
  38. package/dist/internal/runtime-reconciliation.js +119 -24
  39. package/dist/internal/woodstock/agent-reply.d.ts +5 -0
  40. package/dist/internal/woodstock/agent-reply.js +3 -0
  41. package/dist/internal/woodstock/client-projection.js +9 -1
  42. package/dist/internal/woodstock/index.d.ts +4 -0
  43. package/dist/internal/woodstock/index.js +7 -0
  44. package/dist/internal/woodstock/session-delegator.d.ts +12 -0
  45. package/dist/internal/woodstock/session-delegator.js +44 -0
  46. package/dist/internal/woodstock/session-entry.d.ts +12 -2
  47. package/dist/internal/woodstock/session-store.d.ts +2 -0
  48. package/dist/internal/woodstock/session-store.js +20 -0
  49. package/dist/internal/woodstock/work-senders.d.ts +32 -0
  50. package/dist/internal/woodstock/work-senders.js +70 -0
  51. package/dist/remote-client.js +7 -2
  52. package/dist/server-visitor-stream.d.ts +13 -0
  53. package/dist/server-visitor-stream.js +115 -0
  54. package/dist/session-attention.d.ts +30 -0
  55. package/dist/session-attention.js +14 -0
  56. package/docs/api/agent-control.md +209 -13
  57. package/docs/api/client-entry.md +3 -0
  58. package/docs/api/client.md +136 -1
  59. package/docs/api/cloudflare.md +7 -1
  60. package/docs/api/delegation.md +234 -61
  61. package/docs/api/effect.md +16 -6
  62. package/docs/api/extensions.md +65 -4
  63. package/docs/api/fleet.md +69 -16
  64. package/docs/api/index.md +6 -4
  65. package/package.json +10 -6
@@ -9,7 +9,7 @@ import { ExtensionId } from "../extension-contract.js";
9
9
  import { publishOwnerObservation } from "./observation-kernel.js";
10
10
  import { returnedFailureReason } from "./run-failure-reason.js";
11
11
  import { coreProtocolDigest } from "./extension-roster.js";
12
- import { runAcceptedCommand, runAcceptedCompaction, ToolSetupError, } from "./run-scope.js";
12
+ import { runAcceptedCommand, runAcceptedCompaction, RunRejectedError, ToolSetupError, } from "./run-scope.js";
13
13
  import { CompactionCommitError } from "./compaction/session-compaction.js";
14
14
  import { BrokenSessionError, isCompactCommand, loadAcceptedRun, readExtensionAdmissionOrigin, recordWorkSettlement, } from "./woodstock/index.js";
15
15
  /** Bounded process-loss recovery for every accepted Agent turn. */
@@ -25,6 +25,7 @@ function runtimeFailure(message) {
25
25
  function failedDecision(error) {
26
26
  const category = `${error.boundary}:${error.cause}`;
27
27
  const diagnostic = error instanceof ToolSetupError ||
28
+ error instanceof RunRejectedError ||
28
29
  error instanceof CompactionCommitError ||
29
30
  error instanceof BrokenSessionError
30
31
  ? error.diagnostic
@@ -34,6 +34,11 @@ export type AdmitRequest = Readonly<{
34
34
  reply?: VerifiedAgentReply;
35
35
  /** Synchronous owner-local side effect committed with a mail admission. */
36
36
  onAccepted?: (sessionId: string, commandId: string) => void;
37
+ /**
38
+ * New work that continues earlier work in this Session (for example, the
39
+ * follow-up that delivers an answer) keeps that work's sender.
40
+ */
41
+ continuesWorkId?: string | undefined;
37
42
  }>;
38
43
  /** Outcome of one door admission. */
39
44
  export type AdmitResult = Readonly<{
@@ -1,10 +1,11 @@
1
+ import { messageAuthor } from "@fungi.computer/agar/agents";
1
2
  import * as Effect from "effect/Effect";
2
3
  import * as Schema from "effect/Schema";
3
4
  import { checkHostMeta } from "./host-meta.js";
4
5
  import { ShiitakeError } from "../errors.js";
5
6
  import { decodeClientAcceptedCommand, decodeClientSessionId, decodeClientSessionCreationRequestId, requireDecoded, } from "./client-boundary.js";
6
7
  import { publishOwnerObservation } from "./observation-kernel.js";
7
- import { AdmissionOriginSchema, acceptSessionCommandOwned, readSessionCommandAdmissionByRequest, withdrawSessionRequestOwned, assertSessionRequestNotWithdrawn, hasStoredSession, VerifiedAgentReplySchema, } from "./woodstock/index.js";
8
+ import { AdmissionOriginSchema, acceptSessionCommandOwned, readSessionCommandAdmissionByRequest, withdrawSessionRequestOwned, assertSessionRequestNotWithdrawn, hasStoredSession, readWorkSender, recordWorkSender, recordSessionDelegator, VerifiedAgentReplySchema, } from "./woodstock/index.js";
8
9
  const decodeOrigin = Schema.decodeUnknownResult(AdmissionOriginSchema, {
9
10
  onExcessProperty: "error",
10
11
  });
@@ -29,6 +30,24 @@ function decodeReply(request) {
29
30
  throw new ShiitakeError("contract", "invalid", "Verified Agent reply does not match its mail");
30
31
  return reply;
31
32
  }
33
+ /**
34
+ * The Agent Session whose work this admission is: the sender of delegated
35
+ * mail that expects a reply, or the sender of the earlier work this one
36
+ * continues. Replies and every other admission have none.
37
+ */
38
+ function workSender(owner, request) {
39
+ const mail = request.mail;
40
+ if (mail?.from.kind === "agent" && mail.replyTo.length > 0)
41
+ return {
42
+ agentId: mail.from.agentId,
43
+ sessionId: mail.from.sessionId,
44
+ // The host-authenticated author names the sender; mail.meta is the sender's own claim.
45
+ name: messageAuthor(request.meta)?.name ?? mail.from.agentId,
46
+ };
47
+ return request.continuesWorkId === undefined
48
+ ? undefined
49
+ : readWorkSender(owner, request.sessionId, request.continuesWorkId);
50
+ }
32
51
  /** Bind the door to one owner, run contract, Watchdog queue and host wake. */
33
52
  export function openAdmissionDoor(input) {
34
53
  /** The door's one decode, done before any owner operation when possible. */
@@ -41,6 +60,7 @@ export function openAdmissionDoor(input) {
41
60
  mail: request.mail,
42
61
  reply: decodeReply(request),
43
62
  onAccepted: request.onAccepted,
63
+ continuesWorkId: request.continuesWorkId,
44
64
  meta: request.meta === undefined
45
65
  ? undefined
46
66
  : checkHostMeta(input.hostMeta, request.meta),
@@ -75,6 +95,12 @@ export function openAdmissionDoor(input) {
75
95
  input.enqueue(work, request.mail);
76
96
  }
77
97
  : undefined, targetWorkId, request.reply);
98
+ if (accepted.delivery === "new" && targetWorkId === undefined) {
99
+ const sender = workSender(input.owner, request);
100
+ if (sender !== undefined)
101
+ recordWorkSender(input.owner, sessionId, accepted.accepted.commandId, sender);
102
+ }
103
+ recordSessionDelegator(input.owner, sessionId, request.mail);
78
104
  request.onAccepted?.(sessionId, accepted.accepted.commandId);
79
105
  touched.set(sessionId, (touched.get(sessionId) ?? false) || accepted.delivery === "new");
80
106
  return Object.freeze({
@@ -3,7 +3,7 @@ import type { SessionTerminal, ShiitakeAgent } from "../client.js";
3
3
  import type { GrantsDelivery, GrantsDeliveryResult } from "./woodstock/index.js";
4
4
  import type { Json } from "../extensions.js";
5
5
  import type { ExtensionRosterV0 } from "../extensions.js";
6
- import type { Api, AssistantMessage, Model, MuleStreamFunction } from "@fungi.computer/mule/effect";
6
+ import type { Api, AssistantMessage, Model, MuleStreamFunction, ProviderRejection } from "@fungi.computer/mule/effect";
7
7
  import type { AccessOptions, CapabilitySource, ExecutionOptions, ModuleRegistration, Sandbox } from "@fungi.computer/mycelium";
8
8
  import type { ModelPreference } from "./model-preference.js";
9
9
  import type { WatchdogSqliteValue, WatchdogSqliteRow, WatchdogSqliteOwner, WatchdogSqliteCursor } from "@fungi.computer/watchdog";
@@ -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
+ /** Bounded provider facts for this door's exact failure, never SDK prose. */
51
+ rejection?: (message: AssistantMessage) => ProviderRejection | undefined;
50
52
  /** Provider prose for this door's exact failed turn; never a diagnostic. */
51
53
  providerError?: (message: AssistantMessage) => string | undefined;
52
54
  /** Release the acquired model resources. */
@@ -253,6 +255,8 @@ export type { HostMetaOptions, StandardHostMetaResult, StandardHostMetaSchema, }
253
255
  export type CommandSettlement = Readonly<{
254
256
  /** Original Agent sender recorded at acceptance, never the terminal's canceller. */
255
257
  acceptedSender: string | null;
258
+ /** Agent that first sent this Session work and receives its reports, if any. */
259
+ delegator: string | null;
256
260
  /** Durable terminal and the command that ended the work's opaque metadata. */
257
261
  terminal: SessionTerminal;
258
262
  /** Last completed assistant text; cancelled work returns no answer. */
@@ -261,4 +265,13 @@ export type CommandSettlement = Readonly<{
261
265
  usage: readonly Pick<AssistantMessage, "provider" | "model" | "usage">[];
262
266
  /** Distinct tool-call intentions belonging to this exact work. */
263
267
  toolCalls: number;
268
+ /**
269
+ * Open items of the Session that now wait on the Agent that sent this work.
270
+ * Child-authored data: summaries inform, only the sender's host checks decide.
271
+ */
272
+ waitingOnYou: readonly Readonly<{
273
+ itemId: string;
274
+ kind: string;
275
+ summary: string;
276
+ }>[];
264
277
  }>;
@@ -1,6 +1,7 @@
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";
@@ -348,7 +349,7 @@ export function openCoordinator(options, runtime) {
348
349
  })), Effect.asVoid))),
349
350
  }),
350
351
  extension: (extensionId) => Object.freeze({
351
- call: (command, input) => runtime.assertOpen.pipe(Effect.andThen(runtime.extensions.call({ scope: "session", sessionId }, extensionId, command, input))),
352
+ call: (command, input, caller) => runtime.assertOpen.pipe(Effect.andThen(runtime.extensions.call({ scope: "session", sessionId }, extensionId, command, input, caller))),
352
353
  }),
353
354
  read: (input) => runtime.assertOpen.pipe(Effect.andThen(readSessionWithContext(options, runtime, sessionId, input))),
354
355
  readSettlement: (commandId, timeoutMs = 0) => Effect.scoped(Effect.gen(function* () {
@@ -410,6 +411,11 @@ export function openCoordinator(options, runtime) {
410
411
  }),
411
412
  });
412
413
  }
414
+ /** Whether the sender asked this mail to steer the recipient's running turn. */
415
+ function steersRunningTurn(mail) {
416
+ const meta = FungiMessageMetaSchema.safeParse(mail.meta);
417
+ return meta.success && meta.data.delegation?.steer === true;
418
+ }
413
419
  /**
414
420
  * Accept one addressed mail envelope; the door decodes it as a prompt. `reply`
415
421
  * is the host's authentication of a delegated Agent's reply; only then does
@@ -458,7 +464,7 @@ export function acceptAddressedMail(runtime, mail, meta, reply, stageMail, sessi
458
464
  }
459
465
  return admitUserWork(runtime, mail.to.sessionId, {
460
466
  requestId: mail.key,
461
- delivery: reply === undefined ? "followUp" : "steer",
467
+ delivery: reply !== undefined || steersRunningTurn(mail) ? "steer" : "followUp",
462
468
  type: "prompt",
463
469
  text: mail.content,
464
470
  }, 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. */
@@ -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"),