@agentshouse/mdruntime 0.1.0-alpha.9 → 0.1.0-c037654ea7e04

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 (133) hide show
  1. package/AGENTS.md +232 -0
  2. package/README.md +44 -0
  3. package/dist/adapter.d.ts +207 -36
  4. package/dist/adapter.d.ts.map +1 -1
  5. package/dist/adapter.js +1 -10
  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 +34 -0
  9. package/dist/attachment.d.ts +3 -0
  10. package/dist/attachment.d.ts.map +1 -0
  11. package/dist/attachment.js +8 -0
  12. package/dist/bootstrap.d.ts +7 -9
  13. package/dist/bootstrap.d.ts.map +1 -1
  14. package/dist/bootstrap.js +30 -28
  15. package/dist/change-set.d.ts +33 -0
  16. package/dist/change-set.d.ts.map +1 -0
  17. package/dist/change-set.js +83 -0
  18. package/dist/change.d.ts +9 -0
  19. package/dist/change.d.ts.map +1 -1
  20. package/dist/change.js +6 -5
  21. package/dist/client.d.ts +39 -32
  22. package/dist/client.d.ts.map +1 -1
  23. package/dist/client.js +114 -183
  24. package/dist/collection.d.ts +13 -16
  25. package/dist/collection.d.ts.map +1 -1
  26. package/dist/collection.js +45 -88
  27. package/dist/connection.d.ts +51 -46
  28. package/dist/connection.d.ts.map +1 -1
  29. package/dist/connection.js +1551 -1161
  30. package/dist/conversation.d.ts +57 -24
  31. package/dist/conversation.d.ts.map +1 -1
  32. package/dist/conversation.js +631 -330
  33. package/dist/durability.js +6 -6
  34. package/dist/file.d.ts +2 -2
  35. package/dist/file.d.ts.map +1 -1
  36. package/dist/file.js +48 -71
  37. package/dist/host.d.ts +12 -2
  38. package/dist/host.d.ts.map +1 -1
  39. package/dist/host.js +97 -119
  40. package/dist/identity.d.ts +1 -0
  41. package/dist/identity.d.ts.map +1 -1
  42. package/dist/identity.js +10 -9
  43. package/dist/index.d.ts +12 -11
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +1 -3
  46. package/dist/log.js +5 -6
  47. package/dist/memory.d.ts +16 -7
  48. package/dist/memory.d.ts.map +1 -1
  49. package/dist/memory.js +267 -220
  50. package/dist/model.d.ts +7 -29
  51. package/dist/model.d.ts.map +1 -1
  52. package/dist/model.js +19 -21
  53. package/dist/observation.d.ts +27 -0
  54. package/dist/observation.d.ts.map +1 -0
  55. package/dist/observation.js +95 -0
  56. package/dist/outcome.d.ts +10 -29
  57. package/dist/outcome.d.ts.map +1 -1
  58. package/dist/outcome.js +1 -23
  59. package/dist/overlay.d.ts +9 -2
  60. package/dist/overlay.d.ts.map +1 -1
  61. package/dist/overlay.js +187 -43
  62. package/dist/paths.d.ts +0 -2
  63. package/dist/paths.d.ts.map +1 -1
  64. package/dist/paths.js +6 -29
  65. package/dist/query.d.ts +215 -10
  66. package/dist/query.d.ts.map +1 -1
  67. package/dist/query.js +429 -15
  68. package/dist/reach.d.ts +40 -0
  69. package/dist/reach.d.ts.map +1 -0
  70. package/dist/reach.js +270 -0
  71. package/dist/react.d.ts +31 -0
  72. package/dist/react.d.ts.map +1 -0
  73. package/dist/react.js +62 -0
  74. package/dist/read-side.d.ts +18 -0
  75. package/dist/read-side.d.ts.map +1 -0
  76. package/dist/read-side.js +377 -0
  77. package/dist/refresh.d.ts +5 -5
  78. package/dist/refresh.d.ts.map +1 -1
  79. package/dist/refresh.js +49 -30
  80. package/dist/replica.d.ts +25 -5
  81. package/dist/replica.d.ts.map +1 -1
  82. package/dist/replica.js +227 -38
  83. package/dist/room-draft.d.ts +127 -0
  84. package/dist/room-draft.d.ts.map +1 -0
  85. package/dist/room-draft.js +329 -0
  86. package/dist/rows.d.ts +41 -0
  87. package/dist/rows.d.ts.map +1 -0
  88. package/dist/rows.js +97 -0
  89. package/dist/save.d.ts +7 -4
  90. package/dist/save.d.ts.map +1 -1
  91. package/dist/save.js +314 -356
  92. package/dist/search.d.ts +61 -0
  93. package/dist/search.d.ts.map +1 -0
  94. package/dist/search.js +125 -0
  95. package/dist/status.d.ts +4 -6
  96. package/dist/status.d.ts.map +1 -1
  97. package/dist/status.js +9 -13
  98. package/dist/store.d.ts +60 -32
  99. package/dist/store.d.ts.map +1 -1
  100. package/dist/store.js +338 -134
  101. package/dist/transfer.d.ts +5 -0
  102. package/dist/transfer.d.ts.map +1 -1
  103. package/dist/transfer.js +1 -5
  104. package/dist/wire.d.ts +52 -16
  105. package/dist/wire.d.ts.map +1 -1
  106. package/dist/wire.js +78 -108
  107. package/package.json +17 -11
  108. package/dist/adapter.js.map +0 -1
  109. package/dist/bootstrap.js.map +0 -1
  110. package/dist/change.js.map +0 -1
  111. package/dist/client.js.map +0 -1
  112. package/dist/collection.js.map +0 -1
  113. package/dist/connection.js.map +0 -1
  114. package/dist/conversation.js.map +0 -1
  115. package/dist/durability.js.map +0 -1
  116. package/dist/file.js.map +0 -1
  117. package/dist/host.js.map +0 -1
  118. package/dist/identity.js.map +0 -1
  119. package/dist/index.js.map +0 -1
  120. package/dist/log.js.map +0 -1
  121. package/dist/memory.js.map +0 -1
  122. package/dist/model.js.map +0 -1
  123. package/dist/outcome.js.map +0 -1
  124. package/dist/overlay.js.map +0 -1
  125. package/dist/paths.js.map +0 -1
  126. package/dist/query.js.map +0 -1
  127. package/dist/refresh.js.map +0 -1
  128. package/dist/replica.js.map +0 -1
  129. package/dist/save.js.map +0 -1
  130. package/dist/status.js.map +0 -1
  131. package/dist/store.js.map +0 -1
  132. package/dist/transfer.js.map +0 -1
  133. package/dist/wire.js.map +0 -1
@@ -1,401 +1,702 @@
1
+ import { conversationChangeSet } from './change-set.js';
1
2
  import { declaredFile } from './file.js';
2
- const NO_TURNS = new Set();
3
- function carriedMessage(value) {
4
- if (typeof value !== 'object' || value === null) return null;
5
- const record = value;
6
- if (typeof record.turn !== 'string') return null;
7
- return structuredClone(value);
8
- }
9
- function historyPage(result) {
10
- if (result.kind !== 'action') return null;
11
- const answer = result.result;
12
- if (typeof answer !== 'object' || answer === null) return null;
13
- const record = answer;
14
- if (!Array.isArray(record.messages)) return null;
15
- const messages = [];
16
- for (const entry of record.messages){
17
- const carried = carriedMessage(entry);
18
- if (carried !== null) messages.push(carried);
19
- }
3
+ const QUIET = {
4
+ turn: null,
5
+ questions: [],
6
+ process: null,
7
+ commands: [],
8
+ options: [],
9
+ };
10
+ export function unheldView(conversation) {
20
11
  return {
21
- messages,
22
- cursor: typeof record.cursor === 'string' ? record.cursor : null
12
+ conversation,
13
+ messages: [],
14
+ turn: null,
15
+ questions: [],
16
+ process: null,
17
+ commands: [],
18
+ options: [],
19
+ chatDraft: null,
23
20
  };
24
21
  }
25
- function turnOf(result) {
26
- if (result.kind !== 'action') return null;
27
- const answer = result.result;
28
- if (typeof answer !== 'object' || answer === null) return null;
29
- const record = answer;
30
- return typeof record.turn === 'string' ? record.turn : null;
22
+ function startedTurn(turn, operation) {
23
+ if (operation.op === 'begin')
24
+ return { turn, started: operation.at, parts: [], context: null };
25
+ if (operation.op === 'context') {
26
+ return {
27
+ turn,
28
+ started: null,
29
+ parts: [],
30
+ context: { used: operation.used, window: operation.window },
31
+ };
32
+ }
33
+ return operation.op === 'start' && operation.index === 0
34
+ ? { turn, started: null, parts: [structuredClone(operation.part)], context: null }
35
+ : null;
36
+ }
37
+ function changedFields(part, operation) {
38
+ if (operation.op === 'append') {
39
+ return { text: `${String(part.text ?? '')}${operation.text}` };
40
+ }
41
+ if (operation.op === 'status') {
42
+ return { status: operation.status, result: structuredClone(operation.result) };
43
+ }
44
+ if (operation.op === 'count')
45
+ return { count: operation.count };
46
+ return { entries: structuredClone(operation.entries) };
31
47
  }
32
- function newest(held, page, arrived) {
33
- const arriving = new Set(page.messages.map((entry)=>entry.turn));
34
- const overlap = held.messages.findIndex((entry)=>arriving.has(entry.turn));
35
- const carried = overlap === -1 ? [] : held.messages.slice(0, overlap);
36
- const placed = new Set([
37
- ...carried,
38
- ...page.messages
39
- ].map((entry)=>entry.turn));
40
- const live = held.messages.filter((entry)=>arrived.has(entry.turn) && !placed.has(entry.turn));
48
+ function appliedTurn(held, operation) {
49
+ if (operation.op === 'begin')
50
+ return { ...held, started: operation.at };
51
+ if (operation.op === 'context') {
52
+ return { ...held, context: { used: operation.used, window: operation.window } };
53
+ }
54
+ if (operation.op === 'start') {
55
+ if (operation.index !== 0 && operation.index !== held.parts.length)
56
+ return null;
57
+ return {
58
+ ...held,
59
+ parts: [...held.parts.slice(0, operation.index), structuredClone(operation.part)],
60
+ };
61
+ }
62
+ const part = held.parts[operation.index];
63
+ if (typeof part !== 'object' || part === null)
64
+ return null;
65
+ const changed = { ...part, ...changedFields(part, operation) };
41
66
  return {
42
67
  ...held,
43
- messages: [
44
- ...carried,
45
- ...page.messages,
46
- ...live
47
- ],
48
- cursor: overlap === -1 ? page.cursor : held.cursor
68
+ parts: held.parts.map((kept, at) => (at === operation.index ? changed : kept)),
49
69
  };
50
70
  }
51
- function older(held, page) {
52
- const present = new Set(held.messages.map((entry)=>entry.turn));
71
+ function ordered(left, right) {
72
+ if (left.position !== right.position)
73
+ return left.position < right.position ? -1 : 1;
74
+ if (left.identity !== right.identity)
75
+ return left.identity < right.identity ? -1 : 1;
76
+ return 0;
77
+ }
78
+ function merged(held, arriving) {
79
+ const byIdentity = new Map(held.map((message) => [message.identity, message]));
80
+ for (const message of arriving)
81
+ byIdentity.set(message.identity, message);
82
+ return [...byIdentity.values()].sort(ordered);
83
+ }
84
+ function acceptedMessage(value) {
85
+ if (typeof value !== 'object' || value === null)
86
+ return null;
87
+ const record = value;
88
+ if (typeof record.identity !== 'string' ||
89
+ typeof record.turn !== 'string' ||
90
+ typeof record.position !== 'string' ||
91
+ typeof record.text !== 'string') {
92
+ return null;
93
+ }
53
94
  return {
54
- ...held,
55
- messages: [
56
- ...page.messages.filter((entry)=>!present.has(entry.turn)),
57
- ...held.messages
58
- ],
59
- cursor: page.cursor
95
+ identity: record.identity,
96
+ turn: record.turn,
97
+ position: record.position,
98
+ text: record.text,
99
+ message: structuredClone(record.message),
60
100
  };
61
101
  }
62
- function sealed(held, arriving) {
63
- const at = held.messages.findIndex((entry)=>entry.turn === arriving.turn);
64
- if (at === -1) return {
65
- ...held,
66
- messages: [
67
- ...held.messages,
68
- arriving
69
- ]
70
- };
71
- const messages = [
72
- ...held.messages
73
- ];
74
- messages[at] = arriving;
102
+ function historyPage(value) {
103
+ if (typeof value !== 'object' || value === null)
104
+ return null;
105
+ const record = value;
106
+ if (typeof record.conversation !== 'string' || !Array.isArray(record.messages))
107
+ return null;
108
+ if (record.next !== undefined && typeof record.next !== 'string')
109
+ return null;
110
+ const messages = [];
111
+ for (const entry of record.messages) {
112
+ const message = acceptedMessage(entry);
113
+ if (message === null)
114
+ return null;
115
+ messages.push(message);
116
+ }
75
117
  return {
76
- ...held,
77
- messages
118
+ conversation: record.conversation,
119
+ messages,
120
+ ...(record.next === undefined ? {} : { next: record.next }),
78
121
  };
79
122
  }
123
+ function historyPages(result) {
124
+ if (result.kind !== 'action')
125
+ return null;
126
+ const answer = result.result;
127
+ if (typeof answer !== 'object' || answer === null)
128
+ return null;
129
+ const { histories } = answer;
130
+ if (!Array.isArray(histories))
131
+ return null;
132
+ const pages = [];
133
+ for (const entry of histories) {
134
+ const page = historyPage(entry);
135
+ if (page === null)
136
+ return null;
137
+ pages.push(page);
138
+ }
139
+ return pages;
140
+ }
141
+ function turnOf(result) {
142
+ if (result.kind !== 'action')
143
+ return null;
144
+ const answer = result.result;
145
+ if (typeof answer !== 'object' || answer === null)
146
+ return null;
147
+ const record = answer;
148
+ return typeof record.turn === 'string' ? record.turn : null;
149
+ }
150
+ function limitOf(options) {
151
+ const { limit } = options;
152
+ if (limit === undefined)
153
+ return undefined;
154
+ if (!Number.isInteger(limit) || limit < 0) {
155
+ throw new Error(`conversation limit must be a non-negative integer, got ${String(limit)}`);
156
+ }
157
+ return limit;
158
+ }
80
159
  export function createConversationStore(deps) {
81
- const held = new Map();
82
- const composed = new Map();
83
- const growing = new Map();
84
- const viewers = new Set();
85
- const issued = new Map();
86
- const live = new Map();
87
- let handed;
88
- function heldOf(conversation) {
89
- return held.get(conversation) ?? {
90
- messages: [],
91
- cursor: null,
92
- summary: null
93
- };
160
+ const messages = new Map();
161
+ const reads = new Map();
162
+ const drafts = new Map();
163
+ const sending = new Map();
164
+ const behind = new Set();
165
+ const gaps = new Map();
166
+ const lives = new Map();
167
+ const views = new Map();
168
+ const listing = new Map();
169
+ const listWatchers = new Set();
170
+ let listKnown = false;
171
+ let agentNames = new Map();
172
+ let agentsRead = null;
173
+ let replicated = new Set();
174
+ let handedReplicated;
175
+ function liveOf(conversation) {
176
+ return lives.get(conversation) ?? QUIET;
94
177
  }
95
- function instructionOf(conversation) {
96
- return composed.get(conversation) ?? {
97
- text: '',
98
- files: [],
99
- turn: null
100
- };
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
+ }));
101
187
  }
102
- function viewOf(conversation) {
103
- const carried = heldOf(conversation);
104
- const unsent = composed.get(conversation);
188
+ function viewOf(conversation, limit, shown = shownMessages(conversation, limit)) {
189
+ const draft = drafts.get(conversation);
190
+ const facts = liveOf(conversation);
105
191
  return {
106
192
  conversation,
107
- messages: structuredClone(carried.messages),
108
- olderPages: carried.cursor !== null,
109
- growingText: growing.get(conversation) ?? null,
110
- summary: structuredClone(carried.summary),
111
- instruction: unsent === undefined || unsent.turn !== null ? null : {
112
- text: unsent.text,
113
- files: [
114
- ...unsent.files
115
- ]
116
- }
193
+ messages: shown,
194
+ turn: structuredClone(facts.turn),
195
+ questions: structuredClone(facts.questions),
196
+ process: facts.process === null ? null : { ...facts.process },
197
+ commands: structuredClone(facts.commands),
198
+ options: structuredClone(facts.options),
199
+ chatDraft: draft === undefined || draft.turn !== null || sending.get(conversation)?.draft === draft
200
+ ? null
201
+ : { text: draft.text, files: [...draft.files] },
117
202
  };
118
203
  }
119
- function emit(conversation) {
120
- if (deps.closed()) return;
121
- for (const viewer of viewers){
122
- if (viewer.conversation === conversation && viewer.ready) viewer.notify(viewOf(conversation));
204
+ function emit(conversation, messagesChanged = true) {
205
+ if (deps.closed())
206
+ return;
207
+ for (const view of views.values()) {
208
+ if (view.conversation !== conversation)
209
+ continue;
210
+ if (messagesChanged)
211
+ view.messages = null;
212
+ const ready = [...view.watchers].filter((watcher) => watcher.ready);
213
+ if (ready.length === 0)
214
+ continue;
215
+ view.messages ??= shownMessages(conversation, view.limit);
216
+ const value = viewOf(conversation, view.limit, view.messages);
217
+ for (const watcher of ready)
218
+ watcher.notify(value);
123
219
  }
124
220
  }
125
- function viewed() {
126
- return [
127
- ...new Set([
128
- ...viewers
129
- ].map((viewer)=>viewer.conversation))
130
- ].sort();
221
+ function stateOf(conversation) {
222
+ const facts = liveOf(conversation);
223
+ if (facts.questions.length > 0)
224
+ return 'asking';
225
+ return facts.turn === null ? 'idle' : 'working';
131
226
  }
132
- function hand() {
133
- const path = deps.path();
134
- if (path === undefined) return;
135
- const open = viewed();
136
- const named = JSON.stringify(open);
137
- if (named === handed) return;
138
- handed = named;
139
- path.view(open);
227
+ function entries() {
228
+ return [...listing.values()].map((entry) => ({
229
+ conversation: entry.conversation,
230
+ agent: { identity: entry.agent, name: agentNames.get(entry.agent) ?? null },
231
+ name: entry.name,
232
+ created: entry.created,
233
+ finished: entry.finished,
234
+ outcome: entry.outcome,
235
+ state: stateOf(entry.conversation),
236
+ started: lives.get(entry.conversation)?.turn?.started ?? null,
237
+ }));
140
238
  }
141
- function keptRecord(conversation) {
142
- const carried = heldOf(conversation);
143
- return {
144
- conversation,
145
- messages: carried.messages,
146
- cursor: carried.cursor,
147
- summary: carried.summary
148
- };
239
+ function emitList() {
240
+ if (deps.closed() || listWatchers.size === 0)
241
+ return;
242
+ const value = entries();
243
+ for (const watcher of listWatchers)
244
+ watcher(value);
149
245
  }
150
- async function settle(local, conversation) {
151
- const sent = instructionOf(conversation).turn;
152
- if (sent === null) return;
153
- if (!heldOf(conversation).messages.some((entry)=>entry.turn === sent)) return;
154
- composed.delete(conversation);
155
- await local.dropInstruction(conversation);
246
+ function setLive(conversation, next) {
247
+ const before = stateOf(conversation);
248
+ const began = lives.get(conversation)?.turn?.started;
249
+ if (next === null)
250
+ lives.delete(conversation);
251
+ else
252
+ lives.set(conversation, next);
253
+ if (!listing.has(conversation))
254
+ return;
255
+ if (stateOf(conversation) !== before || next?.turn?.started !== began)
256
+ emitList();
257
+ }
258
+ async function readAgents() {
259
+ const declaration = deps.agents();
260
+ if (declaration === undefined)
261
+ return;
262
+ const { documents } = await deps.documents(declaration.locator);
263
+ const names = new Map();
264
+ for (const document of documents) {
265
+ if (document.type !== declaration.type)
266
+ continue;
267
+ const identity = document.fields?.[declaration.identity];
268
+ const name = document.fields?.[declaration.name];
269
+ if (typeof identity === 'string' && typeof name === 'string')
270
+ names.set(identity, name);
271
+ }
272
+ if (JSON.stringify([...names]) === JSON.stringify([...agentNames]))
273
+ return;
274
+ agentNames = names;
275
+ emitList();
156
276
  }
157
- async function keep(conversation, next) {
158
- const kept = await deps.durably(async (local)=>{
159
- const following = next(heldOf(conversation));
160
- if (following === null) return false;
161
- held.set(conversation, following);
162
- await local.keepConversation(keptRecord(conversation));
163
- await settle(local, conversation);
164
- return true;
277
+ function agentsReady() {
278
+ agentsRead ??= readAgents().catch(() => {
279
+ agentsRead = null;
165
280
  });
166
- if (kept) emit(conversation);
167
- return kept;
281
+ return agentsRead;
168
282
  }
169
- async function keepInstruction(conversation, next) {
170
- await deps.durably(async (local)=>{
171
- const stored = next(instructionOf(conversation));
172
- composed.set(conversation, stored);
173
- await local.keepInstruction({
283
+ function live(conversation, change) {
284
+ setLive(conversation, { ...liveOf(conversation), ...change });
285
+ emit(conversation, false);
286
+ }
287
+ function relayed(conversation, turn, operation) {
288
+ const held = liveOf(conversation).turn;
289
+ const next = held?.turn === turn ? appliedTurn(held, operation) : startedTurn(turn, operation);
290
+ if (next !== null)
291
+ live(conversation, { turn: next });
292
+ }
293
+ function sealTurns(conversation, turns) {
294
+ const held = lives.get(conversation);
295
+ if (held?.turn == null || !turns.has(held.turn.turn))
296
+ return false;
297
+ setLive(conversation, { ...held, turn: null });
298
+ return true;
299
+ }
300
+ function handReplicated() {
301
+ const stream = deps.stream();
302
+ if (stream === undefined)
303
+ return;
304
+ const held = [...replicated].sort();
305
+ const named = JSON.stringify(held);
306
+ if (named === handedReplicated)
307
+ return;
308
+ handedReplicated = named;
309
+ stream.replicate(held);
310
+ }
311
+ async function remove(leaving) {
312
+ await deps.durably(async (local) => {
313
+ await local.dropConversations(leaving);
314
+ const removed = [];
315
+ const changed = [];
316
+ for (const conversation of leaving) {
317
+ for (const message of messages.get(conversation) ?? []) {
318
+ removed.push({ conversation, identity: message.identity });
319
+ }
320
+ if (messages.has(conversation) || drafts.has(conversation) || lives.has(conversation)) {
321
+ changed.push(conversation);
322
+ }
323
+ messages.delete(conversation);
324
+ reads.delete(conversation);
325
+ drafts.delete(conversation);
326
+ behind.delete(conversation);
327
+ setLive(conversation, null);
328
+ }
329
+ if (changed.length > 0)
330
+ deps.publish(conversationChangeSet(removed, changed));
331
+ });
332
+ }
333
+ function gap(conversation) {
334
+ gaps.set(conversation, (gaps.get(conversation) ?? 0) + 1);
335
+ behind.add(conversation);
336
+ }
337
+ function keep(conversation, message) {
338
+ return deps.durably(async (local) => {
339
+ if (!replicated.has(conversation))
340
+ return;
341
+ const read = reads.get(conversation);
342
+ const advances = read !== undefined &&
343
+ !behind.has(conversation) &&
344
+ (read === null || message.position > read);
345
+ const sealed = drafts.get(conversation)?.turn === message.turn;
346
+ await local.keepMessages({
174
347
  conversation,
175
- ...stored
348
+ messages: [message],
349
+ ...(advances ? { read: message.position } : {}),
350
+ sealed,
176
351
  });
177
- await settle(local, conversation);
352
+ messages.set(conversation, merged(messages.get(conversation) ?? [], [message]));
353
+ if (advances)
354
+ reads.set(conversation, message.position);
355
+ if (sealed)
356
+ drafts.delete(conversation);
357
+ sealTurns(conversation, new Set([message.turn]));
358
+ deps.publish(conversationChangeSet([{ conversation, identity: message.identity }], [conversation]));
178
359
  });
179
- emit(conversation);
180
360
  }
181
- async function dropInstruction(conversation) {
182
- await deps.durably(async (local)=>{
183
- composed.delete(conversation);
184
- await local.dropInstruction(conversation);
361
+ async function readHistories(conversations) {
362
+ 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
+ let asking = conversations.map((conversation) => ({
367
+ conversation,
368
+ after: starts.get(conversation) ?? undefined,
369
+ }));
370
+ while (asking.length > 0) {
371
+ const result = await deps.call('conversation.history', {
372
+ conversations: asking.map(({ conversation, after }) => ({
373
+ conversation,
374
+ ...(after === undefined ? {} : { after }),
375
+ })),
376
+ });
377
+ const pages = historyPages(result);
378
+ if (pages === null)
379
+ return result;
380
+ const asked = new Set(asking.map(({ conversation }) => conversation));
381
+ const following = [];
382
+ for (const page of pages) {
383
+ if (!asked.has(page.conversation))
384
+ 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
392
+ following.push({ conversation: page.conversation, after: page.next });
393
+ }
394
+ asking = following;
395
+ }
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
+ return null;
402
+ }
403
+ function keepHistory(conversation, arriving, from, generation) {
404
+ return deps.durably(async (local) => {
405
+ if (!replicated.has(conversation))
406
+ 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);
415
+ sealTurns(conversation, new Set(arriving.map((message) => message.turn)));
416
+ if ((gaps.get(conversation) ?? 0) === generation)
417
+ behind.delete(conversation);
418
+ if (arriving.length > 0 || sealed) {
419
+ deps.publish(conversationChangeSet(arriving.map((message) => ({ conversation, identity: message.identity })), [conversation]));
420
+ }
185
421
  });
186
- emit(conversation);
187
422
  }
188
- function supersede(conversation) {
189
- issued.set(conversation, (issued.get(conversation) ?? 0) + 1);
423
+ function heard(event) {
424
+ const { conversation } = event;
425
+ if (event.kind === 'gap') {
426
+ if (replicated.has(conversation))
427
+ gap(conversation);
428
+ const held = lives.get(conversation);
429
+ if (held === undefined)
430
+ return;
431
+ setLive(conversation, { ...QUIET, turn: held.turn });
432
+ emit(conversation, false);
433
+ return;
434
+ }
435
+ if (event.kind === 'turn') {
436
+ relayed(conversation, event.turn, event.operation);
437
+ return;
438
+ }
439
+ if (event.kind === 'questions') {
440
+ live(conversation, { questions: structuredClone(event.questions) });
441
+ return;
442
+ }
443
+ if (event.kind === 'process') {
444
+ live(conversation, { process: { running: event.running } });
445
+ return;
446
+ }
447
+ if (event.kind === 'commands') {
448
+ live(conversation, { commands: structuredClone(event.commands) });
449
+ return;
450
+ }
451
+ if (event.kind === 'options') {
452
+ live(conversation, { options: structuredClone(event.options) });
453
+ return;
454
+ }
455
+ const arriving = acceptedMessage(event.message);
456
+ if (arriving === null)
457
+ return;
458
+ if (replicated.has(conversation)) {
459
+ void keep(conversation, arriving).catch(() => { });
460
+ return;
461
+ }
462
+ if (sealTurns(conversation, new Set([arriving.turn])))
463
+ emit(conversation);
190
464
  }
191
- async function fill(conversation, cursor) {
192
- const generation = issued.get(conversation);
193
- const result = await deps.call('conversation.history', {
194
- conversation,
195
- ...cursor === null ? {} : {
196
- cursor
465
+ function withDraft(conversation, next) {
466
+ return deps.durably(async (local) => {
467
+ const following = next(drafts.get(conversation));
468
+ if (following === undefined)
469
+ return;
470
+ if (following === null) {
471
+ await local.dropChatDraft(conversation);
472
+ drafts.delete(conversation);
197
473
  }
474
+ else {
475
+ await local.keepChatDraft(following);
476
+ drafts.set(conversation, following);
477
+ }
478
+ deps.publish(conversationChangeSet([], [conversation]));
198
479
  });
199
- const page = historyPage(result);
200
- if (page === null) return result;
201
- const landed = await keep(conversation, (carried)=>{
202
- if (issued.get(conversation) !== generation) return null;
203
- if (cursor !== null) return carried.cursor === cursor ? older(carried, page) : null;
204
- return newest(carried, page, live.get(conversation) ?? NO_TURNS);
205
- });
206
- if (landed && cursor === null) live.delete(conversation);
207
- return result;
208
480
  }
209
481
  function invoke(name, conversation, extra = {}) {
210
- return deps.call(name, {
211
- conversation,
212
- ...extra
213
- });
482
+ return deps.call(name, { conversation, ...extra });
214
483
  }
215
484
  const surface = {
216
- list () {
217
- const declaration = deps.cards();
218
- if (declaration === undefined) return Promise.resolve([]);
219
- const documents = deps.documents().documents.filter((document)=>document.room === declaration.locator);
220
- const entries = [];
221
- for (const card of documents){
222
- if (card.type !== declaration.type) continue;
223
- const identity = card.fields[declaration.identity];
224
- if (typeof identity !== 'string') continue;
225
- const agent = documents.find((document)=>document.backlinks.some((backlink)=>backlink.path === card.path && backlink.field === declaration.agent));
226
- entries.push({
227
- conversation: identity,
228
- card,
229
- agent: agent ?? null
230
- });
231
- }
232
- entries.sort((left, right)=>left.conversation < right.conversation ? -1 : 1);
233
- return Promise.resolve(entries);
234
- },
235
- read (conversation) {
236
- return deps.durably(()=>Promise.resolve(viewOf(conversation)));
237
- },
238
- older (conversation) {
239
- const cursor = heldOf(conversation).cursor;
240
- if (cursor === null) return Promise.resolve(null);
241
- return fill(conversation, cursor);
485
+ async list() {
486
+ await agentsReady();
487
+ return entries();
242
488
  },
243
- compose (conversation, supplied) {
244
- return keepInstruction(conversation, ()=>({
245
- text: supplied.text,
246
- files: [
247
- ...supplied.files
248
- ],
249
- turn: null
250
- }));
489
+ async read(conversation, options = {}) {
490
+ const limit = limitOf(options);
491
+ return deps.durably(() => Promise.resolve(viewOf(conversation, limit)));
251
492
  },
252
- discard (conversation) {
253
- return dropInstruction(conversation);
254
- },
255
- create (input) {
493
+ create(input) {
256
494
  return deps.call('conversation.create', input);
257
495
  },
258
- async send (conversation) {
259
- const unsent = instructionOf(conversation);
260
- const files = await Promise.all(unsent.files.map(declaredFile));
261
- const result = await invoke('conversation.send', conversation, {
262
- text: unsent.text,
263
- files
264
- });
265
- const turn = turnOf(result);
266
- if (turn === null) return result;
267
- await keepInstruction(conversation, (stored)=>({
268
- ...stored,
269
- turn
270
- }));
271
- return result;
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
+ });
521
+ return result;
522
+ }
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
+ }
530
+ }
272
531
  },
273
- answer (conversation, answer) {
274
- return invoke('conversation.answer', conversation, {
275
- answer
276
- });
532
+ answer(conversation, answer) {
533
+ return invoke('conversation.answer', conversation, { answer });
277
534
  },
278
- interrupt (conversation) {
535
+ interrupt(conversation) {
279
536
  return invoke('conversation.interrupt', conversation);
280
537
  },
281
- kill (conversation) {
538
+ kill(conversation) {
282
539
  return invoke('conversation.kill', conversation);
283
540
  },
284
- async cancel (conversation, turn) {
285
- const result = await invoke('conversation.cancel', conversation, {
286
- turn
287
- });
288
- if (result.kind !== 'action') return result;
289
- if (instructionOf(conversation).turn === turn) {
290
- await keepInstruction(conversation, (stored)=>({
291
- ...stored,
292
- turn: null
293
- }));
294
- }
541
+ async cancel(conversation, turn) {
542
+ const result = await invoke('conversation.cancel', conversation, { turn });
543
+ if (result.kind !== 'action')
544
+ return result;
545
+ await withDraft(conversation, (current) => current === undefined || current.turn !== turn ? undefined : { ...current, turn: null });
295
546
  return result;
296
547
  },
297
- async delete (conversation) {
298
- const result = await invoke('conversation.delete', conversation);
299
- if (result.kind !== 'action') return result;
300
- growing.delete(conversation);
301
- live.delete(conversation);
302
- supersede(conversation);
303
- await deps.durably(async (local)=>{
304
- held.delete(conversation);
305
- composed.delete(conversation);
306
- await local.dropConversation(conversation);
307
- await local.dropInstruction(conversation);
308
- });
309
- emit(conversation);
310
- return result;
311
- }
548
+ delete(conversation) {
549
+ return invoke('conversation.delete', conversation);
550
+ },
551
+ };
552
+ const chatDraft = {
553
+ write(conversation, draft) {
554
+ return withDraft(conversation, () => ({
555
+ conversation,
556
+ text: draft.text,
557
+ files: [...draft.files],
558
+ turn: null,
559
+ }));
560
+ },
561
+ discard(conversation) {
562
+ return withDraft(conversation, (current) => (current === undefined ? undefined : null));
563
+ },
312
564
  };
313
565
  return {
314
566
  surface,
315
- hydrate (conversations, instructions) {
316
- held.clear();
317
- for (const stored of conversations){
318
- held.set(stored.conversation, {
319
- messages: stored.messages,
320
- cursor: stored.cursor,
321
- summary: stored.summary
322
- });
567
+ chatDraft,
568
+ hydrate(held, stored) {
569
+ messages.clear();
570
+ reads.clear();
571
+ for (const view of views.values())
572
+ view.messages = null;
573
+ for (const entry of held) {
574
+ messages.set(entry.conversation, [...entry.messages].sort(ordered));
575
+ if (entry.read !== undefined)
576
+ reads.set(entry.conversation, entry.read);
323
577
  }
324
- composed.clear();
325
- for (const stored of instructions){
326
- composed.set(stored.conversation, {
327
- text: stored.text,
328
- files: stored.files,
329
- turn: stored.turn
330
- });
578
+ drafts.clear();
579
+ for (const record of stored)
580
+ drafts.set(record.conversation, record);
581
+ },
582
+ async reconcile() {
583
+ if (!listKnown)
584
+ return;
585
+ const named = new Set(listing.keys());
586
+ const leaving = [
587
+ ...new Set([...replicated, ...messages.keys(), ...reads.keys(), ...drafts.keys()]),
588
+ ].filter((conversation) => !named.has(conversation));
589
+ for (const conversation of named) {
590
+ if (!replicated.has(conversation))
591
+ behind.add(conversation);
331
592
  }
593
+ replicated = named;
594
+ handReplicated();
595
+ if (leaving.length > 0)
596
+ await remove(leaving);
597
+ },
598
+ 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);
602
+ if (failed?.kind === 'transport-uncertainty')
603
+ progress.reachable = false;
604
+ }
605
+ if (behind.size > 0)
606
+ progress.complete = false;
607
+ },
608
+ current() {
609
+ return behind.size === 0;
610
+ },
611
+ reconnected() {
612
+ for (const conversation of replicated)
613
+ gap(conversation);
332
614
  },
333
- observe (conversation, notify) {
334
- const first = !viewed().includes(conversation);
335
- const viewer = {
615
+ watch(conversation, options, deliver) {
616
+ const limit = limitOf(options);
617
+ const key = JSON.stringify([conversation, limit ?? null]);
618
+ const view = views.get(key) ?? {
336
619
  conversation,
337
- notify,
338
- ready: false
620
+ limit,
621
+ watchers: new Set(),
622
+ messages: null,
339
623
  };
340
- viewers.add(viewer);
341
- if (first) {
342
- live.set(conversation, new Set());
343
- supersede(conversation);
344
- }
345
- hand();
346
- void deps.durably(()=>Promise.resolve()).then(async ()=>{
347
- if (deps.closed() || !viewers.has(viewer)) return;
348
- viewer.ready = true;
349
- viewer.notify(viewOf(conversation));
350
- if (first) await fill(conversation, null);
351
- }).catch(()=>{});
352
- return ()=>{
353
- if (!viewers.delete(viewer)) return;
354
- if (!viewed().includes(conversation)) {
355
- growing.delete(conversation);
356
- live.delete(conversation);
357
- }
358
- hand();
624
+ views.set(key, view);
625
+ const watcher = { notify: deliver, ready: false };
626
+ view.watchers.add(watcher);
627
+ void deps
628
+ .durably(() => Promise.resolve())
629
+ .then(() => {
630
+ if (deps.closed() || !view.watchers.has(watcher))
631
+ return;
632
+ watcher.ready = true;
633
+ watcher.notify(viewOf(conversation, limit));
634
+ })
635
+ .catch(() => { });
636
+ return () => {
637
+ if (!view.watchers.delete(watcher))
638
+ return;
639
+ if (view.watchers.size === 0)
640
+ views.delete(key);
641
+ };
642
+ },
643
+ watchList(deliver) {
644
+ listWatchers.add(deliver);
645
+ void agentsReady().then(() => {
646
+ if (!deps.closed() && listWatchers.has(deliver))
647
+ deliver(entries());
648
+ });
649
+ return () => {
650
+ listWatchers.delete(deliver);
359
651
  };
360
652
  },
361
- receive (event) {
362
- if (deps.closed() || !viewed().includes(event.conversation)) return;
363
- if (event.kind === 'growing-text') {
364
- growing.set(event.conversation, {
365
- turn: event.turn,
366
- text: event.text
653
+ changed(set) {
654
+ for (const conversation of set.conversations)
655
+ emit(conversation);
656
+ const declaration = deps.agents();
657
+ if (declaration !== undefined &&
658
+ set.documents.some((change) => change.locator === declaration.locator)) {
659
+ agentsRead = readAgents().catch(() => {
660
+ agentsRead = null;
367
661
  });
368
- emit(event.conversation);
369
- return;
370
- }
371
- if (event.kind === 'session-summary') {
372
- const summary = structuredClone(event.summary);
373
- void keep(event.conversation, (carried)=>({
374
- ...carried,
375
- summary
376
- })).catch(()=>{});
377
- return;
378
662
  }
379
- const arriving = carriedMessage(event.message);
380
- if (arriving === null) return;
381
- live.get(event.conversation)?.add(arriving.turn);
382
- if (growing.get(event.conversation)?.turn === arriving.turn) {
383
- growing.delete(event.conversation);
663
+ },
664
+ receive(event) {
665
+ if (deps.closed())
666
+ return false;
667
+ if (event.kind === 'list' || event.kind === 'listed' || event.kind === 'unlisted') {
668
+ if (event.kind === 'list') {
669
+ listing.clear();
670
+ for (const entry of event.entries)
671
+ listing.set(entry.conversation, { ...entry });
672
+ listKnown = true;
673
+ }
674
+ else if (event.kind === 'listed') {
675
+ listing.set(event.entry.conversation, { ...event.entry });
676
+ }
677
+ else {
678
+ listing.delete(event.conversation);
679
+ }
680
+ emitList();
681
+ return (event.kind !== 'list' &&
682
+ listKnown &&
683
+ (listing.size !== replicated.size ||
684
+ [...listing.keys()].some((conversation) => !replicated.has(conversation))));
384
685
  }
385
- void deps.durably(async (local)=>{
386
- held.set(event.conversation, sealed(heldOf(event.conversation), arriving));
387
- await local.keepConversation(keptRecord(event.conversation));
388
- await settle(local, event.conversation);
389
- }).then(()=>{
390
- emit(event.conversation);
391
- }).catch(()=>{});
686
+ heard(event);
687
+ return false;
688
+ },
689
+ messages() {
690
+ return [...messages].flatMap(([conversation, held]) => held.map(({ identity, text }) => ({ conversation, identity, text })));
691
+ },
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 };
695
+ },
696
+ close() {
697
+ views.clear();
698
+ lives.clear();
699
+ listWatchers.clear();
392
700
  },
393
- close () {
394
- viewers.clear();
395
- growing.clear();
396
- live.clear();
397
- }
398
701
  };
399
702
  }
400
-
401
- //# sourceMappingURL=conversation.js.map