logisheets-logician 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +89 -0
  2. package/dist/agent/loop.d.ts +101 -0
  3. package/dist/agent/loop.js +260 -0
  4. package/dist/conversation.d.ts +105 -0
  5. package/dist/conversation.js +10 -0
  6. package/dist/craft-interactions-api.d.ts +81 -0
  7. package/dist/craft-interactions-api.js +25 -0
  8. package/dist/craft-interactions-core.d.ts +14 -0
  9. package/dist/craft-interactions-core.js +33 -0
  10. package/dist/crafts/manifest.d.ts +39 -0
  11. package/dist/crafts/manifest.js +10 -0
  12. package/dist/crafts/skill-tools.d.ts +33 -0
  13. package/dist/crafts/skill-tools.js +146 -0
  14. package/dist/crafts/store.d.ts +47 -0
  15. package/dist/crafts/store.js +69 -0
  16. package/dist/index.d.ts +22 -0
  17. package/dist/index.js +22 -0
  18. package/dist/index.node.js +5065 -0
  19. package/dist/projection.d.ts +129 -0
  20. package/dist/projection.js +249 -0
  21. package/dist/storage.d.ts +99 -0
  22. package/dist/storage.js +223 -0
  23. package/dist/tool.d.ts +137 -0
  24. package/dist/tool.js +57 -0
  25. package/dist/tools/block-ops.d.ts +30 -0
  26. package/dist/tools/block-ops.js +97 -0
  27. package/dist/tools/builder.d.ts +206 -0
  28. package/dist/tools/builder.js +1502 -0
  29. package/dist/tools/cells.d.ts +58 -0
  30. package/dist/tools/cells.js +263 -0
  31. package/dist/tools/comments.d.ts +54 -0
  32. package/dist/tools/comments.js +234 -0
  33. package/dist/tools/craft-interactions.d.ts +22 -0
  34. package/dist/tools/craft-interactions.js +454 -0
  35. package/dist/tools/edit.d.ts +55 -0
  36. package/dist/tools/edit.js +356 -0
  37. package/dist/tools/format.d.ts +58 -0
  38. package/dist/tools/format.js +208 -0
  39. package/dist/tools/history.d.ts +13 -0
  40. package/dist/tools/history.js +39 -0
  41. package/dist/tools/inspect.d.ts +93 -0
  42. package/dist/tools/inspect.js +488 -0
  43. package/dist/tools/links.d.ts +30 -0
  44. package/dist/tools/links.js +122 -0
  45. package/dist/tools/structure.d.ts +62 -0
  46. package/dist/tools/structure.js +174 -0
  47. package/dist/tools/taxonomy.d.ts +32 -0
  48. package/dist/tools/taxonomy.js +93 -0
  49. package/package.json +53 -0
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Pure projections from the event stream to:
3
+ * - Agent IR messages (Anthropic-compatible shape, used as canonical IR
4
+ * by all LlmClient implementations — Anthropic adapters consume it
5
+ * directly, other providers translate into their wire format)
6
+ * → toLlmMessages
7
+ * - UI-friendly chat bubbles → toUiBubbles
8
+ *
9
+ * Both are referentially transparent: same events → same output, no I/O.
10
+ * The agent loop calls toLlmMessages before each request; the UI calls
11
+ * toUiBubbles whenever events change. Cache-control breakpoints for
12
+ * prompt caching are NOT placed here — they're a property of the
13
+ * request, not the message content.
14
+ */
15
+ import type { ConversationEvent, SystemNoteEvent, ToolCallEvent } from './conversation.js';
16
+ export type AgentRole = 'user' | 'assistant';
17
+ export type AgentContentBlock = {
18
+ type: 'text';
19
+ text: string;
20
+ } | {
21
+ type: 'tool_use';
22
+ id: string;
23
+ name: string;
24
+ input: unknown;
25
+ } | {
26
+ type: 'tool_result';
27
+ tool_use_id: string;
28
+ content: string | AgentContentBlock[];
29
+ is_error?: boolean;
30
+ };
31
+ export interface AgentMessage {
32
+ role: AgentRole;
33
+ content: string | AgentContentBlock[];
34
+ }
35
+ /**
36
+ * Hook for materializing blob refs into real payloads. The store-aware
37
+ * caller (agent loop) passes a resolver; pure callers can omit it and
38
+ * blob refs render as a short placeholder.
39
+ */
40
+ export type BlobResolver = (ref: string) => string | null;
41
+ export interface ToLlmMessagesOpts {
42
+ /** Resolve `{kind:'blob_ref', ref}` payloads into strings. */
43
+ resolveBlob?: BlobResolver;
44
+ /**
45
+ * Cap on a single tool_result's serialized size, in chars. Larger
46
+ * payloads are truncated with a trailing "(... truncated, N more
47
+ * chars)" marker. Default 8000.
48
+ */
49
+ max_tool_result_chars?: number;
50
+ /**
51
+ * Drop tool_results older than this index (0-based, counting only
52
+ * tool_result events). Use to keep ancient results out of context
53
+ * once they're no longer load-bearing. -1 disables (default).
54
+ */
55
+ truncate_tool_results_before?: number;
56
+ }
57
+ /**
58
+ * Project events into the agent IR (Anthropic-compatible).
59
+ *
60
+ * Rules:
61
+ * - user_message → {role: 'user', content: text}
62
+ * - assistant_text +
63
+ * tool_call → merged into one {role: 'assistant', content: [...]}
64
+ * per turn_id
65
+ * - tool_result → {role: 'user', content: [{type:'tool_result', ...}]}
66
+ * - user_confirm → dropped (internal signal)
67
+ * - system_note:debug → dropped
68
+ * - system_note:ui_only→ dropped
69
+ * - system_note:summary→ {role: 'user', content: text} with a
70
+ * "[summary]" prefix so the model recognizes it
71
+ */
72
+ export declare function toLlmMessages(events: readonly ConversationEvent[], opts?: ToLlmMessagesOpts): AgentMessage[];
73
+ /**
74
+ * Each bubble is one row in the chat UI. tool_call + tool_result are
75
+ * fused into a single ToolBubble so the UI renders one collapsible card
76
+ * per tool invocation rather than two stacked rows.
77
+ */
78
+ export type ChatBubble = UserBubble | AssistantTextBubble | ToolBubble | NoteBubble;
79
+ export interface UserBubble {
80
+ kind: 'user';
81
+ id: string;
82
+ ts: number;
83
+ text: string;
84
+ }
85
+ export interface AssistantTextBubble {
86
+ kind: 'assistant_text';
87
+ id: string;
88
+ ts: number;
89
+ text: string;
90
+ turn_id: string;
91
+ /** Set on the last assistant_text of a turn for tail spacing. */
92
+ last_in_turn: boolean;
93
+ }
94
+ export interface ToolBubble {
95
+ kind: 'tool';
96
+ id: string;
97
+ ts: number;
98
+ turn_id: string;
99
+ tool_use_id: string;
100
+ name: string;
101
+ input: unknown;
102
+ /** Populated once the matching tool_result arrives. */
103
+ output?: unknown;
104
+ error?: string;
105
+ duration_ms?: number;
106
+ /** True from request → response window. */
107
+ pending: boolean;
108
+ /** Captures the user's decision on a confirmation prompt. */
109
+ user_confirm?: {
110
+ approved: boolean;
111
+ reason?: string;
112
+ };
113
+ }
114
+ export interface NoteBubble {
115
+ kind: 'note';
116
+ id: string;
117
+ ts: number;
118
+ text: string;
119
+ /** 'summary' notes are also worth showing; 'debug' is filtered out. */
120
+ severity: 'info' | 'summary';
121
+ }
122
+ export interface ToUiBubblesOpts {
123
+ /** Include debug-level system_notes (for a "show internals" toggle). */
124
+ include_debug?: boolean;
125
+ }
126
+ export declare function toUiBubbles(events: readonly ConversationEvent[], opts?: ToUiBubblesOpts): ChatBubble[];
127
+ /** Type guard, exposed for downstream tooling. */
128
+ export declare function isToolCall(e: ConversationEvent): e is ToolCallEvent;
129
+ export declare function isSystemNote(e: ConversationEvent): e is SystemNoteEvent;
@@ -0,0 +1,249 @@
1
+ /**
2
+ * Pure projections from the event stream to:
3
+ * - Agent IR messages (Anthropic-compatible shape, used as canonical IR
4
+ * by all LlmClient implementations — Anthropic adapters consume it
5
+ * directly, other providers translate into their wire format)
6
+ * → toLlmMessages
7
+ * - UI-friendly chat bubbles → toUiBubbles
8
+ *
9
+ * Both are referentially transparent: same events → same output, no I/O.
10
+ * The agent loop calls toLlmMessages before each request; the UI calls
11
+ * toUiBubbles whenever events change. Cache-control breakpoints for
12
+ * prompt caching are NOT placed here — they're a property of the
13
+ * request, not the message content.
14
+ */
15
+ /**
16
+ * Project events into the agent IR (Anthropic-compatible).
17
+ *
18
+ * Rules:
19
+ * - user_message → {role: 'user', content: text}
20
+ * - assistant_text +
21
+ * tool_call → merged into one {role: 'assistant', content: [...]}
22
+ * per turn_id
23
+ * - tool_result → {role: 'user', content: [{type:'tool_result', ...}]}
24
+ * - user_confirm → dropped (internal signal)
25
+ * - system_note:debug → dropped
26
+ * - system_note:ui_only→ dropped
27
+ * - system_note:summary→ {role: 'user', content: text} with a
28
+ * "[summary]" prefix so the model recognizes it
29
+ */
30
+ export function toLlmMessages(events, opts = {}) {
31
+ var _a, _b, _c;
32
+ const max = (_a = opts.max_tool_result_chars) !== null && _a !== void 0 ? _a : 8000;
33
+ const dropBefore = (_b = opts.truncate_tool_results_before) !== null && _b !== void 0 ? _b : -1;
34
+ const messages = [];
35
+ let toolResultIdx = -1;
36
+ // Buffer for in-progress assistant turn. Same turn_id collapses
37
+ // multiple assistant_text + tool_call events into one message.
38
+ let pendingTurnId = null;
39
+ let pendingBlocks = [];
40
+ const flushAssistant = () => {
41
+ if (pendingTurnId === null)
42
+ return;
43
+ if (pendingBlocks.length > 0) {
44
+ messages.push({ role: 'assistant', content: pendingBlocks });
45
+ }
46
+ pendingTurnId = null;
47
+ pendingBlocks = [];
48
+ };
49
+ for (const e of events) {
50
+ switch (e.kind) {
51
+ case 'user_confirm':
52
+ // internal — never crosses to LLM
53
+ continue;
54
+ case 'system_note':
55
+ if (e.visibility !== 'summary')
56
+ continue;
57
+ flushAssistant();
58
+ messages.push({
59
+ role: 'user',
60
+ content: `[summary] ${e.text}`,
61
+ });
62
+ continue;
63
+ case 'user_message':
64
+ flushAssistant();
65
+ messages.push({ role: 'user', content: e.text });
66
+ continue;
67
+ case 'assistant_text':
68
+ if (pendingTurnId !== e.turn_id) {
69
+ flushAssistant();
70
+ pendingTurnId = e.turn_id;
71
+ }
72
+ pendingBlocks.push({ type: 'text', text: e.text });
73
+ continue;
74
+ case 'tool_call':
75
+ if (pendingTurnId !== e.turn_id) {
76
+ flushAssistant();
77
+ pendingTurnId = e.turn_id;
78
+ }
79
+ pendingBlocks.push({
80
+ type: 'tool_use',
81
+ id: e.tool_use_id,
82
+ name: e.name,
83
+ input: (_c = e.input) !== null && _c !== void 0 ? _c : {},
84
+ });
85
+ continue;
86
+ case 'tool_result': {
87
+ flushAssistant();
88
+ toolResultIdx++;
89
+ if (toolResultIdx < dropBefore)
90
+ continue;
91
+ const serialized = serializeToolOutput(e, opts.resolveBlob, max);
92
+ messages.push({
93
+ role: 'user',
94
+ content: [
95
+ {
96
+ type: 'tool_result',
97
+ tool_use_id: e.tool_use_id,
98
+ content: serialized.content,
99
+ is_error: serialized.is_error,
100
+ },
101
+ ],
102
+ });
103
+ continue;
104
+ }
105
+ }
106
+ }
107
+ flushAssistant();
108
+ return messages;
109
+ }
110
+ function serializeToolOutput(e, resolveBlob, max) {
111
+ if (e.error) {
112
+ return { content: e.error.slice(0, max), is_error: true };
113
+ }
114
+ let raw = stringifyOutput(e.output, resolveBlob);
115
+ if (raw.length > max) {
116
+ const dropped = raw.length - max;
117
+ raw = raw.slice(0, max) + `\n(… truncated, ${dropped} more chars)`;
118
+ }
119
+ return { content: raw };
120
+ }
121
+ function stringifyOutput(output, resolveBlob) {
122
+ if (typeof output === 'string')
123
+ return output;
124
+ if (output && typeof output === 'object') {
125
+ const ref = output.kind === 'blob_ref' &&
126
+ typeof output.ref === 'string'
127
+ ? output.ref
128
+ : null;
129
+ if (ref) {
130
+ const resolved = resolveBlob ? resolveBlob(ref) : null;
131
+ return resolved !== null && resolved !== void 0 ? resolved : `(blob ${ref} — not resolved)`;
132
+ }
133
+ }
134
+ try {
135
+ return JSON.stringify(output);
136
+ }
137
+ catch (_a) {
138
+ return String(output);
139
+ }
140
+ }
141
+ export function toUiBubbles(events, opts = {}) {
142
+ const bubbles = [];
143
+ /** tool_use_id → index in bubbles array, for back-patching results. */
144
+ const toolBubbleByUseId = new Map();
145
+ for (const e of events) {
146
+ switch (e.kind) {
147
+ case 'user_message':
148
+ bubbles.push({
149
+ kind: 'user',
150
+ id: e.id,
151
+ ts: e.ts,
152
+ text: e.text,
153
+ });
154
+ continue;
155
+ case 'assistant_text':
156
+ bubbles.push({
157
+ kind: 'assistant_text',
158
+ id: e.id,
159
+ ts: e.ts,
160
+ text: e.text,
161
+ turn_id: e.turn_id,
162
+ last_in_turn: false,
163
+ });
164
+ continue;
165
+ case 'tool_call': {
166
+ const idx = bubbles.push({
167
+ kind: 'tool',
168
+ id: e.id,
169
+ ts: e.ts,
170
+ turn_id: e.turn_id,
171
+ tool_use_id: e.tool_use_id,
172
+ name: e.name,
173
+ input: e.input,
174
+ pending: true,
175
+ }) - 1;
176
+ toolBubbleByUseId.set(e.tool_use_id, idx);
177
+ continue;
178
+ }
179
+ case 'tool_result': {
180
+ const idx = toolBubbleByUseId.get(e.tool_use_id);
181
+ if (idx === undefined) {
182
+ // Orphan tool_result — surface as a note for debugging.
183
+ bubbles.push({
184
+ kind: 'note',
185
+ id: e.id,
186
+ ts: e.ts,
187
+ text: `orphan tool_result for ${e.tool_use_id}`,
188
+ severity: 'info',
189
+ });
190
+ continue;
191
+ }
192
+ const tb = bubbles[idx];
193
+ tb.output = e.output;
194
+ tb.error = e.error;
195
+ tb.duration_ms = e.duration_ms;
196
+ tb.pending = false;
197
+ continue;
198
+ }
199
+ case 'user_confirm': {
200
+ const idx = toolBubbleByUseId.get(e.tool_use_id);
201
+ if (idx !== undefined) {
202
+ const tb = bubbles[idx];
203
+ tb.user_confirm = {
204
+ approved: e.approved,
205
+ reason: e.reason,
206
+ };
207
+ }
208
+ continue;
209
+ }
210
+ case 'system_note': {
211
+ if (e.visibility === 'debug' && !opts.include_debug)
212
+ continue;
213
+ if (e.visibility === 'ui_only' || e.visibility === 'summary') {
214
+ bubbles.push({
215
+ kind: 'note',
216
+ id: e.id,
217
+ ts: e.ts,
218
+ text: e.text,
219
+ severity: e.visibility === 'summary' ? 'summary' : 'info',
220
+ });
221
+ }
222
+ continue;
223
+ }
224
+ }
225
+ }
226
+ // Tag the last assistant_text of each turn so the UI can add trailing
227
+ // spacing / show a "done" marker without inspecting siblings at render.
228
+ const lastAssistantIdxByTurn = new Map();
229
+ for (let i = 0; i < bubbles.length; i++) {
230
+ const b = bubbles[i];
231
+ if (b.kind === 'assistant_text')
232
+ lastAssistantIdxByTurn.set(b.turn_id, i);
233
+ }
234
+ for (const i of lastAssistantIdxByTurn.values()) {
235
+ ;
236
+ bubbles[i].last_in_turn = true;
237
+ }
238
+ return bubbles;
239
+ }
240
+ // ---------------------------------------------------------------------------
241
+ // Helpers re-exported for tests / external composition
242
+ // ---------------------------------------------------------------------------
243
+ /** Type guard, exposed for downstream tooling. */
244
+ export function isToolCall(e) {
245
+ return e.kind === 'tool_call';
246
+ }
247
+ export function isSystemNote(e) {
248
+ return e.kind === 'system_note';
249
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Storage abstraction for conversations + events + blobs.
3
+ *
4
+ * The logician package never imports a concrete storage backend.
5
+ * Hosts plug one in:
6
+ * - Watson (browser craft) → IdbConversationStore via Dexie
7
+ * - Node CLI / MCP server → FileConversationStore (json-on-disk)
8
+ * - Tests / smoke runs / REPLs → MemoryConversationStore (this file)
9
+ *
10
+ * Same pattern as `WorkbookClient` in tool.ts — declare the surface
11
+ * here, let the platform provide the implementation.
12
+ */
13
+ import type { Conversation, ConversationEvent, ConversationExport, ConversationSummary } from './conversation.js';
14
+ export interface ConversationStoreOpts {
15
+ /** Cap on listEvents() output for safety; 0 means no cap. */
16
+ max_events_per_query?: number;
17
+ }
18
+ /**
19
+ * Persistence surface used by the agent loop. All methods are async so
20
+ * IDB / fs / network backends fit without changing call sites.
21
+ */
22
+ export interface ConversationStore {
23
+ createConversation(meta: Omit<Conversation, 'id' | 'created_at' | 'updated_at'> & {
24
+ id?: string;
25
+ }): Promise<Conversation>;
26
+ getConversation(id: string): Promise<Conversation | null>;
27
+ listConversations(filter?: {
28
+ workbook_id?: string;
29
+ limit?: number;
30
+ }): Promise<ConversationSummary[]>;
31
+ updateConversation(id: string, patch: Partial<Pick<Conversation, 'title' | 'extra'>>): Promise<void>;
32
+ deleteConversation(id: string): Promise<void>;
33
+ /**
34
+ * Append a single event. Implementations should `updated_at` the
35
+ * parent conversation atomically with the insert.
36
+ */
37
+ appendEvent(event: ConversationEvent): Promise<void>;
38
+ /**
39
+ * Batched append. Useful when replaying a remote sync or restoring
40
+ * a workbook-archived conversation. Atomic on backends that support
41
+ * transactions; otherwise best-effort sequential.
42
+ */
43
+ appendEvents(events: ConversationEvent[]): Promise<void>;
44
+ /** Read events in ts-ascending order. */
45
+ listEvents(conversation_id: string, opts?: {
46
+ since_ts?: number;
47
+ limit?: number;
48
+ }): Promise<ConversationEvent[]>;
49
+ /** Returns a stable ref string the caller stores in the event. */
50
+ putBlob(value: string | Uint8Array): Promise<string>;
51
+ getBlob(ref: string): Promise<string | Uint8Array | null>;
52
+ deleteBlob(ref: string): Promise<void>;
53
+ /**
54
+ * Reactive subscription. Browser stores (IDB/Dexie) implement this
55
+ * to drive UI updates; headless backends may omit it.
56
+ * Returns an unsubscribe function.
57
+ */
58
+ subscribeEvents?(conversation_id: string, cb: (events: ConversationEvent[]) => void): () => void;
59
+ exportConversation?(id: string): Promise<ConversationExport>;
60
+ importConversation?(bundle: ConversationExport): Promise<string>;
61
+ }
62
+ /**
63
+ * Volatile store. Survives the lifetime of the process, nothing more.
64
+ * Useful for:
65
+ * - tests
66
+ * - node CLI runs that don't care about persistence across invocations
67
+ * - Watson MVP before IdbConversationStore lands
68
+ */
69
+ export declare class MemoryConversationStore implements ConversationStore {
70
+ private conversations;
71
+ /** Events grouped by conversation_id, kept sorted by ts. */
72
+ private events;
73
+ private blobs;
74
+ private subscribers;
75
+ private blobCounter;
76
+ createConversation(meta: Omit<Conversation, 'id' | 'created_at' | 'updated_at'> & {
77
+ id?: string;
78
+ }): Promise<Conversation>;
79
+ getConversation(id: string): Promise<Conversation | null>;
80
+ listConversations(filter?: {
81
+ workbook_id?: string;
82
+ limit?: number;
83
+ }): Promise<ConversationSummary[]>;
84
+ updateConversation(id: string, patch: Partial<Pick<Conversation, 'title' | 'extra'>>): Promise<void>;
85
+ deleteConversation(id: string): Promise<void>;
86
+ appendEvent(event: ConversationEvent): Promise<void>;
87
+ appendEvents(events: ConversationEvent[]): Promise<void>;
88
+ listEvents(conversation_id: string, opts?: {
89
+ since_ts?: number;
90
+ limit?: number;
91
+ }): Promise<ConversationEvent[]>;
92
+ putBlob(value: string | Uint8Array): Promise<string>;
93
+ getBlob(ref: string): Promise<string | Uint8Array | null>;
94
+ deleteBlob(ref: string): Promise<void>;
95
+ subscribeEvents(conversation_id: string, cb: (events: ConversationEvent[]) => void): () => void;
96
+ exportConversation(id: string): Promise<ConversationExport>;
97
+ importConversation(bundle: ConversationExport): Promise<string>;
98
+ private notify;
99
+ }
@@ -0,0 +1,223 @@
1
+ /**
2
+ * Storage abstraction for conversations + events + blobs.
3
+ *
4
+ * The logician package never imports a concrete storage backend.
5
+ * Hosts plug one in:
6
+ * - Watson (browser craft) → IdbConversationStore via Dexie
7
+ * - Node CLI / MCP server → FileConversationStore (json-on-disk)
8
+ * - Tests / smoke runs / REPLs → MemoryConversationStore (this file)
9
+ *
10
+ * Same pattern as `WorkbookClient` in tool.ts — declare the surface
11
+ * here, let the platform provide the implementation.
12
+ */
13
+ // ---------------------------------------------------------------------------
14
+ // In-memory implementation
15
+ // ---------------------------------------------------------------------------
16
+ /**
17
+ * Volatile store. Survives the lifetime of the process, nothing more.
18
+ * Useful for:
19
+ * - tests
20
+ * - node CLI runs that don't care about persistence across invocations
21
+ * - Watson MVP before IdbConversationStore lands
22
+ */
23
+ export class MemoryConversationStore {
24
+ constructor() {
25
+ this.conversations = new Map();
26
+ /** Events grouped by conversation_id, kept sorted by ts. */
27
+ this.events = new Map();
28
+ this.blobs = new Map();
29
+ this.subscribers = new Map();
30
+ this.blobCounter = 0;
31
+ }
32
+ async createConversation(meta) {
33
+ var _a;
34
+ const id = (_a = meta.id) !== null && _a !== void 0 ? _a : randomId('conv');
35
+ const now = nowMs();
36
+ const conv = {
37
+ ...meta,
38
+ id,
39
+ created_at: now,
40
+ updated_at: now,
41
+ };
42
+ this.conversations.set(id, conv);
43
+ this.events.set(id, []);
44
+ return conv;
45
+ }
46
+ async getConversation(id) {
47
+ var _a;
48
+ return (_a = this.conversations.get(id)) !== null && _a !== void 0 ? _a : null;
49
+ }
50
+ async listConversations(filter) {
51
+ let rows = [...this.conversations.values()];
52
+ if ((filter === null || filter === void 0 ? void 0 : filter.workbook_id) !== undefined) {
53
+ rows = rows.filter((c) => c.workbook_id === filter.workbook_id);
54
+ }
55
+ rows.sort((a, b) => b.updated_at - a.updated_at);
56
+ if (filter === null || filter === void 0 ? void 0 : filter.limit)
57
+ rows = rows.slice(0, filter.limit);
58
+ return rows.map((c) => {
59
+ var _a, _b;
60
+ return ({
61
+ id: c.id,
62
+ title: c.title,
63
+ workbook_id: c.workbook_id,
64
+ updated_at: c.updated_at,
65
+ event_count: (_b = (_a = this.events.get(c.id)) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0,
66
+ });
67
+ });
68
+ }
69
+ async updateConversation(id, patch) {
70
+ const c = this.conversations.get(id);
71
+ if (!c)
72
+ return;
73
+ if (patch.title !== undefined)
74
+ c.title = patch.title;
75
+ if (patch.extra !== undefined)
76
+ c.extra = patch.extra;
77
+ c.updated_at = nowMs();
78
+ }
79
+ async deleteConversation(id) {
80
+ this.conversations.delete(id);
81
+ this.events.delete(id);
82
+ this.subscribers.delete(id);
83
+ }
84
+ async appendEvent(event) {
85
+ const bucket = this.events.get(event.conversation_id);
86
+ if (!bucket) {
87
+ throw new Error(`appendEvent: conversation not found: ${event.conversation_id}`);
88
+ }
89
+ bucket.push(event);
90
+ const conv = this.conversations.get(event.conversation_id);
91
+ if (conv)
92
+ conv.updated_at = event.ts;
93
+ this.notify(event.conversation_id, bucket);
94
+ }
95
+ async appendEvents(events) {
96
+ if (events.length === 0)
97
+ return;
98
+ // Group by conversation for one notify per affected conversation.
99
+ const touched = new Set();
100
+ for (const e of events) {
101
+ const bucket = this.events.get(e.conversation_id);
102
+ if (!bucket) {
103
+ throw new Error(`appendEvents: conversation not found: ${e.conversation_id}`);
104
+ }
105
+ bucket.push(e);
106
+ const conv = this.conversations.get(e.conversation_id);
107
+ if (conv && e.ts > conv.updated_at)
108
+ conv.updated_at = e.ts;
109
+ touched.add(e.conversation_id);
110
+ }
111
+ for (const cid of touched) {
112
+ this.notify(cid, this.events.get(cid));
113
+ }
114
+ }
115
+ async listEvents(conversation_id, opts) {
116
+ var _a;
117
+ const bucket = (_a = this.events.get(conversation_id)) !== null && _a !== void 0 ? _a : [];
118
+ let rows = bucket;
119
+ if ((opts === null || opts === void 0 ? void 0 : opts.since_ts) !== undefined) {
120
+ const since = opts.since_ts;
121
+ rows = rows.filter((e) => e.ts >= since);
122
+ }
123
+ if (opts === null || opts === void 0 ? void 0 : opts.limit)
124
+ rows = rows.slice(0, opts.limit);
125
+ // bucket is already in insertion order, which matches ts-ascending
126
+ // because appendEvent never inserts out-of-order events.
127
+ return rows.slice();
128
+ }
129
+ async putBlob(value) {
130
+ const ref = `blob_${++this.blobCounter}_${randomShort()}`;
131
+ this.blobs.set(ref, value);
132
+ return ref;
133
+ }
134
+ async getBlob(ref) {
135
+ var _a;
136
+ return (_a = this.blobs.get(ref)) !== null && _a !== void 0 ? _a : null;
137
+ }
138
+ async deleteBlob(ref) {
139
+ this.blobs.delete(ref);
140
+ }
141
+ subscribeEvents(conversation_id, cb) {
142
+ var _a;
143
+ let set = this.subscribers.get(conversation_id);
144
+ if (!set) {
145
+ set = new Set();
146
+ this.subscribers.set(conversation_id, set);
147
+ }
148
+ set.add(cb);
149
+ // Fire once with current snapshot.
150
+ cb([...((_a = this.events.get(conversation_id)) !== null && _a !== void 0 ? _a : [])]);
151
+ return () => {
152
+ set.delete(cb);
153
+ };
154
+ }
155
+ async exportConversation(id) {
156
+ var _a;
157
+ const conversation = this.conversations.get(id);
158
+ if (!conversation)
159
+ throw new Error(`exportConversation: not found: ${id}`);
160
+ const events = ((_a = this.events.get(id)) !== null && _a !== void 0 ? _a : []).slice();
161
+ // Inline every blob referenced by these events.
162
+ const blobs = {};
163
+ for (const e of events) {
164
+ if (e.kind !== 'tool_result')
165
+ continue;
166
+ const ref = extractBlobRef(e.output);
167
+ if (ref) {
168
+ const v = this.blobs.get(ref);
169
+ if (v !== undefined)
170
+ blobs[ref] = v;
171
+ }
172
+ }
173
+ return { conversation, events, blobs };
174
+ }
175
+ async importConversation(bundle) {
176
+ const { conversation, events, blobs } = bundle;
177
+ this.conversations.set(conversation.id, { ...conversation });
178
+ this.events.set(conversation.id, [...events]);
179
+ for (const [ref, v] of Object.entries(blobs)) {
180
+ this.blobs.set(ref, v);
181
+ }
182
+ return conversation.id;
183
+ }
184
+ notify(conversation_id, snapshot) {
185
+ const set = this.subscribers.get(conversation_id);
186
+ if (!set || set.size === 0)
187
+ return;
188
+ const frozen = [...snapshot];
189
+ for (const cb of set) {
190
+ try {
191
+ cb(frozen);
192
+ }
193
+ catch (_a) {
194
+ // Subscribers shouldn't throw; swallow to protect the store.
195
+ }
196
+ }
197
+ }
198
+ }
199
+ // ---------------------------------------------------------------------------
200
+ // Helpers
201
+ // ---------------------------------------------------------------------------
202
+ /**
203
+ * Recognise `{kind: 'blob_ref', ref: '...'}` payloads in tool_result.output
204
+ * so exportConversation can inline the referenced blob.
205
+ */
206
+ function extractBlobRef(output) {
207
+ if (output &&
208
+ typeof output === 'object' &&
209
+ output.kind === 'blob_ref' &&
210
+ typeof output.ref === 'string') {
211
+ return output.ref;
212
+ }
213
+ return null;
214
+ }
215
+ function nowMs() {
216
+ return Date.now();
217
+ }
218
+ function randomId(prefix) {
219
+ return `${prefix}_${nowMs().toString(36)}_${randomShort()}`;
220
+ }
221
+ function randomShort() {
222
+ return Math.random().toString(36).slice(2, 10);
223
+ }