@poodle64/librarian 2026.9.28 → 2026.10.1
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.
- package/README.md +66 -116
- package/dist/attachments.d.ts +2 -2
- package/dist/attachments.js +3 -3
- package/dist/chat.svelte.d.ts +22 -23
- package/dist/chat.svelte.js +41 -64
- package/dist/citations.d.ts +7 -7
- package/dist/citations.js +6 -6
- package/dist/client.d.ts +16 -45
- package/dist/client.js +15 -23
- package/dist/components/activity-group/activity-group.svelte +1 -1
- package/dist/components/agent-transcript/agent-transcript.svelte +9 -110
- package/dist/components/agent-transcript/agent-transcript.svelte.d.ts +4 -16
- package/dist/components/answer-mark/answer-mark.svelte +2 -2
- package/dist/components/answer-mark/answer-mark.svelte.d.ts +2 -2
- package/dist/components/answer-share/answer-share.svelte +2 -2
- package/dist/components/answer-share/answer-share.svelte.d.ts +2 -2
- package/dist/components/artefact-card/artefact-card.svelte +2 -2
- package/dist/components/artefact-card/artefact-card.svelte.d.ts +2 -2
- package/dist/components/artefact-pane/artefact-pane.svelte +2 -2
- package/dist/components/artefact-pane/artefact-pane.svelte.d.ts +2 -2
- package/dist/components/composer/composer.svelte +51 -112
- package/dist/components/composer/composer.svelte.d.ts +21 -18
- package/dist/components/composer/index.d.ts +1 -2
- package/dist/components/conversation/conversation.svelte +7 -18
- package/dist/components/conversation/conversation.svelte.d.ts +7 -12
- package/dist/components/conversation-list/conversation-list.svelte +2 -2
- package/dist/components/conversation-list/conversation-list.svelte.d.ts +2 -2
- package/dist/components/document-pane/document-pane.svelte +2 -2
- package/dist/components/document-pane/document-pane.svelte.d.ts +2 -2
- package/dist/components/fair-use-notice/fair-use-notice.svelte +2 -2
- package/dist/components/fair-use-notice/fair-use-notice.svelte.d.ts +2 -2
- package/dist/components/scope-statement/scope-statement.svelte +5 -5
- package/dist/components/scope-statement/scope-statement.svelte.d.ts +2 -2
- package/dist/components/source-list/source-list.svelte +2 -2
- package/dist/components/source-list/source-list.svelte.d.ts +2 -2
- package/dist/components/working/working.svelte +2 -2
- package/dist/components/working/working.svelte.d.ts +2 -2
- package/dist/copy.d.ts +16 -24
- package/dist/copy.js +10 -16
- package/dist/session.svelte.d.ts +1 -1
- package/dist/session.svelte.js +6 -7
- package/dist/transcript.svelte.d.ts +18 -32
- package/dist/transcript.svelte.js +19 -40
- package/package.json +1 -5
- package/dist/history.svelte.d.ts +0 -37
- 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.
|
|
10
|
-
|
|
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
|
-
|
|
13
|
-
|
|
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
|
|
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
|
|
35
|
-
agent-transcript/ one question and everything
|
|
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,
|
|
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/
|
|
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
|
-
|
|
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
|
-
|
|
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 })`:
|
|
176
|
-
| Attachments | nothing: `ask()` posts multipart (`question`, `resume`, `
|
|
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
|
-
|
|
|
177
|
+
| Choosing a collection | `Composer`'s `collections` (`{ name, title?, description? }[]`, 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 title (the name made readable where there is none) with the description as its hover, never a retrieval summary |
|
|
180
178
|
| The empty state | `welcome` and up to three `examples` |
|
|
181
|
-
| What this surface covers | `scope`: one statement per
|
|
182
|
-
| The words themselves | `copy`: a partial of `
|
|
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
|
|
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
|
|
188
|
+
### A persona's page, whole: `Chat`
|
|
191
189
|
|
|
192
|
-
The hand-wired page above is the parts. A stamped
|
|
193
|
-
the
|
|
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(
|
|
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
|
-
###
|
|
255
|
+
### Asking one collection, and what it cost
|
|
257
256
|
|
|
258
|
-
`
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
|
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
|
-
| `
|
|
281
|
+
| `ChatTransport` | Route |
|
|
302
282
|
| --------------- | ----------------------------------------------------- |
|
|
303
|
-
| `ask` | `POST
|
|
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
|
|
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 (`
|
|
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
|
|
346
|
-
|
|
|
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
|
|
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
|
|
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.
|
|
451
|
-
|
|
452
|
-
|
|
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
|
|
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
|
-
"
|
|
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
|
|
475
|
-
|
|
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
|
|
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" {
|
|
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 `
|
|
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
|
|
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
|
|
562
|
-
`{
|
|
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 `/
|
|
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
|
|
595
|
-
|
|
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 `
|
|
600
|
-
scene is `Chat` itself over a fake of the
|
|
601
|
-
|
|
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
|
package/dist/attachments.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* What a reader may hand
|
|
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
|
|
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;
|
package/dist/attachments.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* What a reader may hand
|
|
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
|
|
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
|
|
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
|
};
|
package/dist/chat.svelte.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The page's controller: one conversation with one agent, over a
|
|
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
|
|
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 {
|
|
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
|
-
/**
|
|
47
|
-
|
|
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
|
-
/**
|
|
81
|
-
|
|
80
|
+
/** The one collection to ask, by name: `client.ask()`'s `collection`. */
|
|
81
|
+
collection?: string;
|
|
82
82
|
}
|
|
83
83
|
/**
|
|
84
|
-
* A
|
|
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
|
|
88
|
-
/** `POST
|
|
89
|
-
* refusal arrives as `
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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,
|
|
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,
|
|
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
|
|
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>;
|