@agentshouse/mdruntime 0.1.0-c3903a68dcbbf → 0.1.0-c4478f1a5ac2a

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