@fungi.computer/shiitake 0.6.0 → 0.8.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 (67) 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 +26 -15
  8. package/dist/client.d.ts +23 -11
  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 +14 -2
  19. package/dist/fleet.js +81 -19
  20. package/dist/http-routes.d.ts +12 -16
  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/observation-kernel.js +12 -3
  31. package/dist/internal/promise-facade.js +2 -0
  32. package/dist/internal/run-scope.d.ts +1 -0
  33. package/dist/internal/run-scope.js +29 -7
  34. package/dist/internal/runtime-coordinator.d.ts +14 -0
  35. package/dist/internal/runtime-coordinator.js +57 -10
  36. package/dist/internal/runtime-core.d.ts +3 -0
  37. package/dist/internal/runtime-core.js +1 -0
  38. package/dist/internal/runtime-reconciliation.js +4 -1
  39. package/dist/internal/session-context.d.ts +5 -5
  40. package/dist/internal/session-context.js +80 -83
  41. package/dist/internal/session-status.d.ts +4 -2
  42. package/dist/internal/session-status.js +22 -4
  43. package/dist/internal/woodstock/client-projection.js +5 -11
  44. package/dist/internal/woodstock/fleet-store.d.ts +3 -2
  45. package/dist/internal/woodstock/fleet-store.js +24 -4
  46. package/dist/internal/woodstock/index.d.ts +3 -3
  47. package/dist/internal/woodstock/index.js +18 -7
  48. package/dist/internal/woodstock/progress.d.ts +13 -1
  49. package/dist/internal/woodstock/progress.js +130 -24
  50. package/dist/internal/woodstock/session-delegator.d.ts +2 -1
  51. package/dist/internal/woodstock/session-store.d.ts +14 -2
  52. package/dist/internal/woodstock/session-store.js +329 -66
  53. package/dist/node.js +11 -2
  54. package/dist/server-visitor-stream.js +33 -24
  55. package/dist/testing.d.ts +2 -0
  56. package/dist/testing.js +2 -0
  57. package/docs/api/agent-control.md +3 -2
  58. package/docs/api/client.md +79 -19
  59. package/docs/api/cloudflare.md +16 -0
  60. package/docs/api/delegation.md +84 -50
  61. package/docs/api/effect.md +85 -7
  62. package/docs/api/extensions.md +8 -8
  63. package/docs/api/fleet.md +118 -29
  64. package/docs/api/http.md +28 -0
  65. package/docs/api/index.md +39 -15
  66. package/docs/api/testing.md +20 -7
  67. package/package.json +5 -5
@@ -3,6 +3,92 @@ import * as Schema from "effect/Schema";
3
3
  import * as Result from "effect/Result";
4
4
  import { ShiitakeError } from "../../errors.js";
5
5
  import { publishOwnerObservation } from "../observation-kernel.js";
6
+ const tailBytes = 65_536;
7
+ const chunkUnits = 2048;
8
+ const encoder = new TextEncoder();
9
+ const emptyTail = () => ({ chunks: [], length: 0, bytes: 0 });
10
+ /** Bounded chunks: no concatenation or encoding of the reply-so-far. */
11
+ function appendTail(tail, append) {
12
+ for (let index = 0; index < append.length;) {
13
+ let end = Math.min(index + chunkUnits, append.length);
14
+ // Do not cut a Unicode scalar when bounding a provider's large delta.
15
+ const code = append.charCodeAt(end - 1);
16
+ if (end < append.length && code >= 0xd800 && code <= 0xdbff)
17
+ end--;
18
+ const text = append.slice(index, end);
19
+ const bytes = encoder.encode(text).byteLength;
20
+ const last = tail.chunks.at(-1);
21
+ if (last !== undefined && last.text.length + text.length <= chunkUnits) {
22
+ last.text += text;
23
+ last.bytes += bytes;
24
+ }
25
+ else {
26
+ tail.chunks.push({ offset: tail.length, text, bytes });
27
+ }
28
+ tail.length += text.length;
29
+ tail.bytes += bytes;
30
+ index = end;
31
+ while (tail.bytes > tailBytes && tail.chunks.length > 1) {
32
+ const dropped = tail.chunks.shift();
33
+ if (dropped !== undefined)
34
+ tail.bytes -= dropped.bytes;
35
+ }
36
+ }
37
+ }
38
+ function tailOffset(tail) {
39
+ return tail.chunks[0]?.offset ?? 0;
40
+ }
41
+ function tailAfter(tail, offset) {
42
+ return tail.chunks
43
+ .flatMap((chunk) => chunk.offset + chunk.text.length <= offset
44
+ ? []
45
+ : [chunk.text.slice(Math.max(0, offset - chunk.offset))])
46
+ .join("");
47
+ }
48
+ function snapshot(value) {
49
+ return value === null
50
+ ? null
51
+ : {
52
+ messageId: value.messageId,
53
+ revision: value.revision,
54
+ text: tailAfter(value.text, tailOffset(value.text)),
55
+ thinking: tailAfter(value.thinking, tailOffset(value.thinking)),
56
+ textOffset: tailOffset(value.text),
57
+ thinkingOffset: tailOffset(value.thinking),
58
+ textTruncated: tailOffset(value.text) > 0,
59
+ thinkingTruncated: tailOffset(value.thinking) > 0,
60
+ };
61
+ }
62
+ export function progressPosition(value) {
63
+ return value === null
64
+ ? null
65
+ : {
66
+ messageId: value.messageId,
67
+ revision: value.revision,
68
+ text: value.textOffset + value.text.length,
69
+ thinking: value.thinkingOffset + value.thinking.length,
70
+ };
71
+ }
72
+ /** Undefined means continuity was lost; null means no new model progress. */
73
+ export function readProcessProgressDelta(owner, sessionId, workId, after) {
74
+ const current = progressByOwner.get(owner)?.get(sessionId);
75
+ const value = current?.workId === workId ? current.value : null;
76
+ if (value === null)
77
+ return null;
78
+ const position = after?.messageId === value.messageId ? after : null;
79
+ if (position !== null && position.revision >= value.revision)
80
+ return null;
81
+ const text = position?.text ?? 0;
82
+ const thinking = position?.thinking ?? 0;
83
+ if (text < tailOffset(value.text) || thinking < tailOffset(value.thinking))
84
+ return undefined;
85
+ return {
86
+ messageId: value.messageId,
87
+ revision: value.revision,
88
+ text: { offset: text, append: tailAfter(value.text, text) },
89
+ thinking: { offset: thinking, append: tailAfter(value.thinking, thinking) },
90
+ };
91
+ }
6
92
  const progressByOwner = new WeakMap();
7
93
  export function readProcessToolOutputForWork(owner, sessionId, workId) {
8
94
  const current = progressByOwner.get(owner)?.get(sessionId);
@@ -96,23 +182,17 @@ function rejected(message) {
96
182
  }
97
183
  /** Read the current process-local progress value for one addressed Session. */
98
184
  export function readProcessProgress(owner, sessionId) {
99
- return progressFor(owner).get(sessionId)?.value ?? null;
185
+ return snapshot(progressFor(owner).get(sessionId)?.value ?? null);
100
186
  }
101
187
  /** Read progress only when it belongs to the current durable work head. */
102
188
  export function readProcessProgressForWork(owner, sessionId, workId) {
103
189
  const current = progressFor(owner).get(sessionId);
104
- return current?.workId === workId ? current.value : null;
190
+ return snapshot(current?.workId === workId ? current.value : null);
105
191
  }
106
- function textFor(event) {
107
- let text = "";
108
- let thinking = "";
109
- for (const content of event.value.content) {
110
- if (content.type === "text")
111
- text += content.text;
112
- if (content.type === "thinking")
113
- thinking += content.thinking;
114
- }
115
- return { text, thinking };
192
+ /** Read a phase fact without flattening another Session's private text tails. */
193
+ export function hasProcessTextForWork(owner, sessionId, workId) {
194
+ const current = progressByOwner.get(owner)?.get(sessionId);
195
+ return current?.workId === workId && (current.value?.text.length ?? 0) > 0;
116
196
  }
117
197
  /**
118
198
  * Keep live model progress in the current process only.
@@ -124,7 +204,23 @@ export function openProgress(owner, accepted, complete) {
124
204
  return Effect.gen(function* () {
125
205
  const current = yield* openProcessProgress(owner, accepted);
126
206
  let started;
207
+ let publishedLength = 0;
208
+ let flushTimer;
209
+ const cancelFlush = () => {
210
+ if (flushTimer !== undefined)
211
+ clearTimeout(flushTimer);
212
+ flushTimer = undefined;
213
+ };
214
+ const publish = () => {
215
+ cancelFlush();
216
+ const value = current.value;
217
+ if (value === null)
218
+ return;
219
+ publishedLength = value.text.length + value.thinking.length;
220
+ publishOwnerObservation(owner, accepted.sessionId, "progress");
221
+ };
127
222
  const clear = Effect.sync(() => {
223
+ cancelFlush();
128
224
  const hadProgress = current.value !== null;
129
225
  current.value = null;
130
226
  if (hadProgress)
@@ -134,8 +230,10 @@ export function openProgress(owner, accepted, complete) {
134
230
  yield* Effect.addFinalizer(() => clear);
135
231
  return Object.freeze({
136
232
  started: (event) => Effect.sync(() => {
233
+ cancelFlush();
137
234
  started = event;
138
235
  current.value = null;
236
+ publishedLength = 0;
139
237
  }),
140
238
  update: (event) => Effect.suspend(() => {
141
239
  const messageStart = started;
@@ -145,20 +243,28 @@ export function openProgress(owner, accepted, complete) {
145
243
  messageStart.turn.value !== event.turn.value ||
146
244
  messageStart.message.value !== event.message.value)
147
245
  return Effect.fail(rejected("Mule progress identity conflicts"));
148
- const previous = current.value;
149
- const content = textFor(event);
150
- if (previous?.text === content.text &&
151
- previous.thinking === content.thinking)
246
+ const delta = event.event;
247
+ if ((delta.type !== "text_delta" && delta.type !== "thinking_delta") ||
248
+ delta.delta.length === 0)
152
249
  return Effect.void;
153
- current.value = Object.freeze({
250
+ const first = current.value === null;
251
+ const value = current.value ?? {
154
252
  messageId: event.message.value,
155
- revision: (previous?.revision ?? 0) + 1,
156
- text: content.text,
157
- thinking: content.thinking,
158
- textTruncated: false,
159
- thinkingTruncated: false,
160
- });
161
- publishOwnerObservation(owner, accepted.sessionId, "progress");
253
+ revision: 0,
254
+ text: emptyTail(),
255
+ thinking: emptyTail(),
256
+ };
257
+ appendTail(delta.type === "text_delta" ? value.text : value.thinking, delta.delta);
258
+ value.revision++;
259
+ current.value = value;
260
+ const length = value.text.length + value.thinking.length;
261
+ // Coalesce tiny tokens, but flush a paused provider within 100 ms.
262
+ // One scope-owned timer holds no reply text and never writes SQLite.
263
+ if (first || length - publishedLength >= 4096) {
264
+ publish();
265
+ }
266
+ else
267
+ flushTimer ??= setTimeout(publish, 100);
162
268
  return Effect.void;
163
269
  }),
164
270
  terminal: (event) => clear.pipe(Effect.andThen(complete(event))),
@@ -1,4 +1,5 @@
1
1
  import type { BirdDogDelegator, BirdDogMail } from "@fungi.computer/bird-dog";
2
+ import type { WatchdogSqliteOwner } from "@fungi.computer/watchdog";
2
3
  import type { SqliteOwner } from "../contracts.js";
3
4
  /** Woodstock keeps each Session's delegator of record beside the Session. */
4
5
  export declare function prepareSessionDelegators(owner: SqliteOwner): void;
@@ -9,4 +10,4 @@ export declare function prepareSessionDelegators(owner: SqliteOwner): void;
9
10
  */
10
11
  export declare function recordSessionDelegator(owner: SqliteOwner, sessionId: string, mail: BirdDogMail | undefined): void;
11
12
  /** The Session's delegator of record, if any Agent has sent it work. */
12
- export declare function readSessionDelegator(owner: SqliteOwner, sessionId: string): BirdDogDelegator | undefined;
13
+ export declare function readSessionDelegator(owner: WatchdogSqliteOwner, sessionId: string): BirdDogDelegator | undefined;
@@ -167,6 +167,7 @@ export type EnqueueAcceptedWork = (work: Readonly<{
167
167
  jobId: string;
168
168
  sessionId: string;
169
169
  commandFactId: string;
170
+ delivery: "steer" | "followUp";
170
171
  }>) => void;
171
172
  export type RecordAcceptedCancellation = (input: Readonly<{
172
173
  jobId: string;
@@ -192,6 +193,8 @@ export type ForkSessionAtInput = SelectSessionBranchInput;
192
193
  export declare function hasStoredSession(owner: SqliteOwner, sessionId: string): boolean;
193
194
  /** Read one retained Session entry by its globally unique row identity. */
194
195
  export declare function readSessionEntryById(owner: SqliteOwner, id: string): StoredSessionRow | undefined;
196
+ /** A written run terminal closes the window for attaching another steer. */
197
+ export declare function hasRecordedRunTerminal(owner: SqliteOwner, sessionId: string, workId: string): boolean;
195
198
  /** Host-owned metadata identity, interpreted by SQLite's JSON decoder only. */
196
199
  export type AdmissionIdentity = Readonly<{
197
200
  path: string;
@@ -332,9 +335,18 @@ export declare const lineageRowLimit = 200;
332
335
  * deduplicating; the fork walk keeps every link for its whole-lineage check.
333
336
  */
334
337
  declare const lineageWalks: {
335
- readonly selected: "WITH RECURSIVE branch (id, parent_id, sequence, state) AS (\n SELECT id, parent_id, sequence, state\n FROM session_entries\n WHERE session_id = ? AND id = ?\n UNION ALL\n SELECT parent.id, parent.parent_id, parent.sequence, parent.state\n FROM session_entries AS parent\n JOIN branch AS child ON parent.id = child.parent_id\n WHERE parent.session_id = ? AND child.sequence > ?\n AND +parent.sequence < child.sequence\n )";
336
- readonly fork: "WITH RECURSIVE branch (id, parent_id, sequence, state) AS (\n SELECT id, parent_id, sequence, state\n FROM session_entries\n WHERE session_id = ? AND id = ? AND state != 'streaming'\n UNION\n SELECT parent.id, parent.parent_id, parent.sequence, parent.state\n FROM session_entries AS parent\n JOIN branch AS child ON parent.id = child.parent_id\n WHERE parent.session_id = ? AND child.sequence > ?\n AND parent.state != 'streaming'\n )";
338
+ readonly selected: "WITH RECURSIVE branch (id, parent_id, sequence, state) AS (\n SELECT id, parent_id, sequence, state\n FROM session_entries\n WHERE session_id = ? AND id = ?\n UNION ALL\n SELECT parent.id, parent.parent_id, parent.sequence, parent.state\n FROM branch AS child\n CROSS JOIN session_entries AS parent\n WHERE parent.id = child.parent_id AND parent.session_id = ?\n AND child.sequence > ?\n AND +parent.sequence < child.sequence\n )";
339
+ readonly fork: "WITH RECURSIVE branch (id, parent_id, sequence, state) AS (\n SELECT id, parent_id, sequence, state\n FROM session_entries\n WHERE session_id = ? AND id = ? AND state != 'streaming'\n UNION\n SELECT parent.id, parent.parent_id, parent.sequence, parent.state\n FROM branch AS child\n CROSS JOIN session_entries AS parent\n WHERE parent.id = child.parent_id AND parent.session_id = ?\n AND child.sequence > ?\n AND parent.state != 'streaming'\n )";
337
340
  };
341
+ /**
342
+ * One synchronous, transaction-consistent Session baseline. Activity, timeline,
343
+ * extension projections and context figures share its identity-only ancestry;
344
+ * asynchronous model description runs after this scope has ended. Nothing is
345
+ * retained between reads, and payload windows keep their row and byte bounds.
346
+ */
347
+ export declare function withSessionRead<A>(owner: SqliteOwner, sessionId: string, read: () => A, before?: number): A;
348
+ /** Join the shared Session baseline, or keep a direct projection atomic. */
349
+ export declare function withSessionReadTransaction<A>(owner: SqliteOwner, sessionId: string, read: () => A): A;
338
350
  /**
339
351
  * One root-to-tip lineage. Its integrity is enforced where entries are
340
352
  * written (`guardSessionLineage`), so locating it reads only its tip and its