@fungi.computer/shiitake 0.6.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 (63) hide show
  1. package/README.md +59 -21
  2. package/dist/agent-control.d.ts +1 -1
  3. package/dist/agent-control.js +40 -21
  4. package/dist/client-visitor.d.ts +2 -0
  5. package/dist/client-visitor.js +6 -1
  6. package/dist/client-wire.d.ts +4 -0
  7. package/dist/client-wire.js +18 -3
  8. package/dist/client.d.ts +24 -4
  9. package/dist/client.js +22 -0
  10. package/dist/cloudflare.d.ts +20 -0
  11. package/dist/cloudflare.js +4 -1
  12. package/dist/delegation.d.ts +5 -0
  13. package/dist/delegation.js +24 -8
  14. package/dist/effect.d.ts +1 -1
  15. package/dist/errors.d.ts +2 -2
  16. package/dist/errors.js +2 -0
  17. package/dist/extensions.d.ts +1 -1
  18. package/dist/fleet.d.ts +10 -2
  19. package/dist/fleet.js +77 -19
  20. package/dist/http-routes.d.ts +12 -9
  21. package/dist/http.d.ts +1 -0
  22. package/dist/http.js +1 -0
  23. package/dist/index.d.ts +1 -1
  24. package/dist/internal/admission-door.d.ts +1 -1
  25. package/dist/internal/admission-door.js +8 -4
  26. package/dist/internal/contracts.d.ts +5 -0
  27. package/dist/internal/coordinator.js +107 -50
  28. package/dist/internal/effect-contracts.d.ts +5 -0
  29. package/dist/internal/extension-runtime.js +5 -3
  30. package/dist/internal/promise-facade.js +2 -0
  31. package/dist/internal/run-scope.js +27 -7
  32. package/dist/internal/runtime-coordinator.d.ts +14 -0
  33. package/dist/internal/runtime-coordinator.js +52 -10
  34. package/dist/internal/runtime-core.d.ts +3 -0
  35. package/dist/internal/runtime-core.js +1 -0
  36. package/dist/internal/runtime-reconciliation.js +4 -1
  37. package/dist/internal/session-context.d.ts +5 -5
  38. package/dist/internal/session-context.js +80 -83
  39. package/dist/internal/session-status.d.ts +2 -2
  40. package/dist/internal/session-status.js +5 -3
  41. package/dist/internal/woodstock/client-projection.js +5 -11
  42. package/dist/internal/woodstock/fleet-store.d.ts +3 -2
  43. package/dist/internal/woodstock/fleet-store.js +24 -4
  44. package/dist/internal/woodstock/index.d.ts +3 -3
  45. package/dist/internal/woodstock/index.js +16 -6
  46. package/dist/internal/woodstock/progress.d.ts +11 -1
  47. package/dist/internal/woodstock/progress.js +126 -25
  48. package/dist/internal/woodstock/session-delegator.d.ts +2 -1
  49. package/dist/internal/woodstock/session-store.d.ts +14 -2
  50. package/dist/internal/woodstock/session-store.js +329 -66
  51. package/dist/node.js +11 -2
  52. package/dist/server-visitor-stream.js +33 -24
  53. package/docs/api/agent-control.md +3 -2
  54. package/docs/api/client.md +77 -8
  55. package/docs/api/cloudflare.md +16 -0
  56. package/docs/api/delegation.md +84 -50
  57. package/docs/api/effect.md +85 -7
  58. package/docs/api/extensions.md +8 -8
  59. package/docs/api/fleet.md +87 -27
  60. package/docs/api/http.md +28 -0
  61. package/docs/api/index.md +39 -15
  62. package/docs/api/testing.md +9 -7
  63. package/package.json +5 -5
package/README.md CHANGED
@@ -55,10 +55,12 @@ without waiting for the command response or matching text.
55
55
  npm install @fungi.computer/shiitake
56
56
  ```
57
57
 
58
- The package ships ESM only and depends on Effect, Zod, and Hono, plus the Fungi
59
- packages [Cairn](../cairn/README.md), [Mule](../mule/README.md),
58
+ The package ships ESM only. Its runtime dependencies include Effect, Zod, Pi and
59
+ the Fungi packages [Cairn](../cairn/README.md), [Mule](../mule/README.md),
60
60
  [Mycelium](../mycelium/README.md), [Watchdog](../watchdog/README.md),
61
- [Bird Dog](../bird-dog/README.md) and Knapsack, all installed automatically.
61
+ [Bird Dog](../bird-dog/README.md) and Knapsack. See the [manifest](package.json)
62
+ for the complete list. Hono `^4.13.5` is a peer dependency for the HTTP
63
+ adapters.
62
64
 
63
65
  `Shiitake.make(options)` is the Promise constructor. The Effect 4 constructor is
64
66
  `make` from `@fungi.computer/shiitake/effect`. Both use the same owner. The host
@@ -134,6 +136,17 @@ a Session performs no I/O. A client uses `createShiitakeClient` from the
134
136
  `/client` entry with the host's authorized transport. Releasing that client does
135
137
  not cancel host work.
136
138
 
139
+ Live `progress` frames carry `SessionProgressDelta`: independent text and
140
+ thinking `{ offset, append }` values, with offsets in UTF-16 string units.
141
+ `appendSessionProgress` from `/client` folds appends idempotently; a gap waits
142
+ for the owner's reset. A read/watch/reset baseline carries only the latest 64
143
+ KiB per channel, its start offsets, and accurate truncation flags. A watch
144
+ continues after that tail; the eventual durable message is always complete.
145
+ Progress stays process-local, in bounded chunks, and never writes SQLite. Tiny
146
+ provider deltas are coalesced (first append immediately, then each 4,096 string
147
+ units or within 100 ms); durable completion carries any remaining partial chunk.
148
+ Tool tails travel only in `tool_output`, not in message progress.
149
+
137
150
  `session.readSettlement(commandId, { timeoutMs, signal })` returns that exact
138
151
  command's `CommandSettlement` (terminal, answer, usage and tool-call count),
139
152
  independent of the selected transcript branch. `timeoutMs` defaults to zero and
@@ -354,24 +367,31 @@ The `/agent-control` source exposes
354
367
  `agents.setModel({agentId, model, reasoning?})` and `agents.models({})` when its
355
368
  host authorizes those operations. `name` labels the new Agent and `reasoning`
356
369
  (`minimal` to `max`, one of `modelReasoningLevels`) needs a `model`.
357
- `agents.create` returns the Agent's `name`, the requested `model` and
358
- `reasoning` (the stored default; absent when it inherits), and the
359
- `workingDirectory` its tools use; `agents.list` reports the same facts for
360
- existing Agents. A host that can archive also accepts `archived: true` there to
361
- list only the archived ones (other hosts refuse that input). A host can offer
362
- `agents.archive({agentId})` and `agents.unarchive`: an Agent archiving an Agent
363
- it created hides it from lists and delegation without deleting anything, and the
364
- host refuses any other target, the caller itself, or an Agent with a running
365
- command or delegated work still waiting. Each refusal, and a send to an archived
366
- Agent, reaches the caller as a `CapabilityFailed` diagnostic such as
367
- `archive:not_creator: …`, `archive:self`, `archive:running`, `archive:pending`,
368
- `archive:unreadable` (its store predates the schema) or `archive:archived`.
369
- `agents.wait` accepts `timeoutMs` from 1 to 30000. Models use `provider/model`
370
- spelling and must be offered by that host; `agents.models` returns at most 200
371
- offered addresses from signed-in or configured providers. A host model refusal
372
- reaches the caller as a `CapabilityFailed` diagnostic such as
373
- `model:rejected: …`, while other failures propagate unchanged. The
374
- `/model-preference` resolver orders explicit Session, explicit Agent, the
370
+ `agents.create` creates the Agent and its first Session through the same
371
+ idempotent Session-creation door as `agents.createSession`. It returns `agentId`
372
+ and a usable `sessionId`: send work immediately with `agents.send` and
373
+ `delivery: "steer"`, without a separate `createSession` call. Retrying the same
374
+ creation request reuses that first Session.
375
+
376
+ It also returns the Agent's `name`, the requested `model` and `reasoning` (the
377
+ stored default; absent when it inherits), and the `workingDirectory` its tools
378
+ use; `agents.list` reports the same facts for existing Agents. A host that can
379
+ archive also accepts `archived: true` there to list only the archived ones
380
+ (other hosts refuse that input). A host can offer `agents.archive({agentId})`
381
+ and `agents.unarchive`: an Agent archiving an Agent it created hides it from
382
+ lists and delegation without deleting anything, and the host refuses any other
383
+ target, the caller itself, or an Agent with a running command or delegated work
384
+ still waiting. Each refusal, and a send to an archived Agent, reaches the caller
385
+ as a `CapabilityFailed` diagnostic such as `archive:not_creator: …`,
386
+ `archive:self`, `archive:running`, `archive:pending`, `archive:unreadable` (its
387
+ store predates the schema) or `archive:archived`. `agents.wait` accepts
388
+ `timeoutMs` from 1 to 30000. Models use `provider/model` spelling and must be
389
+ offered by that host; `agents.models` returns at most 200 offered addresses from
390
+ signed-in or configured providers. A host model refusal reaches the caller as a
391
+ `CapabilityFailed` diagnostic such as `model:rejected: …` with up to ten
392
+ addresses from the host's offered catalog. If catalog diagnostics are
393
+ unavailable, the original refusal remains; other failures propagate unchanged.
394
+ The `/model-preference` resolver orders explicit Session, explicit Agent, the
375
395
  person's last manual switch, Team default, then available login. Hosts own those
376
396
  stored layers and credential admission: choosing a model never bypasses an
377
397
  owner's Team or Agent key-source restriction. A host can refuse an unrunnable
@@ -402,6 +422,24 @@ a host-supplied fetch.
402
422
  interrupted tools against the existing replay rules. They are local proofs, not
403
423
  a guarantee about another deployment's storage or sandbox.
404
424
 
425
+ ## Breaking live-progress API
426
+
427
+ The S4 offset/append contract replaces cumulative progress frames:
428
+
429
+ - `progress` frames carry `SessionProgressDelta`, with separate text and
430
+ thinking `{ offset, append }` channels. Offsets count UTF-16 code units.
431
+ - `SessionProgress` is the bounded snapshot shape and now requires `textOffset`
432
+ and `thinkingOffset`, alongside its truncation flags.
433
+ - `appendSessionProgress(current, delta)` is the new shared fold for progress
434
+ frames. Seed it from the **same watch's `snapshot.progress`**, replace it on a
435
+ reset, and retain it across unrelated updates until the message completes or
436
+ the queue becomes idle. A separate earlier `read()` is not a watch baseline.
437
+ - The visitor stream sends live `activity` frames as well as `progress` frames.
438
+ An activity frame updates activity facts; it is not a replacement snapshot.
439
+
440
+ All producers and consumers must use this contract together. There is no
441
+ cumulative-frame compatibility format.
442
+
405
443
  ## Client entries
406
444
 
407
445
  A host selects client entries separately from its durable extension roster.
@@ -127,7 +127,7 @@ export type AgentDelegationInput = Readonly<{
127
127
  sessionId: string;
128
128
  delivery: "steer";
129
129
  }> | Readonly<{
130
- /** Omitted for an Agent whose first Session is created on acceptance. */
130
+ /** Omitted to open a new Session when the recipient accepts. */
131
131
  sessionId?: string;
132
132
  delivery: "followUp";
133
133
  /**
@@ -1,6 +1,7 @@
1
1
  import { CapabilityFailed, Mycelium, } from "@fungi.computer/mycelium";
2
2
  import { z } from "zod";
3
3
  import { ShiitakeError } from "./errors.js";
4
+ import { reportCatchFailure } from "./internal/failure.js";
4
5
  import { AGENT_REPLY_TAG } from "./internal/woodstock/index.js";
5
6
  import { modelReasoningLevels, parseModelAddress, } from "./model-preference.js";
6
7
  const id = z.string().min(1).max(256);
@@ -149,6 +150,7 @@ const agentOutput = z.strictObject({
149
150
  workingDirectory: z.string().min(1).max(4096).exactOptional(),
150
151
  });
151
152
  const agentsOutput = z.array(agentOutput);
153
+ const createdAgentOutput = agentOutput.extend({ sessionId: id });
152
154
  const offeredModelLimit = 200;
153
155
  const modelsOutput = z.strictObject({
154
156
  models: z
@@ -340,21 +342,32 @@ export class AgentCreateRefused extends Error {
340
342
  }
341
343
  }
342
344
  /** Admit the host's model refusal as a typed capability failure; rethrow anything else. */
343
- function admitModelRefusal(operation, cause) {
344
- if (cause instanceof ShiitakeError && cause.boundary === "model")
345
- throw new CapabilityFailed({
346
- namespace: "agents",
347
- operation,
348
- cause: {
349
- kind: "OperationFailed",
350
- message: `model:${cause.cause}: ${cause.message}`,
351
- },
352
- rawCause: cause,
353
- });
354
- throw cause;
345
+ async function admitModelRefusal(operation, cause, models, signal) {
346
+ if (!(cause instanceof ShiitakeError && cause.boundary === "model"))
347
+ throw cause;
348
+ let offered = [];
349
+ try {
350
+ offered = (await models?.(signal)) ?? [];
351
+ }
352
+ catch (catalogFailure) {
353
+ // Catalog diagnostics must not replace the original model refusal.
354
+ signal.throwIfAborted();
355
+ reportCatchFailure("agent-control.model-catalog", catalogFailure);
356
+ }
357
+ const addresses = offered.slice(0, 10).map((model) => model.address);
358
+ const suffix = addresses.length === 0 ? "" : ` Offered: ${addresses.join(", ")}.`;
359
+ throw new CapabilityFailed({
360
+ namespace: "agents",
361
+ operation,
362
+ cause: {
363
+ kind: "OperationFailed",
364
+ message: `model:${cause.cause}: ${cause.message}${suffix}`,
365
+ },
366
+ rawCause: cause,
367
+ });
355
368
  }
356
369
  /** Admit a creation refusal or a model refusal as a typed failure; rethrow anything else. */
357
- function admitCreateFailure(cause) {
370
+ function admitCreateFailure(cause, models, signal) {
358
371
  if (cause instanceof AgentCreateRefused)
359
372
  throw new CapabilityFailed({
360
373
  namespace: "agents",
@@ -365,10 +378,11 @@ function admitCreateFailure(cause) {
365
378
  },
366
379
  rawCause: cause,
367
380
  });
368
- return admitModelRefusal("create", cause);
381
+ return admitModelRefusal("create", cause, models, signal);
369
382
  }
370
383
  /** Compose one authorized Agent-control projection for a single run lease. */
371
384
  export function createAgentControlSource(directory) {
385
+ const createSession = (operation, agentId, requestId, signal) => withAgent(directory, operation, agentId, signal, (agent) => agent.createSession(requestId));
372
386
  const create = directory.create;
373
387
  const send = directory.send;
374
388
  const recall = directory.recall;
@@ -378,9 +392,9 @@ export function createAgentControlSource(directory) {
378
392
  ? undefined
379
393
  : Mycelium.operation({
380
394
  replay: "unsafe",
381
- description: "Create one ordinary Agent (a subagent to spawn work on) in the current authorized scope, optionally with a display name, a host-offered provider/model from agents.models, and a thinking level (reasoning: minimal, low, medium, high, xhigh or max; needs model). The new Agent works in the project directory this host runs in; the result returns that workingDirectory, the name, and the model and reasoning requested for it (the stored default, absent when it inherits). Then send it work with agents.send and collect the reply with agents.wait. Reuse requestId when retrying the same creation. A model the host does not offer fails with model:rejected. An Agent creating an Agent starts the new Agent with the base roster only; it can pass extensions, a list of extension ids it holds itself, to enable on the new Agent. Asking for one it does not hold fails with create:exceeds_creator and creates nothing.",
395
+ description: "Create one ordinary Agent (a subagent to spawn work on) and its first Session in the current authorized scope. Returns agentId and a usable sessionId: immediately agents.send with delivery: steer, then collect the reply with agents.wait; no separate createSession is needed. Reuse requestId when retrying the same creation and first Session. Optional name labels the Agent; model is a host-offered provider/model from agents.models; reasoning (minimal, low, medium, high, xhigh or max) needs model. The result also returns name, workingDirectory (the host's project directory), and the requested model/reasoning default, absent when inherited. An unoffered model fails with model:rejected and up to ten offered addresses. A new Agent starts with the base roster; extensions may enable only extension ids the creating Agent holds. An unheld extension fails with create:exceeds_creator and creates nothing.",
382
396
  input: createInput,
383
- output: agentOutput,
397
+ output: createdAgentOutput,
384
398
  execute: async ({ requestId, name, model, reasoning, extensions }, context) => {
385
399
  let request = { requestId };
386
400
  if (name !== undefined)
@@ -394,7 +408,12 @@ export function createAgentControlSource(directory) {
394
408
  model: reasoning === undefined ? address : { ...address, reasoning },
395
409
  };
396
410
  }
397
- return agentOutput.parse(agentAnswer(await create(request, context.signal).catch(admitCreateFailure)));
411
+ const created = await create(request, context.signal).catch((cause) => admitCreateFailure(cause, directory.models, context.signal));
412
+ const sessionId = await createSession("create", created.agentId, requestId, context.signal);
413
+ return createdAgentOutput.parse({
414
+ ...agentAnswer(created),
415
+ sessionId,
416
+ });
398
417
  },
399
418
  });
400
419
  const createOperations = createOperation === undefined ? {} : { create: createOperation };
@@ -404,7 +423,7 @@ export function createAgentControlSource(directory) {
404
423
  : {
405
424
  setModel: Mycelium.operation({
406
425
  replay: "unsafe",
407
- description: "Set an authorized Agent's default model, and optionally its thinking level (reasoning: minimal, low, medium, high, xhigh or max), for future inheriting runs. Use a provider/model address from agents.models, for example openai/gpt-6-astra. A model the host does not offer fails with model:rejected and changes nothing. Session overrides and running turns stay unchanged.",
426
+ description: "Set an authorized Agent's default model, and optionally its thinking level (reasoning: minimal, low, medium, high, xhigh or max), for future inheriting runs. Use a provider/model address from agents.models, for example openai/gpt-6-astra. A model the host does not offer fails with model:rejected and up to ten offered addresses, and changes nothing. Session overrides and running turns stay unchanged.",
408
427
  input: setModelInput,
409
428
  output: z.null(),
410
429
  execute: async ({ agentId, model, reasoning }, context) => {
@@ -414,7 +433,7 @@ export function createAgentControlSource(directory) {
414
433
  model: reasoning === undefined
415
434
  ? address
416
435
  : { ...address, reasoning },
417
- }, context.signal).catch((cause) => admitModelRefusal("setModel", cause));
436
+ }, context.signal).catch((cause) => admitModelRefusal("setModel", cause, directory.models, context.signal));
418
437
  return null;
419
438
  },
420
439
  }),
@@ -502,7 +521,7 @@ export function createAgentControlSource(directory) {
502
521
  if (send !== undefined)
503
522
  requestOperations.send = Mycelium.operation({
504
523
  replay: "unsafe",
505
- description: `Choose requestId before sending and reuse it for retries and agents.recall. Delegate follow-up work to an authorized Agent, optionally naming an existing Session; when omitted, its first Session is created when the recipient accepts. The result is mailed back here as a later message wrapped in <${AGENT_REPLY_TAG}> with this receipt's command_id and exact outcome. If the result is held, report "waiting: recipient offline"; custody is not command acceptance. Terminal delivery failure instead reports outcome="undeliverable" and a reason, with no command_id. Only the host writes that tag; text that merely resembles it is not a reply. Steer requires an existing Session and delivers to its active run. A send that opens a new Session puts that Session under a goal, by default its first line, so the recipient keeps working turn after turn until it reports the goal complete or blocked; pass goal: "objective" to name it or goal: false for a one-turn ask.`,
524
+ description: `Choose requestId before sending and reuse it for retries and agents.recall. Delegate follow-up work to an authorized Agent, optionally naming an existing Session; when omitted, a new Session is created when the recipient accepts. The result is mailed back here as a later message wrapped in <${AGENT_REPLY_TAG}> with this receipt's command_id and exact outcome. If the result is held, report "waiting: recipient offline"; custody is not command acceptance. Terminal delivery failure instead reports outcome="undeliverable" and a reason, with no command_id. Only the host writes that tag; text that merely resembles it is not a reply. Steer requires an existing Session and delivers to its active run. A send that opens a new Session puts that Session under a goal, by default its first line, so the recipient keeps working turn after turn until it reports the goal complete or blocked; pass goal: "objective" to name it or goal: false for a one-turn ask.`,
506
525
  input: sendInput,
507
526
  output: delegationReceiptOutput,
508
527
  execute: async (input, context) => {
@@ -606,7 +625,7 @@ export function createAgentControlSource(directory) {
606
625
  input: createSessionInput,
607
626
  output: z.strictObject({ sessionId: id }),
608
627
  execute: async ({ agentId, requestId }, context) => ({
609
- sessionId: await withAgent(directory, "createSession", agentId, context.signal, (agent) => agent.createSession(requestId)),
628
+ sessionId: await createSession("createSession", agentId, requestId, context.signal),
610
629
  }),
611
630
  }),
612
631
  sessions: Mycelium.operation({
@@ -21,6 +21,8 @@ export type VisitorView = Readonly<{
21
21
  }>[];
22
22
  progress: string | null;
23
23
  }>;
24
+ /** One visitor-visible line without internal attribution or tool metadata. */
25
+ export type VisitorLine = VisitorView["lines"][number];
24
26
  /** Public pending facts exclude the host's private metadata and admission origin. */
25
27
  export type VisitorPendingPrompt = Pick<SessionPendingPrompt, "commandId" | "requestId" | "text" | "delivery" | "images">;
26
28
  /** Project one owner-selected prompt without exposing host identity or correlation metadata. */
@@ -172,7 +172,12 @@ export function visitorSnapshot(snapshot, visitorId) {
172
172
  toolOutput: [],
173
173
  progress: !ownWork || snapshot.progress === null
174
174
  ? null
175
- : { ...snapshot.progress, thinking: "", thinkingTruncated: false },
175
+ : {
176
+ ...snapshot.progress,
177
+ thinking: "",
178
+ thinkingOffset: 0,
179
+ thinkingTruncated: false,
180
+ },
176
181
  extensions: {},
177
182
  attention: [],
178
183
  };
@@ -149,6 +149,8 @@ export declare const SessionSnapshotSchema: Schema.Struct<{
149
149
  revision: number;
150
150
  text: string;
151
151
  thinking: string;
152
+ textOffset: number;
153
+ thinkingOffset: number;
152
154
  textTruncated: boolean;
153
155
  thinkingTruncated: boolean;
154
156
  }>, Readonly<{
@@ -156,6 +158,8 @@ export declare const SessionSnapshotSchema: Schema.Struct<{
156
158
  revision: number;
157
159
  text: string;
158
160
  thinking: string;
161
+ textOffset: number;
162
+ thinkingOffset: number;
159
163
  textTruncated: boolean;
160
164
  thinkingTruncated: boolean;
161
165
  }>, never, never>>;
@@ -33,6 +33,7 @@ const SessionActivitySchema = Schema.Struct({
33
33
  kind: Schema.Literal("session"),
34
34
  sessionId: Schema.NonEmptyString,
35
35
  commandId: Schema.NonEmptyString,
36
+ title: Schema.String,
36
37
  }),
37
38
  Schema.Struct({
38
39
  kind: Schema.Literal("host"),
@@ -110,11 +111,14 @@ const SessionCompactionNoticeSchema = Schema.Struct({
110
111
  ]),
111
112
  diagnostic: Schema.NullOr(Schema.String),
112
113
  });
114
+ const ProgressTailSchema = Schema.String.check(Schema.makeFilter((value) => new TextEncoder().encode(value).byteLength <= 65_536));
113
115
  const SessionProgressSchema = Schema.Struct({
114
116
  messageId: Schema.String,
115
117
  revision: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
116
- text: Schema.String,
117
- thinking: Schema.String,
118
+ text: ProgressTailSchema,
119
+ thinking: ProgressTailSchema,
120
+ textOffset: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
121
+ thinkingOffset: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
118
122
  textTruncated: Schema.Boolean,
119
123
  thinkingTruncated: Schema.Boolean,
120
124
  });
@@ -190,7 +194,18 @@ export const SessionFrameSchema = Schema.Union([
190
194
  }),
191
195
  Schema.Struct({
192
196
  kind: Schema.Literal("progress"),
193
- progress: SessionProgressSchema,
197
+ progress: Schema.Struct({
198
+ messageId: Schema.String,
199
+ revision: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
200
+ text: Schema.Struct({
201
+ offset: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
202
+ append: ProgressTailSchema,
203
+ }),
204
+ thinking: Schema.Struct({
205
+ offset: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
206
+ append: ProgressTailSchema,
207
+ }),
208
+ }),
194
209
  activity: SessionActivitySchema,
195
210
  }),
196
211
  Schema.Struct({
package/dist/client.d.ts CHANGED
@@ -11,7 +11,7 @@ export { createShiitakeClient, type ShiitakeClientOptions, } from "./remote-clie
11
11
  export { bindAgentExtension, bindSessionExtension, type BoundAgentExtensionV0, type BoundCallV0, type BoundSessionExtensionV0, type ExtensionViewV0, type ScopedCommandsV0, } from "./extension-client.js";
12
12
  export { AcceptedSchema, DelegationReceiptSchema, HeldSchema, parseAccepted, parseDelegationReceipt, parseCancelResult, parseLatestPendingPrompt, parseWithdrawResult, parseForkedSession, parseSessionFrame, parseSessionSnapshot, parseSessionSummaryPage, parseShiitakeError, } from "./client-wire.js";
13
13
  export type { AbortCommand, BranchCommand, CompactCommand, CancelCommand, ForkCommand, OperatorCommand, PromptCommand, PromptDelivery, SessionCommand, TitleCommand, } from "./internal/client-boundary.js";
14
- export { participantSnapshot, visitorPendingPrompt, visitorSnapshot, visitorView, type ParticipantPolicy, type VisitorPendingPrompt, type VisitorView, } from "./client-visitor.js";
14
+ export { participantSnapshot, visitorPendingPrompt, visitorSnapshot, visitorView, type ParticipantPolicy, type VisitorLine, type VisitorPendingPrompt, type VisitorView, } from "./client-visitor.js";
15
15
  /** Semantic acknowledgement returned after one durable acceptance commit. */
16
16
  export type Accepted = Readonly<{
17
17
  /** Indicates that the owner transaction committed. */
@@ -132,11 +132,13 @@ export type SessionTerminal = Readonly<{
132
132
  */
133
133
  meta?: Json;
134
134
  }>;
135
- /** Work occupying the executor partition that this queued command requires, without message or provider data. */
135
+ /** Blocking executor identity and Session display metadata, never messages or provider data. */
136
136
  export type AgentRunningWork = Readonly<{
137
137
  kind: "session";
138
138
  sessionId: string;
139
139
  commandId: string;
140
+ /** Owner-issued display title; empty when no title is known. */
141
+ title: string;
140
142
  }> | Readonly<{
141
143
  kind: "host";
142
144
  lane: string;
@@ -213,9 +215,27 @@ export type SessionProgress = Readonly<{
213
215
  revision: number;
214
216
  text: string;
215
217
  thinking: string;
218
+ /** UTF-16 offsets of the retained tails, not byte offsets. */
219
+ textOffset: number;
220
+ thinkingOffset: number;
216
221
  textTruncated: boolean;
217
222
  thinkingTruncated: boolean;
218
223
  }>;
224
+ /** Append-only progress on the wire. Offsets count JavaScript string units. */
225
+ export type SessionProgressDelta = Readonly<{
226
+ messageId: string;
227
+ revision: number;
228
+ text: Readonly<{
229
+ offset: number;
230
+ append: string;
231
+ }>;
232
+ thinking: Readonly<{
233
+ offset: number;
234
+ append: string;
235
+ }>;
236
+ }>;
237
+ /** Fold a decoded append once; a missing prefix waits for an owner reset. */
238
+ export declare function appendSessionProgress(current: SessionProgress | null, delta: SessionProgressDelta): SessionProgress | null;
219
239
  /** Accepted prompt waiting to enter the conversation, not a scheduling command. */
220
240
  export type SessionPendingPrompt = Readonly<{
221
241
  /** Caller identity, available even before the command response reaches its client. */
@@ -370,9 +390,9 @@ export type SessionFrame = Readonly<{
370
390
  */
371
391
  context?: SessionContext;
372
392
  }> | Readonly<{
373
- /** Bounded process-local assistant progress. */
393
+ /** Only new text/thinking; snapshots, not frames, carry retained tails. */
374
394
  kind: "progress";
375
- progress: SessionProgress;
395
+ progress: SessionProgressDelta;
376
396
  /** Raw facts about the Session's activity read with this progress. */
377
397
  activity: SessionActivity;
378
398
  }> | Readonly<{
package/dist/client.js CHANGED
@@ -3,3 +3,25 @@ export { createShiitakeClient, } from "./remote-client.js";
3
3
  export { bindAgentExtension, bindSessionExtension, } from "./extension-client.js";
4
4
  export { AcceptedSchema, DelegationReceiptSchema, HeldSchema, parseAccepted, parseDelegationReceipt, parseCancelResult, parseLatestPendingPrompt, parseWithdrawResult, parseForkedSession, parseSessionFrame, parseSessionSnapshot, parseSessionSummaryPage, parseShiitakeError, } from "./client-wire.js";
5
5
  export { participantSnapshot, visitorPendingPrompt, visitorSnapshot, visitorView, } from "./client-visitor.js";
6
+ /** Fold a decoded append once; a missing prefix waits for an owner reset. */
7
+ export function appendSessionProgress(current, delta) {
8
+ const previous = current?.messageId === delta.messageId ? current : null;
9
+ if (previous !== null && previous.revision >= delta.revision)
10
+ return current;
11
+ const textEnd = previous === null ? 0 : previous.textOffset + previous.text.length;
12
+ const thinkingEnd = previous === null ? 0 : previous.thinkingOffset + previous.thinking.length;
13
+ if (delta.text.offset > textEnd || delta.thinking.offset > thinkingEnd)
14
+ return current;
15
+ return {
16
+ messageId: delta.messageId,
17
+ revision: delta.revision,
18
+ text: (previous?.text ?? "") +
19
+ delta.text.append.slice(Math.max(0, textEnd - delta.text.offset)),
20
+ thinking: (previous?.thinking ?? "") +
21
+ delta.thinking.append.slice(Math.max(0, thinkingEnd - delta.thinking.offset)),
22
+ textOffset: previous?.textOffset ?? 0,
23
+ thinkingOffset: previous?.thinkingOffset ?? 0,
24
+ textTruncated: previous?.textTruncated ?? false,
25
+ thinkingTruncated: previous?.thinkingTruncated ?? false,
26
+ };
27
+ }
@@ -102,6 +102,11 @@ export type CloudflareShiitakeOptions = Readonly<{
102
102
  * refuses for storage pressure. The budget and thresholds are the host's.
103
103
  */
104
104
  storagePressure?: StoragePressureCheck;
105
+ /** Host-owned synchronous check at the single new-work admission door. */
106
+ admission?: Readonly<{
107
+ /** Check the host's current flag synchronously inside admission's transaction. */
108
+ assertOpen: () => void;
109
+ }>;
105
110
  /** Compaction budgets; absent uses Pi's defaults for every model. */
106
111
  compaction?: CompactionOptions;
107
112
  /** The host's explicit extension roster and grants; there is no default. */
@@ -127,6 +132,21 @@ export type CloudflareShiitakeOptions = Readonly<{
127
132
  }>;
128
133
  /** Cloudflare host lifecycle; alarm forwarding stays outside the neutral client. */
129
134
  export type CloudflareShiitake = ShiitakeOwnerAgent & Readonly<{
135
+ /** Read durable queued/running Session or unresolved delegated work in the
136
+ * host's SQLite transaction, immediately before changing archived state.
137
+ * A confirmed remote settlement must be recorded locally first; unavailable
138
+ * remote settlement remains busy, not idle. Never use an in-memory counter.
139
+ */
140
+ busySync: () => Readonly<{
141
+ kind: "session";
142
+ sessionId: string;
143
+ commandId: string;
144
+ }> | Readonly<{
145
+ kind: "delegation";
146
+ agentId: string;
147
+ sessionId: string;
148
+ commandId: string;
149
+ }> | undefined;
130
150
  /** Exact settled command facts, outside the neutral Session transport. */
131
151
  readSettlement: (sessionId: string, commandId: string) => Promise<CommandSettlement | undefined>;
132
152
  /**
@@ -277,6 +277,8 @@ function effectOptions(state, options, runtime) {
277
277
  Object.assign(base, { meta: options.meta });
278
278
  if (options.storagePressure !== undefined)
279
279
  Object.assign(base, { storagePressure: options.storagePressure });
280
+ if (options.admission !== undefined)
281
+ Object.assign(base, { admission: options.admission });
280
282
  if (options.compaction !== undefined)
281
283
  Object.assign(base, { compaction: options.compaction });
282
284
  return tools === undefined ? base : { ...base, tools };
@@ -319,7 +321,7 @@ export function makeCloudflareShiitake(unnamed, options) {
319
321
  const coreOptions = effectOptions(state, options, host.runtime);
320
322
  return import("./internal/runtime-core.js")
321
323
  .then(({ openRuntimeCore }) => runShiitakePromise(openRuntimeCore(coreOptions, options.mailRoom)))
322
- .then(async ({ agent, activeSessions, acceptMail, readSettlement }) => {
324
+ .then(async ({ agent, activeSessions, busySync, acceptMail, readSettlement, }) => {
323
325
  const handle = toPromiseOwner(agent);
324
326
  try {
325
327
  await options.initialize?.(handle);
@@ -340,6 +342,7 @@ export function makeCloudflareShiitake(unnamed, options) {
340
342
  },
341
343
  alarm: host.alarm,
342
344
  activeSessions: () => runShiitakePromise(activeSessions),
345
+ busySync,
343
346
  abortActive: async () => {
344
347
  for (const sessionId of await runShiitakePromise(activeSessions))
345
348
  await runShiitakePromise(agent.session(sessionId).abort);
@@ -31,6 +31,11 @@ export declare class AgentDelegation {
31
31
  }>);
32
32
  private fleetOwner;
33
33
  private fleet;
34
+ /** Best-effort remote refresh before an owner-local archive transaction.
35
+ * Confirmed settlements are recorded by Shiitake; unreadable targets leave
36
+ * their local accepted receipt outstanding for busySync to refuse.
37
+ */
38
+ refreshOutstanding(readSettlement: (agentId: string, sessionId: string, commandId: string) => Promise<CommandSettlement | undefined>): Promise<void>;
34
39
  private scope;
35
40
  /** Recipient-local disclosure through the public host, never another Agent's RPC. */
36
41
  settlement(sender: DelegationIdentity, sessionId: string, commandId: string, host: Pick<ShiitakeHost, "readSettlement">): Promise<CommandSettlement | undefined>;
@@ -3,7 +3,7 @@
3
3
  * addressed Agent mail over Carrier Post, and the Agent grammar a host hands
4
4
  * to Oyster's machine link as ports. Oyster core never imports this engine.
5
5
  */
6
- import { commandSettlementSchema, createAgentDirectory, createFleetCorrespondence, agentDelegationRequestKey, delegationProvenance, delegationReplyMeta, delegationSendMeta, settlementReadableBy, } from "./fleet.js";
6
+ import { commandSettlementSchema, createAgentDirectory, createFleetCorrespondence, createFleetDelegation, agentDelegationRequestKey, delegationProvenance, delegationReplyMeta, delegationSendMeta, settlementReadableBy, } from "./fleet.js";
7
7
  import * as Schema from "effect/Schema";
8
8
  import { agentMailAddressSchema } from "@fungi.computer/seam/machine-link-protocol";
9
9
  import { BirdDogMailSchema, BirdDogError, settlementReplies, } from "@fungi.computer/bird-dog/effect";
@@ -27,6 +27,19 @@ export class AgentDelegation {
27
27
  deliver: this.deps.deliver,
28
28
  }));
29
29
  }
30
+ /** Best-effort remote refresh before an owner-local archive transaction.
31
+ * Confirmed settlements are recorded by Shiitake; unreadable targets leave
32
+ * their local accepted receipt outstanding for busySync to refuse.
33
+ */
34
+ async refreshOutstanding(readSettlement) {
35
+ const fleet = createFleetDelegation({
36
+ storage: this.deps.storage,
37
+ deliver: this.deps.deliver,
38
+ open: (agentId) => this.deps.directory().open(agentId, new AbortController().signal),
39
+ readSettlement,
40
+ });
41
+ await fleet.outstanding();
42
+ }
30
43
  async scope(signal) {
31
44
  const agents = await this.deps.directory().list(signal);
32
45
  const identity = await this.deps.identity();
@@ -72,19 +85,22 @@ export class AgentDelegation {
72
85
  });
73
86
  const meta = delegationSendMeta({ id: scope.agentId, name: scope.source.name }, invocation.meta);
74
87
  signal.throwIfAborted();
88
+ let task = {
89
+ agentId: input.agentId,
90
+ sessionId: input.sessionId,
91
+ delivery: input.delivery,
92
+ text: input.text,
93
+ requestId,
94
+ };
95
+ if (input.delivery === "followUp" && input.goal !== undefined)
96
+ task = { ...task, goal: input.goal };
75
97
  const accepted = await this.fleet().start({
76
98
  agents: scope.agents.map(({ agentId }) => agentId),
77
99
  actions: ["delegate"],
78
100
  expiresAt: this.deps.expiresAt(),
79
101
  provenance,
80
102
  meta,
81
- }, {
82
- agentId: input.agentId,
83
- sessionId: input.sessionId,
84
- delivery: input.delivery,
85
- text: input.text,
86
- requestId,
87
- });
103
+ }, task);
88
104
  return accepted;
89
105
  }
90
106
  /** Resolve this sender's exact request, then let the recipient guard pending work. */
package/dist/effect.d.ts CHANGED
@@ -8,7 +8,7 @@ import type { Accepted } from "./client.js";
8
8
  import type { ShiitakeError } from "./errors.js";
9
9
  import type { ShiitakeEffectOptions, ShiitakeEffectOwner } from "./internal/effect-contracts.js";
10
10
  import type { SqliteOwner } from "./internal/contracts.js";
11
- export type { AbortCommand, BranchCommand, CompactCommand, CancelCommand, CancelResult, Accepted, ForkCommand, ForkedSession, PromptCommand, PromptDelivery, ReadOptions, SessionCommand, SessionCommandHandler, SessionFrame, SessionMessage, SessionProgress, SessionToolOutput, SessionSnapshot, SessionActivity, AgentRunningWork, SubscribeOptions, TooLate, WatchOptions, } from "./client.js";
11
+ export type { AbortCommand, BranchCommand, CompactCommand, CancelCommand, CancelResult, Accepted, ForkCommand, ForkedSession, PromptCommand, PromptDelivery, ReadOptions, SessionCommand, SessionCommandHandler, SessionFrame, SessionMessage, SessionProgress, SessionProgressDelta, SessionToolOutput, SessionSnapshot, SessionActivity, AgentRunningWork, SubscribeOptions, TooLate, WatchOptions, } from "./client.js";
12
12
  export type { InstructionSnapshot, InstructionSource, ModelAcquisition, ModelDescription, ModelSource, RuntimeHost, RuntimePendingWork, SqliteCursor, SqliteOwner, SqliteValue, SqliteRow, GrantsDelivery, GrantsDeliveryResult, ShiitakeOptions, ToolAccess, HostMetaOptions, StandardHostMetaResult, StandardHostMetaSchema, ToolSource, } from "./internal/contracts.js";
13
13
  export type { InstructionSourceEffect, ModelAcquisitionEffect, ModelSourceEffect, RuntimeHostEffect, ShiitakeEffectAgent, ShiitakeEffectAgentExtensions, ShiitakeEffectCommandHandler, ShiitakeEffectOptions, ShiitakeEffectOwner, ShiitakeEffectSession, ShiitakeEffectSessionExtension, ShiitakeEffectSendOptions, ShiitakeEffectWatch, ToolSourceEffect, } from "./internal/effect-contracts.js";
14
14
  export type { SessionSummary } from "./client.js";
package/dist/errors.d.ts CHANGED
@@ -4,14 +4,14 @@ declare const ShiitakeErrorBoundarySchema: Schema.Literals<readonly ["contract",
4
4
  /** Stable boundary that rejected a Shiitake operation. */
5
5
  export type ShiitakeErrorBoundary = Schema.Schema.Type<typeof ShiitakeErrorBoundarySchema>;
6
6
  /** Safe failure category exposed by both Shiitake facades. */
7
- declare const ShiitakeErrorCauseSchema: Schema.Literals<readonly ["closed", "conflict", "incompatible", "invalid", "rejected", "unavailable", "state_reset_required", "storage_pressure"]>;
7
+ declare const ShiitakeErrorCauseSchema: Schema.Literals<readonly ["closed", "conflict", "incompatible", "invalid", "rejected", "unavailable", "state_reset_required", "storage_pressure", "archived"]>;
8
8
  /** Safe failure category exposed by both Shiitake facades. */
9
9
  export type ShiitakeErrorCause = Schema.Schema.Type<typeof ShiitakeErrorCauseSchema>;
10
10
  /** Canonical serialized Shiitake failure. */
11
11
  export declare const ShiitakeErrorWireSchema: Schema.Struct<{
12
12
  readonly _tag: Schema.Literal<"ShiitakeError">;
13
13
  readonly boundary: Schema.Literals<readonly ["contract", "extensions", "instructions", "model", "runtime", "store", "tools"]>;
14
- readonly cause: Schema.Literals<readonly ["closed", "conflict", "incompatible", "invalid", "rejected", "unavailable", "state_reset_required", "storage_pressure"]>;
14
+ readonly cause: Schema.Literals<readonly ["closed", "conflict", "incompatible", "invalid", "rejected", "unavailable", "state_reset_required", "storage_pressure", "archived"]>;
15
15
  readonly message: Schema.String;
16
16
  readonly requestId: Schema.optionalKey<Schema.String>;
17
17
  }>;
package/dist/errors.js CHANGED
@@ -21,6 +21,8 @@ const ShiitakeErrorCauseSchema = Schema.Literals([
21
21
  "state_reset_required",
22
22
  /** Owner storage is near its budget; new Sessions and prompts wait. */
23
23
  "storage_pressure",
24
+ /** This Agent is archived; new Sessions and prompts require restoration. */
25
+ "archived",
24
26
  ]);
25
27
  const requestIdSchema = Schema.String.pipe(Schema.check(Schema.isMinLength(1)), Schema.check(Schema.isMaxLength(256)));
26
28
  /** Canonical serialized Shiitake failure. */
@@ -77,7 +77,7 @@ declare const AdmissionError_base: new <A extends Record<string, any> = {}>(args
77
77
  } & Readonly<A>;
78
78
  /** The door refused an extension admission. */
79
79
  export declare class AdmissionError extends AdmissionError_base<{
80
- reason: "invalid" | "conflict" | "closed";
80
+ reason: "invalid" | "conflict" | "closed" | "archived";
81
81
  message: string;
82
82
  }> {
83
83
  }