@agentshouse/mdruntime 0.1.0-cfbf80d8d4644 → 0.1.0-cfcbb238a5fa0

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 (201) hide show
  1. package/AGENTS.md +270 -203
  2. package/README.md +20 -10
  3. package/dist/acquisition.d.ts +14 -0
  4. package/dist/acquisition.d.ts.map +1 -0
  5. package/dist/acquisition.js +24 -0
  6. package/dist/adapter.d.ts +83 -20
  7. package/dist/adapter.d.ts.map +1 -1
  8. package/dist/attached-model.d.ts.map +1 -1
  9. package/dist/attached-model.js +15 -2
  10. package/dist/attempt.d.ts +16 -0
  11. package/dist/attempt.d.ts.map +1 -0
  12. package/dist/attempt.js +70 -0
  13. package/dist/binary.d.ts +23 -0
  14. package/dist/binary.d.ts.map +1 -0
  15. package/dist/binary.js +8 -0
  16. package/dist/bootstrap.d.ts +18 -5
  17. package/dist/bootstrap.d.ts.map +1 -1
  18. package/dist/bootstrap.js +113 -37
  19. package/dist/change-set.d.ts +2 -0
  20. package/dist/change-set.d.ts.map +1 -1
  21. package/dist/change-set.js +49 -16
  22. package/dist/change.d.ts +15 -0
  23. package/dist/change.d.ts.map +1 -1
  24. package/dist/change.js +26 -1
  25. package/dist/chat/acts.d.ts +33 -0
  26. package/dist/chat/acts.d.ts.map +1 -0
  27. package/dist/chat/acts.js +36 -0
  28. package/dist/chat/context.d.ts +3 -0
  29. package/dist/chat/context.d.ts.map +1 -0
  30. package/dist/chat/context.js +10 -0
  31. package/dist/chat/input.d.ts +9 -0
  32. package/dist/chat/input.d.ts.map +1 -0
  33. package/dist/chat/input.js +99 -0
  34. package/dist/chat/kit.d.ts +61 -0
  35. package/dist/chat/kit.d.ts.map +1 -0
  36. package/dist/chat/kit.js +123 -0
  37. package/dist/chat/lasting.d.ts +6 -0
  38. package/dist/chat/lasting.d.ts.map +1 -0
  39. package/dist/chat/lasting.js +19 -0
  40. package/dist/chat/list.d.ts +27 -0
  41. package/dist/chat/list.d.ts.map +1 -0
  42. package/dist/chat/list.js +63 -0
  43. package/dist/chat/markdown.d.ts +20 -0
  44. package/dist/chat/markdown.d.ts.map +1 -0
  45. package/dist/chat/markdown.js +188 -0
  46. package/dist/chat/message.d.ts +6 -0
  47. package/dist/chat/message.d.ts.map +1 -0
  48. package/dist/chat/message.js +192 -0
  49. package/dist/chat/messages.d.ts +22 -0
  50. package/dist/chat/messages.d.ts.map +1 -0
  51. package/dist/chat/messages.js +155 -0
  52. package/dist/chat/name.d.ts +6 -0
  53. package/dist/chat/name.d.ts.map +1 -0
  54. package/dist/chat/name.js +37 -0
  55. package/dist/chat/opened.d.ts +11 -0
  56. package/dist/chat/opened.d.ts.map +1 -0
  57. package/dist/chat/opened.js +17 -0
  58. package/dist/chat/overlay.d.ts +31 -0
  59. package/dist/chat/overlay.d.ts.map +1 -0
  60. package/dist/chat/overlay.js +97 -0
  61. package/dist/chat/parts.d.ts +57 -0
  62. package/dist/chat/parts.d.ts.map +1 -0
  63. package/dist/chat/parts.js +119 -0
  64. package/dist/chat/process.d.ts +20 -0
  65. package/dist/chat/process.d.ts.map +1 -0
  66. package/dist/chat/process.js +32 -0
  67. package/dist/chat/provider.d.ts +63 -0
  68. package/dist/chat/provider.d.ts.map +1 -0
  69. package/dist/chat/provider.js +629 -0
  70. package/dist/chat/question.d.ts +9 -0
  71. package/dist/chat/question.d.ts.map +1 -0
  72. package/dist/chat/question.js +80 -0
  73. package/dist/chat/questions.d.ts +31 -0
  74. package/dist/chat/questions.d.ts.map +1 -0
  75. package/dist/chat/questions.js +111 -0
  76. package/dist/chat/read.d.ts +5 -0
  77. package/dist/chat/read.d.ts.map +1 -0
  78. package/dist/chat/read.js +13 -0
  79. package/dist/chat/ready.d.ts +34 -0
  80. package/dist/chat/ready.d.ts.map +1 -0
  81. package/dist/chat/ready.js +91 -0
  82. package/dist/chat/style.d.ts +4 -0
  83. package/dist/chat/style.d.ts.map +1 -0
  84. package/dist/chat/style.js +302 -0
  85. package/dist/chat.d.ts +14 -0
  86. package/dist/chat.d.ts.map +1 -0
  87. package/dist/chat.js +8 -0
  88. package/dist/client.d.ts +25 -12
  89. package/dist/client.d.ts.map +1 -1
  90. package/dist/client.js +75 -16
  91. package/dist/collection.d.ts +21 -12
  92. package/dist/collection.d.ts.map +1 -1
  93. package/dist/collection.js +271 -23
  94. package/dist/connection.d.ts +44 -15
  95. package/dist/connection.d.ts.map +1 -1
  96. package/dist/connection.js +1788 -400
  97. package/dist/conversation.d.ts +26 -10
  98. package/dist/conversation.d.ts.map +1 -1
  99. package/dist/conversation.js +414 -148
  100. package/dist/durability.d.ts +7 -1
  101. package/dist/durability.d.ts.map +1 -1
  102. package/dist/entry.d.ts +17 -0
  103. package/dist/entry.d.ts.map +1 -0
  104. package/dist/entry.js +156 -0
  105. package/dist/expectation.d.ts +21 -0
  106. package/dist/expectation.d.ts.map +1 -0
  107. package/dist/expectation.js +70 -0
  108. package/dist/failure.d.ts +13 -0
  109. package/dist/failure.d.ts.map +1 -0
  110. package/dist/failure.js +19 -0
  111. package/dist/fence.d.ts +6 -0
  112. package/dist/fence.d.ts.map +1 -0
  113. package/dist/fence.js +10 -0
  114. package/dist/host.d.ts +18 -2
  115. package/dist/host.d.ts.map +1 -1
  116. package/dist/host.js +97 -19
  117. package/dist/identity.d.ts +5 -0
  118. package/dist/identity.d.ts.map +1 -1
  119. package/dist/identity.js +19 -1
  120. package/dist/index.d.ts +16 -8
  121. package/dist/index.d.ts.map +1 -1
  122. package/dist/index.js +5 -0
  123. package/dist/memory.d.ts +11 -4
  124. package/dist/memory.d.ts.map +1 -1
  125. package/dist/memory.js +87 -25
  126. package/dist/model.d.ts +14 -4
  127. package/dist/model.d.ts.map +1 -1
  128. package/dist/model.js +12 -1
  129. package/dist/observation.d.ts +5 -2
  130. package/dist/observation.d.ts.map +1 -1
  131. package/dist/observation.js +22 -11
  132. package/dist/open.d.ts +2 -0
  133. package/dist/open.d.ts.map +1 -0
  134. package/dist/open.js +1 -0
  135. package/dist/outcome.d.ts +32 -3
  136. package/dist/outcome.d.ts.map +1 -1
  137. package/dist/outcome.js +42 -1
  138. package/dist/overlay.d.ts +16 -3
  139. package/dist/overlay.d.ts.map +1 -1
  140. package/dist/overlay.js +118 -77
  141. package/dist/query.d.ts +40 -4
  142. package/dist/query.d.ts.map +1 -1
  143. package/dist/query.js +60 -21
  144. package/dist/reach.d.ts +8 -3
  145. package/dist/reach.d.ts.map +1 -1
  146. package/dist/reach.js +60 -24
  147. package/dist/react.d.ts +31 -18
  148. package/dist/react.d.ts.map +1 -1
  149. package/dist/react.js +15 -11
  150. package/dist/read-side.d.ts +13 -6
  151. package/dist/read-side.d.ts.map +1 -1
  152. package/dist/read-side.js +394 -129
  153. package/dist/readiness.d.ts +53 -0
  154. package/dist/readiness.d.ts.map +1 -0
  155. package/dist/readiness.js +33 -0
  156. package/dist/reads.d.ts +11 -0
  157. package/dist/reads.d.ts.map +1 -0
  158. package/dist/reads.js +98 -0
  159. package/dist/recovery.d.ts +25 -0
  160. package/dist/recovery.d.ts.map +1 -0
  161. package/dist/recovery.js +197 -0
  162. package/dist/refresh.d.ts +13 -3
  163. package/dist/refresh.d.ts.map +1 -1
  164. package/dist/refresh.js +49 -38
  165. package/dist/replica.d.ts +9 -6
  166. package/dist/replica.d.ts.map +1 -1
  167. package/dist/replica.js +125 -68
  168. package/dist/room-draft.d.ts +38 -9
  169. package/dist/room-draft.d.ts.map +1 -1
  170. package/dist/room-draft.js +134 -25
  171. package/dist/rows.d.ts +11 -14
  172. package/dist/rows.d.ts.map +1 -1
  173. package/dist/rows.js +89 -49
  174. package/dist/save.d.ts +31 -1
  175. package/dist/save.d.ts.map +1 -1
  176. package/dist/save.js +376 -47
  177. package/dist/search.d.ts +39 -8
  178. package/dist/search.d.ts.map +1 -1
  179. package/dist/search.js +132 -46
  180. package/dist/seed.d.ts +16 -0
  181. package/dist/seed.d.ts.map +1 -0
  182. package/dist/seed.js +61 -0
  183. package/dist/shared.d.ts +11 -0
  184. package/dist/shared.d.ts.map +1 -0
  185. package/dist/shared.js +368 -0
  186. package/dist/status.d.ts +6 -2
  187. package/dist/status.d.ts.map +1 -1
  188. package/dist/status.js +0 -8
  189. package/dist/store.d.ts +75 -18
  190. package/dist/store.d.ts.map +1 -1
  191. package/dist/store.js +539 -200
  192. package/dist/turn.d.ts +7 -0
  193. package/dist/turn.d.ts.map +1 -0
  194. package/dist/turn.js +45 -0
  195. package/dist/wire.d.ts +141 -33
  196. package/dist/wire.d.ts.map +1 -1
  197. package/dist/wire.js +61 -31
  198. package/package.json +26 -14
  199. package/dist/attachment.d.ts +0 -3
  200. package/dist/attachment.d.ts.map +0 -1
  201. package/dist/attachment.js +0 -8
@@ -1,5 +1,8 @@
1
1
  import { conversationChangeSet } from './change-set.js';
2
+ import { runtimeError } from './failure.js';
2
3
  import { declaredFile } from './file.js';
4
+ import { changeBatchIdentity, chatDraftRevision } from './identity.js';
5
+ import { OFFLINE } from './outcome.js';
3
6
  const QUIET = {
4
7
  turn: null,
5
8
  questions: [],
@@ -19,6 +22,17 @@ export function unheldView(conversation) {
19
22
  chatDraft: null,
20
23
  };
21
24
  }
25
+ export const SESSION_STARTED = 'Session started';
26
+ const NEW_CHAT = 'new:';
27
+ export function newChat(agent) {
28
+ return `${NEW_CHAT}${agent}`;
29
+ }
30
+ function creationOf(input) {
31
+ if (typeof input !== 'object' || input === null)
32
+ return null;
33
+ const { agent_id: agent, operation_id: operation } = input;
34
+ return typeof agent === 'string' && typeof operation === 'string' ? { agent, operation } : null;
35
+ }
22
36
  function startedTurn(turn, operation) {
23
37
  if (operation.op === 'begin')
24
38
  return { turn, started: operation.at, parts: [], context: null };
@@ -75,11 +89,15 @@ function ordered(left, right) {
75
89
  return left.identity < right.identity ? -1 : 1;
76
90
  return 0;
77
91
  }
78
- function merged(held, arriving) {
92
+ function windowOf(held, arriving, limit) {
79
93
  const byIdentity = new Map(held.map((message) => [message.identity, message]));
80
94
  for (const message of arriving)
81
95
  byIdentity.set(message.identity, message);
82
- return [...byIdentity.values()].sort(ordered);
96
+ const next = [...byIdentity.values()].sort(ordered);
97
+ return limit === undefined ? next : next.slice(Math.max(0, next.length - limit));
98
+ }
99
+ function newestOf(arriving, from) {
100
+ return arriving.reduce((top, message) => (top === null || message.position > top ? message.position : top), from);
83
101
  }
84
102
  function acceptedMessage(value) {
85
103
  if (typeof value !== 'object' || value === null)
@@ -138,86 +156,118 @@ function historyPages(result) {
138
156
  }
139
157
  return pages;
140
158
  }
141
- function turnOf(result) {
159
+ function answered(result, field) {
142
160
  if (result.kind !== 'action')
143
161
  return null;
144
162
  const answer = result.result;
145
163
  if (typeof answer !== 'object' || answer === null)
146
164
  return null;
147
- const record = answer;
148
- return typeof record.turn === 'string' ? record.turn : null;
165
+ const value = answer[field];
166
+ return typeof value === 'string' ? value : null;
149
167
  }
150
168
  function limitOf(options) {
151
169
  const { limit } = options;
152
170
  if (limit === undefined)
153
171
  return undefined;
154
172
  if (!Number.isInteger(limit) || limit < 0) {
155
- throw new Error(`conversation limit must be a non-negative integer, got ${String(limit)}`);
173
+ throw runtimeError('invalid-limit', `conversation limit must be a non-negative integer, got ${String(limit)}`);
156
174
  }
157
175
  return limit;
158
176
  }
177
+ const EXPIRED = { kind: 'rejected', code: 'expired', fields: {}, retryable: false };
178
+ function unwritten(draft) {
179
+ return draft.text === '' && draft.files.length === 0;
180
+ }
181
+ function pending(draft) {
182
+ return {
183
+ ...(draft.submitted === undefined ? {} : { submitted: draft.submitted }),
184
+ ...(draft.moves === undefined ? {} : { moves: draft.moves }),
185
+ };
186
+ }
187
+ function empty(draft) {
188
+ return unwritten(draft) && draft.submitted === undefined && draft.moves === undefined;
189
+ }
190
+ function withSubmitted(draft, submitted) {
191
+ const { submitted: _held, ...unsent } = draft;
192
+ return submitted.length === 0 ? unsent : { ...unsent, submitted };
193
+ }
194
+ function returned(draft, submission, others) {
195
+ return withSubmitted({
196
+ conversation: draft.conversation,
197
+ text: [submission.text, draft.text].filter((one) => one !== '').join('\n'),
198
+ files: [...submission.files, ...draft.files],
199
+ revision: chatDraftRevision(),
200
+ }, others);
201
+ }
159
202
  export function createConversationStore(deps) {
160
- const messages = new Map();
203
+ const stored = new Set();
161
204
  const reads = new Map();
205
+ const partials = new Set();
162
206
  const drafts = new Map();
163
207
  const sending = new Map();
164
208
  const behind = new Set();
209
+ const catching = new Map();
165
210
  const gaps = new Map();
166
211
  const lives = new Map();
167
212
  const views = new Map();
168
213
  const listing = new Map();
214
+ const creations = new Set();
169
215
  const listWatchers = new Set();
170
216
  let listKnown = false;
217
+ const agentWatchers = new Set();
171
218
  let agentNames = new Map();
219
+ let retiredAgents = new Set();
172
220
  let agentsRead = null;
173
221
  let replicated = new Set();
174
222
  let handedReplicated;
175
223
  function liveOf(conversation) {
176
224
  return lives.get(conversation) ?? QUIET;
177
225
  }
178
- function shownMessages(conversation, limit) {
179
- const held = messages.get(conversation) ?? [];
180
- const shown = limit === undefined ? held : held.slice(Math.max(0, held.length - limit));
181
- return shown.map(({ identity, turn, text, message }) => ({
182
- identity,
183
- turn,
184
- text,
185
- message: structuredClone(message),
186
- }));
187
- }
188
- function viewOf(conversation, limit, shown = shownMessages(conversation, limit)) {
226
+ function viewOf(conversation, held) {
189
227
  const draft = drafts.get(conversation);
190
228
  const facts = liveOf(conversation);
191
229
  return {
192
230
  conversation,
193
- messages: shown,
231
+ messages: held.map(({ identity, turn, text, message }) => ({
232
+ identity,
233
+ turn,
234
+ text,
235
+ message,
236
+ })),
194
237
  turn: structuredClone(facts.turn),
195
238
  questions: structuredClone(facts.questions),
196
239
  process: facts.process === null ? null : { ...facts.process },
197
240
  commands: structuredClone(facts.commands),
198
241
  options: structuredClone(facts.options),
199
- chatDraft: draft === undefined || draft.turn !== null || sending.get(conversation)?.draft === draft
242
+ chatDraft: draft === undefined || unwritten(draft)
200
243
  ? null
201
- : { text: draft.text, files: [...draft.files] },
244
+ : { text: draft.text, files: [...draft.files], revision: draft.revision },
202
245
  };
203
246
  }
204
- function emit(conversation, messagesChanged = true) {
247
+ function emit(conversation) {
205
248
  if (deps.closed())
206
249
  return;
207
250
  for (const view of views.values()) {
208
- if (view.conversation !== conversation)
251
+ if (view.conversation !== conversation || view.window === null)
209
252
  continue;
210
- if (messagesChanged)
211
- view.messages = null;
212
253
  const ready = [...view.watchers].filter((watcher) => watcher.ready);
213
254
  if (ready.length === 0)
214
255
  continue;
215
- view.messages ??= shownMessages(conversation, view.limit);
216
- const value = viewOf(conversation, view.limit, view.messages);
256
+ const value = viewOf(conversation, view.window);
217
257
  for (const watcher of ready)
218
258
  watcher.notify(value);
219
259
  }
220
260
  }
261
+ function slide(conversation, arriving) {
262
+ for (const view of views.values()) {
263
+ if (view.conversation !== conversation || view.window === null)
264
+ continue;
265
+ view.window = arriving === null ? [] : windowOf(view.window, arriving, view.limit);
266
+ }
267
+ }
268
+ function changesOf(conversation, arriving) {
269
+ return arriving.map(({ identity, text }) => ({ conversation, identity, text }));
270
+ }
221
271
  function stateOf(conversation) {
222
272
  const facts = liveOf(conversation);
223
273
  if (facts.questions.length > 0)
@@ -243,6 +293,21 @@ export function createConversationStore(deps) {
243
293
  for (const watcher of listWatchers)
244
294
  watcher(value);
245
295
  }
296
+ function agents() {
297
+ return [...agentNames]
298
+ .filter(([identity]) => !retiredAgents.has(identity))
299
+ .map(([identity, name]) => ({ identity, name }))
300
+ .sort((left, right) => left.name === right.name
301
+ ? left.identity.localeCompare(right.identity)
302
+ : left.name.localeCompare(right.name));
303
+ }
304
+ function emitAgents() {
305
+ if (deps.closed() || agentWatchers.size === 0)
306
+ return;
307
+ const value = agents();
308
+ for (const watcher of agentWatchers)
309
+ watcher(value);
310
+ }
246
311
  function setLive(conversation, next) {
247
312
  const before = stateOf(conversation);
248
313
  const began = lives.get(conversation)?.turn?.started;
@@ -259,20 +324,38 @@ export function createConversationStore(deps) {
259
324
  const declaration = deps.agents();
260
325
  if (declaration === undefined)
261
326
  return;
262
- const { documents } = await deps.documents(declaration.locator);
327
+ const held = await deps.documents(declaration);
328
+ if ('pending' in held)
329
+ return;
330
+ const { documents } = held;
263
331
  const names = new Map();
332
+ const retired = new Set();
264
333
  for (const document of documents) {
265
334
  if (document.type !== declaration.type)
266
335
  continue;
267
336
  const identity = document.fields?.[declaration.identity];
268
337
  const name = document.fields?.[declaration.name];
269
- if (typeof identity === 'string' && typeof name === 'string')
270
- names.set(identity, name);
338
+ if (typeof identity !== 'string' || typeof name !== 'string')
339
+ continue;
340
+ names.set(identity, name);
341
+ const ended = declaration.retired === undefined ? null : document.fields?.[declaration.retired];
342
+ if (ended !== undefined && ended !== null)
343
+ retired.add(identity);
271
344
  }
272
- if (JSON.stringify([...names]) === JSON.stringify([...agentNames]))
345
+ const named = JSON.stringify([...names]) !== JSON.stringify([...agentNames]);
346
+ if (!named && JSON.stringify([...retired]) === JSON.stringify([...retiredAgents]))
273
347
  return;
274
348
  agentNames = names;
275
- emitList();
349
+ retiredAgents = retired;
350
+ if (named)
351
+ emitList();
352
+ emitAgents();
353
+ const ended = [...drafts.keys()].filter(retiredChat);
354
+ if (ended.length > 0)
355
+ await remove(ended);
356
+ }
357
+ function retiredChat(conversation) {
358
+ return (conversation.startsWith(NEW_CHAT) && retiredAgents.has(conversation.slice(NEW_CHAT.length)));
276
359
  }
277
360
  function agentsReady() {
278
361
  agentsRead ??= readAgents().catch(() => {
@@ -282,7 +365,7 @@ export function createConversationStore(deps) {
282
365
  }
283
366
  function live(conversation, change) {
284
367
  setLive(conversation, { ...liveOf(conversation), ...change });
285
- emit(conversation, false);
368
+ emit(conversation);
286
369
  }
287
370
  function relayed(conversation, turn, operation) {
288
371
  const held = liveOf(conversation).turn;
@@ -310,21 +393,25 @@ export function createConversationStore(deps) {
310
393
  }
311
394
  async function remove(leaving) {
312
395
  await deps.durably(async (local) => {
313
- await local.dropConversations(leaving);
314
396
  const removed = [];
315
- const changed = [];
316
397
  for (const conversation of leaving) {
317
- for (const message of messages.get(conversation) ?? []) {
318
- removed.push({ conversation, identity: message.identity });
398
+ for (const identity of await local.messageIdentities(conversation)) {
399
+ removed.push({ conversation, identity, text: null });
319
400
  }
320
- if (messages.has(conversation) || drafts.has(conversation) || lives.has(conversation)) {
401
+ }
402
+ await local.dropConversations(leaving);
403
+ const changed = [];
404
+ for (const conversation of leaving) {
405
+ if (stored.has(conversation) || drafts.has(conversation) || lives.has(conversation)) {
321
406
  changed.push(conversation);
322
407
  }
323
- messages.delete(conversation);
408
+ stored.delete(conversation);
324
409
  reads.delete(conversation);
410
+ partials.delete(conversation);
325
411
  drafts.delete(conversation);
326
412
  behind.delete(conversation);
327
413
  setLive(conversation, null);
414
+ slide(conversation, null);
328
415
  }
329
416
  if (changed.length > 0)
330
417
  deps.publish(conversationChangeSet(removed, changed));
@@ -342,30 +429,25 @@ export function createConversationStore(deps) {
342
429
  const advances = read !== undefined &&
343
430
  !behind.has(conversation) &&
344
431
  (read === null || message.position > read);
345
- const sealed = drafts.get(conversation)?.turn === message.turn;
432
+ await sealed(local, conversation, new Set([message.turn]));
346
433
  await local.keepMessages({
347
434
  conversation,
348
435
  messages: [message],
349
- ...(advances ? { read: message.position } : {}),
350
- sealed,
436
+ ...(advances ? { read: { at: message.position, partial: false } } : {}),
351
437
  });
352
- messages.set(conversation, merged(messages.get(conversation) ?? [], [message]));
438
+ stored.add(conversation);
353
439
  if (advances)
354
440
  reads.set(conversation, message.position);
355
- if (sealed)
356
- drafts.delete(conversation);
357
441
  sealTurns(conversation, new Set([message.turn]));
358
- deps.publish(conversationChangeSet([{ conversation, identity: message.identity }], [conversation]));
442
+ slide(conversation, [message]);
443
+ deps.publish(conversationChangeSet(changesOf(conversation, [message]), [conversation]));
359
444
  });
360
445
  }
361
- async function readHistories(conversations) {
446
+ async function readHistories(conversations, ended) {
362
447
  const generations = new Map(conversations.map((one) => [one, gaps.get(one) ?? 0]));
363
- const starts = new Map(conversations.map((one) => [one, reads.get(one)]));
364
- const arriving = new Map();
365
- const complete = new Set();
366
448
  let asking = conversations.map((conversation) => ({
367
449
  conversation,
368
- after: starts.get(conversation) ?? undefined,
450
+ after: reads.get(conversation) ?? undefined,
369
451
  }));
370
452
  while (asking.length > 0) {
371
453
  const result = await deps.call('conversation.history', {
@@ -377,46 +459,64 @@ export function createConversationStore(deps) {
377
459
  const pages = historyPages(result);
378
460
  if (pages === null)
379
461
  return result;
380
- const asked = new Set(asking.map(({ conversation }) => conversation));
462
+ const asked = new Map(asking.map(({ conversation, after }) => [conversation, after]));
381
463
  const following = [];
382
464
  for (const page of pages) {
383
465
  if (!asked.has(page.conversation))
384
466
  continue;
385
- arriving.set(page.conversation, [
386
- ...(arriving.get(page.conversation) ?? []),
387
- ...page.messages,
388
- ]);
389
- if (page.next === undefined)
390
- complete.add(page.conversation);
391
- else
467
+ await keepPage(page.conversation, page.messages, asked.get(page.conversation) ?? null, page.next, generations.get(page.conversation) ?? 0);
468
+ if (page.next !== undefined) {
392
469
  following.push({ conversation: page.conversation, after: page.next });
470
+ }
471
+ }
472
+ for (const { conversation } of asking) {
473
+ if (!following.some((next) => next.conversation === conversation))
474
+ ended(conversation);
393
475
  }
394
476
  asking = following;
395
477
  }
396
- for (const [conversation, held] of arriving) {
397
- if (!complete.has(conversation))
398
- continue;
399
- await keepHistory(conversation, held, starts.get(conversation), generations.get(conversation) ?? 0);
400
- }
401
478
  return null;
402
479
  }
403
- function keepHistory(conversation, arriving, from, generation) {
480
+ function caughtUp(conversations) {
481
+ const ends = new Map();
482
+ for (const conversation of conversations) {
483
+ const held = new Promise((end) => ends.set(conversation, end));
484
+ catching.set(conversation, held);
485
+ void held.then(() => {
486
+ if (catching.get(conversation) === held)
487
+ catching.delete(conversation);
488
+ });
489
+ }
490
+ const ended = (conversation) => ends.get(conversation)?.();
491
+ const read = readHistories(conversations, ended);
492
+ const endAll = () => {
493
+ for (const conversation of conversations)
494
+ ended(conversation);
495
+ };
496
+ read.then(endAll, endAll);
497
+ return read;
498
+ }
499
+ function keepPage(conversation, arriving, from, next, generation) {
404
500
  return deps.durably(async (local) => {
405
501
  if (!replicated.has(conversation))
406
502
  return;
407
- const newest = arriving.reduce((top, message) => (top === null || message.position > top ? message.position : top), from ?? null);
408
- const turn = drafts.get(conversation)?.turn;
409
- const sealed = arriving.some((message) => message.turn === turn);
410
- await local.keepMessages({ conversation, messages: arriving, read: newest, sealed });
411
- messages.set(conversation, merged(messages.get(conversation) ?? [], arriving));
412
- reads.set(conversation, newest);
413
- if (sealed)
414
- drafts.delete(conversation);
503
+ const whole = next === undefined;
504
+ const cursor = next ?? newestOf(arriving, from);
505
+ const partial = !whole && (partials.has(conversation) || !reads.has(conversation));
506
+ const sealing = await sealed(local, conversation, new Set(arriving.map((message) => message.turn)));
507
+ await local.keepMessages({ conversation, messages: arriving, read: { at: cursor, partial } });
508
+ stored.add(conversation);
509
+ reads.set(conversation, cursor);
510
+ if (partial)
511
+ partials.add(conversation);
512
+ else
513
+ partials.delete(conversation);
415
514
  sealTurns(conversation, new Set(arriving.map((message) => message.turn)));
416
- if ((gaps.get(conversation) ?? 0) === generation)
515
+ if (whole && (gaps.get(conversation) ?? 0) === generation)
417
516
  behind.delete(conversation);
418
- if (arriving.length > 0 || sealed) {
419
- deps.publish(conversationChangeSet(arriving.map((message) => ({ conversation, identity: message.identity })), [conversation]));
517
+ slide(conversation, arriving);
518
+ if (arriving.length > 0 || sealing || whole) {
519
+ deps.publish(conversationChangeSet(changesOf(conversation, arriving), [conversation]));
420
520
  }
421
521
  });
422
522
  }
@@ -429,7 +529,7 @@ export function createConversationStore(deps) {
429
529
  if (held === undefined)
430
530
  return;
431
531
  setLive(conversation, { ...QUIET, turn: held.turn });
432
- emit(conversation, false);
532
+ emit(conversation);
433
533
  return;
434
534
  }
435
535
  if (event.kind === 'turn') {
@@ -462,22 +562,126 @@ export function createConversationStore(deps) {
462
562
  if (sealTurns(conversation, new Set([arriving.turn])))
463
563
  emit(conversation);
464
564
  }
565
+ async function kept(local, conversation, following) {
566
+ if (following === null || empty(following)) {
567
+ await local.dropChatDraft(conversation);
568
+ drafts.delete(conversation);
569
+ }
570
+ else {
571
+ await local.keepChatDrafts([following]);
572
+ drafts.set(conversation, following);
573
+ }
574
+ }
575
+ async function sealed(local, conversation, turns) {
576
+ const held = drafts.get(conversation);
577
+ const submitted = held?.submitted ?? [];
578
+ const left = submitted.filter((one) => one.turn === null || !turns.has(one.turn));
579
+ if (held === undefined || left.length === submitted.length)
580
+ return false;
581
+ await kept(local, conversation, withSubmitted(held, left));
582
+ return true;
583
+ }
584
+ function moved(agent, operation, to) {
585
+ const from = newChat(agent);
586
+ return deps.durably(async (local) => {
587
+ const held = drafts.get(from);
588
+ const earlier = (held?.moves ?? []).filter((one) => !deps.expired(one));
589
+ if (earlier.includes(operation))
590
+ return;
591
+ const blank = {
592
+ conversation: from,
593
+ text: '',
594
+ files: [],
595
+ revision: chatDraftRevision(),
596
+ };
597
+ const { moves: _answered, ...unsent } = held ?? blank;
598
+ const taken = drafts.has(to) || empty(unsent) ? [] : [{ ...unsent, conversation: to }];
599
+ const left = { ...(taken.length === 0 ? unsent : blank), moves: [...earlier, operation] };
600
+ await local.keepChatDrafts([left, ...taken]);
601
+ drafts.set(from, left);
602
+ for (const one of taken)
603
+ drafts.set(to, one);
604
+ deps.publish(conversationChangeSet([], [from, to]));
605
+ });
606
+ }
465
607
  function withDraft(conversation, next) {
466
608
  return deps.durably(async (local) => {
467
- const following = next(drafts.get(conversation));
609
+ const following = await next(drafts.get(conversation), local);
468
610
  if (following === undefined)
469
611
  return;
470
- if (following === null) {
471
- await local.dropChatDraft(conversation);
472
- drafts.delete(conversation);
473
- }
474
- else {
475
- await local.keepChatDraft(following);
476
- drafts.set(conversation, following);
477
- }
612
+ await kept(local, conversation, following);
478
613
  deps.publish(conversationChangeSet([], [conversation]));
479
614
  });
480
615
  }
616
+ async function submitted(conversation, operation, draft) {
617
+ let submission;
618
+ let earlier = false;
619
+ await withDraft(conversation, (current) => {
620
+ submission = current?.submitted?.find((one) => one.operation === operation);
621
+ earlier = submission !== undefined;
622
+ if (submission !== undefined)
623
+ return undefined;
624
+ const said = draft ?? current;
625
+ submission = {
626
+ operation,
627
+ text: said?.text ?? '',
628
+ files: [...(said?.files ?? [])],
629
+ turn: null,
630
+ };
631
+ return {
632
+ conversation,
633
+ ...(draft === undefined || current === undefined
634
+ ? { text: '', files: [], revision: chatDraftRevision() }
635
+ : { text: current.text, files: current.files, revision: current.revision }),
636
+ submitted: [...(current?.submitted ?? []), submission],
637
+ };
638
+ });
639
+ return { submission: submission, earlier };
640
+ }
641
+ function forgotten(conversation, submission) {
642
+ return withDraft(conversation, (current) => {
643
+ const held = current?.submitted ?? [];
644
+ if (current === undefined || !held.includes(submission))
645
+ return undefined;
646
+ return withSubmitted(current, held.filter((one) => one !== submission));
647
+ });
648
+ }
649
+ async function dispatched(conversation, submission) {
650
+ const dispatch = {};
651
+ sending.set(submission.operation, dispatch);
652
+ try {
653
+ const files = await Promise.all(submission.files.map(async (file) => ({ ...(await declaredFile(file)), blob: file.blob })));
654
+ const result = await invoke('conversation.send', conversation, {
655
+ text: submission.text,
656
+ files,
657
+ operation_id: submission.operation,
658
+ });
659
+ if (result.kind === 'revoked-authorization')
660
+ return result;
661
+ if (result.kind !== 'action' && result.retryable) {
662
+ deps.unanswered();
663
+ return result;
664
+ }
665
+ const turn = answered(result, 'turn');
666
+ await withDraft(conversation, async (current, local) => {
667
+ const held = current?.submitted?.find((one) => one.operation === submission.operation);
668
+ if (current === undefined || held === undefined || held.turn !== null)
669
+ return undefined;
670
+ const others = current.submitted.filter((one) => one !== held);
671
+ if (result.kind !== 'action')
672
+ return returned(current, held, others);
673
+ const arrived = turn !== null && (await local.turnArrived(conversation, turn));
674
+ return withSubmitted(current, turn === null || arrived
675
+ ? others
676
+ : current.submitted.map((one) => (one === held ? { ...held, turn } : one)));
677
+ });
678
+ return result;
679
+ }
680
+ finally {
681
+ if (sending.get(submission.operation) === dispatch)
682
+ sending.delete(submission.operation);
683
+ }
684
+ }
481
685
  function invoke(name, conversation, extra = {}) {
482
686
  return deps.call(name, { conversation, ...extra });
483
687
  }
@@ -488,46 +692,34 @@ export function createConversationStore(deps) {
488
692
  },
489
693
  async read(conversation, options = {}) {
490
694
  const limit = limitOf(options);
491
- return deps.durably(() => Promise.resolve(viewOf(conversation, limit)));
492
- },
493
- create(input) {
494
- return deps.call('conversation.create', input);
695
+ return deps.durably(async (local) => structuredClone(viewOf(conversation, await local.messageWindow(conversation, limit))));
495
696
  },
496
- async send(conversation) {
497
- const draft = drafts.get(conversation);
498
- const pending = draft === undefined ? undefined : { draft };
499
- if (pending !== undefined) {
500
- sending.set(conversation, pending);
501
- deps.publish(conversationChangeSet([], [conversation]));
502
- }
503
- try {
504
- const files = await Promise.all((draft?.files ?? []).map(async (file) => ({
505
- ...(await declaredFile(file)),
506
- blob: file.blob,
507
- })));
508
- const result = await invoke('conversation.send', conversation, {
509
- text: draft?.text ?? '',
510
- files,
511
- });
512
- const turn = turnOf(result);
513
- if (turn === null || draft === undefined)
514
- return result;
515
- await withDraft(conversation, (current) => {
516
- if (current !== draft)
517
- return undefined;
518
- const sealed = (messages.get(conversation) ?? []).some((message) => message.turn === turn);
519
- return sealed ? null : { ...draft, turn };
520
- });
697
+ async create(input) {
698
+ const result = await deps.call('conversation.create', input);
699
+ const conversation = answered(result, 'conversation');
700
+ if (conversation === null)
521
701
  return result;
702
+ if (!listing.has(conversation))
703
+ creations.add(conversation);
704
+ const creation = creationOf(input);
705
+ if (creation !== null)
706
+ await moved(creation.agent, creation.operation, conversation);
707
+ return result;
708
+ },
709
+ async send(conversation, operation = changeBatchIdentity(), draft) {
710
+ if (deps.online()) {
711
+ const { submission, earlier } = await submitted(conversation, operation, draft);
712
+ if (!earlier || !deps.expired(operation))
713
+ return dispatched(conversation, submission);
714
+ await forgotten(conversation, submission);
715
+ return EXPIRED;
522
716
  }
523
- finally {
524
- if (pending !== undefined && sending.get(conversation) === pending) {
525
- sending.delete(conversation);
526
- if (drafts.get(conversation) === draft) {
527
- deps.publish(conversationChangeSet([], [conversation]));
528
- }
529
- }
717
+ if (draft !== undefined) {
718
+ await withDraft(conversation, (current) => current?.submitted?.some((one) => one.operation === operation) === true
719
+ ? undefined
720
+ : returned(current ?? { conversation, text: '', files: [], revision: '' }, { operation, ...draft, turn: null }, current?.submitted ?? []));
530
721
  }
722
+ return OFFLINE;
531
723
  },
532
724
  answer(conversation, answer) {
533
725
  return invoke('conversation.answer', conversation, { answer });
@@ -542,7 +734,12 @@ export function createConversationStore(deps) {
542
734
  const result = await invoke('conversation.cancel', conversation, { turn });
543
735
  if (result.kind !== 'action')
544
736
  return result;
545
- await withDraft(conversation, (current) => current === undefined || current.turn !== turn ? undefined : { ...current, turn: null });
737
+ await withDraft(conversation, (current) => {
738
+ const held = current?.submitted?.find((one) => one.turn === turn);
739
+ if (current === undefined || held === undefined)
740
+ return undefined;
741
+ return returned(current, held, current.submitted.filter((one) => one !== held));
742
+ });
546
743
  return result;
547
744
  },
548
745
  delete(conversation) {
@@ -550,33 +747,43 @@ export function createConversationStore(deps) {
550
747
  },
551
748
  };
552
749
  const chatDraft = {
553
- write(conversation, draft) {
554
- return withDraft(conversation, () => ({
750
+ write(conversation, draft, revision = chatDraftRevision()) {
751
+ return withDraft(conversation, (current) => ({
555
752
  conversation,
556
753
  text: draft.text,
557
754
  files: [...draft.files],
558
- turn: null,
755
+ revision,
756
+ ...(current === undefined ? {} : pending(current)),
559
757
  }));
560
758
  },
561
759
  discard(conversation) {
562
- return withDraft(conversation, (current) => (current === undefined ? undefined : null));
760
+ return withDraft(conversation, (current) => current === undefined
761
+ ? undefined
762
+ : {
763
+ conversation,
764
+ text: '',
765
+ files: [],
766
+ revision: chatDraftRevision(),
767
+ ...pending(current),
768
+ });
563
769
  },
564
770
  };
565
771
  return {
566
772
  surface,
567
773
  chatDraft,
568
- hydrate(held, stored) {
569
- messages.clear();
774
+ hydrate(held, kept) {
775
+ stored.clear();
570
776
  reads.clear();
571
- for (const view of views.values())
572
- view.messages = null;
777
+ partials.clear();
573
778
  for (const entry of held) {
574
- messages.set(entry.conversation, [...entry.messages].sort(ordered));
779
+ stored.add(entry.conversation);
575
780
  if (entry.read !== undefined)
576
781
  reads.set(entry.conversation, entry.read);
782
+ if (entry.partial)
783
+ partials.add(entry.conversation);
577
784
  }
578
785
  drafts.clear();
579
- for (const record of stored)
786
+ for (const record of kept)
580
787
  drafts.set(record.conversation, record);
581
788
  },
582
789
  async reconcile() {
@@ -584,8 +791,10 @@ export function createConversationStore(deps) {
584
791
  return;
585
792
  const named = new Set(listing.keys());
586
793
  const leaving = [
587
- ...new Set([...replicated, ...messages.keys(), ...reads.keys(), ...drafts.keys()]),
588
- ].filter((conversation) => !named.has(conversation));
794
+ ...new Set([...replicated, ...stored, ...reads.keys(), ...drafts.keys()]),
795
+ ].filter((conversation) => !named.has(conversation) &&
796
+ !creations.has(conversation) &&
797
+ (!conversation.startsWith(NEW_CHAT) || retiredChat(conversation)));
589
798
  for (const conversation of named) {
590
799
  if (!replicated.has(conversation))
591
800
  behind.add(conversation);
@@ -596,22 +805,64 @@ export function createConversationStore(deps) {
596
805
  await remove(leaving);
597
806
  },
598
807
  async catchUp(progress) {
599
- const reading = [...behind].filter((conversation) => replicated.has(conversation)).sort();
600
- if (reading.length > 0 && !deps.closed()) {
601
- const failed = await readHistories(reading);
808
+ const owed = (conversation) => behind.has(conversation) && replicated.has(conversation);
809
+ const read = async (conversations) => {
810
+ if (conversations.length === 0 || deps.closed())
811
+ return;
812
+ const failed = await caughtUp(conversations);
602
813
  if (failed?.kind === 'transport-uncertainty')
603
814
  progress.reachable = false;
604
- }
815
+ };
816
+ const asking = [...behind].filter(owed).sort();
817
+ const awaited = asking.filter((conversation) => catching.has(conversation));
818
+ await Promise.all([
819
+ read(asking.filter((conversation) => !catching.has(conversation))),
820
+ Promise.all(awaited.map((conversation) => catching.get(conversation))).then(() => read(awaited.filter((conversation) => owed(conversation) && !catching.has(conversation)))),
821
+ ]);
605
822
  if (behind.size > 0)
606
823
  progress.complete = false;
607
824
  },
825
+ async resend(progress) {
826
+ for (const [conversation, held] of [...drafts]) {
827
+ for (const submission of held.submitted ?? []) {
828
+ if (submission.turn !== null || sending.has(submission.operation))
829
+ continue;
830
+ if (deps.closed())
831
+ return;
832
+ if (deps.expired(submission.operation)) {
833
+ await forgotten(conversation, submission);
834
+ continue;
835
+ }
836
+ const result = await dispatched(conversation, submission);
837
+ if (result.kind === 'revoked-authorization')
838
+ return;
839
+ if (result.kind === 'transport-uncertainty') {
840
+ progress.reachable = false;
841
+ return;
842
+ }
843
+ if (result.kind !== 'action' && result.retryable) {
844
+ progress.complete = false;
845
+ return;
846
+ }
847
+ }
848
+ }
849
+ },
608
850
  current() {
609
851
  return behind.size === 0;
610
852
  },
853
+ known() {
854
+ return listKnown;
855
+ },
611
856
  reconnected() {
612
857
  for (const conversation of replicated)
613
858
  gap(conversation);
614
859
  },
860
+ reopened() {
861
+ sending.clear();
862
+ },
863
+ lost() {
864
+ catching.clear();
865
+ },
615
866
  watch(conversation, options, deliver) {
616
867
  const limit = limitOf(options);
617
868
  const key = JSON.stringify([conversation, limit ?? null]);
@@ -619,18 +870,20 @@ export function createConversationStore(deps) {
619
870
  conversation,
620
871
  limit,
621
872
  watchers: new Set(),
622
- messages: null,
873
+ window: null,
623
874
  };
624
875
  views.set(key, view);
625
876
  const watcher = { notify: deliver, ready: false };
626
877
  view.watchers.add(watcher);
627
878
  void deps
628
- .durably(() => Promise.resolve())
879
+ .durably(async (local) => {
880
+ view.window ??= await local.messageWindow(conversation, limit);
881
+ })
629
882
  .then(() => {
630
883
  if (deps.closed() || !view.watchers.has(watcher))
631
884
  return;
632
885
  watcher.ready = true;
633
- watcher.notify(viewOf(conversation, limit));
886
+ watcher.notify(viewOf(conversation, view.window ?? []));
634
887
  })
635
888
  .catch(() => { });
636
889
  return () => {
@@ -650,6 +903,16 @@ export function createConversationStore(deps) {
650
903
  listWatchers.delete(deliver);
651
904
  };
652
905
  },
906
+ watchAgents(deliver) {
907
+ agentWatchers.add(deliver);
908
+ void agentsReady().then(() => {
909
+ if (!deps.closed() && agentWatchers.has(deliver))
910
+ deliver(agents());
911
+ });
912
+ return () => {
913
+ agentWatchers.delete(deliver);
914
+ };
915
+ },
653
916
  changed(set) {
654
917
  for (const conversation of set.conversations)
655
918
  emit(conversation);
@@ -667,12 +930,15 @@ export function createConversationStore(deps) {
667
930
  if (event.kind === 'list' || event.kind === 'listed' || event.kind === 'unlisted') {
668
931
  if (event.kind === 'list') {
669
932
  listing.clear();
670
- for (const entry of event.entries)
933
+ for (const entry of event.entries) {
671
934
  listing.set(entry.conversation, { ...entry });
935
+ creations.delete(entry.conversation);
936
+ }
672
937
  listKnown = true;
673
938
  }
674
939
  else if (event.kind === 'listed') {
675
940
  listing.set(event.entry.conversation, { ...event.entry });
941
+ creations.delete(event.entry.conversation);
676
942
  }
677
943
  else {
678
944
  listing.delete(event.conversation);
@@ -686,17 +952,17 @@ export function createConversationStore(deps) {
686
952
  heard(event);
687
953
  return false;
688
954
  },
689
- messages() {
690
- return [...messages].flatMap(([conversation, held]) => held.map(({ identity, text }) => ({ conversation, identity, text })));
955
+ searchable() {
956
+ return [...(listKnown ? listing.keys() : stored)].sort();
691
957
  },
692
- message(conversation, identity) {
693
- const held = messages.get(conversation)?.find((message) => message.identity === identity);
694
- return held === undefined ? null : { conversation, identity, text: held.text };
958
+ acquired(conversation) {
959
+ return reads.has(conversation) && !partials.has(conversation);
695
960
  },
696
961
  close() {
697
962
  views.clear();
698
963
  lives.clear();
699
964
  listWatchers.clear();
965
+ agentWatchers.clear();
700
966
  },
701
967
  };
702
968
  }