@fungi.computer/shiitake 0.5.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 (88) hide show
  1. package/README.md +59 -21
  2. package/dist/agent-control.d.ts +94 -2
  3. package/dist/agent-control.js +147 -26
  4. package/dist/client-entry.d.ts +20 -0
  5. package/dist/client-entry.js +5 -0
  6. package/dist/client-visitor.d.ts +33 -0
  7. package/dist/client-visitor.js +231 -0
  8. package/dist/client-wire.d.ts +9 -0
  9. package/dist/client-wire.js +24 -3
  10. package/dist/client.d.ts +27 -6
  11. package/dist/client.js +23 -0
  12. package/dist/cloudflare.d.ts +20 -0
  13. package/dist/cloudflare.js +4 -1
  14. package/dist/delegation.d.ts +79 -53
  15. package/dist/delegation.js +26 -21
  16. package/dist/effect.d.ts +26 -1
  17. package/dist/effect.js +14 -1
  18. package/dist/errors.d.ts +2 -2
  19. package/dist/errors.js +2 -0
  20. package/dist/extensions.d.ts +22 -3
  21. package/dist/extensions.js +1 -1
  22. package/dist/fleet.d.ts +25 -2
  23. package/dist/fleet.js +108 -24
  24. package/dist/hosted.js +3 -0
  25. package/dist/http-routes.d.ts +23 -9
  26. package/dist/http.d.ts +1 -0
  27. package/dist/http.js +1 -0
  28. package/dist/index.d.ts +1 -1
  29. package/dist/internal/accepted-run-executor.js +2 -1
  30. package/dist/internal/admission-door.d.ts +6 -1
  31. package/dist/internal/admission-door.js +33 -3
  32. package/dist/internal/contracts.d.ts +19 -1
  33. package/dist/internal/coordinator.js +115 -52
  34. package/dist/internal/effect-client.js +1 -1
  35. package/dist/internal/effect-contracts.d.ts +9 -2
  36. package/dist/internal/extension-command-registry.d.ts +2 -1
  37. package/dist/internal/extension-command-registry.js +3 -1
  38. package/dist/internal/extension-runtime.d.ts +3 -1
  39. package/dist/internal/extension-runtime.js +18 -8
  40. package/dist/internal/model-acquisition.js +2 -0
  41. package/dist/internal/observation-kernel.d.ts +2 -0
  42. package/dist/internal/observation-kernel.js +17 -6
  43. package/dist/internal/promise-facade.js +2 -0
  44. package/dist/internal/run-scope.d.ts +14 -0
  45. package/dist/internal/run-scope.js +58 -15
  46. package/dist/internal/runtime-coordinator.d.ts +14 -0
  47. package/dist/internal/runtime-coordinator.js +60 -11
  48. package/dist/internal/runtime-core.d.ts +3 -0
  49. package/dist/internal/runtime-core.js +1 -0
  50. package/dist/internal/runtime-reconciliation.js +123 -25
  51. package/dist/internal/session-context.d.ts +5 -5
  52. package/dist/internal/session-context.js +80 -83
  53. package/dist/internal/session-status.d.ts +2 -2
  54. package/dist/internal/session-status.js +5 -3
  55. package/dist/internal/woodstock/agent-reply.d.ts +5 -0
  56. package/dist/internal/woodstock/agent-reply.js +3 -0
  57. package/dist/internal/woodstock/client-projection.js +13 -11
  58. package/dist/internal/woodstock/fleet-store.d.ts +3 -2
  59. package/dist/internal/woodstock/fleet-store.js +24 -4
  60. package/dist/internal/woodstock/index.d.ts +7 -3
  61. package/dist/internal/woodstock/index.js +23 -6
  62. package/dist/internal/woodstock/progress.d.ts +11 -1
  63. package/dist/internal/woodstock/progress.js +126 -25
  64. package/dist/internal/woodstock/session-delegator.d.ts +13 -0
  65. package/dist/internal/woodstock/session-delegator.js +44 -0
  66. package/dist/internal/woodstock/session-entry.d.ts +12 -2
  67. package/dist/internal/woodstock/session-store.d.ts +16 -2
  68. package/dist/internal/woodstock/session-store.js +349 -66
  69. package/dist/internal/woodstock/work-senders.d.ts +32 -0
  70. package/dist/internal/woodstock/work-senders.js +70 -0
  71. package/dist/node.js +11 -2
  72. package/dist/remote-client.js +7 -2
  73. package/dist/server-visitor-stream.d.ts +13 -0
  74. package/dist/server-visitor-stream.js +124 -0
  75. package/dist/session-attention.d.ts +30 -0
  76. package/dist/session-attention.js +14 -0
  77. package/docs/api/agent-control.md +212 -15
  78. package/docs/api/client-entry.md +3 -0
  79. package/docs/api/client.md +209 -5
  80. package/docs/api/cloudflare.md +23 -1
  81. package/docs/api/delegation.md +284 -77
  82. package/docs/api/effect.md +100 -12
  83. package/docs/api/extensions.md +73 -12
  84. package/docs/api/fleet.md +138 -25
  85. package/docs/api/http.md +28 -0
  86. package/docs/api/index.md +43 -17
  87. package/docs/api/testing.md +9 -7
  88. package/package.json +10 -6
@@ -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,12 @@ 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;
105
- }
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 };
190
+ return snapshot(current?.workId === workId ? current.value : null);
116
191
  }
117
192
  /**
118
193
  * Keep live model progress in the current process only.
@@ -124,7 +199,23 @@ export function openProgress(owner, accepted, complete) {
124
199
  return Effect.gen(function* () {
125
200
  const current = yield* openProcessProgress(owner, accepted);
126
201
  let started;
202
+ let publishedLength = 0;
203
+ let flushTimer;
204
+ const cancelFlush = () => {
205
+ if (flushTimer !== undefined)
206
+ clearTimeout(flushTimer);
207
+ flushTimer = undefined;
208
+ };
209
+ const publish = () => {
210
+ cancelFlush();
211
+ const value = current.value;
212
+ if (value === null)
213
+ return;
214
+ publishedLength = value.text.length + value.thinking.length;
215
+ publishOwnerObservation(owner, accepted.sessionId, "progress");
216
+ };
127
217
  const clear = Effect.sync(() => {
218
+ cancelFlush();
128
219
  const hadProgress = current.value !== null;
129
220
  current.value = null;
130
221
  if (hadProgress)
@@ -134,8 +225,10 @@ export function openProgress(owner, accepted, complete) {
134
225
  yield* Effect.addFinalizer(() => clear);
135
226
  return Object.freeze({
136
227
  started: (event) => Effect.sync(() => {
228
+ cancelFlush();
137
229
  started = event;
138
230
  current.value = null;
231
+ publishedLength = 0;
139
232
  }),
140
233
  update: (event) => Effect.suspend(() => {
141
234
  const messageStart = started;
@@ -145,20 +238,28 @@ export function openProgress(owner, accepted, complete) {
145
238
  messageStart.turn.value !== event.turn.value ||
146
239
  messageStart.message.value !== event.message.value)
147
240
  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)
241
+ const delta = event.event;
242
+ if ((delta.type !== "text_delta" && delta.type !== "thinking_delta") ||
243
+ delta.delta.length === 0)
152
244
  return Effect.void;
153
- current.value = Object.freeze({
245
+ const first = current.value === null;
246
+ const value = current.value ?? {
154
247
  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");
248
+ revision: 0,
249
+ text: emptyTail(),
250
+ thinking: emptyTail(),
251
+ };
252
+ appendTail(delta.type === "text_delta" ? value.text : value.thinking, delta.delta);
253
+ value.revision++;
254
+ current.value = value;
255
+ const length = value.text.length + value.thinking.length;
256
+ // Coalesce tiny tokens, but flush a paused provider within 100 ms.
257
+ // One scope-owned timer holds no reply text and never writes SQLite.
258
+ if (first || length - publishedLength >= 4096) {
259
+ publish();
260
+ }
261
+ else
262
+ flushTimer ??= setTimeout(publish, 100);
162
263
  return Effect.void;
163
264
  }),
164
265
  terminal: (event) => clear.pipe(Effect.andThen(complete(event))),
@@ -0,0 +1,13 @@
1
+ import type { BirdDogDelegator, BirdDogMail } from "@fungi.computer/bird-dog";
2
+ import type { WatchdogSqliteOwner } from "@fungi.computer/watchdog";
3
+ import type { SqliteOwner } from "../contracts.js";
4
+ /** Woodstock keeps each Session's delegator of record beside the Session. */
5
+ export declare function prepareSessionDelegators(owner: SqliteOwner): void;
6
+ /**
7
+ * Record who sent a Session its work first, in the admitting transaction.
8
+ * Only an original mail that asks for replies names a delegator; the first one
9
+ * stays, so the relationship never moves once it exists.
10
+ */
11
+ export declare function recordSessionDelegator(owner: SqliteOwner, sessionId: string, mail: BirdDogMail | undefined): void;
12
+ /** The Session's delegator of record, if any Agent has sent it work. */
13
+ export declare function readSessionDelegator(owner: WatchdogSqliteOwner, sessionId: string): BirdDogDelegator | undefined;
@@ -0,0 +1,44 @@
1
+ import { BirdDogAgentReferenceSchema } from "@fungi.computer/bird-dog/effect";
2
+ import * as Schema from "effect/Schema";
3
+ const decodeRow = Schema.decodeUnknownSync(Schema.Struct({
4
+ mail_key: Schema.String,
5
+ agent_id: Schema.String,
6
+ reply_to_json: Schema.fromJsonString(Schema.Array(BirdDogAgentReferenceSchema)),
7
+ }));
8
+ const encodeReplyTo = Schema.encodeSync(Schema.fromJsonString(Schema.Array(BirdDogAgentReferenceSchema)));
9
+ /** Woodstock keeps each Session's delegator of record beside the Session. */
10
+ export function prepareSessionDelegators(owner) {
11
+ owner.sql.exec(`CREATE TABLE IF NOT EXISTS session_delegators (
12
+ session_id TEXT PRIMARY KEY,
13
+ mail_key TEXT NOT NULL,
14
+ agent_id TEXT NOT NULL,
15
+ reply_to_json TEXT NOT NULL
16
+ )`);
17
+ }
18
+ /**
19
+ * Record who sent a Session its work first, in the admitting transaction.
20
+ * Only an original mail that asks for replies names a delegator; the first one
21
+ * stays, so the relationship never moves once it exists.
22
+ */
23
+ export function recordSessionDelegator(owner, sessionId, mail) {
24
+ if (mail === undefined ||
25
+ mail.reply !== undefined ||
26
+ mail.from.kind !== "agent" ||
27
+ mail.replyTo.length === 0)
28
+ return;
29
+ owner.sql.exec("INSERT OR IGNORE INTO session_delegators (session_id, mail_key, agent_id, reply_to_json) VALUES (?, ?, ?, ?)", sessionId, mail.key, mail.to.agentId, encodeReplyTo(mail.replyTo));
30
+ }
31
+ /** The Session's delegator of record, if any Agent has sent it work. */
32
+ export function readSessionDelegator(owner, sessionId) {
33
+ const [row] = owner.sql
34
+ .exec("SELECT mail_key, agent_id, reply_to_json FROM session_delegators WHERE session_id = ?", sessionId)
35
+ .toArray();
36
+ if (row === undefined)
37
+ return undefined;
38
+ const decoded = decodeRow(row);
39
+ return {
40
+ key: decoded.mail_key,
41
+ agentId: decoded.agent_id,
42
+ replyTo: decoded.reply_to_json,
43
+ };
44
+ }
@@ -74,6 +74,11 @@ declare const StoredSessionEntrySchema: Schema.Union<readonly [Schema.Struct<{
74
74
  readonly commandId: Schema.String;
75
75
  readonly outcome: Schema.Literals<readonly ["completed", "failed", "interrupted", "cancelled", "outcome_unknown"]>;
76
76
  readonly diagnostic: Schema.optionalKey<Schema.String>;
77
+ readonly waitingOnYou: Schema.optionalKey<Schema.$Array<Schema.Struct<{
78
+ readonly itemId: Schema.String;
79
+ readonly kind: Schema.String;
80
+ readonly summary: Schema.String;
81
+ }>>>;
77
82
  readonly agentId: Schema.String;
78
83
  readonly sessionId: Schema.String;
79
84
  }>, Schema.Struct<{
@@ -357,15 +362,20 @@ export declare function decodeStoredSessionEntry(value: string): Result.Result<{
357
362
  readonly delivery?: "followUp" | "steer";
358
363
  readonly meta?: import("../../extensions.js").Json;
359
364
  readonly reply?: {
365
+ readonly agentId: string;
360
366
  readonly sessionId: string;
361
367
  readonly commandId: string;
362
368
  readonly outcome: "completed" | "failed" | "cancelled" | "interrupted" | "outcome_unknown";
363
- readonly agentId: string;
364
369
  readonly diagnostic?: string;
370
+ readonly waitingOnYou?: readonly {
371
+ readonly itemId: string;
372
+ readonly kind: string;
373
+ readonly summary: string;
374
+ }[];
365
375
  } | {
376
+ readonly agentId: string;
366
377
  readonly reason: "recipient_offline" | "recipient_left";
367
378
  readonly outcome: "undeliverable";
368
- readonly agentId: string;
369
379
  readonly sessionId?: string;
370
380
  };
371
381
  readonly targetWorkId?: string;
@@ -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;
@@ -237,6 +240,8 @@ export declare function settleSessionControls(owner: SqliteOwner, input: Readonl
237
240
  export declare function readSessionCommandAdmission(owner: SqliteOwner, sessionId: string, id: string): StoredCommandAdmission | undefined;
238
241
  /** Read one stable private command admission by accepted request identity. */
239
242
  export declare function readSessionCommandAdmissionByRequest(owner: SqliteOwner, sessionId: string, requestId: string): StoredCommandAdmission | undefined;
243
+ /** Probe the indexed terminal facts without materializing a live work's body. */
244
+ export declare function hasSessionWorkTerminal(owner: SqliteOwner, sessionId: string, workId: string): boolean;
240
245
  /** Read entries correlated to one accepted work identity in Session order. */
241
246
  export declare function readSessionEntriesForWork(owner: SqliteOwner, sessionId: string, workId: string): readonly StoredSessionRow[];
242
247
  /** Read the latest retained live-retry event for one accepted work item. */
@@ -330,9 +335,18 @@ export declare const lineageRowLimit = 200;
330
335
  * deduplicating; the fork walk keeps every link for its whole-lineage check.
331
336
  */
332
337
  declare const lineageWalks: {
333
- 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 )";
334
- 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 )";
335
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;
336
350
  /**
337
351
  * One root-to-tip lineage. Its integrity is enforced where entries are
338
352
  * written (`guardSessionLineage`), so locating it reads only its tip and its