@fungi.computer/shiitake 0.1.0 → 0.3.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 (112) hide show
  1. package/README.md +155 -34
  2. package/dist/agent-control.d.ts +78 -11
  3. package/dist/agent-control.js +273 -70
  4. package/dist/client-entry.d.ts +15 -8
  5. package/dist/client-entry.js +7 -5
  6. package/dist/client-wire.d.ts +24 -8
  7. package/dist/client-wire.js +53 -11
  8. package/dist/client.d.ts +56 -11
  9. package/dist/client.js +1 -1
  10. package/dist/cloudflare.d.ts +42 -23
  11. package/dist/cloudflare.js +76 -54
  12. package/dist/cold-sessions.d.ts +28 -0
  13. package/dist/cold-sessions.js +1 -0
  14. package/dist/delegation.d.ts +214 -0
  15. package/dist/delegation.js +319 -0
  16. package/dist/effect.d.ts +1 -1
  17. package/dist/errors.d.ts +2 -2
  18. package/dist/errors.js +2 -2
  19. package/dist/extension-contract.d.ts +10 -2
  20. package/dist/extensions.d.ts +6 -2
  21. package/dist/fleet.d.ts +52 -34
  22. package/dist/fleet.js +101 -63
  23. package/dist/hosted.js +1 -0
  24. package/dist/http-routes.d.ts +106 -22
  25. package/dist/http-routes.js +18 -1
  26. package/dist/index.d.ts +3 -2
  27. package/dist/internal/accepted-run-executor.d.ts +5 -0
  28. package/dist/internal/accepted-run-executor.js +10 -4
  29. package/dist/internal/admission-door.d.ts +3 -0
  30. package/dist/internal/admission-door.js +12 -2
  31. package/dist/internal/client-boundary.js +7 -1
  32. package/dist/internal/compaction/compaction-slot.d.ts +36 -0
  33. package/dist/internal/compaction/compaction-slot.js +41 -0
  34. package/dist/internal/compaction/compaction.d.ts +7 -4
  35. package/dist/internal/compaction/compaction.js +59 -30
  36. package/dist/internal/compaction/session-compaction.d.ts +8 -16
  37. package/dist/internal/compaction/session-compaction.js +358 -119
  38. package/dist/internal/compaction/settings.js +5 -0
  39. package/dist/internal/compaction/utils.d.ts +19 -16
  40. package/dist/internal/compaction/utils.js +30 -84
  41. package/dist/internal/contracts.d.ts +40 -13
  42. package/dist/internal/coordinator.js +28 -10
  43. package/dist/internal/effect-client.d.ts +4 -2
  44. package/dist/internal/effect-client.js +29 -0
  45. package/dist/internal/effect-contracts.d.ts +17 -4
  46. package/dist/internal/extension-runtime.d.ts +5 -0
  47. package/dist/internal/extension-runtime.js +184 -89
  48. package/dist/internal/host-meta.d.ts +7 -0
  49. package/dist/internal/model-acquisition.js +7 -1
  50. package/dist/internal/model-preference.d.ts +2 -0
  51. package/dist/internal/model-preference.js +2 -1
  52. package/dist/internal/promise-facade.js +4 -16
  53. package/dist/internal/run-failure-reason.d.ts +10 -0
  54. package/dist/internal/run-failure-reason.js +19 -0
  55. package/dist/internal/run-scope.d.ts +4 -1
  56. package/dist/internal/run-scope.js +38 -5
  57. package/dist/internal/runtime-coordinator.d.ts +2 -1
  58. package/dist/internal/runtime-coordinator.js +65 -4
  59. package/dist/internal/runtime-core.js +1 -0
  60. package/dist/internal/runtime-reconciliation.js +42 -6
  61. package/dist/internal/session-context.d.ts +1 -2
  62. package/dist/internal/session-context.js +8 -30
  63. package/dist/internal/session-status.d.ts +2 -1
  64. package/dist/internal/session-status.js +4 -1
  65. package/dist/internal/woodstock/client-projection.d.ts +6 -5
  66. package/dist/internal/woodstock/client-projection.js +79 -53
  67. package/dist/internal/woodstock/cold-sessions.d.ts +4 -0
  68. package/dist/internal/woodstock/cold-sessions.js +90 -0
  69. package/dist/internal/woodstock/extension-store.d.ts +2 -0
  70. package/dist/internal/woodstock/extension-store.js +4 -0
  71. package/dist/internal/woodstock/fleet-store.d.ts +3 -3
  72. package/dist/internal/woodstock/fleet-store.js +16 -11
  73. package/dist/internal/woodstock/index.d.ts +11 -3
  74. package/dist/internal/woodstock/index.js +33 -14
  75. package/dist/internal/woodstock/progress.d.ts +14 -1
  76. package/dist/internal/woodstock/progress.js +97 -19
  77. package/dist/internal/woodstock/run-recovery.js +6 -4
  78. package/dist/internal/woodstock/session-attention.d.ts +1 -1
  79. package/dist/internal/woodstock/session-attention.js +2 -2
  80. package/dist/internal/woodstock/session-entry.d.ts +109 -42
  81. package/dist/internal/woodstock/session-entry.js +63 -20
  82. package/dist/internal/woodstock/session-inbox.js +2 -0
  83. package/dist/internal/woodstock/session-store.d.ts +104 -24
  84. package/dist/internal/woodstock/session-store.js +937 -246
  85. package/dist/internal/woodstock/storage-admission.d.ts +8 -8
  86. package/dist/internal/woodstock/storage-admission.js +8 -34
  87. package/dist/internal/woodstock/storage-conformance.d.ts +15 -0
  88. package/dist/internal/woodstock/storage-conformance.js +489 -0
  89. package/dist/model-preference.d.ts +2 -2
  90. package/dist/model-preference.js +2 -2
  91. package/dist/node.js +1 -1
  92. package/dist/remote-client.js +8 -1
  93. package/dist/session-attention.d.ts +8 -1
  94. package/dist/session-attention.js +6 -1
  95. package/dist/testing.js +1 -0
  96. package/docs/api/README.md +18 -16
  97. package/docs/api/agent-control.md +172 -2
  98. package/docs/api/client-entry.md +26 -10
  99. package/docs/api/client.md +153 -37
  100. package/docs/api/cloudflare.md +76 -23
  101. package/docs/api/delegation.md +485 -0
  102. package/docs/api/effect.md +20 -2
  103. package/docs/api/extensions.md +21 -9
  104. package/docs/api/fleet.md +149 -122
  105. package/docs/api/http.md +4 -1
  106. package/docs/api/index.md +116 -20
  107. package/docs/api/internal/woodstock/storage-conformance.md +81 -0
  108. package/docs/api/model-preference.md +17 -0
  109. package/docs/api/testing.md +16 -4
  110. package/package.json +26 -14
  111. package/dist/internal/browser-sessions.d.ts +0 -31
  112. package/dist/internal/browser-sessions.js +0 -173
@@ -1,10 +1,17 @@
1
1
  /**
2
2
  * Adapted from the maintained Pi v0.84.4 coding-agent compaction utilities.
3
+ * Token estimates are pi-ai's own (`@earendil-works/pi-ai/utils/estimate`).
3
4
  * The only destination delta is carrying Pi's native Message values directly.
4
5
  */
5
- import type { AssistantMessage, Message } from "@earendil-works/pi-ai";
6
- /** Pi's native compaction boundary projection, retained without a custom role. */
7
- export declare function compactionSummaryMessage(summary: string, timestamp: number): Message;
6
+ import type { AssistantMessage, Message, UserMessage } from "@earendil-works/pi-ai";
7
+ /**
8
+ * Pi's compaction entry model message (Pi Durable `compaction.ts`): the
9
+ * summary as the user message the model reads, built once when the entry is
10
+ * appended and stored with it.
11
+ */
12
+ export declare function compactionSummaryMessage(summary: string, timestamp: number): UserMessage;
13
+ /** The summary text inside a stored compaction model message. */
14
+ export declare function compactionSummaryText(message: UserMessage): string;
8
15
  export type FileOperations = {
9
16
  edited: Set<string>;
10
17
  read: Set<string>;
@@ -18,24 +25,20 @@ export declare function computeFileLists(fileOps: FileOperations): {
18
25
  };
19
26
  export declare function formatFileOperations(readFiles: readonly string[], modifiedFiles: readonly string[]): string;
20
27
  export declare function serializeConversation(messages: readonly Message[]): string;
21
- /** Pi's chars/4 estimator, including its image and tool-call constants. */
22
- export declare function estimateTokens(message: Message): number;
23
- export declare function estimateContextTokens(messages: readonly Message[]): {
24
- tokens: number;
25
- usageTokens: number;
26
- trailingTokens: number;
27
- lastUsageIndex: null;
28
- } | {
29
- tokens: number;
30
- usageTokens: number;
31
- trailingTokens: number;
32
- lastUsageIndex: number;
33
- };
34
28
  export declare function shouldCompact(contextTokens: number, contextWindow: number, settings: CompactionSettings): boolean;
29
+ /**
30
+ * A background compaction starts above the blocking threshold minus
31
+ * `backgroundTokens` and at or below the blocking threshold; past that the
32
+ * blocking check owns it. Token-based only: byte limits are Zookeeper's.
33
+ * Unclamped like Pi Durable generation.ts:322-339 (Levi, 2026-10-17: match Pi); keepRecentTokens gates small windows.
34
+ */
35
+ export declare function shouldCompactInBackground(contextTokens: number, contextWindow: number, settings: CompactionSettings): boolean;
35
36
  export type CompactionSettings = {
36
37
  enabled: boolean;
37
38
  reserveTokens: number;
38
39
  keepRecentTokens: number;
40
+ /** Tokens before the blocking threshold at which a background compaction starts; 0 turns it off. */
41
+ backgroundTokens: number;
39
42
  };
40
43
  export declare const DEFAULT_COMPACTION_SETTINGS: CompactionSettings;
41
44
  export declare const SUMMARIZATION_SYSTEM_PROMPT = "You are a context summarization assistant. Your task is to read a conversation between a user and an AI coding assistant, then produce a structured summary following the exact format specified.\n\nDo NOT continue the conversation. Do NOT respond to any questions in the conversation. ONLY output the structured summary.";
@@ -6,7 +6,11 @@ const COMPACTION_SUMMARY_PREFIX = `The conversation history before this point wa
6
6
  `;
7
7
  const COMPACTION_SUMMARY_SUFFIX = `
8
8
  </summary>`;
9
- /** Pi's native compaction boundary projection, retained without a custom role. */
9
+ /**
10
+ * Pi's compaction entry model message (Pi Durable `compaction.ts`): the
11
+ * summary as the user message the model reads, built once when the entry is
12
+ * appended and stored with it.
13
+ */
10
14
  export function compactionSummaryMessage(summary, timestamp) {
11
15
  return {
12
16
  role: "user",
@@ -19,6 +23,16 @@ export function compactionSummaryMessage(summary, timestamp) {
19
23
  timestamp,
20
24
  };
21
25
  }
26
+ /** The summary text inside a stored compaction model message. */
27
+ export function compactionSummaryText(message) {
28
+ const block = Array.isArray(message.content) ? message.content[0] : undefined;
29
+ const text = block?.type === "text" ? block.text : undefined;
30
+ if (text === undefined ||
31
+ !text.startsWith(COMPACTION_SUMMARY_PREFIX) ||
32
+ !text.endsWith(COMPACTION_SUMMARY_SUFFIX))
33
+ throw new Error("Compaction model message is not a summary");
34
+ return text.slice(COMPACTION_SUMMARY_PREFIX.length, text.length - COMPACTION_SUMMARY_SUFFIX.length);
35
+ }
22
36
  export function createFileOps() {
23
37
  return { edited: new Set(), read: new Set(), written: new Set() };
24
38
  }
@@ -76,14 +90,11 @@ function serializeUserMessage(message) {
76
90
  }
77
91
  function serializeAssistantMessage(message) {
78
92
  const text = [];
79
- const thinking = [];
80
93
  const calls = [];
81
94
  for (const block of message.content) {
82
95
  if (block.type === "text")
83
96
  text.push(block.text);
84
- else if (block.type === "thinking")
85
- thinking.push(block.thinking);
86
- else {
97
+ else if (block.type === "toolCall") {
87
98
  const args = Object.entries(block.arguments)
88
99
  .map(([key, value]) => `${key}=${JSON.stringify(value)}`)
89
100
  .join(", ");
@@ -91,8 +102,6 @@ function serializeAssistantMessage(message) {
91
102
  }
92
103
  }
93
104
  const parts = [];
94
- if (thinking.length > 0)
95
- parts.push(`[Assistant thinking]: ${thinking.join("\n")}`);
96
105
  if (text.length > 0)
97
106
  parts.push(`[Assistant]: ${text.join("\n")}`);
98
107
  if (calls.length > 0)
@@ -129,92 +138,29 @@ export function serializeConversation(messages) {
129
138
  }
130
139
  return parts.join("\n\n");
131
140
  }
132
- const ESTIMATED_IMAGE_CHARS = 4800;
133
- function estimateTextAndImageContentChars(content) {
134
- if (isString(content))
135
- return content.length;
136
- return content.reduce((chars, block) => chars +
137
- (block.type === "text"
138
- ? (block.text?.length ?? 0)
139
- : block.type === "image"
140
- ? ESTIMATED_IMAGE_CHARS
141
- : 0), 0);
142
- }
143
- /** Pi's chars/4 estimator, including its image and tool-call constants. */
144
- export function estimateTokens(message) {
145
- let chars = 0;
146
- switch (message.role) {
147
- case "system":
148
- return Math.ceil(estimateTextAndImageContentChars(message.content) / 4);
149
- case "user":
150
- return Math.ceil(estimateTextAndImageContentChars(message.content) / 4);
151
- case "assistant":
152
- for (const block of message.content) {
153
- if (block.type === "text")
154
- chars += block.text.length;
155
- else if (block.type === "thinking")
156
- chars += block.thinking.length;
157
- else
158
- chars += block.name.length + JSON.stringify(block.arguments).length;
159
- }
160
- return Math.ceil(chars / 4);
161
- case "toolResult":
162
- return Math.ceil(estimateTextAndImageContentChars(message.content) / 4);
163
- }
164
- }
165
- function calculateContextTokens(usage) {
166
- return (usage.totalTokens ||
167
- usage.input + usage.output + usage.cacheRead + usage.cacheWrite);
168
- }
169
- function validAssistantUsage(message) {
170
- if (message.role !== "assistant")
171
- return undefined;
172
- if (message.stopReason === "aborted" || message.stopReason === "error")
173
- return undefined;
174
- return calculateContextTokens(message.usage) > 0 ? message.usage : undefined;
175
- }
176
- export function estimateContextTokens(messages) {
177
- let index = null;
178
- let usageTokens = 0;
179
- for (let cursor = messages.length - 1; cursor >= 0; cursor -= 1) {
180
- const message = messages[cursor];
181
- if (message === undefined)
182
- continue;
183
- const usage = validAssistantUsage(message);
184
- if (usage !== undefined) {
185
- index = cursor;
186
- usageTokens = calculateContextTokens(usage);
187
- break;
188
- }
189
- }
190
- if (index === null) {
191
- const tokens = messages.reduce((sum, message) => sum + estimateTokens(message), 0);
192
- return {
193
- tokens,
194
- usageTokens: 0,
195
- trailingTokens: tokens,
196
- lastUsageIndex: null,
197
- };
198
- }
199
- const trailingTokens = messages
200
- .slice(index + 1)
201
- .reduce((sum, message) => sum + estimateTokens(message), 0);
202
- return {
203
- tokens: usageTokens + trailingTokens,
204
- usageTokens,
205
- trailingTokens,
206
- lastUsageIndex: index,
207
- };
208
- }
209
141
  export function shouldCompact(contextTokens, contextWindow, settings) {
210
142
  if (!settings.enabled)
211
143
  return false;
212
144
  return contextTokens > contextWindow - settings.reserveTokens;
213
145
  }
146
+ /**
147
+ * A background compaction starts above the blocking threshold minus
148
+ * `backgroundTokens` and at or below the blocking threshold; past that the
149
+ * blocking check owns it. Token-based only: byte limits are Zookeeper's.
150
+ * Unclamped like Pi Durable generation.ts:322-339 (Levi, 2026-10-17: match Pi); keepRecentTokens gates small windows.
151
+ */
152
+ export function shouldCompactInBackground(contextTokens, contextWindow, settings) {
153
+ if (!settings.enabled || settings.backgroundTokens === 0)
154
+ return false;
155
+ const blocking = contextWindow - settings.reserveTokens;
156
+ return (contextTokens > blocking - settings.backgroundTokens &&
157
+ !shouldCompact(contextTokens, contextWindow, settings));
158
+ }
214
159
  export const DEFAULT_COMPACTION_SETTINGS = {
215
160
  enabled: true,
216
161
  reserveTokens: 16384,
217
162
  keepRecentTokens: 20000,
163
+ backgroundTokens: 32768,
218
164
  };
219
165
  export const SUMMARIZATION_SYSTEM_PROMPT = `You are a context summarization assistant. Your task is to read a conversation between a user and an AI coding assistant, then produce a structured summary following the exact format specified.
220
166
 
@@ -24,6 +24,12 @@ export type SqliteOwner = Omit<WatchdogSqliteOwner, "sql"> & Readonly<{
24
24
  /** Execute a statement with SQLite scalar bindings. */
25
25
  exec: (statement: string, ...bindings: SqliteValue[]) => SqliteCursor;
26
26
  }>;
27
+ /**
28
+ * How the host names this store when Woodstock refuses part of it, so its
29
+ * operator knows which store to act on: its Durable Object or its file.
30
+ * Absent is "Woodstock store".
31
+ */
32
+ storeName?: string;
27
33
  }>;
28
34
  /** Acquired Pi model and optional explicit Pi stream function. */
29
35
  export type ModelAcquisition = Readonly<{
@@ -31,8 +37,16 @@ export type ModelAcquisition = Readonly<{
31
37
  model: Model<Api>;
32
38
  /** Optional explicit Pi stream function for custom providers and tests. */
33
39
  stream?: MuleStreamFunction;
40
+ /** Typed retry decision for this door's exact message; unknown means false. */
41
+ retryable: (message: AssistantMessage) => boolean;
34
42
  /** Provider's bounded Retry-After for a clean HTTP refusal, if reported. */
35
43
  retryAfterMs?: (message: AssistantMessage) => number | undefined;
44
+ /**
45
+ * Describe only a failure message produced by this acquired model door.
46
+ * Return its owner-authored safe reason, never arbitrary SDK error prose.
47
+ * Unknown messages must return undefined.
48
+ */
49
+ failureReason?: (message: AssistantMessage) => string | undefined;
36
50
  /** Release the acquired model resources. */
37
51
  release: () => Promise<void>;
38
52
  }>;
@@ -124,18 +138,19 @@ export type RuntimeHost = Readonly<{
124
138
  }>>;
125
139
  }>;
126
140
  /**
127
- * A host's disk budget for owner storage, with the platform's own measure of
128
- * it. Near the budget, Shiitake refuses new Sessions and new prompts with a
129
- * typed `storage_pressure` error while admitted work finishes.
141
+ * A host's storage-pressure check. Shiitake owns the refusal door and the
142
+ * typed `storage_pressure` error; the host owns the numbers behind it, such
143
+ * as Zookeeper's budget and thresholds. While the check reports pressure,
144
+ * new Sessions and new prompts are refused and admitted work finishes.
130
145
  */
131
- export type StorageBudget = Readonly<{
146
+ export type StoragePressureCheck = Readonly<{
132
147
  /**
133
- * Bytes the owner database occupies now, read from the platform, such as
134
- * Cloudflare's `ctx.storage.sql.databaseSize`.
148
+ * Whether the owner is under storage pressure now. Called only for a new
149
+ * Session or a new prompt, never for a replay or for writes that finish
150
+ * admitted work. A throw, such as an unreadable measurement, refuses the
151
+ * new work with that error.
135
152
  */
136
- usedBytes: () => number;
137
- /** The host's budget for those bytes. */
138
- budgetBytes: number;
153
+ underPressure: () => boolean;
139
154
  }>;
140
155
  /** One model's compaction token budgets; omitted fields fall back. */
141
156
  export type CompactionBudget = Readonly<{
@@ -143,11 +158,13 @@ export type CompactionBudget = Readonly<{
143
158
  reserveTokens?: number;
144
159
  /** Recent tokens kept verbatim instead of summarized. */
145
160
  keepRecentTokens?: number;
161
+ /** Tokens before the blocking threshold at which compaction starts in the background; 0 turns it off (default 32768). */
162
+ backgroundTokens?: number;
146
163
  }>;
147
164
  /**
148
165
  * Pi's compaction settings. Each budget resolves through the exact
149
166
  * `provider/modelId` override, then the ordinary value, then Pi's default
150
- * (16384 reserve, 20000 kept). `enabled` is global and governs automatic
167
+ * (16384 reserve, 20000 kept, 32768 background). `enabled` is global and governs automatic
151
168
  * compaction only; a manual compaction always runs.
152
169
  */
153
170
  export type CompactionOptions = CompactionBudget & Readonly<{
@@ -159,13 +176,14 @@ export type CompactionOptions = CompactionBudget & Readonly<{
159
176
  export type ShiitakeOptions = Readonly<{
160
177
  /**
161
178
  * The schema of the opaque metadata this host attaches to prompts and
162
- * operator lines. Shiitake checks, stores and returns it; it never reads it.
179
+ * operator lines. Shiitake checks, stores and returns it; the host owns its
180
+ * grammar and may select a principal identity path for recall authorization.
163
181
  */
164
182
  meta?: HostMetaOptions;
165
183
  /** Native owner storage and synchronous transaction capability. */
166
184
  store: SqliteOwner;
167
- /** Disk budget for `store`; absent never refuses for storage pressure. */
168
- storageBudget?: StorageBudget;
185
+ /** Host storage-pressure check; absent never refuses for storage pressure. */
186
+ storagePressure?: StoragePressureCheck;
169
187
  /** Compaction budgets; absent uses Pi's defaults for every model. */
170
188
  compaction?: CompactionOptions;
171
189
  /** Runtime host that schedules the opaque durable reconciler. */
@@ -202,6 +220,13 @@ export type ToolAccess = Pick<AccessOptions,
202
220
  * remote client can.
203
221
  */
204
222
  export type ShiitakeOwnerAgent = ShiitakeAgent & Readonly<{
223
+ /** Stage a configuration without disturbing an in-flight turn. */
224
+ prepareConfiguration: (configuration: Pick<ShiitakeOptions, "extensions" | "tools">) => Promise<Readonly<{
225
+ /** Publish synchronously; the next turn applies the prepared roster. */
226
+ commit: () => void;
227
+ /** Drop the staged configuration without applying it. */
228
+ discard: () => Promise<void>;
229
+ }>>;
205
230
  /** Each Session's delivered tool grants, read at every call. */
206
231
  grants: Readonly<{
207
232
  /**
@@ -224,6 +249,8 @@ export type { GrantsDelivery, GrantsDeliveryResult, } from "./woodstock/index.js
224
249
  export type { HostMetaOptions, StandardHostMetaResult, StandardHostMetaSchema, } from "./host-meta.js";
225
250
  /** Exact accepted-work facts, independent of the current Session branch or page. */
226
251
  export type CommandSettlement = Readonly<{
252
+ /** Original Agent sender recorded at acceptance, never the terminal's canceller. */
253
+ acceptedSender: string | null;
227
254
  /** Durable terminal and the command that ended the work's opaque metadata. */
228
255
  terminal: SessionTerminal;
229
256
  /** Last completed assistant text; cancelled work returns no answer. */
@@ -9,7 +9,7 @@ import { mergeObservationSignals, openObservationKernel, publishOwnerObservation
9
9
  import { checkHostMeta } from "./host-meta.js";
10
10
  import { readSessionActivity } from "./session-status.js";
11
11
  import { readSessionContext } from "./session-context.js";
12
- import { abortSession, cancelSession, branchSession, recordOperatorLine, forkSession, readSession, readSessionModelPreference, readSessionReplayPage, readSessionInbox, createSession, deleteSession, deliverSessionGrants, readSessionGrants, setSessionModelPreference, setSessionTitle, } from "./woodstock/index.js";
12
+ import { abortSession, cancelSession, branchSession, recordOperatorLine, forkSession, readSession, readSessionModelPreference, readSessionReplayPage, readSessionInbox, readLatestPendingPrompt, createSession, deleteSession, deliverSessionGrants, readSessionGrants, setSessionModelPreference, setSessionTitle, readAdmissionIdentity, assertAdmissionIdentity, } from "./woodstock/index.js";
13
13
  /** Generic Agent-scope extension controls over the runtime's one call door. */
14
14
  function agentExtensions(runtime) {
15
15
  return Object.freeze({
@@ -33,7 +33,7 @@ function withContext(snapshot, context) {
33
33
  }
34
34
  /** Read the baseline and its context use for one Session. */
35
35
  function readSessionWithContext(options, runtime, sessionId, input) {
36
- return currentSessionActivity(options, runtime, sessionId).pipe(Effect.flatMap((activity) => readSession(options.store, sessionId, activity, input, () => runtime.extensions.project(sessionId))), Effect.flatMap((snapshot) => readSessionContext(options, runtime, sessionId).pipe(Effect.map((context) => withContext(snapshot, context)))));
36
+ return currentSessionActivity(options, runtime, sessionId).pipe(Effect.flatMap((activity) => readSession(options.store, sessionId, activity, input, () => runtime.extensions.project(sessionId))), Effect.flatMap((snapshot) => readSessionContext(options, sessionId).pipe(Effect.map((context) => withContext(snapshot, context)))));
37
37
  }
38
38
  function sessionOwnerOperation(runtime, sessionId, operation) {
39
39
  return runtime.assertOpen.pipe(Effect.andThen(Effect.try({
@@ -106,14 +106,21 @@ function subscriptionFrames(options, runtime, registration, sessionId, after, de
106
106
  if (signal === "reset")
107
107
  return resetFrame(options, runtime, sessionId, includeSandboxFailures).pipe(Effect.map(continueFromReset));
108
108
  return runtime.assertOpen.pipe(Effect.andThen(currentSessionActivity(options, runtime, sessionId)), Effect.flatMap((activity) => {
109
- if (signal === "progress" &&
110
- state.delivery === "live" &&
111
- activity.progress !== null) {
109
+ if (signal === "progress" && state.delivery === "live") {
112
110
  return Effect.succeed([
113
111
  [
112
+ ...(activity.progress === null
113
+ ? []
114
+ : [
115
+ {
116
+ kind: "progress",
117
+ progress: activity.progress,
118
+ activity: activity.facts,
119
+ },
120
+ ]),
114
121
  {
115
- kind: "progress",
116
- progress: activity.progress,
122
+ kind: "tool_output",
123
+ toolOutput: activity.toolOutput,
117
124
  activity: activity.facts,
118
125
  },
119
126
  ],
@@ -124,7 +131,10 @@ function subscriptionFrames(options, runtime, registration, sessionId, after, de
124
131
  if (page.kind === "reset")
125
132
  return resetFrame(options, runtime, sessionId, includeSandboxFailures).pipe(Effect.map(continueFromReset));
126
133
  const latestCursor = page.frames
127
- .flatMap((frame) => frame.kind === "progress" ? [] : [frame.cursor])
134
+ .flatMap((frame) => frame.kind === "progress" ||
135
+ frame.kind === "tool_output"
136
+ ? []
137
+ : [frame.cursor])
128
138
  .at(-1);
129
139
  const next = {
130
140
  after: latestCursor ?? state.after,
@@ -133,7 +143,7 @@ function subscriptionFrames(options, runtime, registration, sessionId, after, de
133
143
  };
134
144
  // Every update carries the context use it settles on.
135
145
  return (page.frames.some((frame) => frame.kind === "update")
136
- ? readSessionContext(options, runtime, sessionId)
146
+ ? readSessionContext(options, sessionId)
137
147
  : Effect.succeed(undefined)).pipe(Effect.map((context) => [
138
148
  context === undefined
139
149
  ? page.frames
@@ -195,7 +205,10 @@ function admitUserWork(runtime, sessionId, input, meta, mail, reply) {
195
205
  : Effect.fail(new ShiitakeError("runtime", "unavailable", "Session command was not admitted"))));
196
206
  }
197
207
  function cancelRequestedSession(options, runtime, sessionId, input, meta) {
198
- return runtime.assertOpen.pipe(Effect.andThen(cancelSession(options.store, sessionId, input.commandId, input.onlyIfPending, () => runtime.readQueueHeadTransactional(sessionId), runtime.recordAcceptedCancellation, meta)), Effect.flatMap((cancellation) => cancellation === undefined
208
+ return runtime.assertOpen.pipe(Effect.andThen(sessionOwnerOperation(runtime, sessionId, (decoded) => {
209
+ if (options.meta?.identityPath !== undefined && meta !== undefined)
210
+ assertAdmissionIdentity(options.store, decoded, input.commandId, readAdmissionIdentity(options.store, options.meta.identityPath, meta));
211
+ })), Effect.andThen(cancelSession(options.store, sessionId, input.commandId, input.onlyIfPending, () => runtime.readQueueHeadTransactional(sessionId), runtime.recordAcceptedCancellation, meta)), Effect.flatMap((cancellation) => cancellation === undefined
199
212
  ? Effect.succeed({
200
213
  status: "too_late",
201
214
  commandId: input.commandId,
@@ -282,6 +295,7 @@ export function openCoordinator(options, runtime) {
282
295
  }
283
296
  return Object.freeze({
284
297
  command,
298
+ withdraw: (requestId) => runtime.assertOpen.pipe(Effect.andThen(runtime.door.withdraw(sessionId, requestId))),
285
299
  abort: command({ type: "abort" }),
286
300
  send: (text, input) => Effect.sync(() => input?.requestId ?? nanoid()).pipe(Effect.flatMap((requestId) => {
287
301
  const prompt = {
@@ -297,6 +311,9 @@ export function openCoordinator(options, runtime) {
297
311
  : new ShiitakeError(error.boundary, error.cause, error.message, requestId)));
298
312
  })),
299
313
  cancel: (input) => command({ type: "cancel", ...input }),
314
+ latestPending: sessionOwnerOperation(runtime, sessionId, (decoded) => readLatestPendingPrompt(options.store, decoded, options.meta?.identityPath === undefined || meta === undefined
315
+ ? undefined
316
+ : readAdmissionIdentity(options.store, options.meta.identityPath, meta))),
300
317
  modelPreference: Object.freeze({
301
318
  read: sessionOwnerOperation(runtime, sessionId, (decoded) => readSessionModelPreference(options.store, decoded)),
302
319
  set: (preference) => Effect.try({
@@ -335,6 +352,7 @@ export function openCoordinator(options, runtime) {
335
352
  });
336
353
  return Object.freeze({
337
354
  ...agent,
355
+ prepareConfiguration: runtime.prepareConfiguration,
338
356
  grants: Object.freeze({
339
357
  // A synchronous owner write: a delivery lands before any other work
340
358
  // can interleave, so the very next tool call reads it.
@@ -1,7 +1,9 @@
1
1
  import * as Effect from "effect/Effect";
2
2
  import type { ShiitakeAgent } from "../client.js";
3
- import type { ShiitakeEffectAgent, ShiitakeEffectOwner } from "./effect-contracts.js";
4
- import type { ShiitakeOwnerAgent } from "./contracts.js";
3
+ import type { ShiitakeEffectAgent, ShiitakeEffectOwner, ShiitakeEffectOptions } from "./effect-contracts.js";
4
+ import type { ShiitakeOwnerAgent, ShiitakeOptions } from "./contracts.js";
5
+ /** Adapt a Promise tool configuration for both initial open and reload. */
6
+ export declare function liftTools(toolSource: NonNullable<ShiitakeOptions["tools"]>): NonNullable<ShiitakeEffectOptions["tools"]>;
5
7
  /** Run one Shiitake Effect through its sole Promise error adaptation. */
6
8
  export declare function runShiitakePromise<A, E>(effect: Effect.Effect<A, E>): Promise<A>;
7
9
  /** Adapt one canonical Effect agent to Shiitake's ordinary web interface. */
@@ -1,10 +1,23 @@
1
1
  import { nanoid } from "@fungi.computer/cairn";
2
+ import { fromTypedSandbox } from "@fungi.computer/mycelium/effect";
2
3
  import * as Cause from "effect/Cause";
3
4
  import * as Effect from "effect/Effect";
4
5
  import * as Exit from "effect/Exit";
5
6
  import * as Option from "effect/Option";
6
7
  import * as Stream from "effect/Stream";
7
8
  import { ShiitakeError } from "../errors.js";
9
+ /** Adapt a Promise tool configuration for both initial open and reload. */
10
+ export function liftTools(toolSource) {
11
+ const tools = {
12
+ modules: toolSource.modules ?? [],
13
+ sources: toolSource.sources ?? [],
14
+ sandbox: fromTypedSandbox(toolSource.sandbox),
15
+ access: toolSource.access ?? {},
16
+ };
17
+ return toolSource.execution === undefined
18
+ ? tools
19
+ : { ...tools, execution: toolSource.execution };
20
+ }
8
21
  /** Run one Shiitake Effect through its sole Promise error adaptation. */
9
22
  export function runShiitakePromise(effect) {
10
23
  return Effect.runPromiseExit(effect).then((exit) => {
@@ -47,9 +60,11 @@ function sessionFacade(address, session) {
47
60
  return Object.freeze({
48
61
  address,
49
62
  command,
63
+ withdraw: (requestId) => runShiitakePromise(session.withdraw(requestId)),
50
64
  abort: () => runShiitakePromise(session.abort),
51
65
  send: (text, options) => runShiitakePromise(session.send(text, options)),
52
66
  cancel: (input) => runShiitakePromise(session.cancel(input)),
67
+ latestPending: () => runShiitakePromise(session.latestPending),
53
68
  modelPreference: Object.freeze({
54
69
  read: () => runShiitakePromise(session.modelPreference.read),
55
70
  set: (preference) => runShiitakePromise(session.modelPreference.set(preference)),
@@ -93,6 +108,20 @@ export function toPromiseOwner(owner) {
93
108
  const address = `local:${nanoid()}`;
94
109
  return Object.freeze({
95
110
  ...toPromiseAgent(owner, address),
111
+ prepareConfiguration: async (configuration) => {
112
+ const next = {
113
+ extensions: configuration.extensions,
114
+ };
115
+ if (configuration.tools !== undefined)
116
+ Object.assign(next, { tools: liftTools(configuration.tools) });
117
+ const prepared = await runShiitakePromise(owner.prepareConfiguration(next));
118
+ return {
119
+ commit: () => {
120
+ runShiitakeSync(prepared.commit);
121
+ },
122
+ discard: () => runShiitakePromise(prepared.discard),
123
+ };
124
+ },
96
125
  grants: Object.freeze({
97
126
  deliver: (sessionId, delivery) => runShiitakeSync(owner.grants.deliver(sessionId, delivery)),
98
127
  read: (sessionId) => runShiitakeSync(owner.grants.read(sessionId)),
@@ -9,10 +9,10 @@ import * as Stream from "effect/Stream";
9
9
  import type * as Scope from "effect/Scope";
10
10
  import type { WatchdogError } from "@fungi.computer/watchdog";
11
11
  import type { WatchdogExecutionDecision, WatchdogRunningJob, ConcurrentWatchdogRuntimeEffect } from "@fungi.computer/watchdog/effect";
12
- import type { AbortCommand, Accepted, BranchCommand, OperatorCommand, TitleCommand, CancelCommand, CancelResult, ForkCommand, ForkedSession, CompactCommand, PromptCommand, PromptDelivery, ReadOptions, SessionCommand, SessionFrame, SessionSnapshot, SessionSummaryPage, SubscribeOptions, WatchOptions } from "../client.js";
12
+ import type { AbortCommand, Accepted, BranchCommand, OperatorCommand, TitleCommand, CancelCommand, CancelResult, ForkCommand, ForkedSession, CompactCommand, PromptCommand, PromptDelivery, ReadOptions, SessionCommand, SessionFrame, SessionSnapshot, SessionPendingPrompt, SessionSummaryPage, SubscribeOptions, WatchOptions } from "../client.js";
13
13
  import type { ShiitakeError } from "../errors.js";
14
14
  import type { ExtensionStatusV0, Json } from "../extensions.js";
15
- import type { InstructionSnapshot, ModelDescription, RuntimePendingWork, SqliteOwner, StorageBudget, CompactionOptions } from "./contracts.js";
15
+ import type { InstructionSnapshot, ModelDescription, RuntimePendingWork, SqliteOwner, StoragePressureCheck, CompactionOptions } from "./contracts.js";
16
16
  import type { ModelPreference } from "./model-preference.js";
17
17
  /** Effect-native acquired Pi model and finalizer. */
18
18
  export type ModelAcquisitionEffect = Readonly<{
@@ -22,6 +22,10 @@ export type ModelAcquisitionEffect = Readonly<{
22
22
  stream?: MuleStreamFunction;
23
23
  /** Provider’s bounded Retry-After for a clean HTTP refusal, if reported. */
24
24
  retryAfterMs?: (message: AssistantMessage) => number | undefined;
25
+ /** Typed retry decision for this door's exact message; unknown means false. */
26
+ retryable: (message: AssistantMessage) => boolean;
27
+ /** Owner-authored reason for this door's failure message, never SDK prose. */
28
+ failureReason?: (message: AssistantMessage) => string | undefined;
25
29
  /** Effect finalizer for acquired model resources. */
26
30
  release: Effect.Effect<void, ShiitakeError>;
27
31
  }>;
@@ -91,8 +95,8 @@ export type ShiitakeEffectOptions = Readonly<{
91
95
  meta?: HostMetaOptions;
92
96
  /** Native owner storage and synchronous transaction capability. */
93
97
  store: SqliteOwner;
94
- /** Disk budget for `store`; absent never refuses for storage pressure. */
95
- storageBudget?: StorageBudget;
98
+ /** Host storage-pressure check; absent never refuses for storage pressure. */
99
+ storagePressure?: StoragePressureCheck;
96
100
  /** Compaction budgets; absent uses Pi's defaults for every model. */
97
101
  compaction?: CompactionOptions;
98
102
  /** One host lane in Shiitake's existing Watchdog, opened before recovery. */
@@ -161,6 +165,8 @@ export type ShiitakeEffectSendOptions = Readonly<{
161
165
  export type ShiitakeEffectSession = Readonly<{
162
166
  /** Execute one retained type-discriminated Session command. */
163
167
  command: ShiitakeEffectCommandHandler;
168
+ /** Resolve admission or durably fence a request that has not been admitted. */
169
+ withdraw: (requestId: string) => Effect.Effect<import("../client.js").WithdrawResult, ShiitakeError>;
164
170
  /** Cancel the running turn and all queued prompts, retaining their text. */
165
171
  abort: Effect.Effect<void, ShiitakeError>;
166
172
  /** Send text with one generated or explicitly supplied request identity. */
@@ -170,6 +176,8 @@ export type ShiitakeEffectSession = Readonly<{
170
176
  commandId: string;
171
177
  onlyIfPending?: boolean;
172
178
  }) => Effect.Effect<CancelResult, ShiitakeError>;
179
+ /** Newest pending prompt scoped by the host's bound identity. */
180
+ latestPending: Effect.Effect<SessionPendingPrompt | null, ShiitakeError>;
173
181
  /** Address one rostered extension's Session-scope commands. */
174
182
  extension: (extensionId: string) => ShiitakeEffectSessionExtension;
175
183
  /** Lineage-local selection used when the next run acquires its model. */
@@ -221,6 +229,11 @@ export type ShiitakeEffectAgent = Readonly<{
221
229
  * remote client can.
222
230
  */
223
231
  export type ShiitakeEffectOwner = ShiitakeEffectAgent & Readonly<{
232
+ /** Stage a configuration without disturbing an in-flight turn. */
233
+ prepareConfiguration: (configuration: Pick<ShiitakeEffectOptions, "extensions" | "tools">) => Effect.Effect<Readonly<{
234
+ commit: Effect.Effect<void, ShiitakeError>;
235
+ discard: Effect.Effect<void>;
236
+ }>, ShiitakeError>;
224
237
  /** Each Session's delivered tool grants, read at every call. */
225
238
  grants: Readonly<{
226
239
  /** Store one versioned delivery; versions only move forward. */
@@ -11,6 +11,11 @@ import type { AdmittedExtensionV0 } from "./extension-roster.js";
11
11
  export declare const coreConsumer = "core";
12
12
  /** Core's generic view of rostered v1 extensions. */
13
13
  export type ExtensionRuntime = Readonly<{
14
+ /** Prepare additions without replacing any live member; apply only between turns. */
15
+ stage: (admitted: readonly AdmittedExtensionV0[]) => Effect.Effect<Readonly<{
16
+ apply: Effect.Effect<void, ShiitakeError>;
17
+ discard: Effect.Effect<void>;
18
+ }>, ShiitakeError>;
14
19
  /** Running extensions that participate in runs, with their fingerprints. */
15
20
  runParticipants: () => readonly (readonly [
16
21
  ExtensionId,