@poodle64/librarian 2026.9.20 → 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 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. Events are deduped by `uuid`, so opening the watch
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
@@ -413,7 +416,7 @@ pnpm run test # build + vitest
413
416
  pnpm run screenshots # the state grid, real engine (see below)
414
417
  ```
415
418
 
416
- `docs/screenshots/` is sixteen states x three widths x both themes, taken by
419
+ `docs/screenshots/` is seventeen states x three widths x both themes, taken by
417
420
  `scripts/screenshots.mjs` against the console's `/librarian` lab route
418
421
  running from its own static build. The same script asserts what a screenshot
419
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 failed turn settles too, and "Ask again" is the one thing a reader wants
190
- // from it — there is just nothing to copy.
191
- const settled = $derived(!running && (hasAnswer || Boolean(failure)));
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.
@@ -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].kind === 'tool')
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.20",
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",