@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 { FileTool } from "@fungi.computer/knapsack";
2
- import type { AttentionItem } from "./session-attention.js";
3
- export { AttentionItemSchema, type AttentionItem, type SessionAttention, } from "./session-attention.js";
2
+ import type { AttentionItem, AttentionRecipient } from "./session-attention.js";
3
+ export { AttentionItemSchema, AttentionRecipientSchema, type AttentionItem, type AttentionRecipient, type SessionAttention, } from "./session-attention.js";
4
4
  import type { Message, Usage } from "@fungi.computer/mule/effect";
5
5
  import type { ModuleRegistration } from "@fungi.computer/mycelium";
6
6
  import * as Brand from "effect/Brand";
@@ -261,6 +261,8 @@ export type SendInputV0 = Readonly<{
261
261
  when: "now" | "idle";
262
262
  /** Stored with the admission and returned on `SettledRunV0.origin`. */
263
263
  correlation?: string;
264
+ /** This work continues earlier work of the Session and keeps its sender. */
265
+ continuesWorkId?: string;
264
266
  }>;
265
267
  /** Door outcome for one extension admission. */
266
268
  export type SendResultV0 = Readonly<{
@@ -290,17 +292,26 @@ export type CommandSessionIdsV0 = {
290
292
  agent: null;
291
293
  session: string;
292
294
  };
295
+ /** Host-authenticated caller context; never decoded from command JSON or HTTP. */
296
+ export type ExtensionCommandCallerV0 = Readonly<{
297
+ agentId: string;
298
+ sessionId: string;
299
+ }>;
293
300
  /** Handlers for every declared command, run in one core transaction. */
294
301
  export type CommandHandlersV0<C extends CommandsV0> = {
295
302
  readonly [K in keyof C]: (call: Readonly<{
296
303
  sessionId: CommandSessionIdsV0[C[K]["scope"]];
297
304
  input: C[K]["input"]["Type"];
305
+ /** Present only when the owning in-process host authenticates a caller. */
306
+ caller?: ExtensionCommandCallerV0;
298
307
  }>) => C[K]["output"]["Type"] | Effect.Effect<C[K]["output"]["Type"], ExtensionCommandError | ExtensionStateError | AdmissionError, ExtensionTx | ExtensionRead>;
299
308
  };
300
309
  /** Transaction-scoped, extension-owned attention on an existing Session. */
301
310
  export type AttentionV0 = Readonly<{
302
311
  /** Upsert this extension’s item and publish the committed Session change. */
303
312
  set: (sessionId: string, item: AttentionItem) => Effect.Effect<void, ExtensionStateError, ExtensionTx>;
313
+ /** Whether any extension holds an inbox item asking a person to act (a question or approval). */
314
+ awaitsPerson: (sessionId: string) => Effect.Effect<boolean, ExtensionStateError, ExtensionRead>;
304
315
  /** Clear this extension’s item; other owners cannot be changed. */
305
316
  clear: (sessionId: string, id: string) => Effect.Effect<void, ExtensionStateError, ExtensionTx>;
306
317
  }>;
@@ -322,10 +333,18 @@ export type ExtensionContextV0<S extends CollectionsV0, Config extends Json, D e
322
333
  sessionPosition: (sessionId: string) => Effect.Effect<number, ExtensionStateError, ExtensionRead>;
323
334
  /** Observe accepted steering without consuming Mule's next control delivery. Requires run_tools. */
324
335
  hasPendingSteer: (run: Pick<RunIdentityV0, "sessionId" | "workId">) => Effect.Effect<boolean, ExtensionStateError, ExtensionRead>;
336
+ /**
337
+ * The Agent Session that sent this run, or null when a person (or the host)
338
+ * did. Attention an extension raises for the run waits on it first.
339
+ * Requires run_tools.
340
+ */
341
+ workSender: (run: Pick<RunIdentityV0, "sessionId" | "workId">) => Effect.Effect<AttentionRecipient | null, ExtensionStateError, ExtensionRead>;
325
342
  /** Whether this run durably recorded a successful tool outcome. Requires run_tools. */
326
343
  hasSuccessfulToolResult: (run: Pick<RunIdentityV0, "sessionId" | "workId">, toolCallId: string) => Effect.Effect<boolean, ExtensionStateError, ExtensionRead>;
327
344
  /** Publish a Session observation after this transaction commits. */
328
345
  observe: (sessionId: string) => Effect.Effect<void, never, ExtensionTx>;
346
+ /** Process-local live-view demand, excluding exact-command settlement waiters. */
347
+ isObserved: (sessionId: string) => boolean;
329
348
  /**
330
349
  * Run one write transaction from outside a hook (for example a Mycelium
331
350
  * tool execute). Core commits, publishes, and requests a wake when it
@@ -1,4 +1,4 @@
1
- export { AttentionItemSchema, } from "./session-attention.js";
1
+ export { AttentionItemSchema, AttentionRecipientSchema, } from "./session-attention.js";
2
2
  import * as Brand from "effect/Brand";
3
3
  import * as Context from "effect/Context";
4
4
  import * as Data from "effect/Data";
package/dist/fleet.d.ts CHANGED
@@ -180,11 +180,22 @@ export declare function createAgentDirectory(sql: WatchdogSqliteOwner["sql"]): {
180
180
  /** Whether the retained erasure roster covers this store. */
181
181
  erasureRosterComplete: () => boolean;
182
182
  };
183
+ /**
184
+ * Whether an Agent may read one command's settlement: the Agent whose mail the
185
+ * command answered, or the Session's delegator of record for work no Agent's
186
+ * mail admitted (goal, loop and person turns, the work its reports carry).
187
+ * Another Agent's correspondence stays with that Agent.
188
+ */
189
+ export declare function settlementReadableBy(facts: Pick<CommandSettlement, "acceptedSender" | "delegator">, agentId: string): boolean;
183
190
  /** Replay identity and work, with the parent supplied exclusively by the grant. */
184
191
  export declare const agentDelegationTaskSchema: z.ZodLazy<z.ZodObject<{
185
192
  requestId: z.ZodString;
186
193
  agentId: z.ZodString;
187
194
  sessionId: z.ZodOptional<z.ZodString>;
195
+ delivery: z.ZodOptional<z.ZodEnum<{
196
+ followUp: "followUp";
197
+ steer: "steer";
198
+ }>>;
188
199
  text: z.ZodString;
189
200
  }, z.core.$strict>>;
190
201
  /** A caller reuses this task unchanged while polling Pending. */
@@ -255,8 +266,9 @@ export declare function delegationSendMeta(sender: Readonly<{
255
266
  name: string;
256
267
  }>, invocationMeta?: Json): DelegationSendMeta;
257
268
  /**
258
- * The host metadata for one authenticated reply line: the replying Agent is
259
- * its author, and the verified reply rides beside the caller's own path.
269
+ * The host metadata for one authenticated reply line. An ordinary reply is
270
+ * authored by its replying Agent; a delivery failure is a Hub/system fact,
271
+ * while the verified reply retains the failed recipient's identity.
260
272
  */
261
273
  export declare function delegationReplyMeta(delegation: AuthenticatedDelegationReply, replierName: string): DelegationReplyMeta;
262
274
  /**
@@ -279,6 +291,10 @@ export declare function createFleetCorrespondence(options: Omit<FleetDelegationO
279
291
  start: (grant: AgentGrant, raw: AgentDelegationTask) => Promise<DelegationReceipt>;
280
292
  /** Match a reply to frozen mail and restore the caller's delegation path. */
281
293
  authenticateReply: (mail: BirdDogMail) => AuthenticatedDelegationReply | undefined;
294
+ /** Bind exhausted local delivery to its frozen sender correspondence. */
295
+ recordLocalUndeliverable: (original: BirdDogMail, reply: BirdDogMail) => boolean;
296
+ /** Prevent replay after a system undeliverable reply is admitted. */
297
+ recordUndeliverableReply: (mailKey: string) => boolean;
282
298
  /** Read an existing acceptance receipt without reserving or delivering mail. */
283
299
  receipt: (requestId: string) => Accepted | undefined;
284
300
  /** Consume a recipient's stable `<mailKey>:accepted` notice. */
@@ -292,6 +308,10 @@ export declare function createFleetDelegation(options: FleetDelegationOptions):
292
308
  start: (grant: AgentGrant, raw: AgentDelegationTask) => Promise<DelegationReceipt>;
293
309
  /** Match a reply to frozen mail and restore the caller's delegation path. */
294
310
  authenticateReply: (mail: BirdDogMail) => AuthenticatedDelegationReply | undefined;
311
+ /** Bind exhausted local delivery to its frozen sender correspondence. */
312
+ recordLocalUndeliverable: (original: BirdDogMail, reply: BirdDogMail) => boolean;
313
+ /** Prevent replay after a system undeliverable reply is admitted. */
314
+ recordUndeliverableReply: (mailKey: string) => boolean;
295
315
  /** Read an existing acceptance receipt without reserving or delivering mail. */
296
316
  receipt: (requestId: string) => Accepted | undefined;
297
317
  /** Consume a recipient's stable `<mailKey>:accepted` notice. */
package/dist/fleet.js CHANGED
@@ -269,11 +269,23 @@ export function createAgentDirectory(sql) {
269
269
  erasureRosterComplete: store.erasureRosterComplete,
270
270
  };
271
271
  }
272
+ /**
273
+ * Whether an Agent may read one command's settlement: the Agent whose mail the
274
+ * command answered, or the Session's delegator of record for work no Agent's
275
+ * mail admitted (goal, loop and person turns, the work its reports carry).
276
+ * Another Agent's correspondence stays with that Agent.
277
+ */
278
+ export function settlementReadableBy(facts, agentId) {
279
+ return (facts.acceptedSender === agentId ||
280
+ (facts.acceptedSender === null && facts.delegator === agentId));
281
+ }
272
282
  /** Replay identity and work, with the parent supplied exclusively by the grant. */
273
283
  export const agentDelegationTaskSchema = z.lazy(() => z.strictObject({
274
284
  requestId: identity,
275
285
  agentId: identity,
276
286
  sessionId: identity.optional(),
287
+ /** A live steer into the recipient's running turn; queued follow-up when omitted. */
288
+ delivery: z.enum(["followUp", "steer"]).optional(),
277
289
  text: z
278
290
  .string()
279
291
  .min(1)
@@ -318,16 +330,21 @@ export function delegationSendMeta(sender, invocationMeta) {
318
330
  };
319
331
  }
320
332
  /**
321
- * The host metadata for one authenticated reply line: the replying Agent is
322
- * its author, and the verified reply rides beside the caller's own path.
333
+ * The host metadata for one authenticated reply line. An ordinary reply is
334
+ * authored by its replying Agent; a delivery failure is a Hub/system fact,
335
+ * while the verified reply retains the failed recipient's identity.
323
336
  */
324
337
  export function delegationReplyMeta(delegation, replierName) {
325
- return {
326
- ...authorMeta({
338
+ const author = delegation.reply.outcome === "undeliverable" &&
339
+ delegation.reply.reason === "recipient_offline"
340
+ ? authorMeta({ id: "hub/system", name: "Hub/system" })
341
+ : authorMeta({
327
342
  id: delegation.reply.agentId,
328
343
  name: replierName,
329
344
  kind: "agent",
330
- }),
345
+ });
346
+ return {
347
+ ...author,
331
348
  delegation: {
332
349
  ancestors: [...delegation.ancestors],
333
350
  maxDepth: delegation.maxDepth,
@@ -341,7 +358,7 @@ const delegationMetadata = z.object({
341
358
  const delegationRowSchema = z.lazy(() => z.strictObject({
342
359
  input: z.string(),
343
360
  mail: z.string(),
344
- state: z.enum(["pending", "held", "accepted"]),
361
+ state: z.enum(["pending", "held", "accepted", "undeliverable"]),
345
362
  receipt: z.string().nullable(),
346
363
  }));
347
364
  const heldRecipientOffline = {
@@ -351,7 +368,7 @@ const heldRecipientOffline = {
351
368
  };
352
369
  const decodeMail = Schema.decodeUnknownSync(Schema.fromJsonString(BirdDogMailSchema));
353
370
  function bindRecipientSession(mail, sessionId) {
354
- if (mail.from.sessionId === undefined)
371
+ if (mail.from.kind !== "agent")
355
372
  throw new DirectoryError(503, "partial_init");
356
373
  return {
357
374
  ...mail,
@@ -390,7 +407,8 @@ export function createFleetCorrespondence(options) {
390
407
  throw new AgentGrantError("delegation_loop");
391
408
  if (ancestors.length >= maxDepth)
392
409
  throw new AgentGrantError("delegation_depth");
393
- const delegation = { ancestors: [...ancestors, parent.agentId], maxDepth };
410
+ const path = { ancestors: [...ancestors, parent.agentId], maxDepth };
411
+ const delegation = task.delivery === "steer" ? { ...path, steer: true } : path;
394
412
  const outgoingMeta = {
395
413
  ...(objectMeta.success
396
414
  ? objectMeta.data
@@ -434,7 +452,7 @@ export function createFleetCorrespondence(options) {
434
452
  throw new DirectoryError(503, "partial_init");
435
453
  if (row.input !== input)
436
454
  throw new DirectoryError(409, "request_conflict");
437
- if (row.state === "held")
455
+ if (row.state === "held" || row.state === "undeliverable")
438
456
  return heldRecipientOffline;
439
457
  if (row.state === "accepted") {
440
458
  if (row.receipt === null)
@@ -466,25 +484,55 @@ export function createFleetCorrespondence(options) {
466
484
  */
467
485
  function authenticateReply(mail) {
468
486
  // This owner sends to one correspondent, so BirdDog's reply index is zero.
469
- const suffix = ":reply:0";
487
+ // A report names the idle Session's delegator mail and the settled command.
470
488
  if (mail.replyTo.length !== 0 ||
471
489
  mail.reply === undefined ||
472
- mail.reply.outcome === "accepted" ||
473
- !mail.key.endsWith(suffix))
490
+ mail.reply.outcome === "accepted")
474
491
  return undefined;
475
- const row = read(mail.key.slice(0, -suffix.length));
476
- if (row === undefined)
492
+ const reportAt = mail.key.lastIndexOf(":report:");
493
+ // A report's key names the command it settles.
494
+ const isReport = reportAt >= 0 &&
495
+ "commandId" in mail.reply &&
496
+ mail.key.slice(reportAt) === `:report:${mail.reply.commandId}:0`;
497
+ const mailKey = isReport
498
+ ? mail.key.slice(0, reportAt)
499
+ : mail.key.endsWith(":reply:0")
500
+ ? mail.key.slice(0, -":reply:0".length)
501
+ : undefined;
502
+ if (mailKey === undefined)
503
+ return undefined;
504
+ const row = read(mailKey);
505
+ if (row === undefined || row.state === "undeliverable")
477
506
  return undefined;
478
507
  const sent = decodeMail(row.mail);
479
- if (mail.reply.outcome !== "undeliverable" &&
480
- mail.from.sessionId === undefined)
508
+ if (sent.key !== mailKey)
481
509
  return undefined;
482
- if (mail.from.agentId !== sent.to.agentId ||
483
- mail.from.sessionId !== sent.to.sessionId ||
484
- mail.to.agentId !== sent.from.agentId ||
485
- mail.to.sessionId !== sent.from.sessionId)
510
+ const systemUndeliverable = mail.reply.outcome === "undeliverable" &&
511
+ mail.reply.reason === "recipient_offline" &&
512
+ mail.from.kind === "system";
513
+ if (systemUndeliverable) {
514
+ if (row.state !== "held" ||
515
+ sent.from.kind !== "agent" ||
516
+ mail.key !== `${sent.key}:reply:0` ||
517
+ mail.to.agentId !== sent.from.agentId ||
518
+ mail.to.sessionId !== sent.from.sessionId)
519
+ return undefined;
520
+ }
521
+ else {
522
+ if (sent.from.kind !== "agent" ||
523
+ mail.from.kind !== "agent" ||
524
+ mail.from.agentId !== sent.to.agentId ||
525
+ mail.from.sessionId !== sent.to.sessionId ||
526
+ mail.to.agentId !== sent.from.agentId ||
527
+ mail.to.sessionId !== sent.from.sessionId)
528
+ return undefined;
529
+ }
530
+ if (mail.reply.outcome === "undeliverable" &&
531
+ mail.reply.reason === "recipient_offline" &&
532
+ !systemUndeliverable)
486
533
  return undefined;
487
- if (mail.reply.outcome !== "undeliverable" &&
534
+ if (!isReport &&
535
+ mail.reply.outcome !== "undeliverable" &&
488
536
  row.state === "accepted" &&
489
537
  row.receipt !== null) {
490
538
  const sentResult = parseAccepted(row.receipt);
@@ -496,16 +544,28 @@ export function createFleetCorrespondence(options) {
496
544
  return undefined;
497
545
  let reply;
498
546
  if (mail.reply.outcome === "undeliverable") {
499
- const undeliverable = { agentId: mail.from.agentId, ...mail.reply };
547
+ const agentId = systemUndeliverable
548
+ ? sent.to.agentId
549
+ : mail.from.kind === "agent"
550
+ ? mail.from.agentId
551
+ : undefined;
552
+ if (agentId === undefined)
553
+ return undefined;
554
+ const undeliverable = { agentId, ...mail.reply };
555
+ const sessionId = systemUndeliverable
556
+ ? sent.to.sessionId
557
+ : mail.from.kind === "agent"
558
+ ? mail.from.sessionId
559
+ : undefined;
500
560
  reply =
501
- mail.from.sessionId === undefined
561
+ sessionId === undefined
502
562
  ? undeliverable
503
- : { ...undeliverable, sessionId: mail.from.sessionId };
563
+ : { ...undeliverable, sessionId };
504
564
  }
505
565
  else {
506
- const sessionId = mail.from.sessionId;
507
- if (sessionId === undefined)
566
+ if (mail.from.kind !== "agent")
508
567
  return undefined;
568
+ const sessionId = mail.from.sessionId;
509
569
  reply = { agentId: mail.from.agentId, sessionId, ...mail.reply };
510
570
  }
511
571
  return {
@@ -514,6 +574,29 @@ export function createFleetCorrespondence(options) {
514
574
  reply,
515
575
  };
516
576
  }
577
+ /** Bind an exhausted local Carrier row to its frozen sender correspondence. */
578
+ function recordLocalUndeliverable(original, reply) {
579
+ const row = read(original.key);
580
+ if (row === undefined ||
581
+ (row.state !== "pending" && row.state !== "held") ||
582
+ original.from.kind !== "agent" ||
583
+ original.reply !== undefined ||
584
+ JSON.stringify(decodeMail(row.mail)) !== JSON.stringify(original) ||
585
+ reply.key !== `${original.key}:reply:0` ||
586
+ reply.from.kind !== "system" ||
587
+ reply.reply?.outcome !== "undeliverable" ||
588
+ reply.reply.reason !== "recipient_offline" ||
589
+ reply.to.agentId !== original.from.agentId ||
590
+ reply.to.sessionId !== original.from.sessionId ||
591
+ reply.replyTo.length !== 0)
592
+ return false;
593
+ store.recordHeld(original.key);
594
+ return read(original.key)?.state === "held";
595
+ }
596
+ /** Make a successfully admitted system disposition replay-ineligible. */
597
+ function recordUndeliverableReply(mailKey) {
598
+ return store.recordUndeliverable(mailKey);
599
+ }
517
600
  /** Read only the existing acceptance receipt; do not reserve or deliver mail. */
518
601
  function receipt(requestId) {
519
602
  const row = read(requestId);
@@ -524,9 +607,11 @@ export function createFleetCorrespondence(options) {
524
607
  /** Atomically bind the recipient Session carried by a late acceptance notice. */
525
608
  function recordAcceptedNotice(mail) {
526
609
  const suffix = ":accepted";
610
+ if (mail.from.kind !== "agent")
611
+ return undefined;
612
+ const sender = mail.from;
527
613
  if (mail.replyTo.length !== 0 ||
528
614
  mail.reply?.outcome !== "accepted" ||
529
- mail.from.sessionId === undefined ||
530
615
  !mail.key.endsWith(suffix))
531
616
  return undefined;
532
617
  const requestId = mail.key.slice(0, -suffix.length);
@@ -534,29 +619,31 @@ export function createFleetCorrespondence(options) {
534
619
  if (row === undefined)
535
620
  return undefined;
536
621
  const sent = decodeMail(row.mail);
622
+ if (sent.from.kind !== "agent")
623
+ return undefined;
624
+ const sentSender = sent.from;
537
625
  const path = delegationMetadata.safeParse(sent.meta);
538
- if (sent.from.sessionId === undefined ||
539
- !path.success ||
626
+ if (!path.success ||
540
627
  path.data.delegation === undefined ||
541
628
  mail.key !== `${sent.key}:accepted` ||
542
- mail.from.agentId !== sent.to.agentId ||
629
+ sender.agentId !== sent.to.agentId ||
543
630
  (sent.to.sessionId !== undefined &&
544
- sent.to.sessionId !== mail.from.sessionId) ||
545
- mail.to.agentId !== sent.from.agentId ||
546
- mail.to.sessionId !== sent.from.sessionId)
631
+ sent.to.sessionId !== sender.sessionId) ||
632
+ mail.to.agentId !== sentSender.agentId ||
633
+ mail.to.sessionId !== sentSender.sessionId)
547
634
  return undefined;
548
635
  if (row.state === "accepted" && row.receipt !== null) {
549
636
  const current = parseAccepted(row.receipt);
550
637
  if (current.commandId !== mail.reply.commandId ||
551
- current.sessionId !== mail.from.sessionId)
638
+ current.sessionId !== sender.sessionId)
552
639
  return undefined;
553
640
  }
554
641
  const accepted = {
555
642
  status: "accepted",
556
643
  commandId: mail.reply.commandId,
557
- sessionId: mail.from.sessionId,
644
+ sessionId: sender.sessionId,
558
645
  };
559
- store.recordReceipt(requestId, JSON.stringify(accepted), row.mail, JSON.stringify(bindRecipientSession(sent, mail.from.sessionId)));
646
+ store.recordReceipt(requestId, JSON.stringify(accepted), row.mail, JSON.stringify(bindRecipientSession(sent, sender.sessionId)));
560
647
  return accepted;
561
648
  }
562
649
  return {
@@ -564,6 +651,10 @@ export function createFleetCorrespondence(options) {
564
651
  start,
565
652
  /** Match a reply to frozen mail and restore the caller's delegation path. */
566
653
  authenticateReply,
654
+ /** Bind exhausted local delivery to its frozen sender correspondence. */
655
+ recordLocalUndeliverable,
656
+ /** Prevent replay after a system undeliverable reply is admitted. */
657
+ recordUndeliverableReply,
567
658
  /** Read an existing acceptance receipt without reserving or delivering mail. */
568
659
  receipt,
569
660
  /** Consume a recipient's stable `<mailKey>:accepted` notice. */
package/dist/hosted.js CHANGED
@@ -166,12 +166,15 @@ function hostedPort(manifest, revision, grants) {
166
166
  }),
167
167
  attention: Object.freeze({
168
168
  set: () => Effect.fail(unavailable("Attention")),
169
+ awaitsPerson: () => Effect.fail(unavailable("Attention")),
169
170
  clear: () => Effect.fail(unavailable("Attention")),
170
171
  }),
171
172
  hasPendingSteer: () => Effect.fail(unavailable("Steering")),
173
+ workSender: () => Effect.fail(unavailable("Run sender")),
172
174
  sessionPosition: () => Effect.fail(unavailable("Session timeline")),
173
175
  hasSuccessfulToolResult: () => Effect.fail(unavailable("A tool result")),
174
176
  observe: () => Effect.die(unavailable("Observation")),
177
+ isObserved: () => false,
175
178
  transact: () => Effect.fail(new ShiitakeError("extensions", "rejected", "A hosted extension has no Agent transaction")),
176
179
  }),
177
180
  });
@@ -502,6 +502,7 @@ export declare function createShiitakeHttpRoutes(resolveSession: ShiitakeSession
502
502
  };
503
503
  output: {
504
504
  readonly acceptedSender: string | null;
505
+ readonly delegator: string | null;
505
506
  readonly terminal: {
506
507
  readonly commandId: string;
507
508
  readonly outcome: "completed" | "cancelled" | "failed" | "interrupted";
@@ -531,6 +532,11 @@ export declare function createShiitakeHttpRoutes(resolveSession: ShiitakeSession
531
532
  };
532
533
  }[];
533
534
  readonly toolCalls: number;
535
+ readonly waitingOnYou: readonly {
536
+ readonly itemId: string;
537
+ readonly kind: string;
538
+ readonly summary: string;
539
+ }[];
534
540
  } | null;
535
541
  outputFormat: "json";
536
542
  status: 200;
@@ -873,6 +879,11 @@ export declare function createShiitakeHttpRoutes(resolveSession: ShiitakeSession
873
879
  readonly extensionId: string;
874
880
  readonly expiresAt?: number | undefined;
875
881
  readonly inbox?: false | undefined;
882
+ readonly waitingOn?: {
883
+ readonly agentId: string;
884
+ readonly sessionId: string;
885
+ readonly name: string;
886
+ } | undefined;
876
887
  }[];
877
888
  readonly context?: {
878
889
  readonly tokens: number;
@@ -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
  });
@@ -16,14 +17,37 @@ function decodeReply(request) {
16
17
  return undefined;
17
18
  const reply = requireDecoded(decodeVerifiedReply(request.reply), "Verified Agent reply is invalid");
18
19
  const mail = request.mail;
20
+ const agentAuthored = mail?.from.kind === "agent" &&
21
+ mail.from.agentId === reply.agentId &&
22
+ mail.from.sessionId === reply.sessionId;
23
+ const systemUndeliverable = mail?.from.kind === "system" &&
24
+ reply.outcome === "undeliverable" &&
25
+ reply.reason === "recipient_offline";
19
26
  if (mail?.replyTo.length !== 0 ||
20
- mail.from.agentId !== reply.agentId ||
21
- mail.from.sessionId !== reply.sessionId ||
27
+ (!agentAuthored && !systemUndeliverable) ||
22
28
  request.command.type !== "prompt" ||
23
29
  request.command.delivery !== "steer")
24
30
  throw new ShiitakeError("contract", "invalid", "Verified Agent reply does not match its mail");
25
31
  return reply;
26
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
+ }
27
51
  /** Bind the door to one owner, run contract, Watchdog queue and host wake. */
28
52
  export function openAdmissionDoor(input) {
29
53
  /** The door's one decode, done before any owner operation when possible. */
@@ -36,6 +60,7 @@ export function openAdmissionDoor(input) {
36
60
  mail: request.mail,
37
61
  reply: decodeReply(request),
38
62
  onAccepted: request.onAccepted,
63
+ continuesWorkId: request.continuesWorkId,
39
64
  meta: request.meta === undefined
40
65
  ? undefined
41
66
  : checkHostMeta(input.hostMeta, request.meta),
@@ -70,6 +95,12 @@ export function openAdmissionDoor(input) {
70
95
  input.enqueue(work, request.mail);
71
96
  }
72
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);
73
104
  request.onAccepted?.(sessionId, accepted.accepted.commandId);
74
105
  touched.set(sessionId, (touched.get(sessionId) ?? false) || accepted.delivery === "new");
75
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)),