@poodle64/librarian 2026.9.28 → 2026.10.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 (46) hide show
  1. package/README.md +66 -116
  2. package/dist/attachments.d.ts +2 -2
  3. package/dist/attachments.js +3 -3
  4. package/dist/chat.svelte.d.ts +22 -23
  5. package/dist/chat.svelte.js +41 -64
  6. package/dist/citations.d.ts +7 -7
  7. package/dist/citations.js +6 -6
  8. package/dist/client.d.ts +16 -45
  9. package/dist/client.js +15 -23
  10. package/dist/components/activity-group/activity-group.svelte +1 -1
  11. package/dist/components/agent-transcript/agent-transcript.svelte +9 -110
  12. package/dist/components/agent-transcript/agent-transcript.svelte.d.ts +4 -16
  13. package/dist/components/answer-mark/answer-mark.svelte +2 -2
  14. package/dist/components/answer-mark/answer-mark.svelte.d.ts +2 -2
  15. package/dist/components/answer-share/answer-share.svelte +2 -2
  16. package/dist/components/answer-share/answer-share.svelte.d.ts +2 -2
  17. package/dist/components/artefact-card/artefact-card.svelte +2 -2
  18. package/dist/components/artefact-card/artefact-card.svelte.d.ts +2 -2
  19. package/dist/components/artefact-pane/artefact-pane.svelte +2 -2
  20. package/dist/components/artefact-pane/artefact-pane.svelte.d.ts +2 -2
  21. package/dist/components/composer/composer.svelte +39 -112
  22. package/dist/components/composer/composer.svelte.d.ts +18 -18
  23. package/dist/components/composer/index.d.ts +1 -2
  24. package/dist/components/conversation/conversation.svelte +7 -18
  25. package/dist/components/conversation/conversation.svelte.d.ts +7 -12
  26. package/dist/components/conversation-list/conversation-list.svelte +2 -2
  27. package/dist/components/conversation-list/conversation-list.svelte.d.ts +2 -2
  28. package/dist/components/document-pane/document-pane.svelte +2 -2
  29. package/dist/components/document-pane/document-pane.svelte.d.ts +2 -2
  30. package/dist/components/fair-use-notice/fair-use-notice.svelte +2 -2
  31. package/dist/components/fair-use-notice/fair-use-notice.svelte.d.ts +2 -2
  32. package/dist/components/scope-statement/scope-statement.svelte +5 -5
  33. package/dist/components/scope-statement/scope-statement.svelte.d.ts +2 -2
  34. package/dist/components/source-list/source-list.svelte +2 -2
  35. package/dist/components/source-list/source-list.svelte.d.ts +2 -2
  36. package/dist/components/working/working.svelte +2 -2
  37. package/dist/components/working/working.svelte.d.ts +2 -2
  38. package/dist/copy.d.ts +16 -24
  39. package/dist/copy.js +10 -16
  40. package/dist/session.svelte.d.ts +1 -1
  41. package/dist/session.svelte.js +6 -7
  42. package/dist/transcript.svelte.d.ts +18 -32
  43. package/dist/transcript.svelte.js +19 -40
  44. package/package.json +1 -5
  45. package/dist/history.svelte.d.ts +0 -37
  46. package/dist/history.svelte.js +0 -59
@@ -12,17 +12,17 @@
12
12
  * Every Claude Code event carries a `uuid`, and one already folded is
13
13
  * skipped: a watch route replays from the first event, so opening it again
14
14
  * after the session moves on folds only what is new. A frame the server adds
15
- * itself (an error, the library's citations) carries none, and is known by
15
+ * itself (an error, the citations) carries none, and is known by
16
16
  * the event it follows.
17
17
  */
18
18
  import { contentOf, fold, foldState } from './transcript.svelte';
19
19
  /** The events a run is made of. One arriving after its turn settled is the
20
20
  * next run, whether or not its prompt was echoed. */
21
21
  const RUN = new Set(['stream_event', 'assistant', 'user', 'result']);
22
- /** Everything else that lands on a turn: the library's trailing frames, and
23
- * a stream that broke. Anything else (`system`, rate limits) is the CLI
22
+ /** Everything else that lands on a turn: the citations, and a stream that
23
+ * broke. Anything else (`system`, rate limits) is the CLI
24
24
  * talking about itself. */
25
- const FOLDED = new Set([...RUN, 'library_error', 'citations', 'suggestions']);
25
+ const FOLDED = new Set([...RUN, 'agent_error', 'citations']);
26
26
  export class Session {
27
27
  /** One per run, in order: plain objects, so a host may spread one to add
28
28
  * its own `artefact`. */
@@ -66,7 +66,7 @@ export class Session {
66
66
  const last = this.turns.at(-1);
67
67
  const settled = last?.outcome != null && !last.outcome.unreachable;
68
68
  // The watch broke while nothing was running, and no run failed.
69
- if (event.type === 'library_error' && settled)
69
+ if (event.type === 'agent_error' && settled)
70
70
  return;
71
71
  // A run whose prompt was never echoed — a session started without the
72
72
  // replay flag, or one that failed before it said anything — still has
@@ -88,8 +88,7 @@ export class Session {
88
88
  at,
89
89
  blocks: [],
90
90
  outcome: null,
91
- citations: [],
92
- suggestions: []
91
+ citations: []
93
92
  });
94
93
  this.#folds.push(foldState());
95
94
  }
@@ -6,10 +6,8 @@
6
6
  * deltas that belong to each. An event type this does not know about is kept
7
7
  * verbatim under `other`, so nothing is silently dropped.
8
8
  */
9
- import type { AgentEvent, Depth } from './client';
9
+ import type { AgentEvent } from './client';
10
10
  import type { Citation } from './citations';
11
- import type { LibrarianCopy } from './copy';
12
- export type { Depth };
13
11
  export interface TextBlock {
14
12
  kind: 'text';
15
13
  index: number;
@@ -50,9 +48,9 @@ export interface Outcome {
50
48
  /**
51
49
  * The question as the READER asked it.
52
50
  *
53
- * A host prepends a system preamble to every question before it goes to the
54
- * library — cadmus sends `{room.preamble}\n\n{question}` — and echoing that
55
- * back into the transcript shows a colleague the machine room. Leading
51
+ * A host may prepend a system preamble to every question before it goes to
52
+ * the persona, and echoing that back into the transcript shows a colleague
53
+ * the machinery. Leading
56
54
  * blank-line-separated paragraphs addressed to the model are dropped.
57
55
  *
58
56
  * The LAST paragraph is never dropped, whatever it starts with: someone whose
@@ -85,12 +83,12 @@ export interface ToolWords {
85
83
  /** A host's words for its persona's tools. `undefined` leaves the call to
86
84
  * `describe()`, the package's own. */
87
85
  export type DescribeTool = (block: ToolBlock) => ToolWords | undefined;
88
- /** What Milton DID, in a reader's own words — never the tool's name or the
86
+ /** What the persona DID, in a reader's own words — never the tool's name or the
89
87
  * raw command it ran.
90
88
  *
91
89
  * The collapsed row is read by someone asking a question about documents, not
92
90
  * by an engineer: `Bash ls -1 .` tells them nothing and looks like a leak from
93
- * the machine room. Milton never narrates how he searched, so a tool this
91
+ * the machinery. The persona never narrates how it searched, so a tool this
94
92
  * does not recognise falls back to something that names no mechanism at all.
95
93
  * A persona with tools of its own brings a `DescribeTool` in front of this.
96
94
  */
@@ -120,10 +118,8 @@ export declare class Transcript {
120
118
  sessionId: string | null;
121
119
  model: string | null;
122
120
  outcome: Outcome | null;
123
- /** Sources for the answer, from the library's own `citations` frame. */
121
+ /** Sources for the answer, from the app's `citations` frame. */
124
122
  citations: Citation[];
125
- /** Follow-ups the librarian named, from the `suggestions` frame after it. */
126
- suggestions: string[];
127
123
  other: AgentEvent[];
128
124
  reset(): void;
129
125
  apply(event: AgentEvent): void;
@@ -133,7 +129,6 @@ export interface FoldTarget {
133
129
  blocks: Block[];
134
130
  outcome: Outcome | null;
135
131
  citations?: Citation[];
136
- suggestions?: string[];
137
132
  }
138
133
  /** Where one turn's fold keeps its place between events. */
139
134
  export interface FoldState {
@@ -201,18 +196,15 @@ export interface Turn {
201
196
  blocks: Block[];
202
197
  outcome: Outcome | null;
203
198
  citations?: Citation[];
204
- /** Follow-ups offered after this answer. Absent on a turn read back from
205
- * history: they belonged to the moment it was asked. */
206
- suggestions?: string[];
207
199
  /** Something this turn produced for the reader to open, carded under the
208
200
  * answer once the turn settles. */
209
201
  artefact?: Artefact;
210
202
  /** What the host asked for when this was not a plain question
211
203
  * (`'briefing'`), in its own word, as `client.ask()` sent it. */
212
204
  kind?: string;
213
- /** How hard the persona worked on this one, as `client.ask()` sent it.
214
- * Absent, a host that never offers the choice. */
215
- depth?: Depth;
205
+ /** The collection this question was narrowed to, by name, as `client.ask()`
206
+ * sent it. Absent asked across every collection the person may read. */
207
+ collection?: string;
216
208
  /** Where this answer's read-only view lives while it is shared, as the
217
209
  * host's own address. Absent while it is not. */
218
210
  shared?: string;
@@ -239,7 +231,7 @@ export interface Artefact {
239
231
  * 2. "Reading pspf guidelines 2026" appeared five times in a row — five pages
240
232
  * of one document, which is one act of reading to a human. Consecutive
241
233
  * steps with the same label collapse to one row carrying a count.
242
- * 3. Milton's between-tool narration was rendering unfolded, as answer prose.
234
+ * 3. The persona's between-tool narration was rendering unfolded, as answer prose.
243
235
  * The caller stream carries no `thinking` blocks at all — measured on
244
236
  * production 11/09/2026 — so "Let me also check whether…" arrives as an
245
237
  * ordinary `text` block, indistinguishable from the answer except by
@@ -254,8 +246,8 @@ export interface Artefact {
254
246
  * and re-derived on every event, so re-homing is just the next call returning
255
247
  * a different shape.
256
248
  *
257
- * The cost of the rule is a real answer that Milton interrupts to go back to
258
- * the shelf: its first half folds away. That is the trade taken knowingly —
249
+ * The cost of the rule is a real answer that the persona interrupts to go back to
250
+ * its tools: its first half folds away. That is the trade taken knowingly —
259
251
  * the position of a block is the only signal the stream gives, and a rule read
260
252
  * off the prose itself would be unexplainable the first time it misfired.
261
253
  */
@@ -263,26 +255,20 @@ export declare function segment(blocks: Block[], describeTool?: DescribeTool): S
263
255
  /** One line describing a whole investigation, for the collapsed state.
264
256
  *
265
257
  * Counts of what the persona did are fine ("1 search · 2 documents read"); how
266
- * it organises what it knows is not — no collection count, no shelf, no corpus.
258
+ * it organises what it knows is not — no collection count, no corpus.
267
259
  */
268
260
  export declare function summariseActivity(group: ActivityGroup): string;
269
261
  /**
270
- * What a settled answer took, as one quiet line: `Quick · 56 s · about
271
- * A$0.34`. Depth first (absent on a turn the host never asked one for, or
272
- * one read back before the feature shipped), then the duration, then what it
273
- * cost — only when the host can price it (`formatCost`) and the run actually
262
+ * What a settled answer took, as one quiet line: `56.3s`, then what it cost
263
+ * — only when the host can price it (`formatCost`) and the run actually
274
264
  * spent something, since a zero-cost run (a local model) has nothing worth
275
- * saying about money.
265
+ * saying about money: `56.3s · about A$0.34`.
276
266
  *
277
267
  * The host prices `outcome.costUsd`, never this package: the currency and
278
268
  * the rate are the app's own setting, not something a shared component
279
269
  * should be converting.
280
- *
281
- * A turn asked with no depth at all renders exactly what this line always
282
- * has (`56.3s`, one decimal, no gap) — a host that never wires depth sees no
283
- * change here either.
284
270
  */
285
- export declare function outcomeLine(outcome: Outcome, depth: Depth | undefined, words: Pick<LibrarianCopy, 'depthQuick' | 'depthThorough'>, formatCost?: (usd: number) => string): string;
271
+ export declare function outcomeLine(outcome: Outcome, formatCost?: (usd: number) => string): string;
286
272
  /**
287
273
  * What a turn's calls read that a reader can open, numbered in the order it
288
274
  * was first read: the host's `ToolWords.source`, as the sources the answer
@@ -9,9 +9,9 @@
9
9
  /**
10
10
  * The question as the READER asked it.
11
11
  *
12
- * A host prepends a system preamble to every question before it goes to the
13
- * library — cadmus sends `{room.preamble}\n\n{question}` — and echoing that
14
- * back into the transcript shows a colleague the machine room. Leading
12
+ * A host may prepend a system preamble to every question before it goes to
13
+ * the persona, and echoing that back into the transcript shows a colleague
14
+ * the machinery. Leading
15
15
  * blank-line-separated paragraphs addressed to the model are dropped.
16
16
  *
17
17
  * The LAST paragraph is never dropped, whatever it starts with: someone whose
@@ -25,12 +25,12 @@ export function readerQuestion(question) {
25
25
  start += 1;
26
26
  return parts.slice(start).join('\n\n').trim();
27
27
  }
28
- /** What Milton DID, in a reader's own words — never the tool's name or the
28
+ /** What the persona DID, in a reader's own words — never the tool's name or the
29
29
  * raw command it ran.
30
30
  *
31
31
  * The collapsed row is read by someone asking a question about documents, not
32
32
  * by an engineer: `Bash ls -1 .` tells them nothing and looks like a leak from
33
- * the machine room. Milton never narrates how he searched, so a tool this
33
+ * the machinery. The persona never narrates how it searched, so a tool this
34
34
  * does not recognise falls back to something that names no mechanism at all.
35
35
  * A persona with tools of its own brings a `DescribeTool` in front of this.
36
36
  */
@@ -152,17 +152,14 @@ export class Transcript {
152
152
  sessionId = $state(null);
153
153
  model = $state(null);
154
154
  outcome = $state(null);
155
- /** Sources for the answer, from the library's own `citations` frame. */
155
+ /** Sources for the answer, from the app's `citations` frame. */
156
156
  citations = $state([]);
157
- /** Follow-ups the librarian named, from the `suggestions` frame after it. */
158
- suggestions = $state([]);
159
157
  other = $state([]);
160
158
  #fold = foldState();
161
159
  reset() {
162
160
  this.blocks = [];
163
161
  this.outcome = null;
164
162
  this.citations = [];
165
- this.suggestions = [];
166
163
  this.other = [];
167
164
  this.#fold = foldState();
168
165
  }
@@ -201,25 +198,16 @@ export function fold(into, state, event) {
201
198
  };
202
199
  return true;
203
200
  }
204
- // Emitted after the final assistant text, so it lands on a turn that is
205
- // otherwise complete. Both frames put their payload on `items`, so each
206
- // keeps only what its own shape admits: a `citations` frame carrying
207
- // strings, or a `suggestions` frame carrying objects, is the library
208
- // having changed under us, and rendering it would be worse than
209
- // rendering nothing.
201
+ // The app emits this before the persona's answer, from the passages it
202
+ // retrieved for the question.
210
203
  if (event.type === 'citations') {
211
204
  into.citations = (event.items ?? []).filter((item) => typeof item === 'object' && item !== null);
212
205
  return true;
213
206
  }
214
- // Last of the two, and only when the librarian named any.
215
- if (event.type === 'suggestions') {
216
- into.suggestions = (event.items ?? []).filter((item) => typeof item === 'string' && item.trim().length > 0);
217
- return true;
218
- }
219
207
  // No persona and no sentence at this layer: `client.ts` raises this for a
220
208
  // stream that would not open or died half-way, and the turn renders the
221
209
  // unreachable line in whichever persona's voice the host named.
222
- if (event.type === 'library_error') {
210
+ if (event.type === 'agent_error') {
223
211
  into.outcome = { isError: true, unreachable: true, error: event.error };
224
212
  return true;
225
213
  }
@@ -341,7 +329,7 @@ function renderResult(content) {
341
329
  * 2. "Reading pspf guidelines 2026" appeared five times in a row — five pages
342
330
  * of one document, which is one act of reading to a human. Consecutive
343
331
  * steps with the same label collapse to one row carrying a count.
344
- * 3. Milton's between-tool narration was rendering unfolded, as answer prose.
332
+ * 3. The persona's between-tool narration was rendering unfolded, as answer prose.
345
333
  * The caller stream carries no `thinking` blocks at all — measured on
346
334
  * production 11/09/2026 — so "Let me also check whether…" arrives as an
347
335
  * ordinary `text` block, indistinguishable from the answer except by
@@ -356,8 +344,8 @@ function renderResult(content) {
356
344
  * and re-derived on every event, so re-homing is just the next call returning
357
345
  * a different shape.
358
346
  *
359
- * The cost of the rule is a real answer that Milton interrupts to go back to
360
- * the shelf: its first half folds away. That is the trade taken knowingly —
347
+ * The cost of the rule is a real answer that the persona interrupts to go back to
348
+ * its tools: its first half folds away. That is the trade taken knowingly —
361
349
  * the position of a block is the only signal the stream gives, and a rule read
362
350
  * off the prose itself would be unexplainable the first time it misfired.
363
351
  */
@@ -416,7 +404,7 @@ function sameStep(last, block, words) {
416
404
  return false;
417
405
  if (block.kind === 'thinking')
418
406
  return true;
419
- // Two narration sentences are two things Milton said; collapsing them to
407
+ // Two narration sentences are two things the persona said; collapsing them to
420
408
  // one row with a count would lose the second one entirely.
421
409
  if (block.kind === 'text')
422
410
  return false;
@@ -432,33 +420,24 @@ function count(group, [one, many]) {
432
420
  /** One line describing a whole investigation, for the collapsed state.
433
421
  *
434
422
  * Counts of what the persona did are fine ("1 search · 2 documents read"); how
435
- * it organises what it knows is not — no collection count, no shelf, no corpus.
423
+ * it organises what it knows is not — no collection count, no corpus.
436
424
  */
437
425
  export function summariseActivity(group) {
438
426
  const parts = group.tallies.map((t) => `${t.count} ${t.count === 1 ? t.one : t.many}`);
439
427
  return parts.length ? parts.join(' · ') : 'Looked into it';
440
428
  }
441
429
  /**
442
- * What a settled answer took, as one quiet line: `Quick · 56 s · about
443
- * A$0.34`. Depth first (absent on a turn the host never asked one for, or
444
- * one read back before the feature shipped), then the duration, then what it
445
- * cost — only when the host can price it (`formatCost`) and the run actually
430
+ * What a settled answer took, as one quiet line: `56.3s`, then what it cost
431
+ * — only when the host can price it (`formatCost`) and the run actually
446
432
  * spent something, since a zero-cost run (a local model) has nothing worth
447
- * saying about money.
433
+ * saying about money: `56.3s · about A$0.34`.
448
434
  *
449
435
  * The host prices `outcome.costUsd`, never this package: the currency and
450
436
  * the rate are the app's own setting, not something a shared component
451
437
  * should be converting.
452
- *
453
- * A turn asked with no depth at all renders exactly what this line always
454
- * has (`56.3s`, one decimal, no gap) — a host that never wires depth sees no
455
- * change here either.
456
438
  */
457
- export function outcomeLine(outcome, depth, words, formatCost) {
458
- const seconds = (outcome.durationMs ?? 0) / 1000;
459
- if (!depth)
460
- return `${seconds.toFixed(1)}s`;
461
- const parts = [depth === 'quick' ? words.depthQuick : words.depthThorough, `${Math.round(seconds)} s`];
439
+ export function outcomeLine(outcome, formatCost) {
440
+ const parts = [`${((outcome.durationMs ?? 0) / 1000).toFixed(1)}s`];
462
441
  if (formatCost && outcome.costUsd)
463
442
  parts.push(formatCost(outcome.costUsd));
464
443
  return parts.join(' · ');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poodle64/librarian",
3
- "version": "2026.9.28",
3
+ "version": "2026.10.0",
4
4
  "description": "An agent's conversation surface as a consumable Svelte 5 package: the stream client, transcript state and self-styling chat components (transcript, composer, markdown) every household app renders instead of rebuilding, speaking as whichever persona the app names.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -35,10 +35,6 @@
35
35
  "types": "./dist/chat.svelte.d.ts",
36
36
  "svelte": "./dist/chat.svelte.js"
37
37
  },
38
- "./history": {
39
- "types": "./dist/history.svelte.d.ts",
40
- "svelte": "./dist/history.svelte.js"
41
- },
42
38
  "./citations": {
43
39
  "types": "./dist/citations.d.ts",
44
40
  "svelte": "./dist/citations.js"
@@ -1,37 +0,0 @@
1
- /**
2
- * Past conversations.
3
- *
4
- * Held in the browser, deliberately and provisionally. The agent's own session
5
- * survives server-side — Claude Code keeps it and `--resume` reaches it — so
6
- * what is missing is only the RENDERED transcript, which is a display concern.
7
- * Persisting that here needs no schema, no migration and no decision about
8
- * whose conversation it is.
9
- *
10
- * Namespaced by the caller: two apps, or two rooms in one app, must not share
11
- * a `localStorage` key, so the namespace is a constructor argument rather than
12
- * a module-level constant.
13
- */
14
- export interface StoredTurn {
15
- question: string;
16
- answer: string;
17
- }
18
- export interface Conversation {
19
- id: string;
20
- /** The agent's session id, which is what `--resume` needs. */
21
- sessionId: string | null;
22
- title: string;
23
- updated: number;
24
- turns: StoredTurn[];
25
- }
26
- export declare class History {
27
- #private;
28
- items: Conversation[];
29
- constructor(namespace: string);
30
- load(): void;
31
- save(conversation: Conversation): void;
32
- remove(id: string): void;
33
- }
34
- /** One store per namespace — a distinct `localStorage` key per app or room. */
35
- export declare function createHistory(namespace: string): History;
36
- /** A conversation is named by its first question, trimmed to something legible. */
37
- export declare function titleFrom(question: string): string;
@@ -1,59 +0,0 @@
1
- /**
2
- * Past conversations.
3
- *
4
- * Held in the browser, deliberately and provisionally. The agent's own session
5
- * survives server-side — Claude Code keeps it and `--resume` reaches it — so
6
- * what is missing is only the RENDERED transcript, which is a display concern.
7
- * Persisting that here needs no schema, no migration and no decision about
8
- * whose conversation it is.
9
- *
10
- * Namespaced by the caller: two apps, or two rooms in one app, must not share
11
- * a `localStorage` key, so the namespace is a constructor argument rather than
12
- * a module-level constant.
13
- */
14
- const LIMIT = 40;
15
- export class History {
16
- items = $state([]);
17
- #key;
18
- constructor(namespace) {
19
- this.#key = namespace;
20
- }
21
- load() {
22
- if (typeof localStorage === 'undefined')
23
- return;
24
- try {
25
- this.items = JSON.parse(localStorage.getItem(this.#key) ?? '[]');
26
- }
27
- catch {
28
- this.items = [];
29
- }
30
- }
31
- save(conversation) {
32
- const rest = this.items.filter((c) => c.id !== conversation.id);
33
- this.items = [conversation, ...rest].slice(0, LIMIT);
34
- this.#flush();
35
- }
36
- remove(id) {
37
- this.items = this.items.filter((c) => c.id !== id);
38
- this.#flush();
39
- }
40
- #flush() {
41
- if (typeof localStorage === 'undefined')
42
- return;
43
- try {
44
- localStorage.setItem(this.#key, JSON.stringify(this.items));
45
- }
46
- catch {
47
- // A full quota costs history, never the conversation in progress.
48
- }
49
- }
50
- }
51
- /** One store per namespace — a distinct `localStorage` key per app or room. */
52
- export function createHistory(namespace) {
53
- return new History(namespace);
54
- }
55
- /** A conversation is named by its first question, trimmed to something legible. */
56
- export function titleFrom(question) {
57
- const flat = question.replace(/\s+/g, ' ').trim();
58
- return flat.length > 60 ? `${flat.slice(0, 57)}…` : flat;
59
- }