@haikit/core 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 hai contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,34 @@
1
+ # @haikit/core
2
+
3
+ The contract layer. Isomorphic, zero runtime dependencies — imported by the
4
+ server runtime, the browser client, every adapter, and both halves of your app.
5
+
6
+ ```ts
7
+ import { defineSurface, resolve, query } from "@haikit/core";
8
+
9
+ export const picker = defineSurface({
10
+ name: "flight_table",
11
+ version: 1,
12
+ props: z.object({ flights: z.array(Flight) }),
13
+ actions: { select: resolve(z.string()) },
14
+ queries: { filter: query(FilterArgs) },
15
+ });
16
+ ```
17
+
18
+ Schemas are accepted **structurally** — anything with a `.parse()` method works,
19
+ so zod is your choice rather than this package's dependency.
20
+
21
+ ## The four guarantees
22
+
23
+ Enforced in the type system, and asserted by `test/types/guarantees.ts`:
24
+
25
+ | | |
26
+ |---|---|
27
+ | a surface cannot exist without a `digest` | `digest` is a required field |
28
+ | a query's result can only be produced by `cap()` | `Capped` has no other constructor |
29
+ | `mode: "elicit"` needs a `resolve` action | otherwise the call does not typecheck |
30
+ | an undeclared action does not exist | the contract *is* the allowlist |
31
+
32
+ `npm run typetest` at the repo root compiles those tests twice — once with their
33
+ `@ts-expect-error` directives (must be clean) and once stripped (every marked
34
+ line must error). A guarantee that silently stops working fails CI.
@@ -0,0 +1,300 @@
1
+ /**
2
+ * hai-core — the contract layer.
3
+ *
4
+ * Isomorphic. Imported by the server runtime, by adapters, and (as types only)
5
+ * by app code on both sides of the wire. Zero runtime dependencies: schemas are
6
+ * accepted structurally, so zod works but is not required.
7
+ *
8
+ * Four guarantees are enforced here, in the type system:
9
+ * 1. a surface cannot exist without a `digest`
10
+ * 2. a query's return value can only be produced by `cap()`
11
+ * 3. `mode: "elicit"` only accepts a surface declaring a `resolve` action
12
+ * 4. declaring an action is the only way to make it round-trip
13
+ */
14
+ /** Structural match for zod, valibot, or a hand-rolled validator. */
15
+ export interface Schema<T> {
16
+ parse(value: unknown): T;
17
+ }
18
+ export type Infer<S> = S extends Schema<infer T> ? T : never;
19
+ /** JSON Schema for the model-facing tool definition. */
20
+ export type JsonSchema = Record<string, unknown>;
21
+ declare const CAPPED: unique symbol;
22
+ /**
23
+ * The return type of a query. There is no runtime value for CAPPED, so no
24
+ * object literal can satisfy this — `cap()` is the only constructor.
25
+ *
26
+ * This is what stops a filter that matches all 47 rows from returning all 47.
27
+ */
28
+ export interface Capped {
29
+ readonly [CAPPED]: true;
30
+ text: string;
31
+ shown: number;
32
+ total: number;
33
+ }
34
+ export interface CapOptions {
35
+ /** Secondary bound. Default 8. */
36
+ maxRows?: number;
37
+ /** Primary bound — a token proxy. Whichever binds first wins. Default 1800. */
38
+ maxChars?: number;
39
+ }
40
+ export type Cap = <T>(rows: T[], fmt: (row: T) => string, opts?: CapOptions) => Capped;
41
+ /**
42
+ * Bound a result set and — just as important — say that you bounded it.
43
+ * Silent truncation turns a cost problem into a correctness problem: the model
44
+ * reports "there are 8" when there are 30, and it has no way to know better.
45
+ */
46
+ export declare function makeCap(total: number): Cap;
47
+ export type ActionKind = "resolve" | "inform";
48
+ export interface ActionSpec<K extends ActionKind = ActionKind, V = unknown> {
49
+ kind: K;
50
+ input: Schema<V>;
51
+ }
52
+ export interface QuerySpec<A = unknown> {
53
+ input: Schema<A>;
54
+ description?: string;
55
+ }
56
+ export type ActionMap = Record<string, ActionSpec<ActionKind, any>>;
57
+ export type QueryMap = Record<string, QuerySpec<any>>;
58
+ /** Resolves a parked `elicit` turn. The click becomes the tool_result. */
59
+ export declare const resolve: <V>(input: Schema<V>) => ActionSpec<"resolve", V>;
60
+ /** Adds context to the conversation without having blocked it. */
61
+ export declare const inform: <V>(input: Schema<V>) => ActionSpec<"inform", V>;
62
+ /** A named accessor over the stored payload. There is no raw dereference. */
63
+ export declare const query: <A>(input: Schema<A>, description?: string) => QuerySpec<A>;
64
+ export interface DigestCtx {
65
+ handle: string;
66
+ }
67
+ export interface ActionCtx<P> {
68
+ props: P;
69
+ handle: string;
70
+ }
71
+ export interface QueryCtx<P> {
72
+ props: P;
73
+ cap: Cap;
74
+ }
75
+ export interface SurfaceImplDef<P, A extends ActionMap, Q extends QueryMap> {
76
+ /**
77
+ * GUARANTEE 1 — required.
78
+ *
79
+ * A count and a price range is a lazy digest; it invites the model to narrate
80
+ * facts about rows it never received. Precompute whatever the next turn or two
81
+ * will plausibly need.
82
+ */
83
+ digest: (props: P, ctx: DigestCtx) => string;
84
+ actions: {
85
+ [K in keyof A]: (value: Infer<A[K]["input"]>, ctx: ActionCtx<P>) => string;
86
+ };
87
+ /** GUARANTEE 2 — must return `Capped`, i.e. must call `ctx.cap`. */
88
+ queries: {
89
+ [K in keyof Q]: (args: Infer<Q[K]["input"]>, ctx: QueryCtx<P>) => Capped;
90
+ };
91
+ }
92
+ export interface Surface<P, A extends ActionMap, Q extends QueryMap> {
93
+ readonly name: string;
94
+ readonly version: number;
95
+ readonly props: Schema<P>;
96
+ readonly actions: A;
97
+ readonly queries: Q;
98
+ implement(impl: SurfaceImplDef<P, A, Q>): SurfaceImpl<P, A, Q>;
99
+ }
100
+ export interface SurfaceImpl<P, A extends ActionMap, Q extends QueryMap> {
101
+ readonly surface: Surface<P, A, Q>;
102
+ readonly impl: SurfaceImplDef<P, A, Q>;
103
+ }
104
+ export type AnySurfaceImpl = SurfaceImpl<any, ActionMap, QueryMap>;
105
+ /**
106
+ * Declare a surface contract. Import this module from BOTH halves: the server
107
+ * calls `.implement()`, the client renders against the same prop type.
108
+ *
109
+ * GUARANTEE 4: `actions` is the complete list of interactions that may reach the
110
+ * server. Anything a component does that is not declared here is local by
111
+ * construction — there is no channel for it.
112
+ */
113
+ export declare function defineSurface<P, A extends ActionMap = {}, Q extends QueryMap = {}>(def: {
114
+ name: string;
115
+ version: number;
116
+ props: Schema<P>;
117
+ actions?: A;
118
+ queries?: Q;
119
+ }): Surface<P, A, Q>;
120
+ type ResolveKeys<A extends ActionMap> = {
121
+ [K in keyof A]: A[K]["kind"] extends "resolve" ? K : never;
122
+ }[keyof A];
123
+ export type HasResolve<A extends ActionMap> = [ResolveKeys<A>] extends [never] ? false : true;
124
+ /**
125
+ * Passing a surface with no `resolve` action makes this an object type carrying
126
+ * an unsatisfiable required property, so the error names the actual problem
127
+ * instead of "not assignable to never".
128
+ */
129
+ export type ElicitOptions<A extends ActionMap> = HasResolve<A> extends true ? {
130
+ mode: "elicit";
131
+ } : {
132
+ mode: "elicit";
133
+ "⚠ this surface declares no resolve action — an elicit turn could never be unparked": never;
134
+ };
135
+ export interface ToolReturn {
136
+ /** Goes into `tool_result`. The model channel. */
137
+ model: string;
138
+ /** Set when the tool rendered a surface. */
139
+ handle?: string;
140
+ }
141
+ export interface ToolCtx {
142
+ readonly conversationId: string;
143
+ /** A tool with no UI. */
144
+ text(model: string): ToolReturn;
145
+ /** Render a surface as a blocking question. Requires a `resolve` action. */
146
+ render<P, A extends ActionMap, Q extends QueryMap>(surface: SurfaceImpl<P, A, Q>, props: P, options: ElicitOptions<A>): Promise<ToolReturn>;
147
+ /** Render a surface as a side-artifact. Resolves immediately. */
148
+ render<P, A extends ActionMap, Q extends QueryMap>(surface: SurfaceImpl<P, A, Q>, props: P, options?: {
149
+ mode?: "display";
150
+ }): Promise<ToolReturn>;
151
+ }
152
+ export interface Tool<I = any> {
153
+ readonly name: string;
154
+ readonly description: string;
155
+ readonly input: Schema<I>;
156
+ readonly inputJsonSchema: JsonSchema;
157
+ readonly strict: boolean;
158
+ run(input: I, ctx: ToolCtx): Promise<ToolReturn> | ToolReturn;
159
+ }
160
+ export declare function defineTool<I>(def: {
161
+ name: string;
162
+ description: string;
163
+ input: Schema<I>;
164
+ /** JSON Schema sent to the model. Derive it with zod-to-json-schema if you like. */
165
+ inputJsonSchema: JsonSchema;
166
+ strict?: boolean;
167
+ run(input: I, ctx: ToolCtx): Promise<ToolReturn> | ToolReturn;
168
+ }): Tool<I>;
169
+ export type ConversationStatus = "idle" | "streaming" | "awaiting";
170
+ /** Anthropic-shaped message. Kept loose so adapters can map as needed. */
171
+ export interface Message {
172
+ role: "user" | "assistant";
173
+ content: unknown;
174
+ }
175
+ export interface Pending {
176
+ toolUseId: string;
177
+ handle: string;
178
+ /** An elicit tool never received a tool_result, so its digest has not reached
179
+ * the model yet. It ships with the resolution. */
180
+ digest: string;
181
+ /** Sibling results from the same turn. The API is all-or-nothing per batch. */
182
+ results: unknown[];
183
+ }
184
+ export interface Conversation {
185
+ id: string;
186
+ status: ConversationStatus;
187
+ messages: Message[];
188
+ handles: string[];
189
+ pending: Pending | null;
190
+ /** Turn lease. A dead process leaves this in the past. */
191
+ leaseUntil: number | null;
192
+ }
193
+ export interface PayloadRecord {
194
+ handle: string;
195
+ conversationId: string;
196
+ component: string;
197
+ version: number;
198
+ props: unknown;
199
+ mode: "display" | "elicit";
200
+ state: "live" | "frozen";
201
+ createdAt: number;
202
+ }
203
+ export type Block = {
204
+ kind: "user";
205
+ id: string;
206
+ text: string;
207
+ } | {
208
+ kind: "assistant";
209
+ id: string;
210
+ text: string;
211
+ } | {
212
+ kind: "tool";
213
+ id: string;
214
+ name: string;
215
+ input: unknown;
216
+ status: string;
217
+ ms?: number;
218
+ result?: string;
219
+ } | {
220
+ kind: "interaction";
221
+ id: string;
222
+ handle: string;
223
+ label: string;
224
+ };
225
+ export type WireEvent = {
226
+ type: "hello";
227
+ conversationId: string;
228
+ model: string;
229
+ } | {
230
+ type: "block_start";
231
+ block: Block;
232
+ } | {
233
+ type: "text_delta";
234
+ id: string;
235
+ text: string;
236
+ } | {
237
+ type: "block_update";
238
+ id: string;
239
+ status: string;
240
+ ms: number;
241
+ result: string;
242
+ } | {
243
+ type: "ui_open";
244
+ handle: string;
245
+ toolId: string;
246
+ component: string;
247
+ version: number;
248
+ mode: string;
249
+ } | {
250
+ type: "ui_props";
251
+ handle: string;
252
+ props: unknown;
253
+ } | {
254
+ type: "ui_state";
255
+ handle: string;
256
+ state: "frozen";
257
+ selection?: unknown;
258
+ } | {
259
+ type: "status";
260
+ status: ConversationStatus;
261
+ } | {
262
+ type: "context";
263
+ messages: Message[];
264
+ modelTokens: number;
265
+ uiTokens: number;
266
+ } | {
267
+ type: "error";
268
+ message: string;
269
+ };
270
+ export type Emit = (event: WireEvent) => void;
271
+ export interface ModelRequest {
272
+ system: string;
273
+ tools: {
274
+ name: string;
275
+ description: string;
276
+ input_schema: JsonSchema;
277
+ strict?: boolean;
278
+ }[];
279
+ messages: Message[];
280
+ onTextDelta: (text: string) => void;
281
+ }
282
+ export interface ModelResponse {
283
+ content: any[];
284
+ stop_reason: string;
285
+ }
286
+ export interface ModelAdapter {
287
+ readonly id: string;
288
+ generate(request: ModelRequest): Promise<ModelResponse>;
289
+ }
290
+ export interface StoreAdapter {
291
+ loadConversation(id: string | undefined): Promise<Conversation>;
292
+ saveConversation(conversation: Conversation): Promise<void>;
293
+ putPayload(record: Omit<PayloadRecord, "handle" | "createdAt">): Promise<string>;
294
+ getPayload(handle: string, conversationId: string): Promise<PayloadRecord | null>;
295
+ freezePayload(handle: string): Promise<void>;
296
+ }
297
+ /** Rough token estimate. Only used to surface the economics in the UI. */
298
+ export declare const estTokens: (value: unknown) => number;
299
+ export {};
300
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js ADDED
@@ -0,0 +1,77 @@
1
+ /**
2
+ * hai-core — the contract layer.
3
+ *
4
+ * Isomorphic. Imported by the server runtime, by adapters, and (as types only)
5
+ * by app code on both sides of the wire. Zero runtime dependencies: schemas are
6
+ * accepted structurally, so zod works but is not required.
7
+ *
8
+ * Four guarantees are enforced here, in the type system:
9
+ * 1. a surface cannot exist without a `digest`
10
+ * 2. a query's return value can only be produced by `cap()`
11
+ * 3. `mode: "elicit"` only accepts a surface declaring a `resolve` action
12
+ * 4. declaring an action is the only way to make it round-trip
13
+ */
14
+ /**
15
+ * Bound a result set and — just as important — say that you bounded it.
16
+ * Silent truncation turns a cost problem into a correctness problem: the model
17
+ * reports "there are 8" when there are 30, and it has no way to know better.
18
+ */
19
+ export function makeCap(total) {
20
+ return function cap(rows, fmt, opts = {}) {
21
+ const maxRows = opts.maxRows ?? 8;
22
+ const maxChars = opts.maxChars ?? 1800;
23
+ const lines = [];
24
+ let chars = 0;
25
+ for (const row of rows.slice(0, maxRows)) {
26
+ const line = fmt(row);
27
+ if (chars + line.length > maxChars)
28
+ break;
29
+ lines.push(line);
30
+ chars += line.length + 1;
31
+ }
32
+ const omitted = rows.length - lines.length;
33
+ const head = rows.length === 0
34
+ ? `0 of ${total} match.`
35
+ : `${rows.length} of ${total} match. ${lines.length} shown` +
36
+ (omitted > 0 ? `, ${omitted} omitted` : "") +
37
+ ":";
38
+ return {
39
+ text: lines.length ? `${head}\n${lines.join("\n")}` : head,
40
+ shown: lines.length,
41
+ total,
42
+ };
43
+ };
44
+ }
45
+ /** Resolves a parked `elicit` turn. The click becomes the tool_result. */
46
+ export const resolve = (input) => ({ kind: "resolve", input });
47
+ /** Adds context to the conversation without having blocked it. */
48
+ export const inform = (input) => ({ kind: "inform", input });
49
+ /** A named accessor over the stored payload. There is no raw dereference. */
50
+ export const query = (input, description) => ({ input, description });
51
+ /**
52
+ * Declare a surface contract. Import this module from BOTH halves: the server
53
+ * calls `.implement()`, the client renders against the same prop type.
54
+ *
55
+ * GUARANTEE 4: `actions` is the complete list of interactions that may reach the
56
+ * server. Anything a component does that is not declared here is local by
57
+ * construction — there is no channel for it.
58
+ */
59
+ export function defineSurface(def) {
60
+ const surface = {
61
+ name: def.name,
62
+ version: def.version,
63
+ props: def.props,
64
+ actions: (def.actions ?? {}),
65
+ queries: (def.queries ?? {}),
66
+ implement(impl) {
67
+ return { surface, impl };
68
+ },
69
+ };
70
+ return surface;
71
+ }
72
+ export function defineTool(def) {
73
+ return { strict: true, ...def };
74
+ }
75
+ /** Rough token estimate. Only used to surface the economics in the UI. */
76
+ export const estTokens = (value) => Math.ceil((typeof value === "string" ? value : JSON.stringify(value ?? "")).length / 4);
77
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@haikit/core",
3
+ "version": "0.1.0",
4
+ "description": "The haikit contract layer: declare a UI surface once, implement it on the server and in the browser. Isomorphic, zero runtime dependencies.",
5
+ "keywords": [
6
+ "haikit",
7
+ "llm",
8
+ "agent",
9
+ "agentic-ui",
10
+ "generative-ui",
11
+ "tool-use",
12
+ "tool-calling",
13
+ "ai",
14
+ "chat"
15
+ ],
16
+ "type": "module",
17
+ "main": "./dist/index.js",
18
+ "types": "./dist/index.d.ts",
19
+ "exports": {
20
+ ".": {
21
+ "types": "./dist/index.d.ts",
22
+ "default": "./dist/index.js"
23
+ },
24
+ "./package.json": "./package.json"
25
+ },
26
+ "files": [
27
+ "dist",
28
+ "src"
29
+ ],
30
+ "sideEffects": false,
31
+ "engines": {
32
+ "node": ">=22"
33
+ },
34
+ "scripts": {
35
+ "prepack": "npm --prefix ../.. run build"
36
+ },
37
+ "publishConfig": {
38
+ "access": "public"
39
+ },
40
+ "license": "MIT",
41
+ "homepage": "https://github.com/wfoxd/haikit/tree/main/packages/core#readme",
42
+ "bugs": {
43
+ "url": "https://github.com/wfoxd/haikit/issues"
44
+ },
45
+ "repository": {
46
+ "type": "git",
47
+ "url": "git+https://github.com/wfoxd/haikit.git",
48
+ "directory": "packages/core"
49
+ }
50
+ }
package/src/index.ts ADDED
@@ -0,0 +1,351 @@
1
+ /**
2
+ * hai-core — the contract layer.
3
+ *
4
+ * Isomorphic. Imported by the server runtime, by adapters, and (as types only)
5
+ * by app code on both sides of the wire. Zero runtime dependencies: schemas are
6
+ * accepted structurally, so zod works but is not required.
7
+ *
8
+ * Four guarantees are enforced here, in the type system:
9
+ * 1. a surface cannot exist without a `digest`
10
+ * 2. a query's return value can only be produced by `cap()`
11
+ * 3. `mode: "elicit"` only accepts a surface declaring a `resolve` action
12
+ * 4. declaring an action is the only way to make it round-trip
13
+ */
14
+
15
+ // ───────────────────────────────────────────────────────── schemas
16
+
17
+ /** Structural match for zod, valibot, or a hand-rolled validator. */
18
+ export interface Schema<T> {
19
+ parse(value: unknown): T;
20
+ }
21
+
22
+ export type Infer<S> = S extends Schema<infer T> ? T : never;
23
+
24
+ /** JSON Schema for the model-facing tool definition. */
25
+ export type JsonSchema = Record<string, unknown>;
26
+
27
+ // ───────────────────────────────────────────── GUARANTEE 2: Capped
28
+
29
+ declare const CAPPED: unique symbol;
30
+
31
+ /**
32
+ * The return type of a query. There is no runtime value for CAPPED, so no
33
+ * object literal can satisfy this — `cap()` is the only constructor.
34
+ *
35
+ * This is what stops a filter that matches all 47 rows from returning all 47.
36
+ */
37
+ export interface Capped {
38
+ readonly [CAPPED]: true;
39
+ text: string;
40
+ shown: number;
41
+ total: number;
42
+ }
43
+
44
+ export interface CapOptions {
45
+ /** Secondary bound. Default 8. */
46
+ maxRows?: number;
47
+ /** Primary bound — a token proxy. Whichever binds first wins. Default 1800. */
48
+ maxChars?: number;
49
+ }
50
+
51
+ export type Cap = <T>(rows: T[], fmt: (row: T) => string, opts?: CapOptions) => Capped;
52
+
53
+ /**
54
+ * Bound a result set and — just as important — say that you bounded it.
55
+ * Silent truncation turns a cost problem into a correctness problem: the model
56
+ * reports "there are 8" when there are 30, and it has no way to know better.
57
+ */
58
+ export function makeCap(total: number): Cap {
59
+ return function cap<T>(rows: T[], fmt: (row: T) => string, opts: CapOptions = {}): Capped {
60
+ const maxRows = opts.maxRows ?? 8;
61
+ const maxChars = opts.maxChars ?? 1800;
62
+
63
+ const lines: string[] = [];
64
+ let chars = 0;
65
+ for (const row of rows.slice(0, maxRows)) {
66
+ const line = fmt(row);
67
+ if (chars + line.length > maxChars) break;
68
+ lines.push(line);
69
+ chars += line.length + 1;
70
+ }
71
+
72
+ const omitted = rows.length - lines.length;
73
+ const head =
74
+ rows.length === 0
75
+ ? `0 of ${total} match.`
76
+ : `${rows.length} of ${total} match. ${lines.length} shown` +
77
+ (omitted > 0 ? `, ${omitted} omitted` : "") +
78
+ ":";
79
+
80
+ return {
81
+ text: lines.length ? `${head}\n${lines.join("\n")}` : head,
82
+ shown: lines.length,
83
+ total,
84
+ } as Capped;
85
+ };
86
+ }
87
+
88
+ // ────────────────────────────────────────────── actions and queries
89
+
90
+ export type ActionKind = "resolve" | "inform";
91
+
92
+ export interface ActionSpec<K extends ActionKind = ActionKind, V = unknown> {
93
+ kind: K;
94
+ input: Schema<V>;
95
+ }
96
+
97
+ export interface QuerySpec<A = unknown> {
98
+ input: Schema<A>;
99
+ description?: string;
100
+ }
101
+
102
+ export type ActionMap = Record<string, ActionSpec<ActionKind, any>>;
103
+ export type QueryMap = Record<string, QuerySpec<any>>;
104
+
105
+ /** Resolves a parked `elicit` turn. The click becomes the tool_result. */
106
+ export const resolve = <V>(input: Schema<V>): ActionSpec<"resolve", V> => ({ kind: "resolve", input });
107
+
108
+ /** Adds context to the conversation without having blocked it. */
109
+ export const inform = <V>(input: Schema<V>): ActionSpec<"inform", V> => ({ kind: "inform", input });
110
+
111
+ /** A named accessor over the stored payload. There is no raw dereference. */
112
+ export const query = <A>(input: Schema<A>, description?: string): QuerySpec<A> => ({ input, description });
113
+
114
+ // ──────────────────────────────────────────────────────── surfaces
115
+
116
+ export interface DigestCtx {
117
+ handle: string;
118
+ }
119
+ export interface ActionCtx<P> {
120
+ props: P;
121
+ handle: string;
122
+ }
123
+ export interface QueryCtx<P> {
124
+ props: P;
125
+ cap: Cap;
126
+ }
127
+
128
+ export interface SurfaceImplDef<P, A extends ActionMap, Q extends QueryMap> {
129
+ /**
130
+ * GUARANTEE 1 — required.
131
+ *
132
+ * A count and a price range is a lazy digest; it invites the model to narrate
133
+ * facts about rows it never received. Precompute whatever the next turn or two
134
+ * will plausibly need.
135
+ */
136
+ digest: (props: P, ctx: DigestCtx) => string;
137
+
138
+ actions: { [K in keyof A]: (value: Infer<A[K]["input"]>, ctx: ActionCtx<P>) => string };
139
+
140
+ /** GUARANTEE 2 — must return `Capped`, i.e. must call `ctx.cap`. */
141
+ queries: { [K in keyof Q]: (args: Infer<Q[K]["input"]>, ctx: QueryCtx<P>) => Capped };
142
+ }
143
+
144
+ export interface Surface<P, A extends ActionMap, Q extends QueryMap> {
145
+ readonly name: string;
146
+ readonly version: number;
147
+ readonly props: Schema<P>;
148
+ readonly actions: A;
149
+ readonly queries: Q;
150
+ implement(impl: SurfaceImplDef<P, A, Q>): SurfaceImpl<P, A, Q>;
151
+ }
152
+
153
+ export interface SurfaceImpl<P, A extends ActionMap, Q extends QueryMap> {
154
+ readonly surface: Surface<P, A, Q>;
155
+ readonly impl: SurfaceImplDef<P, A, Q>;
156
+ }
157
+
158
+ export type AnySurfaceImpl = SurfaceImpl<any, ActionMap, QueryMap>;
159
+
160
+ /**
161
+ * Declare a surface contract. Import this module from BOTH halves: the server
162
+ * calls `.implement()`, the client renders against the same prop type.
163
+ *
164
+ * GUARANTEE 4: `actions` is the complete list of interactions that may reach the
165
+ * server. Anything a component does that is not declared here is local by
166
+ * construction — there is no channel for it.
167
+ */
168
+ export function defineSurface<P, A extends ActionMap = {}, Q extends QueryMap = {}>(def: {
169
+ name: string;
170
+ version: number;
171
+ props: Schema<P>;
172
+ actions?: A;
173
+ queries?: Q;
174
+ }): Surface<P, A, Q> {
175
+ const surface: Surface<P, A, Q> = {
176
+ name: def.name,
177
+ version: def.version,
178
+ props: def.props,
179
+ actions: (def.actions ?? {}) as A,
180
+ queries: (def.queries ?? {}) as Q,
181
+ implement(impl) {
182
+ return { surface, impl };
183
+ },
184
+ };
185
+ return surface;
186
+ }
187
+
188
+ // ─────────────────────────────────────── GUARANTEE 3: elicit safety
189
+
190
+ type ResolveKeys<A extends ActionMap> = {
191
+ [K in keyof A]: A[K]["kind"] extends "resolve" ? K : never;
192
+ }[keyof A];
193
+
194
+ export type HasResolve<A extends ActionMap> = [ResolveKeys<A>] extends [never] ? false : true;
195
+
196
+ /**
197
+ * Passing a surface with no `resolve` action makes this an object type carrying
198
+ * an unsatisfiable required property, so the error names the actual problem
199
+ * instead of "not assignable to never".
200
+ */
201
+ export type ElicitOptions<A extends ActionMap> = HasResolve<A> extends true
202
+ ? { mode: "elicit" }
203
+ : {
204
+ mode: "elicit";
205
+ "⚠ this surface declares no resolve action — an elicit turn could never be unparked": never;
206
+ };
207
+
208
+ // ─────────────────────────────────────────────────────────── tools
209
+
210
+ export interface ToolReturn {
211
+ /** Goes into `tool_result`. The model channel. */
212
+ model: string;
213
+ /** Set when the tool rendered a surface. */
214
+ handle?: string;
215
+ }
216
+
217
+ export interface ToolCtx {
218
+ readonly conversationId: string;
219
+
220
+ /** A tool with no UI. */
221
+ text(model: string): ToolReturn;
222
+
223
+ /** Render a surface as a blocking question. Requires a `resolve` action. */
224
+ render<P, A extends ActionMap, Q extends QueryMap>(
225
+ surface: SurfaceImpl<P, A, Q>,
226
+ props: P,
227
+ options: ElicitOptions<A>,
228
+ ): Promise<ToolReturn>;
229
+
230
+ /** Render a surface as a side-artifact. Resolves immediately. */
231
+ render<P, A extends ActionMap, Q extends QueryMap>(
232
+ surface: SurfaceImpl<P, A, Q>,
233
+ props: P,
234
+ options?: { mode?: "display" },
235
+ ): Promise<ToolReturn>;
236
+ }
237
+
238
+ export interface Tool<I = any> {
239
+ readonly name: string;
240
+ readonly description: string;
241
+ readonly input: Schema<I>;
242
+ readonly inputJsonSchema: JsonSchema;
243
+ readonly strict: boolean;
244
+ run(input: I, ctx: ToolCtx): Promise<ToolReturn> | ToolReturn;
245
+ }
246
+
247
+ export function defineTool<I>(def: {
248
+ name: string;
249
+ description: string;
250
+ input: Schema<I>;
251
+ /** JSON Schema sent to the model. Derive it with zod-to-json-schema if you like. */
252
+ inputJsonSchema: JsonSchema;
253
+ strict?: boolean;
254
+ run(input: I, ctx: ToolCtx): Promise<ToolReturn> | ToolReturn;
255
+ }): Tool<I> {
256
+ return { strict: true, ...def };
257
+ }
258
+
259
+ // ───────────────────────────────────────────────────── conversation
260
+
261
+ export type ConversationStatus = "idle" | "streaming" | "awaiting";
262
+
263
+ /** Anthropic-shaped message. Kept loose so adapters can map as needed. */
264
+ export interface Message {
265
+ role: "user" | "assistant";
266
+ content: unknown;
267
+ }
268
+
269
+ export interface Pending {
270
+ toolUseId: string;
271
+ handle: string;
272
+ /** An elicit tool never received a tool_result, so its digest has not reached
273
+ * the model yet. It ships with the resolution. */
274
+ digest: string;
275
+ /** Sibling results from the same turn. The API is all-or-nothing per batch. */
276
+ results: unknown[];
277
+ }
278
+
279
+ export interface Conversation {
280
+ id: string;
281
+ status: ConversationStatus;
282
+ messages: Message[];
283
+ handles: string[];
284
+ pending: Pending | null;
285
+ /** Turn lease. A dead process leaves this in the past. */
286
+ leaseUntil: number | null;
287
+ }
288
+
289
+ export interface PayloadRecord {
290
+ handle: string;
291
+ conversationId: string;
292
+ component: string;
293
+ version: number;
294
+ props: unknown;
295
+ mode: "display" | "elicit";
296
+ state: "live" | "frozen";
297
+ createdAt: number;
298
+ }
299
+
300
+ // ─────────────────────────────────────────────────── wire protocol
301
+
302
+ export type Block =
303
+ | { kind: "user"; id: string; text: string }
304
+ | { kind: "assistant"; id: string; text: string }
305
+ | { kind: "tool"; id: string; name: string; input: unknown; status: string; ms?: number; result?: string }
306
+ | { kind: "interaction"; id: string; handle: string; label: string };
307
+
308
+ export type WireEvent =
309
+ | { type: "hello"; conversationId: string; model: string }
310
+ | { type: "block_start"; block: Block }
311
+ | { type: "text_delta"; id: string; text: string }
312
+ | { type: "block_update"; id: string; status: string; ms: number; result: string }
313
+ | { type: "ui_open"; handle: string; toolId: string; component: string; version: number; mode: string }
314
+ | { type: "ui_props"; handle: string; props: unknown }
315
+ | { type: "ui_state"; handle: string; state: "frozen"; selection?: unknown }
316
+ | { type: "status"; status: ConversationStatus }
317
+ | { type: "context"; messages: Message[]; modelTokens: number; uiTokens: number }
318
+ | { type: "error"; message: string };
319
+
320
+ export type Emit = (event: WireEvent) => void;
321
+
322
+ // ───────────────────────────────────────────────────────── adapters
323
+
324
+ export interface ModelRequest {
325
+ system: string;
326
+ tools: { name: string; description: string; input_schema: JsonSchema; strict?: boolean }[];
327
+ messages: Message[];
328
+ onTextDelta: (text: string) => void;
329
+ }
330
+
331
+ export interface ModelResponse {
332
+ content: any[];
333
+ stop_reason: string;
334
+ }
335
+
336
+ export interface ModelAdapter {
337
+ readonly id: string;
338
+ generate(request: ModelRequest): Promise<ModelResponse>;
339
+ }
340
+
341
+ export interface StoreAdapter {
342
+ loadConversation(id: string | undefined): Promise<Conversation>;
343
+ saveConversation(conversation: Conversation): Promise<void>;
344
+ putPayload(record: Omit<PayloadRecord, "handle" | "createdAt">): Promise<string>;
345
+ getPayload(handle: string, conversationId: string): Promise<PayloadRecord | null>;
346
+ freezePayload(handle: string): Promise<void>;
347
+ }
348
+
349
+ /** Rough token estimate. Only used to surface the economics in the UI. */
350
+ export const estTokens = (value: unknown): number =>
351
+ Math.ceil((typeof value === "string" ? value : JSON.stringify(value ?? "")).length / 4);