@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
package/README.md CHANGED
@@ -6,11 +6,11 @@ rebuilding: the transcript with its own follow-scroll, the composer with
6
6
  attachments, citation chips and the source pane they open.
7
7
 
8
8
  The persona is an argument. Name it once — `name="penny"` — and every word the
9
- package says is composed from it. Milton is the library's own persona and the
10
- default; no string in this package names him.
9
+ package says is composed from it. The default name is a neutral
10
+ "Assistant"; no string in this package names one app's persona.
11
11
 
12
- Owned by the library. Design-system is its press: change it here, consume it
13
- there.
12
+ The npm name `@poodle64/librarian` is the package's identity and stays; what a
13
+ person sees never says it.
14
14
 
15
15
  ## What is here
16
16
 
@@ -19,24 +19,23 @@ src/lib/
19
19
  client.ts ask() and watch(): Claude Code's OWN events, unaltered
20
20
  transcript.svelte.ts Transcript state, the fold/segment/describe helpers
21
21
  session.svelte.ts Session: a whole stream of runs, as turns
22
- chat.svelte.ts Chat: the page's controller, over a room's routes or a job's
22
+ chat.svelte.ts Chat: the page's controller, over a persona's chat routes or a job's
23
23
  citations.ts the Citation shape, `[n]` markers, the trust mark
24
24
  copy.ts every word this package says, and the host's overrides
25
25
  attachments.ts what a reader may attach, and the limits
26
26
  follow-scroll.svelte.ts follow the stream until the reader disagrees
27
- history.svelte.ts the browser-held conversation list, per caller
28
27
  components/
29
28
  conversation/ the whole reading surface: scroll, pill, pane, composer slot
30
29
  conversation-list/ past conversations, newest first: reopen, rename, download, delete
31
30
  answer-mark/ helpful or not, with a note, on the last answer
32
31
  answer-share/ share an answer read-only, and stop sharing it
33
32
  fair-use-notice/ today's allowance, beside the composer
34
- scope-statement/ what this room answers from, and what it does not hold
35
- agent-transcript/ one question and everything Milton did answering it
33
+ scope-statement/ what a persona answers from, and what it does not hold
34
+ agent-transcript/ one question and everything the persona did answering it
36
35
  document-pane/ the cited document, open at the cited passage
37
36
  artefact-card/ an artefact's card, in the host's words
38
37
  artefact-pane/ an artefact that is its answer's prose, in the column
39
- composer/ the input box: attachments, scope chips, a note, send/stop
38
+ composer/ the input box: attachments, collection picker, a note, send/stop
40
39
  markdown/ sanitised, streaming-safe markdown + highlighting
41
40
  activity-group/ a whole investigation, as one quiet line
42
41
  tool-row/ one tool call
@@ -113,7 +112,7 @@ gives it a height and a composer and nothing else:
113
112
  for await (const event of ask({
114
113
  question: text,
115
114
  files: attached,
116
- endpoint: '/api/caller/ask',
115
+ endpoint: '/api/agent/outrider/ask',
117
116
  signal: controller.signal
118
117
  })) {
119
118
  transcript.apply(event);
@@ -122,7 +121,6 @@ gives it a height and a composer and nothing else:
122
121
  live.blocks = transcript.blocks;
123
122
  live.outcome = transcript.outcome;
124
123
  live.citations = transcript.citations;
125
- live.suggestions = transcript.suggestions;
126
124
  }
127
125
  }
128
126
  } finally {
@@ -143,12 +141,12 @@ gives it a height and a composer and nothing else:
143
141
  {turns}
144
142
  {running}
145
143
  version={transcript.version}
146
- welcome="Ask Milton about pay, allowances, leave and conditions of service."
144
+ name="Outrider"
145
+ welcome="Ask Outrider about pay, allowances, leave and conditions of service."
147
146
  examples={['How much recreation leave do I get?']}
148
147
  onexample={(q) => (question = q)}
149
148
  onregenerate={() => run(asked, [])}
150
- onsuggest={(q) => run(q, [])}
151
- scope="Milton answers from the ADF Pay and Conditions Manual (PACMAN). He does not hold your own pay records or anything about your individual case."
149
+ scope="Outrider answers from the ADF Pay and Conditions Manual (PACMAN). It does not hold your own pay records or anything about your individual case."
152
150
  {loadDocument}
153
151
  >
154
152
  {#snippet composer()}
@@ -172,25 +170,25 @@ on it fires once and never again.
172
170
 
173
171
  | Concern | How |
174
172
  | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
175
- | The ask route | `ask({ endpoint })`: a room's `/api/rooms/{id}/ask`, a caller's `/api/caller/ask` |
176
- | Attachments | nothing: `ask()` posts multipart (`question`, `resume`, `collections[]`, `files[]`) whenever `files` is non-empty, and JSON when it is not. The route must accept both |
173
+ | The ask route | `ask({ endpoint, collection? })`: the persona's `/api/agent/{persona}/ask`, `endpoint` required. `collection` narrows the question to one collection by name; absent asks across every collection the person may read |
174
+ | Attachments | nothing: `ask()` posts multipart (`question`, `resume`, `collection`, `files[]`) whenever `files` is non-empty, and JSON when it is not. The route must accept both |
177
175
  | Reading a cited document | `loadDocument(document_id) => Promise<{title, sections: [{anchor, heading, text}]}>`, proxied through the app's own authenticated route (caspo: `GET /api/sources/documents/{id}/content`) |
178
176
  | Asking again | `onregenerate`: re-send the last question as a NEW turn; the package exposes the action and never re-asks by itself |
179
- | Asking a follow-up | `onsuggest(question)`: ask it as a NEW turn. Without the handler the chips do not render at all — a chip that does nothing is worse than no chip |
177
+ | Choosing a collection | `Composer`'s `collections` (`{ name, summary }[]`, the ones the person may read) and `collection` (the chosen name; undefined is all of them), with `oncollection(name \| undefined)`. It renders from two collections up, each by its summary, never its name |
180
178
  | The empty state | `welcome` and up to three `examples` |
181
- | What this surface covers | `scope`: one statement per room, in the host's own words. Rendered above the first turn and folded to a line once the conversation starts |
182
- | The words themselves | `copy`: a partial of `LibrarianCopy`. Every component resolves it itself, so overriding one line does not mean restating the rest |
179
+ | What this surface covers | `scope`: one statement per persona, in the host's own words. Rendered above the first turn and folded to a line once the conversation starts |
180
+ | The words themselves | `copy`: a partial of `ChatCopy`. Every component resolves it itself, so overriding one line does not mean restating the rest |
183
181
  | An answer its own way | `turn`: a snippet given each turn's `AgentTranscriptProps` in place of `AgentTranscript`. Render your own from the parts, or `AgentTranscript` inside your own to add to it |
184
182
  | What leads a conversation | `lead`: a snippet inside the scroll, under the opening and the scope, so a tall one scrolls with the conversation instead of taking its height |
185
183
 
186
- Nothing here fetches on its own behalf. The library's document read is
184
+ Nothing here fetches on its own behalf. The document read is
187
185
  authenticated, and a package that called it directly would be reaching past
188
186
  the app's proxy with a session it has no business holding.
189
187
 
190
- ### A room's page, whole: `Chat`
188
+ ### A persona's page, whole: `Chat`
191
189
 
192
- The hand-wired page above is the parts. A stamped room page is `Chat` over
193
- the room's routes, and the components bound to it:
190
+ The hand-wired page above is the parts. A stamped chat page is `Chat` over
191
+ the persona's routes, and the components bound to it:
194
192
 
195
193
  ```svelte
196
194
  <script lang="ts">
@@ -200,7 +198,7 @@ the room's routes, and the components bound to it:
200
198
  import Composer from '@poodle64/librarian/composer';
201
199
  import FairUseNotice from '@poodle64/librarian/fair-use-notice';
202
200
 
203
- const chat = new Chat(roomTransport(room)); // the app's own client, below
201
+ const chat = new Chat(chatTransport(persona, collection)); // the app's own client, below
204
202
  const wanted = $derived(page.url.searchParams.get('c'));
205
203
 
206
204
  // The address names the conversation; `open` and `new` are safe here.
@@ -224,10 +222,8 @@ the room's routes, and the components bound to it:
224
222
  turns={chat.turns}
225
223
  running={chat.busy}
226
224
  version={chat.version}
227
- waiting={chat.waiting}
228
225
  answering={chat.answering}
229
226
  onregenerate={() => chat.again()}
230
- onsuggest={(question) => chat.ask(question)}
231
227
  onmark={(turn, verdict) => chat.mark(turn, verdict)}
232
228
  >
233
229
  {#snippet composer()}
@@ -236,6 +232,9 @@ the room's routes, and the components bound to it:
236
232
  bind:value={chat.draft}
237
233
  bind:files={chat.files}
238
234
  running={chat.busy}
235
+ {collections}
236
+ {collection}
237
+ oncollection={(next) => (collection = next)}
239
238
  sendWhileRunning={chat.sendWhileRunning}
240
239
  onsubmit={() => chat.ask()}
241
240
  onstop={() => chat.stop()}
@@ -253,58 +252,39 @@ artefact })` says that kind opens, on a live turn and a reopened one alike:
253
252
  `{ title: 'Briefing', isAnswer: true }` cards it and opens its prose in the
254
253
  column. `again()` asks it as the same kind.
255
254
 
256
- ### Depth: Quick or Thorough, and what it cost
255
+ ### Asking one collection, and what it cost
257
256
 
258
- `Composer`'s `depth` is optional and bindable, and entirely opt-in: omit it
259
- and the composer renders exactly as it always has. Bind it, the host's own
260
- `$state<Depth>('quick')`, and the switch always renders, both sides always
261
- enabled. The operator's ruling (30/09/2026) is that once a room offers the
262
- choice, it is never disabled, hidden, greyed out, or explained by naming a
263
- model. `depth` is `'quick' | 'thorough'`, exported from
264
- `@poodle64/librarian/composer` and `@poodle64/librarian/client`.
257
+ `chat.ask(question?, kind?, collection?)` carries the collection to
258
+ `client.ask()`'s `collection`, `StoredTurn.collection` reads it back, and
259
+ `again()` asks the same one: the same shape as `kind`, alongside it. The host
260
+ holds the chosen name and the list the person may read (the app's
261
+ `listCollections`); `Composer` shows the choice and the host passes it on.
265
262
 
266
- ```svelte
267
- <Composer bind:value={chat.draft} bind:depth onsubmit={() => chat.ask(undefined, undefined, depth)} ... />
268
- ```
269
-
270
- `chat.ask(question?, kind?, depth?)` carries it to `client.ask()`'s `depth`,
271
- `StoredTurn.depth` reads it back, and `again()` keeps it: the same shape as
272
- `kind`, alongside it rather than replacing it. A host whose route does not
273
- read `depth` yet is unaffected, since the field rides along and is ignored.
274
-
275
- Under a settled answer, `AgentTranscript`'s footer folds the turn's `depth`
276
- and `outcome.costUsd` into the ONE line that already showed the duration:
277
- `Quick · 56 s · about A$0.34`. Both are opt-in the same way: pass `depth` to
278
- `Conversation`/`AgentTranscript` (it already has it, from `Turn.depth`) and a
279
- `formatCost(usd) => string` to price it in the host's own currency and words
280
- ("about A$0.34"; caspo converts USD to AUD with its own setting). Without
281
- `depth` the line renders exactly as before (`56.3s`). Without `formatCost`,
282
- or where `costUsd` is `0` (a local model), it shows depth and duration only,
283
- never a raw USD figure and never which model answered.
263
+ Under a settled answer, `AgentTranscript`'s footer shows the duration, and
264
+ `formatCost(usd) => string` folds what the host can price into the same line
265
+ in its own currency and words (`56.3s · about A$0.34`). Without `formatCost`,
266
+ or where `costUsd` is `0` (a local model), it is the duration alone, never a
267
+ raw USD figure and never which model answered.
284
268
 
285
269
  Past `turnLimit` turns (`Composer`'s prop, 8 by default), a quiet banner
286
270
  appears above the box, "This conversation is getting long. A new question
287
- keeps Milton quick." with a "New question" button, because a follow-up
271
+ keeps Outrider quick." with a "New question" button, because a follow-up
288
272
  re-reads the whole conversation, so each one costs more than the last.
289
273
  `turnCount` and `onnewquestion` are both required for it to render; either
290
274
  missing and the composer stays as it is today.
291
275
 
292
- None of this prices anything before asking, and none of it names a model.
293
- That is the operator's ruling on what a colleague sees, and the package
294
- never puts a control in front of it.
295
-
296
276
  The transport is the app's own client over its routes, and the shapes are
297
277
  the routes' own bodies (`ConversationSummary`, `ConversationRead`,
298
278
  `StoredTurn`, `Quota`), so the stamped slice returns them as they are and
299
279
  the transport passes them through:
300
280
 
301
- | `RoomTransport` | Route |
281
+ | `ChatTransport` | Route |
302
282
  | --------------- | ----------------------------------------------------- |
303
- | `ask` | `POST rooms/{room}/ask`, through `client.ask()` |
283
+ | `ask` | `POST {persona}/ask`, through `client.ask()` |
304
284
  | `read` | `GET conversations/{id}` |
305
285
  | `stop` | `POST conversations/{id}/stop` |
306
286
  | `quota` | `GET quota` |
307
- | `list` | this person's conversations in the room |
287
+ | `list` | this person's conversations with the persona |
308
288
  | `rename` | `PATCH conversations/{id}` |
309
289
  | `remove` | `DELETE conversations/{id}` |
310
290
  | `download` | `GET conversations/{id}/export`, as `{ name, body }` |
@@ -315,16 +295,11 @@ What the controller relies on the routes to say:
315
295
 
316
296
  | The route says | The page |
317
297
  | -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
318
- | a `queued` frame, repeated while the ask waits | `waiting`, by the frame's type alone, until the agent starts: "Milton is answering another question first", beside the clock |
319
298
  | `system/init` with `session_id` | `conversationId`, which the host puts in the address |
320
299
  | 429 on the ask | drops the turn, puts the words and files back in the box, reads the allowance |
321
300
  | 409 on the ask: the conversation is already being answered | drops the turn, keeps the words, shows it still answering |
322
301
  | `answering_since` on a read | `answering`: the question in flight with "still answering" and its clock, read again every 5 s till it clears |
323
- | a stream that dropped (`library_error` with `dropped`) on a named conversation | reads it rather than failing the turn: a locked phone loses the connection, not the answer |
324
-
325
- The library's `queued` frame is `{ type: 'queued', message }`, `message` its
326
- own sentence; `isQueued(event)` from `./client` narrows an event to it
327
- (`QueuedEvent`) for a host that reads it.
302
+ | a stream that dropped (`agent_error` with `dropped`) on a named conversation | reads it rather than failing the turn: a locked phone loses the connection, not the answer |
328
303
 
329
304
  A turn belongs to the server. Leaving a conversation, or the page, leaves
330
305
  its answer being written; `stop()` asks the server to end it, and before the
@@ -342,8 +317,8 @@ the job may go on by itself; `sendWhileRunning` is true.
342
317
  | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ |
343
318
  | The answer mark | `onmark(turn, verdict)` on `Conversation`: resolve true once recorded. Offered on the last settled answer only; without it no mark renders |
344
319
  | Sharing an answer | `onshare(turn, share)` on `Conversation`, over `chat.share`: every settled answer offers Share, which copies the link `StoredTurn.shared` names; a shared one offers Copy link and Stop sharing. Nothing is shared unless the reader asks. A host's own `turn` places `AnswerShare` (`./answer-share`) with the `onshare` and `shared` it is handed |
345
- | A shared answer, read | `Conversation` given `[storedTurn(id, 0, answer)]` from `./chat` and no handlers, no composer: the question, the answer and its sources, read-only, citations opening as in the room |
346
- | Waiting, still answering | `waiting` and `answering` on `Conversation`, from the controller |
320
+ | A shared answer, read | `Conversation` given `[storedTurn(id, 0, answer)]` from `./chat` and no handlers, no composer: the question, the answer and its sources, read-only, citations opening as in the chat |
321
+ | Still answering | `answering` on `Conversation`, from the controller |
347
322
  | The allowance | `FairUseNotice quota={…}` in the composer snippet: a count while there are questions left, a notice with `support_url` once there are none. Nothing for `exempt`, or a `limit` of 0 |
348
323
  | Past conversations | `ConversationList`: newest first, `href` makes each row a link, and each act renders only when its handler is given; a delete asks first |
349
324
 
@@ -421,12 +396,13 @@ end that way.
421
396
  | What an answer read | `ToolWords.source` lists under the answer, under `copy.sources`; tapping one calls `oncite`. Nothing checked it, so it carries no trust mark |
422
397
  | An artefact | `turn.artefact = { title, summary }` cards under the prose once the turn settles; `isAnswer: true` is an artefact that IS the prose, and replaces it |
423
398
  | Opening it | `onopenartefact(turn)` and `showing` (the turn id the host shows). Without a handler, an `isAnswer` artefact opens in `ArtefactPane` and any other offers no action |
424
- | "Answered without a source" | `copy.notHeld: ''` for a persona that answers from no shelf |
399
+ | "Answered without a source" | `copy.notHeld: ''` for a persona that answers from no sources |
425
400
  | Saying something mid-run | `Composer`'s `sendWhileRunning`: while the session works the box stays open and Send sits beside Stop. For a route that reads a message while the run works; a chat leaves it off and waits for its answer |
426
401
 
427
402
  ### Citations
428
403
 
429
- The library emits one SSE frame after the final assistant text:
404
+ The app emits one SSE frame before the persona's answer, from the passages
405
+ it retrieved for the question:
430
406
 
431
407
  ```json
432
408
  {
@@ -447,14 +423,14 @@ The library emits one SSE frame after the final assistant text:
447
423
 
448
424
  `Transcript.apply()` puts it on `transcript.citations`; inline `[n]` markers
449
425
  in the prose become chips, and the chip opens `DocumentPane` at the cited
450
- section. Until that frame ships everywhere, the same chips are DERIVED from
451
- a trailing "## Sources" block in the answer: those render and read, and are
452
- inert, because a title and a section are not an id.
426
+ section. Without that frame, the same chips are DERIVED from a trailing
427
+ "## Sources" block in the answer: those render and read, and are inert,
428
+ because a title and a section are not an id.
453
429
 
454
430
  ### The trust mark
455
431
 
456
432
  `verified_at` is the date the last recheck found the document unchanged at
457
- its publisher, `YYYY-MM-DD`, resolved by the library from its own catalogue —
433
+ its publisher, `YYYY-MM-DD`, resolved by the app from its own catalogue —
458
434
  never from what the model wrote, because a trust mark a model can author is
459
435
  not a trust mark. Null means nothing has ever confirmed it.
460
436
 
@@ -469,26 +445,12 @@ behind it, so "not verified" would be a claim about a record nothing here
469
445
  ever read.
470
446
 
471
447
  An answer that settles having cited nothing says so, once, under the prose:
472
- "Milton answered this one without a source. There may be no document here that
448
+ "Outrider answered this one without a source. There may be no document here that
473
449
  covers it." Only on a turn whose stream this surface actually watched finish
474
- — a turn read back out of `createHistory()` has no citations because history
475
- stores none, and labelling it as holding nothing would be a lie about an
450
+ — a turn read back out of a conversation whose record holds no citations shows
451
+ none, and labelling it as holding nothing would be a lie about an
476
452
  answer that may have cited three documents.
477
453
 
478
- ### Follow-ups
479
-
480
- One more frame follows the citations, and only when the librarian named any:
481
-
482
- ```json
483
- { "type": "suggestions", "items": ["Can I carry leave over when I post?"] }
484
- ```
485
-
486
- At most three, each short enough to fit a chip. `Transcript.suggestions`
487
- carries them; `Conversation` offers them under the LAST answer, and clicking
488
- one asks it through `onsuggest` and takes the whole row with it — the moment
489
- between the click and the new turn arriving is otherwise long enough to ask a
490
- second question by mistake.
491
-
492
454
  ### Where the answer starts
493
455
 
494
456
  The persona narrates between tool calls — "Let me also check whether…" — and the
@@ -502,7 +464,7 @@ is currently last renders as prose, and a tool call arriving after it
502
464
  re-homes it — which is why `segment()` is pure and re-derived per event
503
465
  rather than deciding once.
504
466
 
505
- The cost is an answer the persona interrupts to go back to the shelf: its first
467
+ The cost is an answer the persona interrupts to go back to its tools: its first
506
468
  half folds away. Position is the only signal the stream gives, and a rule
507
469
  read off the prose itself would be unexplainable the first time it misfired.
508
470
 
@@ -513,12 +475,12 @@ of them is composed from the persona's name:
513
475
 
514
476
  ```svelte
515
477
  <Conversation {turns} {running} name="penny" />
516
- <Composer bind:value {running} name="penny" {scope} {onscope} {onsubmit} {onstop} />
478
+ <Composer bind:value {running} name="penny" {collections} {collection} {oncollection} {onsubmit} {onstop} />
517
479
  ```
518
480
 
519
481
  That one prop carries the working line ("Penny is looking…"), both composer
520
482
  placeholders, the uncited-answer note, both failure sentences, the scope
521
- label, the opening line, the name signed on every answer card and the
483
+ label, the collection picker's label, the opening line, the name signed on every answer card and the
522
484
  accessible name of the scroll region. A slug is fine: `penny` renders as
523
485
  "Penny", `chief-engineer` as "Chief Engineer", and a name the host already
524
486
  capitalised is left exactly as written.
@@ -538,7 +500,7 @@ loaded — is ignored rather than rendering nothing where a word belongs.
538
500
  `ask()` never throws. A refused route, a `fetch` the browser blocks on CORS
539
501
  after an expired session redirects it, a 200 that turns out to be a login
540
502
  page, a connection that dies mid-answer and a stream that simply stops
541
- without a terminal frame all arrive as one `library_error` event and a
503
+ without a terminal frame all arrive as one `agent_error` event and a
542
504
  finished iteration. That matters because the host's `for await` loop is what
543
505
  re-enables its Send button: a thrown generator left Pebblestone's console
544
506
  disabled, silent and waiting indefinitely every time a session expired.
@@ -553,29 +515,17 @@ settles, offering "Ask again".
553
515
  card, which is a different fact from the duration badge beside the actions. A
554
516
  host that persists conversations sets it; a host that does not gets one
555
517
  stamped when the turn first appears, and a turn already on screen at mount —
556
- one read back out of history — deliberately shows none rather than claiming
518
+ one read back out of a stored conversation — deliberately shows none rather than claiming
557
519
  it was asked this afternoon.
558
520
 
559
521
  ### The system preamble
560
522
 
561
- A host prepends its own instruction to every question (caspo sends
562
- `{room.preamble}\n\n{question}`). `readerQuestion()` strips leading
523
+ A host may prepend its own instruction to every question
524
+ (`{preamble}\n\n{question}`). `readerQuestion()` strips leading
563
525
  paragraphs addressed to the model before the question renders, so a
564
526
  colleague never sees it. Pass the question as it went on the wire; the
565
527
  transcript shows what they asked.
566
528
 
567
- `createHistory(namespace)` from `@poodle64/librarian/history` keeps a
568
- browser-only list for a page with no server-side one (a page on `Chat` reads
569
- the server's), under its own `localStorage` key for each app, or each room
570
- inside an app:
571
-
572
- ```ts
573
- import { createHistory, titleFrom } from '@poodle64/librarian/history';
574
-
575
- const history = createHistory('caspo.rooms.defence-personnel');
576
- history.load();
577
- ```
578
-
579
529
  ## Verifying a change
580
530
 
581
531
  ```bash
@@ -586,19 +536,19 @@ pnpm run screenshots # the state grid, real engine (see below)
586
536
  ```
587
537
 
588
538
  `docs/screenshots/` is twenty-three states x three widths x both themes, taken by
589
- `scripts/screenshots.mjs` against the console's `/librarian` lab route
539
+ `scripts/screenshots.mjs` against the console's `/chat` lab route
590
540
  running from its own static build. The same script asserts what a screenshot
591
541
  cannot: that nothing scrolls sideways at any width, that the source pane
592
542
  opens and closes from the keyboard with focus returning to the chip and is
593
543
  really draggable, that the scope statement folds once there is a
594
- conversation over it and reopens from that line, that a follow-up chip asks
595
- its question and takes the rest of the row with it, that the persona's
544
+ conversation over it and reopens from that line, that the collection picker
545
+ starts on all of them and marks the one tapped by its summary, that the persona's
596
546
  between-tool narration is nowhere in the answer prose before the activity
597
547
  line is opened, that a job's artefact and the pages it read open in the
598
548
  host's own columns and never in a pane of the package's, and that the reading
599
- column holds its 46rem measure and stays centred at every width. Its `room`
600
- scene is `Chat` itself over a fake of the room's routes, driven: a question
601
- waits behind another, streams, counts against the allowance and takes a mark;
549
+ column holds its 46rem measure and stays centred at every width. Its `live`
550
+ scene is `Chat` itself over a fake of the chat routes, driven: a question
551
+ streams, counts against the allowance and takes a mark;
602
552
  one asked again is stopped; a conversation reopened mid-answer says so and
603
553
  settles; one is renamed and the open one deleted. It exits non-zero on any of
604
554
  them. The job states fold a stream captured from the real CLI
@@ -1,8 +1,8 @@
1
1
  /**
2
- * What a reader may hand Milton with a question.
2
+ * What a reader may hand the persona with a question.
3
3
  *
4
4
  * Session-scoped by design: these files are read for THIS conversation and
5
- * never filed into a shelf, so the limits here are about what a browser can
5
+ * never filed into a collection, so the limits here are about what a browser can
6
6
  * post and a model can read in one turn, not about a corpus.
7
7
  */
8
8
  export declare const MAX_FILES = 10;
@@ -1,8 +1,8 @@
1
1
  /**
2
- * What a reader may hand Milton with a question.
2
+ * What a reader may hand the persona with a question.
3
3
  *
4
4
  * Session-scoped by design: these files are read for THIS conversation and
5
- * never filed into a shelf, so the limits here are about what a browser can
5
+ * never filed into a collection, so the limits here are about what a browser can
6
6
  * post and a model can read in one turn, not about a corpus.
7
7
  */
8
8
  export const MAX_FILES = 10;
@@ -62,7 +62,7 @@ export function rejectionMessage(rejected) {
62
62
  if (rejected.length === 0)
63
63
  return '';
64
64
  const reasons = {
65
- type: "isn't a file type Milton can read",
65
+ type: "isn't a file type that can be read",
66
66
  size: 'is over 20 MB',
67
67
  count: `won't fit — ${MAX_FILES} files is the limit`
68
68
  };
@@ -1,8 +1,8 @@
1
1
  /**
2
- * The page's controller: one conversation with one agent, over a room's routes
3
- * or a job's watch, message and stop.
2
+ * The page's controller: one conversation with one agent, over a persona's
3
+ * chat routes or a job's watch, message and stop.
4
4
  *
5
- * A room is asked. Each question streams its own turn, the agent names the
5
+ * A persona is asked. Each question streams its own turn, the agent names the
6
6
  * conversation on its `init` event, and the conversation is read back whole
7
7
  * when it is reopened. The turn belongs to the server, not the page: a
8
8
  * refresh, a locked phone or a closed tab leaves it running, so a reopened
@@ -19,7 +19,7 @@
19
19
  * routes, and its shapes are the routes' own bodies, so a stamped app passes
20
20
  * what its client returns straight through.
21
21
  */
22
- import { type AgentEvent, type Depth } from './client';
22
+ import type { AgentEvent } from './client';
23
23
  import type { Citation } from './citations';
24
24
  import { type Artefact, type Turn } from './transcript.svelte';
25
25
  /** One conversation in the list. */
@@ -43,8 +43,8 @@ export interface StoredTurn {
43
43
  citations?: Citation[];
44
44
  /** What the host asked for, when it was not a plain question. */
45
45
  kind?: string | null;
46
- /** How hard the persona worked on this one. */
47
- depth?: Depth | null;
46
+ /** The collection this question was narrowed to, by name; null asked across them all. */
47
+ collection?: string | null;
48
48
  /** Where this answer's read-only view lives while it is shared. */
49
49
  shared?: string | null;
50
50
  }
@@ -77,17 +77,18 @@ export interface AskRequest {
77
77
  signal: AbortSignal;
78
78
  /** What the host asked for, when it is not a plain question: `client.ask()`'s `kind`. */
79
79
  kind?: string;
80
- /** How hard the persona should work on this one: `client.ask()`'s `depth`. */
81
- depth?: Depth;
80
+ /** The one collection to ask, by name: `client.ask()`'s `collection`. */
81
+ collection?: string;
82
82
  }
83
83
  /**
84
- * A room's routes, as the host's own client reaches them. Every method but
85
- * `ask` may reject; the controller says what failed.
84
+ * A persona's chat routes, as the host's own client reaches them. Every
85
+ * method but `ask` may reject; the controller says what failed.
86
86
  */
87
- export interface RoomTransport {
88
- /** `POST rooms/{room}/ask`: `client.ask()` over it, which never throws. A
89
- * refusal arrives as `library_error` with its `status`: 429 is a spent
90
- * allowance, 409 a conversation already being answered. */
87
+ export interface ChatTransport {
88
+ /** `POST {persona}/ask`: `client.ask()` over it, which never throws. A
89
+ * refusal arrives as `agent_error` with its `status`: 429 is a spent
90
+ * allowance, 409 a conversation already being answered, 404 a collection
91
+ * the person may not read, 503 a persona that cannot answer yet. */
91
92
  ask(request: AskRequest): AsyncIterable<AgentEvent>;
92
93
  /** `GET conversations/{id}`. */
93
94
  read(id: string): Promise<ConversationRead>;
@@ -95,7 +96,7 @@ export interface RoomTransport {
95
96
  stop(id: string): Promise<void>;
96
97
  /** `GET quota`. */
97
98
  quota(): Promise<Quota>;
98
- /** This person's conversations in this room. */
99
+ /** This person's conversations with this persona. */
99
100
  list(): Promise<ConversationSummary[]>;
100
101
  /** `PATCH conversations/{id}`. */
101
102
  rename(id: string, title: string): Promise<void>;
@@ -109,7 +110,7 @@ export interface RoomTransport {
109
110
  /** The answer mark, where the agent takes one. `turn` is the answer's
110
111
  * zero-based position in the conversation. */
111
112
  mark?(id: string, turn: number, verdict: Verdict): Promise<void>;
112
- /** Shares answer `turn` read-only with whoever may enter the room, or
113
+ /** Shares answer `turn` read-only with whoever may chat with this persona, or
113
114
  * stops sharing it. Resolves to where it is read while shared, null once
114
115
  * it is not. */
115
116
  share?(id: string, turn: number, share: boolean): Promise<string | null>;
@@ -147,8 +148,6 @@ export declare class Chat {
147
148
  /** When an answer this page is not streaming began, epoch ms: a
148
149
  * conversation reopened mid-answer. */
149
150
  answering: number | null;
150
- /** The agent is answering somebody else's question first. */
151
- waiting: boolean;
152
151
  /** Today's allowance; null until read, and in a job. */
153
152
  quota: Quota | null;
154
153
  /** The open conversation, or the job; null for a new one. The host keeps
@@ -157,7 +156,7 @@ export declare class Chat {
157
156
  /** This person's conversations, as `list()` last read them; null until then. */
158
157
  conversations: ConversationSummary[] | null;
159
158
  listFailed: boolean;
160
- constructor(transport: RoomTransport | JobTransport, options?: ChatOptions);
159
+ constructor(transport: ChatTransport | JobTransport, options?: ChatOptions);
161
160
  /** Every turn, in order. A live one grows in place; key on `version`. */
162
161
  get turns(): Turn[];
163
162
  /** Bumped on every event folded, for `Conversation`'s follow-scroll. */
@@ -167,7 +166,7 @@ export declare class Chat {
167
166
  /** Something is being answered, here or out of sight: the composer waits
168
167
  * and offers Stop. */
169
168
  get busy(): boolean;
170
- /** A job reads what the reader says while it works; a room waits for its answer. */
169
+ /** A job reads what the reader says while it works; a persona waits for its answer. */
171
170
  get sendWhileRunning(): boolean;
172
171
  /** Reopen a conversation, or start watching a job. False when it could not
173
172
  * be read, which leaves the page on a new conversation.
@@ -181,13 +180,13 @@ export declare class Chat {
181
180
  new(): void;
182
181
  /**
183
182
  * Ask `question`, or what is in the box, as a `kind` of turn when it is not
184
- * a plain question, at a `depth` when the host offers the choice. Resolves
183
+ * a plain question, narrowed to one `collection` by name when given. Resolves
185
184
  * once the turn settles: true if it was asked, false if it was refused,
186
185
  * here or by the server, in which case typed words go back into the box.
187
186
  */
188
- ask(question?: string, kind?: string, depth?: Depth): Promise<boolean>;
187
+ ask(question?: string, kind?: string, collection?: string): Promise<boolean>;
189
188
  /** The last question again, as a new turn: the conversation is append-only,
190
- * because the agent's transcript is. Same kind, same depth. */
189
+ * because the agent's transcript is. Same kind, same collection. */
191
190
  again(): Promise<boolean>;
192
191
  /** Stop the answer being written, wherever it is being written. */
193
192
  stop(): Promise<void>;