@guuey/chat 0.5.0 → 0.6.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/dist/hitl.d.ts +58 -0
- package/dist/hitl.d.ts.map +1 -0
- package/dist/hitl.js +81 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/native/components.d.ts +18 -2
- package/dist/native/components.d.ts.map +1 -1
- package/dist/native/components.js +49 -0
- package/dist/native/transcript.d.ts +1 -1
- package/dist/native/transcript.d.ts.map +1 -1
- package/dist/native/transcript.js +2 -2
- package/dist/native.d.ts +1 -1
- package/dist/native.d.ts.map +1 -1
- package/dist/native.js +1 -1
- package/dist/plan.d.ts +15 -0
- package/dist/plan.d.ts.map +1 -1
- package/dist/plan.js +213 -15
- package/dist/policy.d.ts +4 -0
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +2 -0
- package/dist/react/components.d.ts +42 -5
- package/dist/react/components.d.ts.map +1 -1
- package/dist/react/components.js +51 -1
- package/dist/react/guuey-chat.d.ts +92 -7
- package/dist/react/guuey-chat.d.ts.map +1 -1
- package/dist/react/guuey-chat.js +114 -11
- package/dist/react/transcript.d.ts +1 -1
- package/dist/react/transcript.d.ts.map +1 -1
- package/dist/react/transcript.js +2 -2
- package/dist/react/use-transcript.d.ts +21 -2
- package/dist/react/use-transcript.d.ts.map +1 -1
- package/dist/react/use-transcript.js +57 -3
- package/dist/react.d.ts +2 -2
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +1 -1
- package/dist/strings.d.ts +13 -0
- package/dist/strings.d.ts.map +1 -1
- package/dist/strings.js +8 -0
- package/dist/types.d.ts +146 -7
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/corpus/README.md +17 -1
- package/src/corpus/__snapshots__/corpus.test.ts.snap +348 -0
- package/src/corpus/drive.ts +3 -3
- package/src/corpus/fixtures.ts +249 -1
- package/src/hitl.ts +103 -0
- package/src/index.ts +15 -0
- package/src/native/components.tsx +136 -1
- package/src/native/transcript.tsx +4 -3
- package/src/native.tsx +1 -0
- package/src/plan.ts +235 -22
- package/src/policy.ts +4 -0
- package/src/react/components.tsx +171 -8
- package/src/react/guuey-chat.tsx +222 -15
- package/src/react/transcript.tsx +4 -3
- package/src/react/use-transcript.ts +83 -5
- package/src/react.tsx +3 -1
- package/src/strings.ts +25 -0
- package/src/types.ts +152 -6
- package/styles.css +21 -0
package/src/corpus/drive.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { Reducer, type AgEvent } from "@silverprotocol/core";
|
|
9
9
|
import type { InvokeTurnEvent } from "@guuey/agent-client";
|
|
10
|
-
import type {
|
|
10
|
+
import type { ProfilePromptInput, TranscriptInputs, TranscriptMessage } from "../types.js";
|
|
11
11
|
|
|
12
12
|
export interface DriveOptions {
|
|
13
13
|
userText?: string;
|
|
@@ -22,7 +22,7 @@ export interface DriveOptions {
|
|
|
22
22
|
historyCards?: TranscriptInputs["historyCards"];
|
|
23
23
|
historyState?: TranscriptInputs["historyState"];
|
|
24
24
|
/** Resolve accumulated prompts to this state (fixture 4's answered leg). */
|
|
25
|
-
promptState?:
|
|
25
|
+
promptState?: ProfilePromptInput["state"];
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
export function driveTurn(events: InvokeTurnEvent[], opts: DriveOptions = {}): TranscriptInputs {
|
|
@@ -32,7 +32,7 @@ export function driveTurn(events: InvokeTurnEvent[], opts: DriveOptions = {}): T
|
|
|
32
32
|
let assistantText = "";
|
|
33
33
|
let activeTool: string | null = null;
|
|
34
34
|
let error: TranscriptInputs["error"] = null;
|
|
35
|
-
const prompts:
|
|
35
|
+
const prompts: ProfilePromptInput[] = [];
|
|
36
36
|
let done = false;
|
|
37
37
|
|
|
38
38
|
for (const ev of events) {
|
package/src/corpus/fixtures.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* THE STANDING RULE (see README.md): a new weird transcript found in
|
|
9
9
|
* production becomes a fixture BEFORE its fix lands. The corpus only grows.
|
|
10
10
|
*/
|
|
11
|
-
import type
|
|
11
|
+
import { Reducer, type AgEvent } from "@silverprotocol/core";
|
|
12
12
|
import type { InvokeTurnEvent } from "@guuey/agent-client";
|
|
13
13
|
import type { TranscriptInputs } from "../types.js";
|
|
14
14
|
import {
|
|
@@ -320,6 +320,99 @@ export function userSendFailure(): TranscriptInputs {
|
|
|
320
320
|
});
|
|
321
321
|
}
|
|
322
322
|
|
|
323
|
+
/**
|
|
324
|
+
* 19. persisted-plus-live — the turn-scoped fold composition (portal's
|
|
325
|
+
* persisted-overlay data model): a persisted prefix the session's Reducer
|
|
326
|
+
* never saw + a session fold covering the trailing turns, one settled (its
|
|
327
|
+
* card ALSO persisted — the dedupe case) and one live with a running tool.
|
|
328
|
+
*/
|
|
329
|
+
export function persistedPlusLive(): TranscriptInputs {
|
|
330
|
+
const reducer = new Reducer();
|
|
331
|
+
let seq = 0;
|
|
332
|
+
const s = (): number => seq++;
|
|
333
|
+
const push = (events: AgEvent[]): void => {
|
|
334
|
+
for (const ev of events) reducer.push(ev);
|
|
335
|
+
};
|
|
336
|
+
// Session turn A (settled — turn.done carries the outcome):
|
|
337
|
+
push([
|
|
338
|
+
{ type: "turn.start", threadId: "thread-corpus", turnId: "turn-a", seq: s() },
|
|
339
|
+
{ type: "message.start", id: "msg-a", role: "assistant", turnId: "turn-a", threadId: "thread-corpus", seq: s() },
|
|
340
|
+
{ type: "text.start", id: "ta", seq: s() },
|
|
341
|
+
{ type: "text.delta", id: "ta", delta: "Booked the second option.", seq: s() },
|
|
342
|
+
{ type: "text.end", id: "ta", seq: s() },
|
|
343
|
+
{ type: "tool.start", toolCallId: "tA", name: "ggui_render", seq: s() },
|
|
344
|
+
{
|
|
345
|
+
type: "tool.done",
|
|
346
|
+
toolCallId: "tA",
|
|
347
|
+
content: [],
|
|
348
|
+
outcome: "ok",
|
|
349
|
+
uiData: { sessionId: GGUI_SLICE.sessionId, resourceUri: GGUI_RESOURCE_URI, action: "create" },
|
|
350
|
+
_meta: { [GGUI_META_KEY]: GGUI_SLICE },
|
|
351
|
+
seq: s(),
|
|
352
|
+
},
|
|
353
|
+
{ type: "turn.done", turnId: "turn-a", outcome: { type: "success" }, seq: s() },
|
|
354
|
+
]);
|
|
355
|
+
// Session turn B (live — no turn.done yet): one completed tool already in
|
|
356
|
+
// the fold, a second still active (the fold materializes a tool-call at
|
|
357
|
+
// `tool.done`; the ACTIVE call is the status line's job, not a row).
|
|
358
|
+
push([
|
|
359
|
+
{ type: "turn.start", threadId: "thread-corpus", turnId: "turn-b", seq: s() },
|
|
360
|
+
{ type: "message.start", id: "msg-b", role: "assistant", turnId: "turn-b", threadId: "thread-corpus", seq: s() },
|
|
361
|
+
{ type: "tool.start", toolCallId: "tB", name: "load_booking", turnId: "turn-b", seq: s() },
|
|
362
|
+
{ type: "tool.args.assembled", toolCallId: "tB", input: { id: "second" }, turnId: "turn-b", seq: s() },
|
|
363
|
+
{
|
|
364
|
+
type: "tool.done",
|
|
365
|
+
toolCallId: "tB",
|
|
366
|
+
content: [{ type: "text", text: "loaded" }],
|
|
367
|
+
outcome: "ok",
|
|
368
|
+
turnId: "turn-b",
|
|
369
|
+
seq: s(),
|
|
370
|
+
},
|
|
371
|
+
]);
|
|
372
|
+
return {
|
|
373
|
+
result: reducer.result(),
|
|
374
|
+
assistantText: "",
|
|
375
|
+
status: "using-tool",
|
|
376
|
+
statusElapsedMs: 0,
|
|
377
|
+
activeTool: "check_calendar",
|
|
378
|
+
error: null,
|
|
379
|
+
prompts: [],
|
|
380
|
+
// The flat transcript: one PERSISTED turn the fold never saw, then the
|
|
381
|
+
// session turns (turn A's settled text row is present — the thread
|
|
382
|
+
// viewer's liveTail/refetch carries it; turn B is in flight, no row).
|
|
383
|
+
messages: [
|
|
384
|
+
{ role: "user", text: "plan my trip" },
|
|
385
|
+
{ role: "assistant", text: "Here are some options." },
|
|
386
|
+
{ role: "user", text: "book the second one" },
|
|
387
|
+
{ role: "assistant", text: "Booked the second option." },
|
|
388
|
+
{ role: "user", text: "now move it to Thursday" },
|
|
389
|
+
],
|
|
390
|
+
historyCards: [
|
|
391
|
+
// An OLD persisted card (pre-session) — must render.
|
|
392
|
+
{
|
|
393
|
+
seq: 1,
|
|
394
|
+
at: "2026-08-15T00:00:00Z",
|
|
395
|
+
cardSnapshot: {
|
|
396
|
+
parts: [
|
|
397
|
+
{ type: "tool-result", toolCallId: "old1", content: [], uiData: { resourceUri: "ui://ggui/render/old/1" } },
|
|
398
|
+
],
|
|
399
|
+
},
|
|
400
|
+
},
|
|
401
|
+
// Turn A's card, ALSO persisted by the read plane — the live fold's
|
|
402
|
+
// mount owns this identity; the snapshot must dedupe away.
|
|
403
|
+
{
|
|
404
|
+
seq: 5,
|
|
405
|
+
at: "2026-08-15T00:05:00Z",
|
|
406
|
+
cardSnapshot: {
|
|
407
|
+
parts: [
|
|
408
|
+
{ type: "tool-result", toolCallId: "tA", content: [], uiData: { resourceUri: GGUI_RESOURCE_URI } },
|
|
409
|
+
],
|
|
410
|
+
},
|
|
411
|
+
},
|
|
412
|
+
],
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
323
416
|
/** 17. stalled-then-adopted — #192: calm identical, debug carries the marker. */
|
|
324
417
|
export function stalledThenAdopted(): { adopted: TranscriptInputs; streamed: TranscriptInputs } {
|
|
325
418
|
const build = (): InvokeTurnEvent[] => {
|
|
@@ -336,3 +429,158 @@ export function stalledThenAdopted(): { adopted: TranscriptInputs; streamed: Tra
|
|
|
336
429
|
streamed: driveTurn(build(), { userText: "move my booking" }),
|
|
337
430
|
};
|
|
338
431
|
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* 20. hitl-grant-modes (spec draft.2, silverprotocol#16) — a paused turn's
|
|
435
|
+
* PERSISTED ask declares three asker-scoped accept variants. Driven through
|
|
436
|
+
* the real Reducer: the declaration the card renders from is
|
|
437
|
+
* `turn.done.outcome:"paused".asks[]`, exactly what a late-joining client
|
|
438
|
+
* sees. Ids are deliberately arbitrary strings — display must key off
|
|
439
|
+
* label/description, never id semantics.
|
|
440
|
+
*/
|
|
441
|
+
export function hitlGrantModes(): TranscriptInputs {
|
|
442
|
+
const reducer = new Reducer();
|
|
443
|
+
let seq = 0;
|
|
444
|
+
const s = (): number => seq++;
|
|
445
|
+
const events: AgEvent[] = [
|
|
446
|
+
{ type: "turn.start", threadId: "thread-hitl", turnId: "turn-h", seq: s() },
|
|
447
|
+
{ type: "message.start", id: "msg-h", role: "assistant", turnId: "turn-h", threadId: "thread-hitl", seq: s() },
|
|
448
|
+
{ type: "text.start", id: "th", seq: s() },
|
|
449
|
+
{ type: "text.delta", id: "th", delta: "I can remember this preference.", seq: s() },
|
|
450
|
+
{ type: "text.end", id: "th", seq: s() },
|
|
451
|
+
{
|
|
452
|
+
type: "turn.done",
|
|
453
|
+
turnId: "turn-h",
|
|
454
|
+
outcome: {
|
|
455
|
+
type: "paused",
|
|
456
|
+
asks: [
|
|
457
|
+
{
|
|
458
|
+
askId: "ask-remember-1",
|
|
459
|
+
kind: "approval",
|
|
460
|
+
message: "Remember your seating preference?",
|
|
461
|
+
grantModes: [
|
|
462
|
+
{ id: "m.durable", label: "Always", description: "Keep it for every future chat" },
|
|
463
|
+
{ id: "m.scoped", label: "Just this chat" },
|
|
464
|
+
],
|
|
465
|
+
requestState: "rs-bytes-1",
|
|
466
|
+
},
|
|
467
|
+
],
|
|
468
|
+
},
|
|
469
|
+
seq: s(),
|
|
470
|
+
},
|
|
471
|
+
];
|
|
472
|
+
for (const ev of events) reducer.push(ev);
|
|
473
|
+
return {
|
|
474
|
+
result: reducer.result(),
|
|
475
|
+
assistantText: "",
|
|
476
|
+
status: "ready",
|
|
477
|
+
statusElapsedMs: 0,
|
|
478
|
+
activeTool: null,
|
|
479
|
+
error: null,
|
|
480
|
+
prompts: [], // the test composes these via hitlPromptsFromFold + the answer ledger
|
|
481
|
+
messages: [{ role: "user", text: "book my usual seat" }],
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* 21. notice-rows (spec draft.2) — both arrival paths: a flat adapter
|
|
487
|
+
* notice (the portal self-hosted mapping) and a fold-borne framework
|
|
488
|
+
* notice (the claude informational promotion). Labeled, non-conversational,
|
|
489
|
+
* never agent-voiced; provenance shows only under debug.
|
|
490
|
+
*/
|
|
491
|
+
export function noticeRows(): TranscriptInputs {
|
|
492
|
+
const reducer = new Reducer();
|
|
493
|
+
let seq = 0;
|
|
494
|
+
const s = (): number => seq++;
|
|
495
|
+
const events: AgEvent[] = [
|
|
496
|
+
{ type: "turn.start", threadId: "thread-n", turnId: "turn-n", seq: s() },
|
|
497
|
+
{
|
|
498
|
+
type: "message.start",
|
|
499
|
+
id: "msg-n0",
|
|
500
|
+
role: "notice",
|
|
501
|
+
noticeSource: "framework",
|
|
502
|
+
turnId: "turn-n",
|
|
503
|
+
threadId: "thread-n",
|
|
504
|
+
seq: s(),
|
|
505
|
+
},
|
|
506
|
+
// The notice's text lands while it is the sole open message, so block
|
|
507
|
+
// routing is unambiguous; the assistant message opens after.
|
|
508
|
+
{ type: "text.start", id: "tn0", seq: s() },
|
|
509
|
+
{ type: "text.delta", id: "tn0", delta: "Model fell back to concise mode", seq: s() },
|
|
510
|
+
{ type: "text.end", id: "tn0", seq: s() },
|
|
511
|
+
{ type: "message.end", id: "msg-n0", turnId: "turn-n", seq: s() },
|
|
512
|
+
{ type: "message.start", id: "msg-n1", role: "assistant", turnId: "turn-n", threadId: "thread-n", seq: s() },
|
|
513
|
+
{ type: "text.start", id: "tn", seq: s() },
|
|
514
|
+
{ type: "text.delta", id: "tn", delta: "Done.", seq: s() },
|
|
515
|
+
{ type: "text.end", id: "tn", seq: s() },
|
|
516
|
+
{ type: "turn.done", turnId: "turn-n", outcome: { type: "success" }, seq: s() },
|
|
517
|
+
];
|
|
518
|
+
for (const ev of events) reducer.push(ev);
|
|
519
|
+
return {
|
|
520
|
+
result: reducer.result(),
|
|
521
|
+
assistantText: "",
|
|
522
|
+
status: "ready",
|
|
523
|
+
statusElapsedMs: 0,
|
|
524
|
+
activeTool: null,
|
|
525
|
+
error: null,
|
|
526
|
+
prompts: [],
|
|
527
|
+
messages: [
|
|
528
|
+
{ role: "notice", text: "Session resumed on a new device", noticeSource: "adapter" },
|
|
529
|
+
{ role: "user", text: "hi" },
|
|
530
|
+
{ role: "assistant", text: "Done." },
|
|
531
|
+
],
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* 22. promoted-view — guuey#204 "promote and reference": the mount a host
|
|
537
|
+
* stage shows chips in the transcript; every other mount stays full; no
|
|
538
|
+
* key (or a stale one) is byte-identical to today.
|
|
539
|
+
*/
|
|
540
|
+
export function promotedView(): { inputs: TranscriptInputs; promotedKey: string } {
|
|
541
|
+
const s = seqSource();
|
|
542
|
+
const events: InvokeTurnEvent[] = [session, frame(boot(s), { status: "thinking" })];
|
|
543
|
+
for (const id of ["t1", "t2"] as const) {
|
|
544
|
+
events.push(
|
|
545
|
+
frame(
|
|
546
|
+
toolPart(s, id, "ggui_render", {
|
|
547
|
+
uiData: { uri: `ui://cards/${id}`, mimeType: "text/html", text: `<div>${id}</div>` },
|
|
548
|
+
content: [],
|
|
549
|
+
}),
|
|
550
|
+
{ status: "using-tool", activeTool: "ggui_render" },
|
|
551
|
+
),
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
events.push(doneEvent);
|
|
555
|
+
const inputs = driveTurn(events, { userText: "show me two cards" });
|
|
556
|
+
return { inputs, promotedKey: "view.t2" };
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* 23. meta-less-locator — guuey#209 route-A finding: a producer that
|
|
561
|
+
* withholds tool-result `_meta` (ggui's non-prod posture; any plain-locator
|
|
562
|
+
* MCP server) yields a fold block with NO uiData and NO _meta — AgJSON §2.1
|
|
563
|
+
* routes the sibling structuredContent to the MODEL channel — and the
|
|
564
|
+
* `ui://` locator rides `structuredContent.resourceUri`. Wire keys per the
|
|
565
|
+
* 2026-08-16 dev dump: type,toolCallId,content,outcome,isError,
|
|
566
|
+
* structuredContent. Before #209 this rendered NOTHING (dark, not
|
|
567
|
+
* "expired") and persisted no placeholder row.
|
|
568
|
+
*/
|
|
569
|
+
export function metaLessLocator(): TranscriptInputs {
|
|
570
|
+
const s = seqSource();
|
|
571
|
+
const events: InvokeTurnEvent[] = [
|
|
572
|
+
session,
|
|
573
|
+
frame(boot(s), { status: "thinking" }),
|
|
574
|
+
frame(
|
|
575
|
+
toolPart(s, "t1", "ggui_render", {
|
|
576
|
+
content: [{ type: "text", text: "rendered" }],
|
|
577
|
+
outcome: "ok",
|
|
578
|
+
isError: false,
|
|
579
|
+
structuredContent: { resourceUri: "ui://ggui/render/render_dark/h1", sessionId: "render_dark" },
|
|
580
|
+
}),
|
|
581
|
+
{ status: "using-tool", activeTool: "ggui_render" },
|
|
582
|
+
),
|
|
583
|
+
doneEvent,
|
|
584
|
+
];
|
|
585
|
+
return driveTurn(events, { userText: "render a slot picker" });
|
|
586
|
+
}
|
package/src/hitl.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* R10's AgJSON HITL half (spec draft.2, silverprotocol/typescript-sdk#16):
|
|
3
|
+
* lifting persisted asks out of the fold, and constructing validated
|
|
4
|
+
* answers.
|
|
5
|
+
*
|
|
6
|
+
* Two contract points are load-bearing here:
|
|
7
|
+
*
|
|
8
|
+
* - **Asks render from the PERSISTED declaration** — `AgPausedAsk` in
|
|
9
|
+
* `turn.done.outcome:"paused".asks[]`. The live `hitl.ask` event is
|
|
10
|
+
* live-only by spec (§1.3); a client that joins a thread late renders
|
|
11
|
+
* the paused record, so this module reads ONLY that record and both
|
|
12
|
+
* paths converge.
|
|
13
|
+
* - **Every answer is validated BEFORE dispatch** — `validateHitlAnswer`
|
|
14
|
+
* from core enforces required-iff-declared, echo-must-be-declared, and
|
|
15
|
+
* the `requestState` byte-echo MUST. `buildHitlAnswer` constructs
|
|
16
|
+
* answers that pass by construction and THROWS if they don't: a failed
|
|
17
|
+
* validation here is a kit bug, never a user-visible state.
|
|
18
|
+
*
|
|
19
|
+
* Answer states encode guuey's ratified dismissal ruling (#16): dismissal
|
|
20
|
+
* maps to `cancelled` (still-pending, re-askable — the card stays
|
|
21
|
+
* answerable), `declined` is the durable explicit deny.
|
|
22
|
+
*/
|
|
23
|
+
import {
|
|
24
|
+
validateHitlAnswer,
|
|
25
|
+
type AgGrantMode,
|
|
26
|
+
type AgHitlAnswer,
|
|
27
|
+
type AgPausedAsk,
|
|
28
|
+
type AgReduceResult,
|
|
29
|
+
} from "@silverprotocol/core";
|
|
30
|
+
import type { HitlPromptInput } from "./types.js";
|
|
31
|
+
|
|
32
|
+
/** A host-side record of how an ask was answered (the assembler's ledger value). */
|
|
33
|
+
export interface HitlAnswerRecord {
|
|
34
|
+
status: "resolved" | "declined" | "cancelled";
|
|
35
|
+
/** Present iff `status === "resolved"` and the ask declared modes. */
|
|
36
|
+
grantModeId?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** A user action on a HITL card: pick a declared mode, decline, or dismiss. */
|
|
40
|
+
export type HitlPromptAction = { grantModeId: string } | "accept" | "decline" | "dismiss";
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Display text for a grant mode: the asker's `label`, else the id as
|
|
44
|
+
* LITERAL text. Mode semantics are asker-scoped (normative, spec §7):
|
|
45
|
+
* falling back to the id displays identity, never interpreted meaning —
|
|
46
|
+
* a renderer must not special-case id strings.
|
|
47
|
+
*/
|
|
48
|
+
export function grantModeDisplay(mode: AgGrantMode): string {
|
|
49
|
+
return mode.label ?? mode.id;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Lift every persisted ask in the fold into an R10 prompt input, merged
|
|
54
|
+
* with the host's answer ledger (keyed by askId). Pure; input order is the
|
|
55
|
+
* fold's turn order.
|
|
56
|
+
*/
|
|
57
|
+
export function hitlPromptsFromFold(
|
|
58
|
+
result: AgReduceResult | null,
|
|
59
|
+
answers: Readonly<Record<string, HitlAnswerRecord>> = {},
|
|
60
|
+
): HitlPromptInput[] {
|
|
61
|
+
if (result === null) return [];
|
|
62
|
+
const prompts: HitlPromptInput[] = [];
|
|
63
|
+
for (const turn of result.turns) {
|
|
64
|
+
if (turn.outcome?.type !== "paused") continue;
|
|
65
|
+
for (const ask of turn.outcome.asks) {
|
|
66
|
+
const answer = answers[ask.askId];
|
|
67
|
+
prompts.push({
|
|
68
|
+
id: ask.askId,
|
|
69
|
+
kind: "hitl",
|
|
70
|
+
ask,
|
|
71
|
+
state: answer?.status ?? "pending",
|
|
72
|
+
...(answer?.grantModeId !== undefined ? { grantModeId: answer.grantModeId } : {}),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return prompts;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Construct the wire answer for a card action and validate it against the
|
|
81
|
+
* ask's own persisted record before anything dispatches it.
|
|
82
|
+
*
|
|
83
|
+
* `"accept"` is the plain-ask affirmative (valid only when the ask
|
|
84
|
+
* declared no modes — with a declaration, accepting IS picking a mode).
|
|
85
|
+
* A validation failure throws: the inputs came from the declaration
|
|
86
|
+
* itself, so a mismatch is a construction bug, not a user error.
|
|
87
|
+
*/
|
|
88
|
+
export function buildHitlAnswer(ask: AgPausedAsk, action: HitlPromptAction): AgHitlAnswer {
|
|
89
|
+
const answer: AgHitlAnswer = {
|
|
90
|
+
askId: ask.askId,
|
|
91
|
+
status:
|
|
92
|
+
action === "decline" ? "declined" : action === "dismiss" ? "cancelled" : "resolved",
|
|
93
|
+
...(typeof action === "object" ? { grantModeId: action.grantModeId } : {}),
|
|
94
|
+
// The pre-existing byte-echo MUST (spec §7): the answer carries the
|
|
95
|
+
// ask's requestState verbatim when one exists.
|
|
96
|
+
...(ask.requestState !== undefined ? { requestState: ask.requestState } : {}),
|
|
97
|
+
};
|
|
98
|
+
const verdict = validateHitlAnswer(ask, answer);
|
|
99
|
+
if (!verdict.ok) {
|
|
100
|
+
throw new Error(`buildHitlAnswer constructed an invalid answer (${verdict.code}): ${verdict.message}`);
|
|
101
|
+
}
|
|
102
|
+
return answer;
|
|
103
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* rendering, tests, and RN bundlers consume the view-model directly.
|
|
12
12
|
*/
|
|
13
13
|
export {
|
|
14
|
+
newestViewKey,
|
|
14
15
|
planTranscript,
|
|
15
16
|
} from "./plan.js";
|
|
16
17
|
export {
|
|
@@ -24,6 +25,13 @@ export {
|
|
|
24
25
|
type ChatStrings,
|
|
25
26
|
} from "./strings.js";
|
|
26
27
|
export { transcriptInputsFromHistory } from "./history-inputs.js";
|
|
28
|
+
export {
|
|
29
|
+
buildHitlAnswer,
|
|
30
|
+
grantModeDisplay,
|
|
31
|
+
hitlPromptsFromFold,
|
|
32
|
+
type HitlAnswerRecord,
|
|
33
|
+
type HitlPromptAction,
|
|
34
|
+
} from "./hitl.js";
|
|
27
35
|
export {
|
|
28
36
|
DEFAULT_CHAT_THEME,
|
|
29
37
|
GUUEY_CHAT_THEME,
|
|
@@ -32,6 +40,7 @@ export {
|
|
|
32
40
|
resolveTheme,
|
|
33
41
|
} from "./theme.js";
|
|
34
42
|
export type {
|
|
43
|
+
ChatDebugEvent,
|
|
35
44
|
CitationsItem,
|
|
36
45
|
CodeItem,
|
|
37
46
|
CompactionItem,
|
|
@@ -40,7 +49,12 @@ export type {
|
|
|
40
49
|
ErrorItem,
|
|
41
50
|
HistoryBoundaryItem,
|
|
42
51
|
ItemKey,
|
|
52
|
+
HitlPromptInput,
|
|
53
|
+
HitlPromptItem,
|
|
43
54
|
MediaItem,
|
|
55
|
+
NoticeItem,
|
|
56
|
+
ProfilePromptInput,
|
|
57
|
+
ProfilePromptItem,
|
|
44
58
|
PromptItem,
|
|
45
59
|
PromptItemInput,
|
|
46
60
|
ReasoningItem,
|
|
@@ -55,4 +69,5 @@ export type {
|
|
|
55
69
|
UnknownItem,
|
|
56
70
|
UserMessageItem,
|
|
57
71
|
ViewMountItem,
|
|
72
|
+
ViewRefItem,
|
|
58
73
|
} from "./types.js";
|
|
@@ -39,6 +39,7 @@ import type {
|
|
|
39
39
|
HistoryBoundaryItem,
|
|
40
40
|
ItemKey,
|
|
41
41
|
MediaItem,
|
|
42
|
+
NoticeItem,
|
|
42
43
|
PromptItem,
|
|
43
44
|
ReasoningItem,
|
|
44
45
|
StatusLineItem,
|
|
@@ -48,6 +49,7 @@ import type {
|
|
|
48
49
|
UserMessageItem,
|
|
49
50
|
TextItem,
|
|
50
51
|
ViewMountItem,
|
|
52
|
+
ViewRefItem,
|
|
51
53
|
} from "../types.js";
|
|
52
54
|
import { Linking } from "react-native";
|
|
53
55
|
import { NativeMarkdown } from "./markdown.js";
|
|
@@ -63,13 +65,21 @@ export interface NativeTranscriptItemContext {
|
|
|
63
65
|
/** R0 failed-send retry. */
|
|
64
66
|
onRetry?: (item: UserMessageItem) => void;
|
|
65
67
|
/** R10 prompt actions — the host owns what accept/decline DO. */
|
|
66
|
-
onPromptAction?: (
|
|
68
|
+
onPromptAction?: (
|
|
69
|
+
item: PromptItem,
|
|
70
|
+
action: "accept" | "decline" | "dismiss" | { grantModeId: string },
|
|
71
|
+
) => void;
|
|
67
72
|
/** R11 action slots (sign-in / retry affordances). */
|
|
68
73
|
onErrorAction?: (item: ErrorItem) => void;
|
|
69
74
|
/** R6: locator mounts resolved by `useTranscript` ("expired" = failed). */
|
|
70
75
|
resolvedMounts: ReadonlyMap<ItemKey, ResolvedViewMount | "expired">;
|
|
71
76
|
/** R6: live phase reports wired back into the next plan. */
|
|
72
77
|
onViewPhase: (key: ItemKey, phase: ViewHostPhase) => void;
|
|
78
|
+
/**
|
|
79
|
+
* guuey#204: a promoted-view reference chip was activated — the host
|
|
80
|
+
* focuses/reveals its stage. Absent ⇒ the chip renders as a label.
|
|
81
|
+
*/
|
|
82
|
+
onViewRef?: (item: ViewRefItem) => void;
|
|
73
83
|
}
|
|
74
84
|
|
|
75
85
|
interface ItemProps<T> {
|
|
@@ -347,6 +357,39 @@ export function NativeView({ item, ctx }: ItemProps<ViewMountItem>): ReactNode {
|
|
|
347
357
|
);
|
|
348
358
|
}
|
|
349
359
|
|
|
360
|
+
/**
|
|
361
|
+
* guuey#204: the "promote and reference" chip — a Pressable when the host
|
|
362
|
+
* wires `onViewRef`, a plain labeled row otherwise. Never a second mount.
|
|
363
|
+
*/
|
|
364
|
+
export function NativeViewRef({ item, ctx }: ItemProps<ViewRefItem>): ReactNode {
|
|
365
|
+
const { tokens } = ctx;
|
|
366
|
+
const chipStyle = {
|
|
367
|
+
alignSelf: "flex-start" as const,
|
|
368
|
+
backgroundColor: tokens.palette.surface,
|
|
369
|
+
borderRadius: 999,
|
|
370
|
+
paddingHorizontal: tokens.pad,
|
|
371
|
+
paddingVertical: 6,
|
|
372
|
+
};
|
|
373
|
+
if (ctx.onViewRef === undefined) {
|
|
374
|
+
return (
|
|
375
|
+
<View accessibilityRole="text" style={chipStyle}>
|
|
376
|
+
<MutedText ctx={ctx}>{item.label}</MutedText>
|
|
377
|
+
</View>
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
const onViewRef = ctx.onViewRef;
|
|
381
|
+
return (
|
|
382
|
+
<Pressable
|
|
383
|
+
accessibilityRole="button"
|
|
384
|
+
accessibilityLabel={item.label}
|
|
385
|
+
onPress={() => onViewRef(item)}
|
|
386
|
+
style={chipStyle}
|
|
387
|
+
>
|
|
388
|
+
<MutedText ctx={ctx}>{item.label}</MutedText>
|
|
389
|
+
</Pressable>
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
|
|
350
393
|
// ─── R7 ────────────────────────────────────────────────────────────────────
|
|
351
394
|
|
|
352
395
|
/** Inline images allow https URLs and image/* base64 data — nothing else. */
|
|
@@ -442,6 +485,70 @@ export function NativeCitations({ item, ctx }: ItemProps<CitationsItem>): ReactN
|
|
|
442
485
|
|
|
443
486
|
export function NativePrompt({ item, ctx }: ItemProps<PromptItem>): ReactNode {
|
|
444
487
|
const { tokens } = ctx;
|
|
488
|
+
if (item.promptKind === "hitl") {
|
|
489
|
+
const s = ctx.strings;
|
|
490
|
+
if (item.state === "resolved" || item.state === "declined") {
|
|
491
|
+
return (
|
|
492
|
+
<MutedText ctx={ctx}>
|
|
493
|
+
{item.state === "resolved"
|
|
494
|
+
? item.chosenModeLabel !== null
|
|
495
|
+
? s.promptAnsweredWith(item.chosenModeLabel)
|
|
496
|
+
: s.promptAccept
|
|
497
|
+
: s.promptDeclinedRecord}
|
|
498
|
+
</MutedText>
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
// `cancelled` = guuey's re-askable dismissal: a record row that reopens
|
|
502
|
+
// to the actions on toggle (silverprotocol#16 ruling).
|
|
503
|
+
const answerable = item.state === "pending" || (item.state === "cancelled" && item.expanded);
|
|
504
|
+
return (
|
|
505
|
+
<View
|
|
506
|
+
accessibilityLiveRegion="polite"
|
|
507
|
+
accessible
|
|
508
|
+
style={{ backgroundColor: tokens.palette.surface, borderRadius: tokens.radius, padding: tokens.pad, gap: 8 }}
|
|
509
|
+
>
|
|
510
|
+
{item.state === "cancelled" && (
|
|
511
|
+
<Pressable accessibilityRole="button" accessibilityState={{ expanded: item.expanded }} onPress={() => ctx.onToggle(item.key)}>
|
|
512
|
+
<Text style={{ color: tokens.palette.inkMuted, fontSize: tokens.fontSize }}>{s.promptDismissed}</Text>
|
|
513
|
+
</Pressable>
|
|
514
|
+
)}
|
|
515
|
+
{answerable && (
|
|
516
|
+
<>
|
|
517
|
+
{item.message !== null && (
|
|
518
|
+
<Text style={{ color: tokens.palette.ink, fontSize: tokens.fontSize, fontFamily: tokens.fontFamily }}>
|
|
519
|
+
{item.message}
|
|
520
|
+
</Text>
|
|
521
|
+
)}
|
|
522
|
+
<View style={{ flexDirection: "row", flexWrap: "wrap", gap: 12 }}>
|
|
523
|
+
{item.grantModes.length === 0 ? (
|
|
524
|
+
<Pressable accessibilityRole="button" onPress={() => ctx.onPromptAction?.(item, "accept")}>
|
|
525
|
+
<Text style={{ color: tokens.palette.accent, fontWeight: "700", fontSize: tokens.fontSize }}>
|
|
526
|
+
{s.promptAccept}
|
|
527
|
+
</Text>
|
|
528
|
+
</Pressable>
|
|
529
|
+
) : (
|
|
530
|
+
item.grantModes.map((mode) => (
|
|
531
|
+
<Pressable
|
|
532
|
+
key={mode.id}
|
|
533
|
+
accessibilityRole="button"
|
|
534
|
+
accessibilityHint={mode.description}
|
|
535
|
+
onPress={() => ctx.onPromptAction?.(item, { grantModeId: mode.id })}
|
|
536
|
+
>
|
|
537
|
+
<Text style={{ color: tokens.palette.accent, fontWeight: "700", fontSize: tokens.fontSize }}>
|
|
538
|
+
{mode.label ?? mode.id}
|
|
539
|
+
</Text>
|
|
540
|
+
</Pressable>
|
|
541
|
+
))
|
|
542
|
+
)}
|
|
543
|
+
<Pressable accessibilityRole="button" onPress={() => ctx.onPromptAction?.(item, "decline")}>
|
|
544
|
+
<Text style={{ color: tokens.palette.inkMuted, fontSize: tokens.fontSize }}>{s.promptDecline}</Text>
|
|
545
|
+
</Pressable>
|
|
546
|
+
</View>
|
|
547
|
+
</>
|
|
548
|
+
)}
|
|
549
|
+
</View>
|
|
550
|
+
);
|
|
551
|
+
}
|
|
445
552
|
if (item.state !== "pending") {
|
|
446
553
|
return (
|
|
447
554
|
<MutedText ctx={ctx}>
|
|
@@ -474,6 +581,22 @@ export function NativePrompt({ item, ctx }: ItemProps<PromptItem>): ReactNode {
|
|
|
474
581
|
);
|
|
475
582
|
}
|
|
476
583
|
|
|
584
|
+
// ─── R16 ──────────────────────────────────────────────────────────────────
|
|
585
|
+
|
|
586
|
+
/** The notice row (spec draft.2) — labeled, non-conversational, never agent-voiced. */
|
|
587
|
+
export function NativeNotice({ item, ctx }: ItemProps<NoticeItem>): ReactNode {
|
|
588
|
+
const { tokens } = ctx;
|
|
589
|
+
return (
|
|
590
|
+
<View accessible accessibilityRole="text" style={{ paddingVertical: 4 }}>
|
|
591
|
+
<Text style={{ color: tokens.palette.inkMuted, fontSize: tokens.fontSize - 2 }}>
|
|
592
|
+
{ctx.strings.noticeLabel}
|
|
593
|
+
{item.sourceLabel !== null ? ` · ${item.sourceLabel}` : ""}
|
|
594
|
+
{item.text !== "" ? ` — ${item.text}` : ""}
|
|
595
|
+
</Text>
|
|
596
|
+
</View>
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
|
|
477
600
|
// ─── R11 ───────────────────────────────────────────────────────────────────
|
|
478
601
|
|
|
479
602
|
export function NativeError({ item, ctx }: ItemProps<ErrorItem>): ReactNode {
|
|
@@ -576,10 +699,12 @@ export interface NativeTranscriptComponents {
|
|
|
576
699
|
toolGroup: ComponentType<ItemProps<ToolGroupItem>>;
|
|
577
700
|
dataResult: ComponentType<ItemProps<DataResultItem>>;
|
|
578
701
|
view: ComponentType<ItemProps<ViewMountItem>>;
|
|
702
|
+
viewRef: ComponentType<ItemProps<ViewRefItem>>;
|
|
579
703
|
media: ComponentType<ItemProps<MediaItem>>;
|
|
580
704
|
code: ComponentType<ItemProps<CodeItem>>;
|
|
581
705
|
citations: ComponentType<ItemProps<CitationsItem>>;
|
|
582
706
|
prompt: ComponentType<ItemProps<PromptItem>>;
|
|
707
|
+
notice: ComponentType<ItemProps<NoticeItem>>;
|
|
583
708
|
error: ComponentType<ItemProps<ErrorItem>>;
|
|
584
709
|
history: ComponentType<ItemProps<HistoryBoundaryItem>>;
|
|
585
710
|
compaction: ComponentType<ItemProps<CompactionItem>>;
|
|
@@ -595,10 +720,12 @@ export const nativeTranscriptComponents: NativeTranscriptComponents = {
|
|
|
595
720
|
toolGroup: NativeToolGroup,
|
|
596
721
|
dataResult: NativeDataResult,
|
|
597
722
|
view: NativeView,
|
|
723
|
+
viewRef: NativeViewRef,
|
|
598
724
|
media: NativeMedia,
|
|
599
725
|
code: NativeCode,
|
|
600
726
|
citations: NativeCitations,
|
|
601
727
|
prompt: NativePrompt,
|
|
728
|
+
notice: NativeNotice,
|
|
602
729
|
error: NativeError,
|
|
603
730
|
history: NativeHistoryBoundary,
|
|
604
731
|
compaction: NativeCompaction,
|
|
@@ -641,6 +768,10 @@ export function renderNativeItem(
|
|
|
641
768
|
const C = components.view;
|
|
642
769
|
return <C key={item.key} item={item} ctx={ctx} />;
|
|
643
770
|
}
|
|
771
|
+
case "viewRef": {
|
|
772
|
+
const C = components.viewRef;
|
|
773
|
+
return <C key={item.key} item={item} ctx={ctx} />;
|
|
774
|
+
}
|
|
644
775
|
case "media": {
|
|
645
776
|
const C = components.media;
|
|
646
777
|
return <C key={item.key} item={item} ctx={ctx} />;
|
|
@@ -657,6 +788,10 @@ export function renderNativeItem(
|
|
|
657
788
|
const C = components.prompt;
|
|
658
789
|
return <C key={item.key} item={item} ctx={ctx} />;
|
|
659
790
|
}
|
|
791
|
+
case "notice": {
|
|
792
|
+
const C = components.notice;
|
|
793
|
+
return <C key={item.key} item={item} ctx={ctx} />;
|
|
794
|
+
}
|
|
660
795
|
case "error": {
|
|
661
796
|
const C = components.error;
|
|
662
797
|
return <C key={item.key} item={item} ctx={ctx} />;
|
|
@@ -66,7 +66,7 @@ export type NativeTranscriptListProps = Pick<
|
|
|
66
66
|
export interface NativeTranscriptProps
|
|
67
67
|
extends Pick<
|
|
68
68
|
NativeTranscriptItemContext,
|
|
69
|
-
"onToggle" | "onRetry" | "onPromptAction" | "onErrorAction" | "resolvedMounts" | "onViewPhase"
|
|
69
|
+
"onToggle" | "onRetry" | "onPromptAction" | "onErrorAction" | "resolvedMounts" | "onViewPhase" | "onViewRef"
|
|
70
70
|
> {
|
|
71
71
|
plan: TranscriptPlan;
|
|
72
72
|
/** Per-slot component overrides (spec §3's override column). */
|
|
@@ -93,6 +93,7 @@ export function NativeTranscript(props: NativeTranscriptProps): ReactNode {
|
|
|
93
93
|
onErrorAction,
|
|
94
94
|
resolvedMounts,
|
|
95
95
|
onViewPhase,
|
|
96
|
+
onViewRef,
|
|
96
97
|
} = props;
|
|
97
98
|
|
|
98
99
|
const tokens = useMemo(() => resolveNativeTheme(theme, mode), [theme, mode]);
|
|
@@ -101,8 +102,8 @@ export function NativeTranscript(props: NativeTranscriptProps): ReactNode {
|
|
|
101
102
|
[components],
|
|
102
103
|
);
|
|
103
104
|
const ctx: NativeTranscriptItemContext = useMemo(
|
|
104
|
-
() => ({ strings, tokens, onToggle, onRetry, onPromptAction, onErrorAction, resolvedMounts, onViewPhase }),
|
|
105
|
-
[strings, tokens, onToggle, onRetry, onPromptAction, onErrorAction, resolvedMounts, onViewPhase],
|
|
105
|
+
() => ({ strings, tokens, onToggle, onRetry, onPromptAction, onErrorAction, resolvedMounts, onViewPhase, onViewRef }),
|
|
106
|
+
[strings, tokens, onToggle, onRetry, onPromptAction, onErrorAction, resolvedMounts, onViewPhase, onViewRef],
|
|
106
107
|
);
|
|
107
108
|
|
|
108
109
|
// Inverted-list data: reversed plan, newest at index 0 (the visual
|