@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/dist/chat.svelte.js
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
|
|
@@ -20,7 +20,6 @@
|
|
|
20
20
|
* what its client returns straight through.
|
|
21
21
|
*/
|
|
22
22
|
import { untrack } from 'svelte';
|
|
23
|
-
import { isQueued } from './client';
|
|
24
23
|
import { Session } from './session.svelte';
|
|
25
24
|
import { fold, foldState } from './transcript.svelte';
|
|
26
25
|
/** The events of a run: the agent has the question. */
|
|
@@ -34,8 +33,6 @@ export class Chat {
|
|
|
34
33
|
/** When an answer this page is not streaming began, epoch ms: a
|
|
35
34
|
* conversation reopened mid-answer. */
|
|
36
35
|
answering = $state(null);
|
|
37
|
-
/** The agent is answering somebody else's question first. */
|
|
38
|
-
waiting = $state(false);
|
|
39
36
|
/** Today's allowance; null until read, and in a job. */
|
|
40
37
|
quota = $state(null);
|
|
41
38
|
/** The open conversation, or the job; null for a new one. The host keeps
|
|
@@ -44,7 +41,7 @@ export class Chat {
|
|
|
44
41
|
/** This person's conversations, as `list()` last read them; null until then. */
|
|
45
42
|
conversations = $state(null);
|
|
46
43
|
listFailed = $state(false);
|
|
47
|
-
#
|
|
44
|
+
#chat = null;
|
|
48
45
|
#job = null;
|
|
49
46
|
#pollMs;
|
|
50
47
|
#rewatchMs;
|
|
@@ -71,7 +68,7 @@ export class Chat {
|
|
|
71
68
|
if ('watch' in transport)
|
|
72
69
|
this.#job = transport;
|
|
73
70
|
else
|
|
74
|
-
this.#
|
|
71
|
+
this.#chat = transport;
|
|
75
72
|
this.#pollMs = options.pollMs ?? 5000;
|
|
76
73
|
this.#rewatchMs = options.rewatchMs ?? 2000;
|
|
77
74
|
this.#artefact = options.artefact;
|
|
@@ -95,7 +92,7 @@ export class Chat {
|
|
|
95
92
|
get busy() {
|
|
96
93
|
return this.running || this.answering !== null;
|
|
97
94
|
}
|
|
98
|
-
/** A job reads what the reader says while it works; a
|
|
95
|
+
/** A job reads what the reader says while it works; a persona waits for its answer. */
|
|
99
96
|
get sendWhileRunning() {
|
|
100
97
|
return this.#job !== null;
|
|
101
98
|
}
|
|
@@ -119,7 +116,7 @@ export class Chat {
|
|
|
119
116
|
void this.refreshQuota();
|
|
120
117
|
const mine = this.#generation;
|
|
121
118
|
try {
|
|
122
|
-
const read = await this.#
|
|
119
|
+
const read = await this.#chat.read(id);
|
|
123
120
|
if (mine === this.#generation)
|
|
124
121
|
this.#show(read, mine);
|
|
125
122
|
return mine === this.#generation;
|
|
@@ -147,11 +144,11 @@ export class Chat {
|
|
|
147
144
|
}
|
|
148
145
|
/**
|
|
149
146
|
* Ask `question`, or what is in the box, as a `kind` of turn when it is not
|
|
150
|
-
* a plain question,
|
|
147
|
+
* a plain question, narrowed to one `collection` by name when given. Resolves
|
|
151
148
|
* once the turn settles: true if it was asked, false if it was refused,
|
|
152
149
|
* here or by the server, in which case typed words go back into the box.
|
|
153
150
|
*/
|
|
154
|
-
async ask(question, kind,
|
|
151
|
+
async ask(question, kind, collection) {
|
|
155
152
|
const typed = question === undefined;
|
|
156
153
|
const text = (question ?? this.draft).trim();
|
|
157
154
|
if (!text)
|
|
@@ -165,14 +162,14 @@ export class Chat {
|
|
|
165
162
|
this.draft = '';
|
|
166
163
|
this.files = [];
|
|
167
164
|
}
|
|
168
|
-
return this.#ask(text, files, typed, kind,
|
|
165
|
+
return this.#ask(text, files, typed, kind, collection);
|
|
169
166
|
}
|
|
170
167
|
/** The last question again, as a new turn: the conversation is append-only,
|
|
171
|
-
* because the agent's transcript is. Same kind, same
|
|
168
|
+
* because the agent's transcript is. Same kind, same collection. */
|
|
172
169
|
again() {
|
|
173
170
|
const last = this.turns.at(-1);
|
|
174
171
|
return last?.question
|
|
175
|
-
? this.ask(last.question, last.kind, last.
|
|
172
|
+
? this.ask(last.question, last.kind, last.collection)
|
|
176
173
|
: Promise.resolve(false);
|
|
177
174
|
}
|
|
178
175
|
/** Stop the answer being written, wherever it is being written. */
|
|
@@ -186,7 +183,6 @@ export class Chat {
|
|
|
186
183
|
const stream = this.#stream;
|
|
187
184
|
if (stream && this.#running) {
|
|
188
185
|
this.#running = false;
|
|
189
|
-
this.waiting = false;
|
|
190
186
|
// Before the agent has named a new conversation there is nothing to
|
|
191
187
|
// address a stop to: the stream is read on, unseen, until it does.
|
|
192
188
|
if (!id) {
|
|
@@ -194,21 +190,21 @@ export class Chat {
|
|
|
194
190
|
return;
|
|
195
191
|
}
|
|
196
192
|
stream.controller.abort();
|
|
197
|
-
await this.#
|
|
193
|
+
await this.#chat.stop(id).catch(() => undefined);
|
|
198
194
|
return;
|
|
199
195
|
}
|
|
200
196
|
if (this.answering !== null && id) {
|
|
201
|
-
await this.#
|
|
197
|
+
await this.#chat.stop(id).catch(() => undefined);
|
|
202
198
|
clearTimeout(this.#poll);
|
|
203
199
|
await this.#settle(id, this.#generation);
|
|
204
200
|
}
|
|
205
201
|
}
|
|
206
202
|
/** Read today's allowance again. One that cannot be read is left as it was. */
|
|
207
203
|
async refreshQuota() {
|
|
208
|
-
if (!this.#
|
|
204
|
+
if (!this.#chat)
|
|
209
205
|
return;
|
|
210
206
|
try {
|
|
211
|
-
this.quota = await this.#
|
|
207
|
+
this.quota = await this.#chat.quota();
|
|
212
208
|
}
|
|
213
209
|
catch {
|
|
214
210
|
// A counter that cannot be read costs the counter, never the ask.
|
|
@@ -217,10 +213,10 @@ export class Chat {
|
|
|
217
213
|
/** Read this person's conversations. Read again after every answer once
|
|
218
214
|
* read at all, so the list follows what they ask. */
|
|
219
215
|
async list() {
|
|
220
|
-
if (!this.#
|
|
216
|
+
if (!this.#chat)
|
|
221
217
|
return false;
|
|
222
218
|
try {
|
|
223
|
-
this.conversations = await this.#
|
|
219
|
+
this.conversations = await this.#chat.list();
|
|
224
220
|
this.listFailed = false;
|
|
225
221
|
return true;
|
|
226
222
|
}
|
|
@@ -231,10 +227,10 @@ export class Chat {
|
|
|
231
227
|
}
|
|
232
228
|
async rename(id, title) {
|
|
233
229
|
const name = title.trim();
|
|
234
|
-
if (!name || !this.#
|
|
230
|
+
if (!name || !this.#chat)
|
|
235
231
|
return false;
|
|
236
232
|
try {
|
|
237
|
-
await this.#
|
|
233
|
+
await this.#chat.rename(id, name);
|
|
238
234
|
}
|
|
239
235
|
catch {
|
|
240
236
|
return false;
|
|
@@ -246,10 +242,10 @@ export class Chat {
|
|
|
246
242
|
}
|
|
247
243
|
/** Forget a conversation everywhere. The open one, forgotten, leaves a new one. */
|
|
248
244
|
async remove(id) {
|
|
249
|
-
if (!this.#
|
|
245
|
+
if (!this.#chat)
|
|
250
246
|
return false;
|
|
251
247
|
try {
|
|
252
|
-
await this.#
|
|
248
|
+
await this.#chat.remove(id);
|
|
253
249
|
}
|
|
254
250
|
catch {
|
|
255
251
|
return false;
|
|
@@ -265,10 +261,10 @@ export class Chat {
|
|
|
265
261
|
* navigated to, so one deleted a moment ago elsewhere is a false here,
|
|
266
262
|
* not the reader's page replaced by an error body. */
|
|
267
263
|
async download(id) {
|
|
268
|
-
if (!this.#
|
|
264
|
+
if (!this.#chat)
|
|
269
265
|
return false;
|
|
270
266
|
try {
|
|
271
|
-
const { name, body } = await this.#
|
|
267
|
+
const { name, body } = await this.#chat.download(id);
|
|
272
268
|
save(body, name);
|
|
273
269
|
return true;
|
|
274
270
|
}
|
|
@@ -279,10 +275,10 @@ export class Chat {
|
|
|
279
275
|
/** Mark an answer the agent holds. False when it could not be recorded. */
|
|
280
276
|
async mark(turn, verdict) {
|
|
281
277
|
const id = this.conversationId;
|
|
282
|
-
if (!id || !this.#
|
|
278
|
+
if (!id || !this.#chat?.mark || !this.#held.has(turn.id))
|
|
283
279
|
return false;
|
|
284
280
|
try {
|
|
285
|
-
await this.#
|
|
281
|
+
await this.#chat.mark(id, this.#position(turn), verdict);
|
|
286
282
|
return true;
|
|
287
283
|
}
|
|
288
284
|
catch {
|
|
@@ -293,10 +289,10 @@ export class Chat {
|
|
|
293
289
|
* was not recorded. */
|
|
294
290
|
async share(turn, share) {
|
|
295
291
|
const id = this.conversationId;
|
|
296
|
-
if (!id || !this.#
|
|
292
|
+
if (!id || !this.#chat?.share || !this.#held.has(turn.id))
|
|
297
293
|
return false;
|
|
298
294
|
try {
|
|
299
|
-
turn.shared = (await this.#
|
|
295
|
+
turn.shared = (await this.#chat.share(id, this.#position(turn), share)) ?? undefined;
|
|
300
296
|
this.#version += 1;
|
|
301
297
|
return true;
|
|
302
298
|
}
|
|
@@ -321,10 +317,9 @@ export class Chat {
|
|
|
321
317
|
this.#watching = false;
|
|
322
318
|
this.#running = false;
|
|
323
319
|
this.answering = null;
|
|
324
|
-
this.waiting = false;
|
|
325
320
|
}
|
|
326
|
-
async #ask(text, files, typed, kind,
|
|
327
|
-
const
|
|
321
|
+
async #ask(text, files, typed, kind, collection) {
|
|
322
|
+
const chat = this.#chat;
|
|
328
323
|
const mine = this.#generation;
|
|
329
324
|
const stream = { controller: new AbortController(), stopWanted: false };
|
|
330
325
|
this.#stream = stream;
|
|
@@ -335,25 +330,23 @@ export class Chat {
|
|
|
335
330
|
blocks: [],
|
|
336
331
|
outcome: null,
|
|
337
332
|
citations: [],
|
|
338
|
-
suggestions: [],
|
|
339
333
|
kind,
|
|
340
|
-
|
|
334
|
+
collection,
|
|
341
335
|
artefact: this.#card(kind)
|
|
342
336
|
});
|
|
343
337
|
const live = this.#turns[this.#turns.length - 1];
|
|
344
338
|
const state = foldState();
|
|
345
339
|
this.#running = true;
|
|
346
|
-
this.waiting = false;
|
|
347
340
|
this.#version += 1;
|
|
348
341
|
const current = () => mine === this.#generation && this.#stream === stream;
|
|
349
342
|
let ending = null;
|
|
350
|
-
for await (const event of
|
|
343
|
+
for await (const event of chat.ask({
|
|
351
344
|
question: text,
|
|
352
345
|
files,
|
|
353
346
|
resume: this.conversationId,
|
|
354
347
|
signal: stream.controller.signal,
|
|
355
348
|
kind,
|
|
356
|
-
|
|
349
|
+
collection
|
|
357
350
|
})) {
|
|
358
351
|
const named = event.type === 'system' && event.subtype === 'init' ? event.session_id : undefined;
|
|
359
352
|
if (stream.stopWanted) {
|
|
@@ -365,29 +358,21 @@ export class Chat {
|
|
|
365
358
|
this.#held.add(live.id);
|
|
366
359
|
}
|
|
367
360
|
stream.controller.abort();
|
|
368
|
-
await
|
|
361
|
+
await chat.stop(named).catch(() => undefined);
|
|
369
362
|
break;
|
|
370
363
|
}
|
|
371
364
|
if (!current())
|
|
372
365
|
break;
|
|
373
|
-
if (isQueued(event)) {
|
|
374
|
-
this.waiting = true;
|
|
375
|
-
this.#version += 1;
|
|
376
|
-
continue;
|
|
377
|
-
}
|
|
378
|
-
// Waiting ends when the agent starts, and no other frame says so.
|
|
379
|
-
if (named || RUN.has(event.type))
|
|
380
|
-
this.waiting = false;
|
|
381
366
|
if (named) {
|
|
382
367
|
this.conversationId = named;
|
|
383
368
|
this.#held.add(live.id);
|
|
384
369
|
continue;
|
|
385
370
|
}
|
|
386
|
-
if (event.type === '
|
|
371
|
+
if (event.type === 'agent_error' && event.status === 429)
|
|
387
372
|
ending = 'refused';
|
|
388
|
-
else if (event.type === '
|
|
373
|
+
else if (event.type === 'agent_error' && event.status === 409)
|
|
389
374
|
ending = 'answering';
|
|
390
|
-
else if (event.type === '
|
|
375
|
+
else if (event.type === 'agent_error' && event.dropped && this.conversationId)
|
|
391
376
|
ending = 'dropped';
|
|
392
377
|
if (ending)
|
|
393
378
|
break;
|
|
@@ -418,13 +403,13 @@ export class Chat {
|
|
|
418
403
|
if (ending === 'dropped' && id) {
|
|
419
404
|
// The page lost the stream, not the turn: read where it has got to.
|
|
420
405
|
try {
|
|
421
|
-
const read = await
|
|
406
|
+
const read = await chat.read(id);
|
|
422
407
|
if (current())
|
|
423
408
|
this.#show(read, mine);
|
|
424
409
|
}
|
|
425
410
|
catch {
|
|
426
411
|
if (current())
|
|
427
|
-
fold(live, state, { type: '
|
|
412
|
+
fold(live, state, { type: 'agent_error' });
|
|
428
413
|
}
|
|
429
414
|
}
|
|
430
415
|
this.#finish();
|
|
@@ -438,7 +423,6 @@ export class Chat {
|
|
|
438
423
|
#finish() {
|
|
439
424
|
this.#stream = null;
|
|
440
425
|
this.#running = false;
|
|
441
|
-
this.waiting = false;
|
|
442
426
|
this.#version += 1;
|
|
443
427
|
void this.refreshQuota();
|
|
444
428
|
if (this.conversations !== null)
|
|
@@ -477,7 +461,7 @@ export class Chat {
|
|
|
477
461
|
async #settle(id, mine) {
|
|
478
462
|
clearTimeout(this.#poll);
|
|
479
463
|
try {
|
|
480
|
-
const read = await this.#
|
|
464
|
+
const read = await this.#chat.read(id);
|
|
481
465
|
if (mine !== this.#generation)
|
|
482
466
|
return;
|
|
483
467
|
this.#show(read, mine);
|
|
@@ -508,12 +492,6 @@ export class Chat {
|
|
|
508
492
|
this.#watching = true;
|
|
509
493
|
try {
|
|
510
494
|
for await (const event of job.watch(id, signal)) {
|
|
511
|
-
if (isQueued(event)) {
|
|
512
|
-
this.waiting = true;
|
|
513
|
-
continue;
|
|
514
|
-
}
|
|
515
|
-
if (RUN.has(event.type))
|
|
516
|
-
this.waiting = false;
|
|
517
495
|
session.apply(event);
|
|
518
496
|
}
|
|
519
497
|
}
|
|
@@ -523,7 +501,6 @@ export class Chat {
|
|
|
523
501
|
if (signal.aborted)
|
|
524
502
|
return;
|
|
525
503
|
this.#watching = false;
|
|
526
|
-
this.waiting = false;
|
|
527
504
|
await this.#idle(signal);
|
|
528
505
|
}
|
|
529
506
|
}
|
|
@@ -578,7 +555,7 @@ export function storedTurn(conversation, index, stored) {
|
|
|
578
555
|
outcome: null,
|
|
579
556
|
citations: stored.citations ?? [],
|
|
580
557
|
kind: stored.kind ?? undefined,
|
|
581
|
-
|
|
558
|
+
collection: stored.collection ?? undefined,
|
|
582
559
|
shared: stored.shared ?? undefined
|
|
583
560
|
};
|
|
584
561
|
}
|
package/dist/citations.d.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Where an answer came from.
|
|
3
3
|
*
|
|
4
|
-
* Two sources, one shape. The
|
|
5
|
-
*
|
|
6
|
-
* so a chip can open the document.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* Two sources, one shape. The app emits a `citations` event before the
|
|
5
|
+
* persona's answer, and that is the real one — it carries a document id,
|
|
6
|
+
* so a chip can open the document. Without one, the same chips are DERIVED
|
|
7
|
+
* from the "## Sources" block the persona writes, which names a title and a
|
|
8
|
+
* section but no id: those chips render and read, and cannot
|
|
9
9
|
* open a pane. A derived citation is marked `derived` so the UI can tell.
|
|
10
10
|
*/
|
|
11
|
-
import type {
|
|
11
|
+
import type { ChatCopy } from './copy';
|
|
12
12
|
export interface Citation {
|
|
13
13
|
n: number;
|
|
14
14
|
document_id: string;
|
|
@@ -86,7 +86,7 @@ export declare function formatVerified(iso: string): string | null;
|
|
|
86
86
|
* A `verified_at` that is present but not a date is treated as no date, for
|
|
87
87
|
* the same reason: what is shown must be what is known.
|
|
88
88
|
*/
|
|
89
|
-
export declare function trustMark(citation: Citation, copy:
|
|
89
|
+
export declare function trustMark(citation: Citation, copy: ChatCopy): string | null;
|
|
90
90
|
/** One addressable slice of a document — the granularity a citation names. */
|
|
91
91
|
export interface DocumentSection {
|
|
92
92
|
anchor: string;
|
package/dist/citations.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Where an answer came from.
|
|
3
3
|
*
|
|
4
|
-
* Two sources, one shape. The
|
|
5
|
-
*
|
|
6
|
-
* so a chip can open the document.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* Two sources, one shape. The app emits a `citations` event before the
|
|
5
|
+
* persona's answer, and that is the real one — it carries a document id,
|
|
6
|
+
* so a chip can open the document. Without one, the same chips are DERIVED
|
|
7
|
+
* from the "## Sources" block the persona writes, which names a title and a
|
|
8
|
+
* section but no id: those chips render and read, and cannot
|
|
9
9
|
* open a pane. A derived citation is marked `derived` so the UI can tell.
|
|
10
10
|
*/
|
|
11
11
|
/** Inline `[n]` markers, in order of first appearance.
|
|
@@ -40,7 +40,7 @@ export function splitSources(markdown) {
|
|
|
40
40
|
if (!heading || heading.index === undefined)
|
|
41
41
|
return { body: markdown, citations: [] };
|
|
42
42
|
// The block runs to the NEXT heading, not to the end of the answer. Cutting
|
|
43
|
-
// to the end loses anything
|
|
43
|
+
// to the end loses anything the persona wrote after its sources — a closing note,
|
|
44
44
|
// a caveat — and loses it silently, which is the worst way to lose it.
|
|
45
45
|
const after = markdown.slice(heading.index + heading[0].length);
|
|
46
46
|
const next = after.search(/^[ \t]{0,3}#{1,6}[ \t]/m);
|
package/dist/client.d.ts
CHANGED
|
@@ -8,21 +8,12 @@
|
|
|
8
8
|
* event type needs no change on either side.
|
|
9
9
|
*/
|
|
10
10
|
import type { Citation } from './citations';
|
|
11
|
-
/**
|
|
12
|
-
* How hard the persona works on one question: a reading budget and a model,
|
|
13
|
-
* both the operator's to set per deployment, never the colleague's concern.
|
|
14
|
-
* `quick` is always the default; `thorough` always means something — a
|
|
15
|
-
* bigger budget on the strongest model the room allows, even where that
|
|
16
|
-
* model is a local one — so a host never has a state where the choice is
|
|
17
|
-
* greyed out or explained away.
|
|
18
|
-
*/
|
|
19
|
-
export type Depth = 'quick' | 'thorough';
|
|
20
11
|
/** A Claude Code message, whole. `content` is a plain string only on a user frame. */
|
|
21
12
|
export interface AgentMessage {
|
|
22
13
|
id?: string;
|
|
23
14
|
content?: Array<Record<string, unknown>> | string;
|
|
24
15
|
}
|
|
25
|
-
/** One Claude Code stream event, or one of the
|
|
16
|
+
/** One Claude Code stream event, or one of the app's own frames. Typed
|
|
26
17
|
* only where we branch on it, and `type` stays open: a new event type needs
|
|
27
18
|
* no change here. */
|
|
28
19
|
export interface AgentEvent {
|
|
@@ -43,7 +34,7 @@ export interface AgentEvent {
|
|
|
43
34
|
id?: string;
|
|
44
35
|
};
|
|
45
36
|
};
|
|
46
|
-
/** A whole message
|
|
37
|
+
/** A whole message. */
|
|
47
38
|
message?: AgentMessage | string;
|
|
48
39
|
/** A user frame the CLI echoed back as the turn that consumes it starts
|
|
49
40
|
* (`--replay-user-messages`): the reader's side of a session. */
|
|
@@ -60,57 +51,37 @@ export interface AgentEvent {
|
|
|
60
51
|
/** What a run returned under `--json-schema`, on its `result`. */
|
|
61
52
|
structured_output?: unknown;
|
|
62
53
|
error?: string;
|
|
63
|
-
/** On
|
|
54
|
+
/** On an `agent_error` this layer raised: the HTTP status of a route that
|
|
64
55
|
* answered and would not stream. 429 is a spent allowance. */
|
|
65
56
|
status?: number;
|
|
66
|
-
/** On
|
|
57
|
+
/** On an `agent_error` this layer raised: a stream that opened and died
|
|
67
58
|
* before its terminal frame. The turn may well go on without the page. */
|
|
68
59
|
dropped?: boolean;
|
|
69
60
|
detail?: string;
|
|
70
61
|
tools?: string[];
|
|
71
62
|
model?: string;
|
|
72
|
-
/**
|
|
73
|
-
*
|
|
74
|
-
|
|
75
|
-
* follow-up questions as plain strings. Narrowed on `type` by whoever
|
|
76
|
-
* reads it (`Transcript.apply`).
|
|
77
|
-
*/
|
|
78
|
-
items?: Citation[] | string[];
|
|
63
|
+
/** The sources of a `citations` frame, which the app emits before the
|
|
64
|
+
* persona's answer. */
|
|
65
|
+
items?: Citation[];
|
|
79
66
|
[key: string]: unknown;
|
|
80
67
|
}
|
|
81
|
-
/**
|
|
82
|
-
* The library's word that an ask waits behind somebody else's: repeated at
|
|
83
|
-
* least every 30 s until the agent starts, and what keeps an idle read
|
|
84
|
-
* timeout from firing meanwhile. `message` is the library's own sentence
|
|
85
|
-
* ("another question is being answered; waiting for it to finish"); a page
|
|
86
|
-
* says it in its persona's words (`copy.waiting`).
|
|
87
|
-
*/
|
|
88
|
-
export interface QueuedEvent extends AgentEvent {
|
|
89
|
-
type: 'queued';
|
|
90
|
-
message: string;
|
|
91
|
-
}
|
|
92
|
-
/** The frame is the library's `queued`, by its type alone. */
|
|
93
|
-
export declare function isQueued(event: AgentEvent): event is QueuedEvent;
|
|
94
68
|
export interface AskOptions {
|
|
95
69
|
question: string;
|
|
96
70
|
resume?: string | null;
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
/** Files
|
|
71
|
+
/** The one collection this question is asked of, by name. Absent asks
|
|
72
|
+
* across every collection the person may read. */
|
|
73
|
+
collection?: string;
|
|
74
|
+
/** Files the persona reads for this question. Switches the request to multipart. */
|
|
101
75
|
files?: File[];
|
|
102
76
|
/** What kind of turn this ask should become. Absent means an ordinary
|
|
103
77
|
* question; a host that offers a study artefact sends its own kind
|
|
104
78
|
* (`'briefing'`) and interprets what comes back accordingly — the wire
|
|
105
79
|
* vocabulary is the host's own, never a fixed set here. */
|
|
106
80
|
kind?: string;
|
|
107
|
-
/** How hard the persona should work on this one. Absent asks for
|
|
108
|
-
* whatever the route does by default, so a host that never reads
|
|
109
|
-
* `Composer`'s `depth` sends exactly what it always has. */
|
|
110
|
-
depth?: Depth;
|
|
111
81
|
signal?: AbortSignal;
|
|
112
|
-
/** Where the ask lands
|
|
113
|
-
|
|
82
|
+
/** Where the ask lands: the persona's ask route,
|
|
83
|
+
* `/api/agent/{persona}/ask`. */
|
|
84
|
+
endpoint: string;
|
|
114
85
|
/** Override for the ambient `fetch`, e.g. a caller-authenticated wrapper. */
|
|
115
86
|
fetch?: typeof fetch;
|
|
116
87
|
}
|
|
@@ -120,7 +91,7 @@ export interface AskOptions {
|
|
|
120
91
|
* It never throws. Every way a stream can fail to open or die half-way through
|
|
121
92
|
* — the network dropping, an expired session redirecting the POST to an
|
|
122
93
|
* identity provider the browser then blocks on CORS, a server closing the
|
|
123
|
-
* connection mid-answer — comes back as one `
|
|
94
|
+
* connection mid-answer — comes back as one `agent_error` event and a
|
|
124
95
|
* finished iteration, because a host's loop is what re-enables its Send
|
|
125
96
|
* button. A thrown generator leaves that loop unfinished: Pebblestone's
|
|
126
97
|
* console sat with Send disabled and no message on screen, indefinitely,
|
|
@@ -145,6 +116,6 @@ export interface WatchOptions {
|
|
|
145
116
|
* difference: a clean close with no `result` is not a failure. A run the
|
|
146
117
|
* reader stopped ends exactly that way, and so does a session that is
|
|
147
118
|
* waiting between runs, so only a stream that would not open, or broke,
|
|
148
|
-
* yields `
|
|
119
|
+
* yields `agent_error`. Fold the events with `Session`.
|
|
149
120
|
*/
|
|
150
121
|
export declare function watch(options: WatchOptions): AsyncGenerator<AgentEvent>;
|
package/dist/client.js
CHANGED
|
@@ -7,17 +7,13 @@
|
|
|
7
7
|
* backend sent, so the Console renders the real thing and a new Claude Code
|
|
8
8
|
* event type needs no change on either side.
|
|
9
9
|
*/
|
|
10
|
-
/** The frame is the library's `queued`, by its type alone. */
|
|
11
|
-
export function isQueued(event) {
|
|
12
|
-
return event.type === 'queued';
|
|
13
|
-
}
|
|
14
10
|
/**
|
|
15
11
|
* The request body for one ask.
|
|
16
12
|
*
|
|
17
13
|
* JSON when there are no files, multipart when there are — one endpoint, two
|
|
18
14
|
* encodings, because a `File` cannot cross a JSON body and base64 would double
|
|
19
15
|
* a 20 MB attachment on the wire for nothing. The multipart field names are
|
|
20
|
-
* the form convention (`
|
|
16
|
+
* the form convention (`files[]`) rather than the JSON key.
|
|
21
17
|
*
|
|
22
18
|
* `Content-Type` is deliberately absent for multipart: setting it by hand
|
|
23
19
|
* omits the boundary the browser generates, and the server then reads zero
|
|
@@ -32,10 +28,8 @@ function requestInit(options, signal) {
|
|
|
32
28
|
body: JSON.stringify({
|
|
33
29
|
question: options.question,
|
|
34
30
|
resume: options.resume ?? null,
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
kind: options.kind,
|
|
38
|
-
depth: options.depth
|
|
31
|
+
collection: options.collection,
|
|
32
|
+
kind: options.kind
|
|
39
33
|
}),
|
|
40
34
|
signal
|
|
41
35
|
};
|
|
@@ -44,14 +38,12 @@ function requestInit(options, signal) {
|
|
|
44
38
|
form.append('question', options.question);
|
|
45
39
|
if (options.resume)
|
|
46
40
|
form.append('resume', options.resume);
|
|
47
|
-
|
|
48
|
-
form.append('
|
|
41
|
+
if (options.collection)
|
|
42
|
+
form.append('collection', options.collection);
|
|
49
43
|
for (const file of options.files)
|
|
50
44
|
form.append('files[]', file, file.name);
|
|
51
45
|
if (options.kind)
|
|
52
46
|
form.append('kind', options.kind);
|
|
53
|
-
if (options.depth)
|
|
54
|
-
form.append('depth', options.depth);
|
|
55
47
|
return { method: 'POST', credentials: 'include', body: form, signal };
|
|
56
48
|
}
|
|
57
49
|
/**
|
|
@@ -60,7 +52,7 @@ function requestInit(options, signal) {
|
|
|
60
52
|
* It never throws. Every way a stream can fail to open or die half-way through
|
|
61
53
|
* — the network dropping, an expired session redirecting the POST to an
|
|
62
54
|
* identity provider the browser then blocks on CORS, a server closing the
|
|
63
|
-
* connection mid-answer — comes back as one `
|
|
55
|
+
* connection mid-answer — comes back as one `agent_error` event and a
|
|
64
56
|
* finished iteration, because a host's loop is what re-enables its Send
|
|
65
57
|
* button. A thrown generator leaves that loop unfinished: Pebblestone's
|
|
66
58
|
* console sat with Send disabled and no message on screen, indefinitely,
|
|
@@ -71,7 +63,7 @@ function requestInit(options, signal) {
|
|
|
71
63
|
* where the turn is rendered.
|
|
72
64
|
*/
|
|
73
65
|
export async function* ask(options) {
|
|
74
|
-
yield* read(() => (options.fetch ?? fetch)(options.endpoint
|
|
66
|
+
yield* read(() => (options.fetch ?? fetch)(options.endpoint, requestInit(options, options.signal)), options.signal, true);
|
|
75
67
|
}
|
|
76
68
|
/**
|
|
77
69
|
* Async-iterate a session somebody else started: every event from the first,
|
|
@@ -81,7 +73,7 @@ export async function* ask(options) {
|
|
|
81
73
|
* difference: a clean close with no `result` is not a failure. A run the
|
|
82
74
|
* reader stopped ends exactly that way, and so does a session that is
|
|
83
75
|
* waiting between runs, so only a stream that would not open, or broke,
|
|
84
|
-
* yields `
|
|
76
|
+
* yields `agent_error`. Fold the events with `Session`.
|
|
85
77
|
*/
|
|
86
78
|
export async function* watch(options) {
|
|
87
79
|
yield* read(() => (options.fetch ?? fetch)(options.endpoint, {
|
|
@@ -101,7 +93,7 @@ async function* read(open, signal, terminal) {
|
|
|
101
93
|
// A reader who pressed stop asked for this one; it is not a failure.
|
|
102
94
|
if (signal?.aborted)
|
|
103
95
|
return;
|
|
104
|
-
yield { type: '
|
|
96
|
+
yield { type: 'agent_error' };
|
|
105
97
|
return;
|
|
106
98
|
}
|
|
107
99
|
// A session that expired mid-conversation is the shape this catches: the
|
|
@@ -115,11 +107,11 @@ async function* read(open, signal, terminal) {
|
|
|
115
107
|
// check at the end of this function catches it if it really says nothing.
|
|
116
108
|
const kind = (response.headers.get('content-type') ?? '').toLowerCase();
|
|
117
109
|
if (!response.ok) {
|
|
118
|
-
yield { type: '
|
|
110
|
+
yield { type: 'agent_error', status: response.status };
|
|
119
111
|
return;
|
|
120
112
|
}
|
|
121
113
|
if (!response.body || (kind && !kind.includes('text/event-stream'))) {
|
|
122
|
-
yield { type: '
|
|
114
|
+
yield { type: 'agent_error' };
|
|
123
115
|
return;
|
|
124
116
|
}
|
|
125
117
|
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
|
|
@@ -157,7 +149,7 @@ async function* read(open, signal, terminal) {
|
|
|
157
149
|
catch {
|
|
158
150
|
if (signal?.aborted)
|
|
159
151
|
return;
|
|
160
|
-
yield { type: '
|
|
152
|
+
yield { type: 'agent_error', dropped: true };
|
|
161
153
|
return;
|
|
162
154
|
}
|
|
163
155
|
if (chunk.done)
|
|
@@ -171,7 +163,7 @@ async function* read(open, signal, terminal) {
|
|
|
171
163
|
const event = parse(frame);
|
|
172
164
|
if (!event)
|
|
173
165
|
continue;
|
|
174
|
-
if (event.type === 'result' || event.type === '
|
|
166
|
+
if (event.type === 'result' || event.type === 'agent_error')
|
|
175
167
|
ended = true;
|
|
176
168
|
yield event;
|
|
177
169
|
}
|
|
@@ -182,10 +174,10 @@ async function* read(open, signal, terminal) {
|
|
|
182
174
|
// finished turn look like one that died.
|
|
183
175
|
const last = parse(buffered);
|
|
184
176
|
if (last) {
|
|
185
|
-
if (last.type === 'result' || last.type === '
|
|
177
|
+
if (last.type === 'result' || last.type === 'agent_error')
|
|
186
178
|
ended = true;
|
|
187
179
|
yield last;
|
|
188
180
|
}
|
|
189
181
|
if (terminal && !ended && !signal?.aborted)
|
|
190
|
-
yield { type: '
|
|
182
|
+
yield { type: 'agent_error', dropped: true };
|
|
191
183
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
A whole investigation, as ONE line the reader can open.
|
|
3
3
|
|
|
4
4
|
Not a stack. Fifteen tool rows between the question and the first word of the
|
|
5
|
-
answer is the
|
|
5
|
+
answer is the machinery on show, and a running commentary of "Let me
|
|
6
6
|
check…" is worse: it narrates a mechanism the persona is under instruction
|
|
7
7
|
never to mention. So there is exactly one row in every state — "Working…"
|
|
8
8
|
while it runs, a count of what was done once it settles — and the steps are
|