@kitn.ai/ui 0.23.0 → 0.24.0
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/state/stream.d.ts +22 -4
- package/package.json +6 -6
- package/src/state/stream.ts +92 -3
package/dist/state/stream.d.ts
CHANGED
|
@@ -1,22 +1,39 @@
|
|
|
1
|
-
import { ChatMessage, Source } from '../elements/chat-types.js';
|
|
1
|
+
import { ChatMessage, MessagePart, Source } from '../elements/chat-types.js';
|
|
2
2
|
import { ToolPart } from '../components/tool-types.js';
|
|
3
3
|
import { CardEnvelope } from '../primitives/card-contract.js';
|
|
4
4
|
import { AttachmentData } from '../components/attachment-types.js';
|
|
5
5
|
import { ReasoningOpts } from './parts.js';
|
|
6
6
|
/** The one universal contract: a functional-updater setter (React setState shape). */
|
|
7
7
|
export type SetMessages = (updater: (prev: ChatMessage[]) => ChatMessage[]) => void;
|
|
8
|
+
/** Every OBJECT payload a `MessagePart` variant carries. `type`/`raw` are the
|
|
9
|
+
* variant's own bookkeeping, not payload; the primitive payloads (`text`,
|
|
10
|
+
* `label`, `index`, ...) drop out at `Extract<..., object>`. */
|
|
11
|
+
type PartPayload = Extract<MessagePart extends infer P ? (P extends object ? P[Exclude<keyof P, 'type' | 'raw'>] : never) : never, object>;
|
|
12
|
+
/** Every bag one of these mutators takes: the part payloads plus the options
|
|
13
|
+
* bags that are not payloads themselves. */
|
|
14
|
+
type MutatorBag = PartPayload | ReasoningOpts;
|
|
15
|
+
/** `keyof` over a union member-by-member. The bare `keyof (A | B)` is the
|
|
16
|
+
* INTERSECTION of their keys, which is the opposite of what this needs. */
|
|
17
|
+
type KeysOf<T> = T extends unknown ? keyof T : never;
|
|
18
|
+
/** `Shape`, but any key that belongs exclusively to a SIBLING bag is a compile
|
|
19
|
+
* error. Keys `Shape` never heard of are untouched, so a consumer's own
|
|
20
|
+
* superset of a citation still passes — only the mix-ups fail. That is a
|
|
21
|
+
* denylist, not an exact type, and deliberately so: boundary point 1. */
|
|
22
|
+
type Unmixed<Shape> = Shape & {
|
|
23
|
+
[K in Exclude<KeysOf<MutatorBag>, keyof Shape>]?: never;
|
|
24
|
+
};
|
|
8
25
|
/** A fluent builder for one in-flight assistant message. Owns no state. */
|
|
9
26
|
export interface AssistantStream {
|
|
10
27
|
readonly id: string;
|
|
11
28
|
appendText(delta: string): AssistantStream;
|
|
12
|
-
appendReasoning(delta: string, opts?: ReasoningOpts): AssistantStream;
|
|
13
|
-
upsertTool(toolCallId: string, patch: Partial<ToolPart
|
|
29
|
+
appendReasoning(delta: string, opts?: Unmixed<ReasoningOpts>): AssistantStream;
|
|
30
|
+
upsertTool(toolCallId: string, patch: Unmixed<Partial<ToolPart>>): AssistantStream;
|
|
14
31
|
/** Adds a card, or REPLACES the existing one with the same `envelope.id`. A
|
|
15
32
|
* model that revises a card mid-turn re-sends the whole envelope, so a second
|
|
16
33
|
* call with a known id revises that card in place rather than rendering a
|
|
17
34
|
* second copy of it. See `upsertCardPart`. */
|
|
18
35
|
addCard(envelope: CardEnvelope): AssistantStream;
|
|
19
|
-
addSource(source: Source): AssistantStream;
|
|
36
|
+
addSource(source: Unmixed<Source>): AssistantStream;
|
|
20
37
|
addFile(attachment: AttachmentData): AssistantStream;
|
|
21
38
|
done(): void;
|
|
22
39
|
abort(reason?: string): void;
|
|
@@ -26,3 +43,4 @@ export declare function createAssistantStream(set: SetMessages, init?: Partial<C
|
|
|
26
43
|
/** Wrap a stream so `onSettle` fires on done/abort (used to toggle a `loading` flag).
|
|
27
44
|
* Preserves the fluent chain by returning the wrapper from every mutator. */
|
|
28
45
|
export declare function onStreamSettled(inner: AssistantStream, onSettle: () => void): AssistantStream;
|
|
46
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kitn.ai/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"nx": {
|
|
5
5
|
"name": "ui",
|
|
6
6
|
"targets": {
|
|
@@ -165,16 +165,16 @@
|
|
|
165
165
|
"build:subpath-dts": "node scripts/emit-subpath-dts.mjs",
|
|
166
166
|
"verify:card-validation": "node scripts/gen-card-validation-schemas.mjs --check",
|
|
167
167
|
"verify:consumer": "node scripts/verify-consumer-sideeffects.mjs",
|
|
168
|
-
"verify:dts": "node scripts/verify-dts-boundaries.mjs",
|
|
168
|
+
"verify:dts": "node scripts/verify-dts-boundaries.mjs --self-test && node scripts/verify-dts-boundaries.mjs",
|
|
169
169
|
"verify:elements-bundle": "node scripts/verify-elements-bundle.mjs --self-test && node scripts/verify-elements-bundle.mjs",
|
|
170
170
|
"verify:dts:consumer": "node scripts/verify-dts-consumer.mjs",
|
|
171
|
-
"verify:generated": "node scripts/verify-generated-sync.mjs",
|
|
172
|
-
"verify:quarantine": "node scripts/verify-quarantine.mjs",
|
|
171
|
+
"verify:generated": "node scripts/verify-generated-sync.mjs --self-test && node scripts/verify-generated-sync.mjs",
|
|
172
|
+
"verify:quarantine": "node scripts/verify-quarantine.mjs --self-test && node scripts/verify-quarantine.mjs",
|
|
173
173
|
"verify:react-wrappers": "node scripts/verify-react-wrappers.mjs --self-test && node scripts/verify-react-wrappers.mjs",
|
|
174
174
|
"verify:scaffold": "node scripts/verify-scaffold-compiles.mjs",
|
|
175
|
-
"verify:schemas": "node scripts/verify-schemas-exported.mjs",
|
|
175
|
+
"verify:schemas": "node scripts/verify-schemas-exported.mjs --self-test && node scripts/verify-schemas-exported.mjs",
|
|
176
176
|
"verify:shader-lazy": "node scripts/verify-shader-lazy.mjs --self-test && node scripts/verify-shader-lazy.mjs",
|
|
177
|
-
"verify:solid-coverage": "node scripts/verify-solid-coverage.mjs",
|
|
177
|
+
"verify:solid-coverage": "node scripts/verify-solid-coverage.mjs --self-test && node scripts/verify-solid-coverage.mjs",
|
|
178
178
|
"verify:tool-schemas": "node scripts/verify-tool-schemas.mjs",
|
|
179
179
|
"verify:ssr": "node scripts/verify-ssr-imports.mjs && node scripts/verify-ssr-render.mjs",
|
|
180
180
|
"verify:ssr:imports": "node scripts/verify-ssr-imports.mjs",
|
package/src/state/stream.ts
CHANGED
|
@@ -8,6 +8,93 @@ import { appendReasoningPart, appendTextPart, upsertCardPart, upsertToolPart, ty
|
|
|
8
8
|
/** The one universal contract: a functional-updater setter (React setState shape). */
|
|
9
9
|
export type SetMessages = (updater: (prev: ChatMessage[]) => ChatMessage[]) => void;
|
|
10
10
|
|
|
11
|
+
/* --------------------------------------------------------------------------
|
|
12
|
+
* Bag exclusivity: why the mutators below do not just take their payload type.
|
|
13
|
+
*
|
|
14
|
+
* `Source`, `Partial<ToolPart>` and `ReasoningOpts` are WEAK types — not one
|
|
15
|
+
* required field between them — and TypeScript only excess-property-checks
|
|
16
|
+
* OBJECT LITERALS. Hand a mutator a VARIABLE and that check never runs, so any
|
|
17
|
+
* bag sharing a single optional key name flows straight in. `addSource(source:
|
|
18
|
+
* Source)` therefore accepted an `AttachmentData` and a `CardEnvelope`, and
|
|
19
|
+
* `addFile(a) { inner.addSource(a); }` compiled clean at exit 0 while writing a
|
|
20
|
+
* file payload into a `source` part. Nine such pairs existed across these three
|
|
21
|
+
* bags and not one of them was a type error; `./stream-types.test.ts` pins all
|
|
22
|
+
* nine, and fails with nine "Unused '@ts-expect-error'" if this guard is lifted.
|
|
23
|
+
*
|
|
24
|
+
* Tightening the PAYLOAD types is not available. A citation with no url and no
|
|
25
|
+
* title is a rendered, tested state (`citationTitle` in components/message.tsx
|
|
26
|
+
* falls through title -> url -> a generic word, and message.stories.tsx ships
|
|
27
|
+
* "a citation with no url at all"), and a patch/opts bag is optional by
|
|
28
|
+
* definition. So the exclusivity lives on the PARAMETER instead, which leaves
|
|
29
|
+
* every published data type — and every artifact generated from them — alone.
|
|
30
|
+
*
|
|
31
|
+
* The forbidden key set is DERIVED from the `MessagePart` union, the same
|
|
32
|
+
* derivation `verify:scaffold` and `lint:silent-drops` read, so a seventh
|
|
33
|
+
* variant re-fires this on its own rather than quietly widening the hole.
|
|
34
|
+
*
|
|
35
|
+
* THE BOUNDARY — three things this deliberately does NOT cover. Each is a
|
|
36
|
+
* decision with a reason, not an unfinished edge; they are collected here so
|
|
37
|
+
* the whole boundary reads in one place instead of being met one at a time.
|
|
38
|
+
*
|
|
39
|
+
* 1. A KEY NO SIBLING BAG OWNS still passes. `Unmixed` is a denylist over the
|
|
40
|
+
* sibling vocabulary, not an exact type, so a consumer's own superset of a
|
|
41
|
+
* citation — `{ url, title, relevance }` — keeps compiling. Full exactness
|
|
42
|
+
* would catch marginally more and push real callers toward a cast, and a
|
|
43
|
+
* cast to `Source` from an attachment is the original bug. A guard people
|
|
44
|
+
* route around is worse than a narrower guard they keep.
|
|
45
|
+
*
|
|
46
|
+
* 2. `addCard` / `addFile` ARE NOT WRAPPED. Both take STRONG types —
|
|
47
|
+
* `CardEnvelope` requires type+id+data, `AttachmentData` requires id+type —
|
|
48
|
+
* so ordinary assignability already does what `Unmixed` exists to do for a
|
|
49
|
+
* weak bag. Measured, not assumed: wrapping `addCard` buys ZERO (no sibling
|
|
50
|
+
* bag is assignable to `CardEnvelope`, because nothing else carries `data`).
|
|
51
|
+
* Wrapping `addFile` buys exactly one pair — `addFile(c)` with
|
|
52
|
+
* `c: CardEnvelope<'file', unknown>` — and costs two ordinary consumer
|
|
53
|
+
* shapes, since the derived denylist owns `state` and `index`:
|
|
54
|
+
*
|
|
55
|
+
* { id, type: 'file', filename, state: 'uploading' } // upload progress
|
|
56
|
+
* { id, type: 'file', filename, index: 3 } // display position
|
|
57
|
+
*
|
|
58
|
+
* That one pair needs a hand-written generic narrowing with no other
|
|
59
|
+
* purpose; an ordinarily-inferred `CardEnvelope` is ALREADY rejected.
|
|
60
|
+
* Rejecting likely code to block contrived code is the wrong trade — the
|
|
61
|
+
* same trade point 1 refused. Both facts this rests on are pinned in
|
|
62
|
+
* ./stream-types.test.ts, so it re-opens if either stops holding.
|
|
63
|
+
*
|
|
64
|
+
* 3. A HAND-BUILT PART BYPASSES ALL OF IT. `parts.push({ type: 'source',
|
|
65
|
+
* source: attachmentVariable })` compiles, because these mutators are not
|
|
66
|
+
* in the path. Closing it means intersecting the guard into `MessagePart`
|
|
67
|
+
* itself — and `docs/web-components.md` and `llms-full.txt` inline that
|
|
68
|
+
* union's fully-expanded structural text, so dozens of consumer-facing
|
|
69
|
+
* prop-table rows would grow `type?: undefined; state?: undefined;
|
|
70
|
+
* data?: undefined`. That cost lands on every consumer reading the docs
|
|
71
|
+
* while the benefit reaches only someone who has already stepped around
|
|
72
|
+
* the provided API. The mutators are the supported path; they are guarded.
|
|
73
|
+
* This is where the supported path ends, not a gap in it.
|
|
74
|
+
* ------------------------------------------------------------------------ */
|
|
75
|
+
|
|
76
|
+
/** Every OBJECT payload a `MessagePart` variant carries. `type`/`raw` are the
|
|
77
|
+
* variant's own bookkeeping, not payload; the primitive payloads (`text`,
|
|
78
|
+
* `label`, `index`, ...) drop out at `Extract<..., object>`. */
|
|
79
|
+
type PartPayload = Extract<
|
|
80
|
+
MessagePart extends infer P ? (P extends object ? P[Exclude<keyof P, 'type' | 'raw'>] : never) : never,
|
|
81
|
+
object
|
|
82
|
+
>;
|
|
83
|
+
|
|
84
|
+
/** Every bag one of these mutators takes: the part payloads plus the options
|
|
85
|
+
* bags that are not payloads themselves. */
|
|
86
|
+
type MutatorBag = PartPayload | ReasoningOpts;
|
|
87
|
+
|
|
88
|
+
/** `keyof` over a union member-by-member. The bare `keyof (A | B)` is the
|
|
89
|
+
* INTERSECTION of their keys, which is the opposite of what this needs. */
|
|
90
|
+
type KeysOf<T> = T extends unknown ? keyof T : never;
|
|
91
|
+
|
|
92
|
+
/** `Shape`, but any key that belongs exclusively to a SIBLING bag is a compile
|
|
93
|
+
* error. Keys `Shape` never heard of are untouched, so a consumer's own
|
|
94
|
+
* superset of a citation still passes — only the mix-ups fail. That is a
|
|
95
|
+
* denylist, not an exact type, and deliberately so: boundary point 1. */
|
|
96
|
+
type Unmixed<Shape> = Shape & { [K in Exclude<KeysOf<MutatorBag>, keyof Shape>]?: never };
|
|
97
|
+
|
|
11
98
|
function newId(): string {
|
|
12
99
|
if (typeof crypto !== 'undefined' && crypto.randomUUID) return crypto.randomUUID();
|
|
13
100
|
return 'kai-' + Math.random().toString(36).slice(2);
|
|
@@ -17,14 +104,16 @@ function newId(): string {
|
|
|
17
104
|
export interface AssistantStream {
|
|
18
105
|
readonly id: string;
|
|
19
106
|
appendText(delta: string): AssistantStream;
|
|
20
|
-
appendReasoning(delta: string, opts?: ReasoningOpts): AssistantStream;
|
|
21
|
-
upsertTool(toolCallId: string, patch: Partial<ToolPart
|
|
107
|
+
appendReasoning(delta: string, opts?: Unmixed<ReasoningOpts>): AssistantStream;
|
|
108
|
+
upsertTool(toolCallId: string, patch: Unmixed<Partial<ToolPart>>): AssistantStream;
|
|
22
109
|
/** Adds a card, or REPLACES the existing one with the same `envelope.id`. A
|
|
23
110
|
* model that revises a card mid-turn re-sends the whole envelope, so a second
|
|
24
111
|
* call with a known id revises that card in place rather than rendering a
|
|
25
112
|
* second copy of it. See `upsertCardPart`. */
|
|
26
113
|
addCard(envelope: CardEnvelope): AssistantStream;
|
|
27
|
-
addSource(source: Source): AssistantStream;
|
|
114
|
+
addSource(source: Unmixed<Source>): AssistantStream;
|
|
115
|
+
/* Unwrapped on purpose — these two take STRONG types, so ordinary
|
|
116
|
+
* assignability already holds. Measurements and the trade: boundary point 2. */
|
|
28
117
|
addFile(attachment: AttachmentData): AssistantStream;
|
|
29
118
|
done(): void;
|
|
30
119
|
abort(reason?: string): void;
|