@poodle64/librarian 2026.9.19 → 2026.9.21
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 +15 -3
- package/dist/components/agent-transcript/agent-transcript.svelte +5 -3
- package/dist/components/composer/composer.svelte +27 -13
- package/dist/components/composer/composer.svelte.d.ts +5 -0
- package/dist/session.svelte.js +6 -0
- package/dist/transcript.svelte.js +16 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -187,7 +187,10 @@ is watched rather than asked. `watch()` GETs its stream; `Session` folds it
|
|
|
187
187
|
into turns: the prompt and every later message on the reader's side (the CLI
|
|
188
188
|
echoes each one back under `--replay-user-messages`), each run's answer on the
|
|
189
189
|
persona's, each settled by its own `result`. A stream without deltas is folded
|
|
190
|
-
from its whole messages.
|
|
190
|
+
from its whole messages. A run under `--json-schema` hands in its answer by
|
|
191
|
+
calling the CLI's `StructuredOutput` tool: that call is no step of the work,
|
|
192
|
+
the words before it stay the answer, and what it handed in is the run's
|
|
193
|
+
`outcome.structuredOutput`, the natural source of the host's `artefact`. Events are deduped by `uuid`, so opening the watch
|
|
191
194
|
again after sending a message folds only what is new.
|
|
192
195
|
|
|
193
196
|
```svelte
|
|
@@ -228,7 +231,15 @@ again after sending a message folds only what is new.
|
|
|
228
231
|
oncite={(citation) => showPage(citation.document_id)}
|
|
229
232
|
>
|
|
230
233
|
{#snippet composer()}
|
|
231
|
-
<Composer
|
|
234
|
+
<Composer
|
|
235
|
+
bind:value
|
|
236
|
+
{running}
|
|
237
|
+
{name}
|
|
238
|
+
sendWhileRunning
|
|
239
|
+
note="It carries on from where it stopped."
|
|
240
|
+
{onsubmit}
|
|
241
|
+
{onstop}
|
|
242
|
+
/>
|
|
232
243
|
{/snippet}
|
|
233
244
|
</Conversation>
|
|
234
245
|
```
|
|
@@ -244,6 +255,7 @@ end that way.
|
|
|
244
255
|
| 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 |
|
|
245
256
|
| 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 |
|
|
246
257
|
| "Answered without a source" | `copy.notHeld: ''` for a persona that answers from no shelf |
|
|
258
|
+
| 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 |
|
|
247
259
|
|
|
248
260
|
### Citations
|
|
249
261
|
|
|
@@ -404,7 +416,7 @@ pnpm run test # build + vitest
|
|
|
404
416
|
pnpm run screenshots # the state grid, real engine (see below)
|
|
405
417
|
```
|
|
406
418
|
|
|
407
|
-
`docs/screenshots/` is
|
|
419
|
+
`docs/screenshots/` is seventeen states x three widths x both themes, taken by
|
|
408
420
|
`scripts/screenshots.mjs` against the console's `/librarian` lab route
|
|
409
421
|
running from its own static build. The same script asserts what a screenshot
|
|
410
422
|
cannot: that nothing scrolls sideways at any width, that the source pane
|
|
@@ -186,9 +186,11 @@
|
|
|
186
186
|
: (outcome?.error ?? (outcome?.isError ? words.answerFailed : null))
|
|
187
187
|
);
|
|
188
188
|
|
|
189
|
-
// A
|
|
190
|
-
//
|
|
191
|
-
|
|
189
|
+
// A run that ended has settled, whatever it said: a failed one, where "Ask
|
|
190
|
+
// again" is the one thing a reader wants, and one that handed in its answer
|
|
191
|
+
// under a schema and said nothing at all, whose artefact is the answer. A
|
|
192
|
+
// turn read back from history has no outcome and settles on its prose.
|
|
193
|
+
const settled = $derived(!running && (hasAnswer || outcome !== null));
|
|
192
194
|
|
|
193
195
|
/**
|
|
194
196
|
* The persona answered and cited nothing, and we WATCHED it happen.
|
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
chrome grows with the text and nothing else moves.
|
|
8
8
|
|
|
9
9
|
While Milton is answering the box is disabled and the send button becomes a
|
|
10
|
-
stop button
|
|
11
|
-
typing it will then discard is worse than one that plainly waits.
|
|
10
|
+
stop button: an ask route takes one question per request, and a box that
|
|
11
|
+
takes typing it will then discard is worse than one that plainly waits. A
|
|
12
|
+
host whose session reads messages while it works (`sendWhileRunning`) keeps
|
|
13
|
+
the box open, with Send beside Stop.
|
|
12
14
|
-->
|
|
13
15
|
<script lang="ts">
|
|
14
16
|
import ArrowUpIcon from '@lucide/svelte/icons/arrow-up';
|
|
@@ -52,6 +54,11 @@
|
|
|
52
54
|
name?: string;
|
|
53
55
|
/** Overrides for the package's own words. */
|
|
54
56
|
copy?: Partial<LibrarianCopy>;
|
|
57
|
+
/** The session reads a message sent while it works — a job whose
|
|
58
|
+
* route writes it onto the running process — so the box stays open
|
|
59
|
+
* and Send sits beside Stop. Words only: the paperclip still waits for
|
|
60
|
+
* the run. Absent, a chat waits for its answer. */
|
|
61
|
+
sendWhileRunning?: boolean;
|
|
55
62
|
/** One quiet line in the box's footer, where a scope choice would
|
|
56
63
|
* sit, in the host's words: what sending does here ("It carries on
|
|
57
64
|
* from where it stopped."). */
|
|
@@ -72,9 +79,13 @@
|
|
|
72
79
|
onbriefing,
|
|
73
80
|
name = DEFAULT_PERSONA,
|
|
74
81
|
copy,
|
|
75
|
-
note
|
|
82
|
+
note,
|
|
83
|
+
sendWhileRunning = false
|
|
76
84
|
}: Props = $props();
|
|
77
85
|
|
|
86
|
+
/** Nothing can be said right now: a chat whose answer is still coming. */
|
|
87
|
+
const waiting = $derived(running && !sendWhileRunning);
|
|
88
|
+
|
|
78
89
|
const words = $derived(resolveCopy(copy, name));
|
|
79
90
|
|
|
80
91
|
// What Milton is being asked about — this document, this collection, or
|
|
@@ -124,21 +135,23 @@
|
|
|
124
135
|
});
|
|
125
136
|
|
|
126
137
|
// Focus comes back once Milton is done, so a reader can keep going without
|
|
127
|
-
// reaching for the mouse. It has to be the transition OUT of `
|
|
138
|
+
// reaching for the mouse. It has to be the transition OUT of `waiting`: a
|
|
128
139
|
// disabled element cannot hold focus, so the focus call in `submit()` below
|
|
129
|
-
// survives only for a
|
|
130
|
-
//
|
|
140
|
+
// survives only for a box that never disables — measured, focus lands on
|
|
141
|
+
// `body` the frame the box disables and comes back here. A box that stayed
|
|
142
|
+
// open never lost focus, so it takes none back from wherever the reader
|
|
143
|
+
// went while the run worked.
|
|
131
144
|
//
|
|
132
145
|
// Plain, not `$state`: read and written in the same effect, which would
|
|
133
146
|
// otherwise re-trigger itself forever.
|
|
134
|
-
let
|
|
147
|
+
let wasWaiting = false;
|
|
135
148
|
$effect(() => {
|
|
136
|
-
if (
|
|
137
|
-
|
|
149
|
+
if (wasWaiting && !waiting) textarea?.focus();
|
|
150
|
+
wasWaiting = waiting;
|
|
138
151
|
});
|
|
139
152
|
|
|
140
153
|
function submit() {
|
|
141
|
-
if (!value.trim() ||
|
|
154
|
+
if (!value.trim() || waiting) return;
|
|
142
155
|
onsubmit();
|
|
143
156
|
textarea?.focus();
|
|
144
157
|
}
|
|
@@ -225,8 +238,8 @@
|
|
|
225
238
|
oninput={grow}
|
|
226
239
|
onkeydown={keydown}
|
|
227
240
|
rows="1"
|
|
228
|
-
disabled={
|
|
229
|
-
placeholder={
|
|
241
|
+
disabled={waiting}
|
|
242
|
+
placeholder={waiting ? words.answeringPlaceholder : words.askPlaceholder}
|
|
230
243
|
class="ds-lib-input"
|
|
231
244
|
></textarea>
|
|
232
245
|
|
|
@@ -287,7 +300,8 @@
|
|
|
287
300
|
<button type="button" class="ds-lib-stop" onclick={onstop} aria-label="Stop">
|
|
288
301
|
<SquareIcon size={14} fill="currentColor" />
|
|
289
302
|
</button>
|
|
290
|
-
{
|
|
303
|
+
{/if}
|
|
304
|
+
{#if !waiting}
|
|
291
305
|
<button
|
|
292
306
|
type="button"
|
|
293
307
|
class="ds-lib-send"
|
|
@@ -25,6 +25,11 @@ interface Props {
|
|
|
25
25
|
name?: string;
|
|
26
26
|
/** Overrides for the package's own words. */
|
|
27
27
|
copy?: Partial<LibrarianCopy>;
|
|
28
|
+
/** The session reads a message sent while it works — a job whose
|
|
29
|
+
* route writes it onto the running process — so the box stays open
|
|
30
|
+
* and Send sits beside Stop. Words only: the paperclip still waits for
|
|
31
|
+
* the run. Absent, a chat waits for its answer. */
|
|
32
|
+
sendWhileRunning?: boolean;
|
|
28
33
|
/** One quiet line in the box's footer, where a scope choice would
|
|
29
34
|
* sit, in the host's words: what sending does here ("It carries on
|
|
30
35
|
* from where it stopped."). */
|
package/dist/session.svelte.js
CHANGED
|
@@ -55,6 +55,12 @@ export class Session {
|
|
|
55
55
|
}
|
|
56
56
|
if (!FOLDED.has(event.type))
|
|
57
57
|
return;
|
|
58
|
+
// A user frame that is neither a replayed message nor a tool's result is
|
|
59
|
+
// the CLI nudging its own model (`isSynthetic`: "[structured-output-
|
|
60
|
+
// enforce] You MUST call the StructuredOutput tool…"): neither side of
|
|
61
|
+
// the conversation.
|
|
62
|
+
if (event.type === 'user' && !contentOf(event).some((b) => b.type === 'tool_result'))
|
|
63
|
+
return;
|
|
58
64
|
// Settled by its own `result`. A stream that broke mid-run is not: the
|
|
59
65
|
// run goes on, and a watch opened again folds the rest onto it.
|
|
60
66
|
const last = this.turns.at(-1);
|
|
@@ -366,9 +366,11 @@ export function segment(blocks, describeTool) {
|
|
|
366
366
|
let current = null;
|
|
367
367
|
let lastTool = -1;
|
|
368
368
|
for (let i = 0; i < blocks.length; i += 1)
|
|
369
|
-
if (blocks[i]
|
|
369
|
+
if (isStep(blocks[i]))
|
|
370
370
|
lastTool = i;
|
|
371
371
|
for (const [i, block] of blocks.entries()) {
|
|
372
|
+
if (block.kind === 'tool' && !isStep(block))
|
|
373
|
+
continue;
|
|
372
374
|
if (block.kind === 'text' && i > lastTool) {
|
|
373
375
|
current = null;
|
|
374
376
|
out.push(block);
|
|
@@ -397,6 +399,18 @@ export function segment(blocks, describeTool) {
|
|
|
397
399
|
}
|
|
398
400
|
return out;
|
|
399
401
|
}
|
|
402
|
+
/**
|
|
403
|
+
* The tool the CLI adds under `--json-schema`. Calling it hands in the run's
|
|
404
|
+
* answer, which arrives again whole on the `result` (`structured_output`): it
|
|
405
|
+
* is not a step of the work, and the words before it are the answer, not
|
|
406
|
+
* narration ahead of more work. Measured on Claude Code 2.1.283, where a run
|
|
407
|
+
* with a schema always ends in this call.
|
|
408
|
+
*/
|
|
409
|
+
const HANDS_IN = 'StructuredOutput';
|
|
410
|
+
/** A call that is part of the work, rather than the handing-in of its answer. */
|
|
411
|
+
function isStep(block) {
|
|
412
|
+
return block.kind === 'tool' && block.name !== HANDS_IN;
|
|
413
|
+
}
|
|
400
414
|
function sameStep(last, block, words) {
|
|
401
415
|
if (last.block.kind !== block.kind)
|
|
402
416
|
return false;
|
|
@@ -436,7 +450,7 @@ export function summariseActivity(group) {
|
|
|
436
450
|
export function readFrom(blocks, describeTool) {
|
|
437
451
|
const out = [];
|
|
438
452
|
for (const block of blocks) {
|
|
439
|
-
if (block.kind !== 'tool' || block.result === undefined || block.isError)
|
|
453
|
+
if (block.kind !== 'tool' || !isStep(block) || block.result === undefined || block.isError)
|
|
440
454
|
continue;
|
|
441
455
|
const source = wordsFor(block, describeTool).source;
|
|
442
456
|
if (!source || out.some((c) => c.document_id === source.id))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@poodle64/librarian",
|
|
3
|
-
"version": "2026.9.
|
|
3
|
+
"version": "2026.9.21",
|
|
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",
|