sygnal 6.2.0 → 6.3.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/CHANGELOG.md +61 -0
- package/README.md +3 -1
- package/dist/ai.cjs.js +36 -0
- package/dist/ai.cjs.js.map +1 -1
- package/dist/ai.esm.js +1 -1
- package/dist/astro/server.cjs.js +3 -3
- package/dist/astro/server.cjs.js.map +1 -1
- package/dist/astro/server.mjs +3 -3
- package/dist/astro/server.mjs.map +1 -1
- package/dist/diagnostics.cjs.js +164 -3
- package/dist/diagnostics.cjs.js.map +1 -1
- package/dist/diagnostics.esm.js +164 -3
- package/dist/diagnostics.esm.js.map +1 -1
- package/dist/guide/agent.md +7 -1
- package/dist/guide/ai-cards.md +319 -0
- package/dist/guide/ai-chat.md +4 -0
- package/dist/guide/ai-compose.md +881 -0
- package/dist/guide/ai-decisions.md +16 -0
- package/dist/guide/http.md +1 -0
- package/dist/index.cjs.js +5695 -347
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +638 -8
- package/dist/index.esm.js +5685 -347
- package/dist/index.esm.js.map +1 -1
- package/dist/sygnal.min.js +1 -1
- package/dist/sygnal.min.js.map +1 -1
- package/dist/vike/config/package.json +1 -1
- package/dist/vike/onRenderClient.cjs.js +3 -2
- package/dist/vike/onRenderClient.cjs.js.map +1 -1
- package/dist/vike/onRenderClient.mjs +3 -2
- package/dist/vike/onRenderClient.mjs.map +1 -1
- package/llms.txt +6 -4
- package/package.json +2 -1
- package/src/ai.d.ts +642 -7
- package/src/ai.ts +6 -0
- package/src/collection.ts +2 -1
- package/src/core/instance.ts +6 -3
- package/src/core/markers/lazy.ts +7 -3
- package/src/core/markers/portal.ts +5 -2
- package/src/core/markers/suspense.ts +3 -2
- package/src/core/markers/transition.ts +6 -3
- package/src/core/registry.ts +9 -5
- package/src/cycle/dom/IsolateModule.ts +25 -22
- package/src/extra/ai/chat/behavior.ts +354 -81
- package/src/extra/ai/chat/driver.ts +7 -4
- package/src/extra/ai/compose/agent.ts +55 -0
- package/src/extra/ai/compose/ask.ts +147 -0
- package/src/extra/ai/compose/behavior.ts +183 -0
- package/src/extra/ai/compose/catalog.ts +211 -0
- package/src/extra/ai/compose/composed.ts +61 -0
- package/src/extra/ai/compose/decisions.ts +1649 -0
- package/src/extra/ai/compose/diag.ts +33 -0
- package/src/extra/ai/compose/index.ts +23 -0
- package/src/extra/ai/compose/layout.ts +441 -0
- package/src/extra/ai/compose/llm.ts +1120 -0
- package/src/extra/ai/compose/render.ts +159 -0
- package/src/extra/ai/compose/testing.ts +71 -0
- package/src/extra/ai/compose/types.ts +159 -0
- package/src/extra/ai/decide.ts +97 -1
- package/src/extra/ai/index.ts +6 -1
- package/src/extra/diagnostics/checks/next.ts +86 -2
- package/src/extra/diagnostics/codes.ts +54 -0
- package/src/extra/fetchDriver.ts +58 -27
- package/src/extra/lens.ts +15 -0
- package/src/extra/ssr.ts +3 -3
- package/src/extra/testing.ts +2 -0
- package/src/extra/widget.ts +3 -3
- package/src/index.d.ts +41 -0
- package/src/index.ts +8 -0
- package/src/lazy.ts +3 -2
- package/src/portal.ts +5 -2
- package/src/pragma/index.ts +2 -1
- package/src/suspense.ts +5 -2
- package/src/switchable.ts +2 -1
- package/src/transition.ts +5 -2
- package/src/vike/onRenderClient.ts +3 -2
package/src/ai.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// Types for 'sygnal/ai' (PLAN-6). The implementation lives in src/extra/ai/ (D253).
|
|
2
|
-
import type { FetchRequest, Behavior, BehaviorTarget, FieldErrors } from 'sygnal'
|
|
2
|
+
import type { FetchRequest, Behavior, BehaviorTarget, FieldErrors, RenderResult } from 'sygnal'
|
|
3
3
|
import type { Stream } from 'xstream'
|
|
4
4
|
|
|
5
5
|
/** A part of a chat message (AI SDK UIMessage-shaped, D252) */
|
|
6
6
|
export type MessagePart =
|
|
7
7
|
| { type: 'text'; text: string }
|
|
8
8
|
| { type: 'reasoning'; text: string; /** provider data to send back with it (an Anthropic thinking signature) */ providerMetadata?: Record<string, Record<string, unknown>> }
|
|
9
|
-
| { type: `tool-${string}`; toolCallId: string; state: string; input?: unknown; output?: unknown; errorText?: string; approval?: { id: string; approved?: boolean; reason?: string; [key: string]: unknown }; providerExecuted?: boolean }
|
|
9
|
+
| { type: `tool-${string}`; toolCallId: string; state: string; input?: unknown; output?: unknown; errorText?: string; approval?: { id: string; approved?: boolean; reason?: string; [key: string]: unknown }; providerExecuted?: boolean; /** PLAN-9 H-2: an `ask_user` call's card */ ui?: ChatCard }
|
|
10
10
|
| { type: 'file'; mediaType: string; url: string; filename?: string }
|
|
11
11
|
| { type: `source-${string}`; [key: string]: unknown }
|
|
12
12
|
| { type: `data-${string}`; id?: string; data: unknown }
|
|
@@ -121,7 +121,7 @@ export interface Decision<Q extends Questions = Questions, R = never> {
|
|
|
121
121
|
type FetchRequestObject = Extract<FetchRequest, { url: string }>
|
|
122
122
|
|
|
123
123
|
/** What `decide()` / `decide.openai()` take: the decision, plus any `makeFetchDriver` request keys */
|
|
124
|
-
export type DecideOptions<Q extends Questions> = Omit<FetchRequestObject, 'url' | 'json' | 'body'> & {
|
|
124
|
+
export type DecideOptions<Q extends Questions> = Omit<FetchRequestObject, 'url' | 'json' | 'body' | 'batch' | 'combine' | 'split'> & {
|
|
125
125
|
/** Default '/api/decide' (the app's own route, which holds the API key) */
|
|
126
126
|
url?: string
|
|
127
127
|
/** The decision model, e.g. 'jev-latest' (TypeSafe), 'nimble' (Ollama), 'gpt-6-luna' (OpenAI) */
|
|
@@ -131,6 +131,23 @@ export type DecideOptions<Q extends Questions> = Omit<FetchRequestObject, 'url'
|
|
|
131
131
|
questions: Q
|
|
132
132
|
/** Base64 PNG / JPEG / WebP files shared by every question (models that read images only) */
|
|
133
133
|
images?: readonly string[]
|
|
134
|
+
/**
|
|
135
|
+
* D472: more questions than this go as parallel sub-requests (a fetch-driver `batch`), their
|
|
136
|
+
* replies merged into one (`answers` by name, `usage` summed). Default 64 (Ollama's limit)
|
|
137
|
+
*/
|
|
138
|
+
maxQuestions?: number
|
|
139
|
+
/**
|
|
140
|
+
* D472: split the same way when the estimated prompt of one question (the state plus every
|
|
141
|
+
* question of the request, as Ollama counts it; the estimate runs high) passes this. Default:
|
|
142
|
+
* none; a request a server rejects as too large is halved anyway (`split`)
|
|
143
|
+
*/
|
|
144
|
+
maxPromptTokens?: number
|
|
145
|
+
/**
|
|
146
|
+
* false: one request whatever its size, and no halving on a "too large" rejection. Default: on
|
|
147
|
+
* (a request under the limits is sent as is; a 400 / 413 that names tokens, fields, context, a
|
|
148
|
+
* limit or "too long / many / large" sends it again in halves, at most 3 deep)
|
|
149
|
+
*/
|
|
150
|
+
split?: boolean
|
|
134
151
|
/** As a resource: keep the previous `data` on a new request */
|
|
135
152
|
keepPrevious?: boolean
|
|
136
153
|
/** As a resource: refetch every this many ms */
|
|
@@ -140,10 +157,15 @@ export type DecideOptions<Q extends Questions> = Omit<FetchRequestObject, 'url'
|
|
|
140
157
|
}
|
|
141
158
|
|
|
142
159
|
/** A decision as a `makeFetchDriver` request (POST JSON); its reply is a `Decision<Q, R>` */
|
|
143
|
-
export type DecideRequest<Q extends Questions = Questions, R = never> = Omit<DecideOptions<Q>, 'model' | 'state' | 'questions' | 'images' | 'url'> & {
|
|
160
|
+
export type DecideRequest<Q extends Questions = Questions, R = never> = Omit<DecideOptions<Q>, 'model' | 'state' | 'questions' | 'images' | 'url' | 'maxQuestions' | 'maxPromptTokens' | 'split'> & {
|
|
144
161
|
url: string
|
|
145
162
|
method: 'POST'
|
|
163
|
+
/** the whole decision (what a test fake answers); over the limits, the parts go in `batch` */
|
|
146
164
|
json: Record<string, unknown>
|
|
165
|
+
/** D472: the parts of a decision over the limits (sent in parallel, merged by `combine`) */
|
|
166
|
+
batch?: ReadonlyArray<{ url: string; method: 'POST'; json: Record<string, unknown>; parse?: (response: Response) => any }>
|
|
167
|
+
combine?: (bodies: any[], requests: any[]) => any
|
|
168
|
+
split?: (request: any, error: Error & { status?: number; body?: any }) => any[] | null
|
|
147
169
|
/** Type only: the reply's shape (`Decision<Q, R>`); never set */
|
|
148
170
|
readonly __decision?: Decision<Q, R>
|
|
149
171
|
}
|
|
@@ -490,6 +512,11 @@ export type ChatRequest<OUT = unknown> = {
|
|
|
490
512
|
* `ok` gets the validated `value`, a failure is the `error` reply with `issues`
|
|
491
513
|
*/
|
|
492
514
|
output?: ChatOutputSchema<OUT>;
|
|
515
|
+
/**
|
|
516
|
+
* `false`: the transport still sends `output`'s JSON Schema, but the driver doesn't read the reply:
|
|
517
|
+
* `ok` has the `text` and no `value` (the caller parses it, e.g. leniently; compose.llm() does)
|
|
518
|
+
*/
|
|
519
|
+
parse?: false;
|
|
493
520
|
/** The request's key for `latest` and `abort` (default: `ok`, else `error`) */
|
|
494
521
|
key?: string;
|
|
495
522
|
/** A new request aborts the sender's request under the same key (default true) */
|
|
@@ -522,7 +549,7 @@ export type ChatRequest<OUT = unknown> = {
|
|
|
522
549
|
}
|
|
523
550
|
|
|
524
551
|
/** Stops the sender's requests under a key (`{ abort: 'reply' }`, `{ abort: true, key: 'reply' }`), or all of them (`{ abort: true }`) */
|
|
525
|
-
export type ChatAbort = { abort: string | true; key?: string }
|
|
552
|
+
export type ChatAbort = { abort: string | readonly string[] | true; key?: string }
|
|
526
553
|
|
|
527
554
|
/** `delta` action data: the reply so far */
|
|
528
555
|
export type ChatDelta = {
|
|
@@ -733,6 +760,16 @@ export interface ChatActions {
|
|
|
733
760
|
DENY: any
|
|
734
761
|
/** The turn is over (a host entry `'assistant.DONE'` runs after the behavior's) */
|
|
735
762
|
DONE: ChatDone
|
|
763
|
+
/**
|
|
764
|
+
* PLAN-9 H-1: answer an open client tool call (`clientTools`, or `ask_user`): an
|
|
765
|
+
* `input-available` client part of the last message (else nothing happens, SYG685). The next
|
|
766
|
+
* request goes once no call is open. The AI SDK's `addToolOutput`
|
|
767
|
+
*/
|
|
768
|
+
TOOL_OUTPUT: { toolCallId: string; output: unknown } | { toolCallId: string; errorText: string }
|
|
769
|
+
/** PLAN-9 H-1: a client tool call opened (a host entry `'assistant.TOOL_CALL'` runs after it, once per call; the AI SDK's `onToolCall`) */
|
|
770
|
+
TOOL_CALL: ChatCall
|
|
771
|
+
/** PLAN-9 H-2: a result part answered a card (`compose.answer(props, value)`, heard through CHILD.select): the call's output is `{ part, answer }` */
|
|
772
|
+
ANSWER: ComposeAnswer
|
|
736
773
|
/** Internal: the prompt field's input */
|
|
737
774
|
PROMPT: string
|
|
738
775
|
}
|
|
@@ -759,8 +796,13 @@ export interface ChatOptions {
|
|
|
759
796
|
* user-entered text named; G-631)
|
|
760
797
|
*/
|
|
761
798
|
instructions?: string
|
|
762
|
-
/**
|
|
763
|
-
|
|
799
|
+
/**
|
|
800
|
+
* A model id for the requests (overrides the transport's), or a function of the host's state
|
|
801
|
+
* that returns one, resolved per request: `model: (state) => state.chatModel` lets an app's model
|
|
802
|
+
* picker switch models with one chat behavior. The default card backend (`ask` without
|
|
803
|
+
* `backend`) uses it too
|
|
804
|
+
*/
|
|
805
|
+
model?: string | ((state: any) => string | undefined)
|
|
764
806
|
/**
|
|
765
807
|
* Whose `agent` declarations become tools: by default the host's and its shown descendants'
|
|
766
808
|
* (D249); `false`: none; `[Comp, …]`: only these components' (in the host's subtree)
|
|
@@ -770,6 +812,13 @@ export interface ChatOptions {
|
|
|
770
812
|
maxSteps?: number
|
|
771
813
|
/** Extra request keys for the transport */
|
|
772
814
|
transportOptions?: Record<string, unknown>
|
|
815
|
+
/**
|
|
816
|
+
* PLAN-9 H-1: tools the app answers itself: sent with the agent tools (a name in both: the
|
|
817
|
+
* client tool, SYG684); their calls stay `input-available` until `TOOL_OUTPUT` answers them
|
|
818
|
+
*/
|
|
819
|
+
clientTools?: Record<string, ChatClientTool>
|
|
820
|
+
/** PLAN-9 H-2: the `ask_user` tool: the model says what it needs, and the app shows a card built from the catalog */
|
|
821
|
+
ask?: ChatAskOptions
|
|
773
822
|
}
|
|
774
823
|
|
|
775
824
|
/**
|
|
@@ -1220,3 +1269,589 @@ export interface McpAppDriverOptions {
|
|
|
1220
1269
|
* run(WeatherCard, { MCP: makeMcpAppDriver() })
|
|
1221
1270
|
*/
|
|
1222
1271
|
export function makeMcpAppDriver(options?: McpAppDriverOptions): (sink$: Stream<McpAppRequest>) => McpAppSource
|
|
1272
|
+
|
|
1273
|
+
// ---- PLAN-9 compose ------------------------------------------------------------------------------
|
|
1274
|
+
// (worker A: catalogs, layouts, render, the compose behavior, composeWith; B and C add their
|
|
1275
|
+
// sub-sections below)
|
|
1276
|
+
|
|
1277
|
+
/** A record for a record param: `from: (state) => records` (the value becomes the node key `part:value`) */
|
|
1278
|
+
export interface CatalogRecord<V extends string | number = string | number> {
|
|
1279
|
+
value: V
|
|
1280
|
+
name?: string
|
|
1281
|
+
aliases?: string[]
|
|
1282
|
+
description?: string
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
/** A part's (or container's) param declaration */
|
|
1286
|
+
export type CatalogParam =
|
|
1287
|
+
/** an enum: the model picks one option (decisions: a choice, `not_stated` = the default; LLM: an enum) */
|
|
1288
|
+
| { options: Record<string, string>; default?: string; label?: string; ask?: string }
|
|
1289
|
+
/** a number (LLM: a number; decisions: a bin, which names the vague words it covers) */
|
|
1290
|
+
| { type: 'number'; min?: number; max?: number; default?: number; bins?: Record<string | number, string>; label?: string }
|
|
1291
|
+
/** free text: LLM only (a decision model keeps the default; `required` with no default makes the part LLM-only) */
|
|
1292
|
+
| { type: 'text'; maxLength?: number; default?: string; required?: boolean; label?: string }
|
|
1293
|
+
/** a list of short texts: LLM only */
|
|
1294
|
+
| { type: 'list'; items?: { maxLength?: number }; min?: number; max?: number; required?: boolean; default?: readonly string[]; label?: string }
|
|
1295
|
+
/** a record: one node per record (at most one per part) */
|
|
1296
|
+
| { from: (state: any) => ReadonlyArray<CatalogRecord>; label?: string }
|
|
1297
|
+
|
|
1298
|
+
/** A param's value type */
|
|
1299
|
+
export type CatalogParamValue<P> =
|
|
1300
|
+
P extends { from: (state: any) => ReadonlyArray<{ value: infer V }> } ? V
|
|
1301
|
+
: P extends { options: infer O } ? keyof O & string
|
|
1302
|
+
: P extends { type: 'number' } ? number
|
|
1303
|
+
: P extends { type: 'text' } ? string
|
|
1304
|
+
: P extends { type: 'list' } ? string[]
|
|
1305
|
+
: unknown
|
|
1306
|
+
|
|
1307
|
+
/** The params of a part or container declaration, as a layout or tree holds them (all optional: defaults are left out) */
|
|
1308
|
+
export type CatalogParamsOf<E> = E extends { params: infer PS } ? { [K in keyof PS]?: CatalogParamValue<PS[K]> } : Record<string, never>
|
|
1309
|
+
|
|
1310
|
+
/** A node of a layout */
|
|
1311
|
+
export interface LayoutNode {
|
|
1312
|
+
part: string
|
|
1313
|
+
/** only the non-default values */
|
|
1314
|
+
params?: Record<string, unknown>
|
|
1315
|
+
/** a container's slots: node keys in order */
|
|
1316
|
+
slots?: Record<string, string[]>
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
/**
|
|
1320
|
+
* A composition's layout: a node map whose keys are identities (the part id; `part:recordValue`;
|
|
1321
|
+
* repeats `~2`, `~3`; containers by id: `row`, `row~2`). Plain JSON state.
|
|
1322
|
+
*/
|
|
1323
|
+
export interface Layout {
|
|
1324
|
+
root: 'page'
|
|
1325
|
+
nodes: Record<string, LayoutNode>
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
/** A nested tree (what a model writes, what tests use), not tied to a catalog */
|
|
1329
|
+
export type ComposeTree = Array<{ part: string; params?: Record<string, unknown>; children?: ComposeTree | Record<string, ComposeTree> }>
|
|
1330
|
+
|
|
1331
|
+
type CatalogLens = { get(state: any): any; set?(state: any, value: any): any }
|
|
1332
|
+
|
|
1333
|
+
/** A part declaration */
|
|
1334
|
+
export interface CatalogPartDecl<STATE = any> {
|
|
1335
|
+
/** a Sygnal component (a child of the host: CHILD.select, lenses, EVENTS and context work) */
|
|
1336
|
+
component?: any
|
|
1337
|
+
/** or a view function, called inline with the props (and `state` read through the lens) */
|
|
1338
|
+
render?: (props: any) => any
|
|
1339
|
+
/** fixed props (params go over them), or `(params, state) => props` */
|
|
1340
|
+
props?: Record<string, any> | ((params: Record<string, any>, state: STATE) => Record<string, any>)
|
|
1341
|
+
/** all a model sees of it: "Noun (what it shows)" */
|
|
1342
|
+
description: string
|
|
1343
|
+
/** its state: a key of the host's state, a lens, or `(node, key) => lens` (a record part: `byId(…)`) */
|
|
1344
|
+
state?: string | CatalogLens | ((node: LayoutNode & { params: Record<string, any> }, key: string) => string | CatalogLens)
|
|
1345
|
+
/** state kept in the composition (`slice.local[nodeKey]`), starting as this */
|
|
1346
|
+
local?: unknown
|
|
1347
|
+
params?: Record<string, CatalogParam>
|
|
1348
|
+
/** where it may go: `'page'`, `'container'` (any slot) or `'container:slot'` (default `['page']`) */
|
|
1349
|
+
in?: string | readonly string[]
|
|
1350
|
+
/** alternatives: the first part of a group in a layout wins */
|
|
1351
|
+
group?: string
|
|
1352
|
+
/** how often one identity may appear (default 1) */
|
|
1353
|
+
maxUses?: number
|
|
1354
|
+
/** always on the page (added by the finishing step; not offered to a model) */
|
|
1355
|
+
required?: boolean
|
|
1356
|
+
pin?: 'start' | 'end'
|
|
1357
|
+
/** parts added right after it */
|
|
1358
|
+
companions?: readonly string[]
|
|
1359
|
+
category?: string
|
|
1360
|
+
/** a part that answers a card (`compose.answer(props, value)`) */
|
|
1361
|
+
result?: boolean
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
/** What a container's view gets */
|
|
1365
|
+
export interface ContainerView {
|
|
1366
|
+
key: string
|
|
1367
|
+
node: LayoutNode
|
|
1368
|
+
/** its params, defaults filled */
|
|
1369
|
+
params: Record<string, any>
|
|
1370
|
+
/** each slot's vnodes */
|
|
1371
|
+
slots: Record<string, any[]>
|
|
1372
|
+
/** every child in order */
|
|
1373
|
+
children: Array<{ key: string; node: LayoutNode; part: CatalogPart | CatalogContainer; slot: string; vnode: any }>
|
|
1374
|
+
/** `slice.ui[key]` */
|
|
1375
|
+
ui: Record<string, any>
|
|
1376
|
+
/** data attributes to spread on an element: a click on it sets `ui[key][name] = value` (the behavior's CONTROL) */
|
|
1377
|
+
control(name: string, value?: unknown): Record<string, string>
|
|
1378
|
+
scope?: string
|
|
1379
|
+
frozen: boolean
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
/** A container declaration: a view function with named slots (the model never chooses one) */
|
|
1383
|
+
export interface CatalogContainerDecl {
|
|
1384
|
+
/** slot name → description (default `{ default: null }`) */
|
|
1385
|
+
slots?: Record<string, string | null>
|
|
1386
|
+
/** fewer filled slots: unwrapped */
|
|
1387
|
+
minSlots?: number
|
|
1388
|
+
/** children per slot: fewer than `min` unwraps it, more than `max` overflow after it (a row splits) */
|
|
1389
|
+
min?: number
|
|
1390
|
+
max?: number
|
|
1391
|
+
/** only parts in its slots */
|
|
1392
|
+
accepts?: 'leaves'
|
|
1393
|
+
/** its order is fixed (no rows inside) */
|
|
1394
|
+
arranged?: boolean
|
|
1395
|
+
direction?: 'row' | 'column'
|
|
1396
|
+
description?: string
|
|
1397
|
+
/** where it may go (default `['page']`) */
|
|
1398
|
+
in?: string | readonly string[]
|
|
1399
|
+
params?: Record<string, CatalogParam>
|
|
1400
|
+
/** a view function, never a component (SYG161) */
|
|
1401
|
+
render?: (view: ContainerView) => any
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
/** defineCatalog's declaration */
|
|
1405
|
+
export interface CatalogDecl {
|
|
1406
|
+
parts: Record<string, CatalogPartDecl>
|
|
1407
|
+
/** `page` and `row` are built in (override their render; `row: false` opts out, as `rows: false` does) */
|
|
1408
|
+
containers?: Record<string, CatalogContainerDecl | false>
|
|
1409
|
+
/** the page's label */
|
|
1410
|
+
description?: string
|
|
1411
|
+
/** `{ id: description }`, named by parts' `category` (a prefilter for large catalogs) */
|
|
1412
|
+
categories?: Record<string, string>
|
|
1413
|
+
/** false: no built-in row (canonical; `containers: { row: false }` too) */
|
|
1414
|
+
rows?: boolean
|
|
1415
|
+
/** quoted in order questions and the LLM prompt */
|
|
1416
|
+
guidance?: string
|
|
1417
|
+
/**
|
|
1418
|
+
* maxParts (default 12): parts on the page. maxDepth (default 4): how deep a container may sit,
|
|
1419
|
+
* the page's children at depth 1; a deeper one moves to the page
|
|
1420
|
+
*/
|
|
1421
|
+
limits?: { maxParts?: number; maxDepth?: number }
|
|
1422
|
+
/** app-written few-shot cases (LLM) */
|
|
1423
|
+
examples?: readonly unknown[]
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
/** A normalized part */
|
|
1427
|
+
export interface CatalogPart {
|
|
1428
|
+
id: string
|
|
1429
|
+
kind: 'part'
|
|
1430
|
+
description: string
|
|
1431
|
+
params: Record<string, CatalogParam & { kind: 'options' | 'number' | 'text' | 'list' | 'record' }>
|
|
1432
|
+
/** the record param's name */
|
|
1433
|
+
record?: string
|
|
1434
|
+
/** 'container:slot' entries */
|
|
1435
|
+
in: string[]
|
|
1436
|
+
group?: string
|
|
1437
|
+
maxUses: number
|
|
1438
|
+
required: boolean
|
|
1439
|
+
pin?: 'start' | 'end'
|
|
1440
|
+
companions: string[]
|
|
1441
|
+
category?: string
|
|
1442
|
+
result: boolean
|
|
1443
|
+
decl: CatalogPartDecl
|
|
1444
|
+
[key: string]: any
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
/** A normalized container */
|
|
1448
|
+
export interface CatalogContainer {
|
|
1449
|
+
id: string
|
|
1450
|
+
kind: 'container'
|
|
1451
|
+
slots: Record<string, string | null>
|
|
1452
|
+
slotNames: string[]
|
|
1453
|
+
minSlots?: number
|
|
1454
|
+
min?: number
|
|
1455
|
+
max?: number
|
|
1456
|
+
accepts?: 'leaves'
|
|
1457
|
+
arranged: boolean
|
|
1458
|
+
direction?: 'row' | 'column'
|
|
1459
|
+
description?: string
|
|
1460
|
+
in: string[]
|
|
1461
|
+
params: Record<string, CatalogParam>
|
|
1462
|
+
render: (view: ContainerView) => any
|
|
1463
|
+
decl: CatalogContainerDecl
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
/** One selectable option: a part, or a record part once per record (`part:value`) */
|
|
1467
|
+
export interface CatalogOption { option: string; part: string; params?: Record<string, unknown>; description: string }
|
|
1468
|
+
|
|
1469
|
+
type CatalogPartIds<D> = D extends { parts: infer P } ? keyof P & string : string
|
|
1470
|
+
type CatalogContainerIds<D> = (D extends { containers: infer C } ? { [K in keyof C & string]: C[K] extends false ? never : K }[keyof C & string] : never) | 'row'
|
|
1471
|
+
type CatalogContainerOf<D, C> = D extends { containers: infer CS } ? C extends keyof CS ? CS[C] : {} : {}
|
|
1472
|
+
type CatalogSlotsOf<D, C> = CatalogContainerOf<D, C> extends { slots: infer S } ? keyof S & string : 'default'
|
|
1473
|
+
type CatalogPartOf<D, P> = D extends { parts: infer PS } ? P extends keyof PS ? PS[P] : never : never
|
|
1474
|
+
|
|
1475
|
+
/** A tree node for a catalog: a part with its params, or a container with its children */
|
|
1476
|
+
export type CatalogTreeNode<D> =
|
|
1477
|
+
| { [P in CatalogPartIds<D>]: { part: P; params?: CatalogParamsOf<CatalogPartOf<D, P>> } }[CatalogPartIds<D>]
|
|
1478
|
+
| { [C in CatalogContainerIds<D>]: { part: C; params?: CatalogParamsOf<CatalogContainerOf<D, C>>; children?: CatalogTreeNode<D>[] | { [S in CatalogSlotsOf<D, C>]?: CatalogTreeNode<D>[] } } }[CatalogContainerIds<D>]
|
|
1479
|
+
|
|
1480
|
+
/** A nested tree for a catalog (part ids, params and slots checked); `ComposeTree` for an untyped one */
|
|
1481
|
+
export type CatalogTree<D> = 0 extends (1 & D) ? ComposeTree : CatalogTreeNode<D>[]
|
|
1482
|
+
|
|
1483
|
+
/** catalog.render's options */
|
|
1484
|
+
export interface ComposeRenderOptions {
|
|
1485
|
+
/** several compositions in one host (one per chat card): instance keys become `scope/nodeKey`; parts get `scope` */
|
|
1486
|
+
scope?: string
|
|
1487
|
+
/** an answered card: wrapped in `<fieldset disabled>`; parts get `frozen` */
|
|
1488
|
+
frozen?: boolean
|
|
1489
|
+
/** a result part's answer: `answer.value` goes to the part whose key is `answer.node` */
|
|
1490
|
+
answer?: { node: string; value: unknown } | null
|
|
1491
|
+
/** the slice's path in the host state (for `local` parts; default: the host key whose value is the slice) */
|
|
1492
|
+
at?: string | ReadonlyArray<string | number>
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
/** What catalog.render renders: a compose slice, or anything with a layout */
|
|
1496
|
+
export type ComposeRenderable = { layout: Layout | null; preview?: Layout | null; ui?: Record<string, Record<string, any>>; local?: Record<string, any> }
|
|
1497
|
+
|
|
1498
|
+
/** A catalog (defineCatalog's result); D is the declaration, for typed trees */
|
|
1499
|
+
export interface Catalog<D = CatalogDecl> {
|
|
1500
|
+
/** (type only) the declaration */
|
|
1501
|
+
readonly __decl?: D
|
|
1502
|
+
parts: Record<string, CatalogPart>
|
|
1503
|
+
containers: Record<string, CatalogContainer>
|
|
1504
|
+
get(id: string): CatalogPart | CatalogContainer | undefined
|
|
1505
|
+
/** every selectable option for this state */
|
|
1506
|
+
options(state: any): CatalogOption[]
|
|
1507
|
+
/** a record part's records (validated: SYG252) */
|
|
1508
|
+
records(part: string | CatalogPart, state: any): CatalogRecord[]
|
|
1509
|
+
/** the deterministic finishing step (idempotent) */
|
|
1510
|
+
finish(layout: Layout | null | undefined, state?: any): Layout
|
|
1511
|
+
/**
|
|
1512
|
+
* The composition as vnodes for the host's own view: `slice.preview ?? slice.layout`, each part a
|
|
1513
|
+
* child of the host (keyed by its node key), containers through their view functions.
|
|
1514
|
+
*/
|
|
1515
|
+
render(slice: ComposeRenderable | null | undefined, state: any, options?: ComposeRenderOptions): any
|
|
1516
|
+
description?: string
|
|
1517
|
+
categories: Record<string, string>
|
|
1518
|
+
rows: boolean
|
|
1519
|
+
guidance?: string
|
|
1520
|
+
limits: { maxParts: number; maxDepth: number }
|
|
1521
|
+
examples: unknown[]
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
/**
|
|
1525
|
+
* PLAN-9 K-1: a catalog of prepared parts a model composes from, one declaration for both
|
|
1526
|
+
* backends. `catalog.render(state.page, state)` renders a composition.
|
|
1527
|
+
*/
|
|
1528
|
+
export function defineCatalog<const D extends CatalogDecl>(decl: D): Catalog<D>
|
|
1529
|
+
|
|
1530
|
+
/** fromTree(catalog, tree, previous?): a tree → a layout, reusing `previous`'s keys (same params, then same identity) */
|
|
1531
|
+
export function fromTree<D>(catalog: Catalog<D>, tree: CatalogTree<D>, previous?: Layout | null): Layout
|
|
1532
|
+
/** toTree(layout): the nested tree (keys dropped) */
|
|
1533
|
+
export function toTree(layout: Layout | null | undefined): ComposeTree
|
|
1534
|
+
/** removeNode(layout, key): the layout without that node and its subtree (not finished) */
|
|
1535
|
+
export function removeNode(layout: Layout, key: string): Layout
|
|
1536
|
+
|
|
1537
|
+
/** The reply action names a backend's requests carry */
|
|
1538
|
+
export interface ComposeNames { ok: string; error: string; delta: string; key: string }
|
|
1539
|
+
|
|
1540
|
+
/** A composition backend (`compose.decisions()`, `compose.llm()`, or your own) */
|
|
1541
|
+
export interface ComposeBackend {
|
|
1542
|
+
/** the sink its requests go to: 'HTTP' (decisions) or the LLM sink */
|
|
1543
|
+
sink: string
|
|
1544
|
+
start(a: { catalog: Catalog<any>; state: any; prompt: string; previous: Layout | null; names: ComposeNames }): { request: unknown; pending: unknown }
|
|
1545
|
+
/**
|
|
1546
|
+
* a layout (done), `layout: null, unavailable: true` (nothing fits), `request` + `pending` (another
|
|
1547
|
+
* step), or `error`. `keyed: true`: the layout and preview already carry the previous layout's
|
|
1548
|
+
* keys (an edit), so the behavior doesn't re-key them by identity
|
|
1549
|
+
*/
|
|
1550
|
+
reply(a: { catalog: Catalog<any>; state: any; pending: any; reply: any; names: ComposeNames }):
|
|
1551
|
+
{ layout?: Layout | null; unavailable?: true; preview?: Layout; request?: unknown; pending?: unknown; decisions?: unknown; error?: string; keyed?: boolean }
|
|
1552
|
+
/** `keyed` (on reply too): the layout already has the previous layout's keys (an edit applied to it): the behavior doesn't re-key it */
|
|
1553
|
+
delta?(a: { catalog: Catalog<any>; state: any; pending: any; delta: any }): { preview?: Layout; keyed?: boolean }
|
|
1554
|
+
/** the reply body that leads to `target` (composeWith) */
|
|
1555
|
+
fake(a: { catalog: Catalog<any>; state: any; pending: any; target: ComposeTree }): unknown
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
export type ComposeStatus = 'idle' | 'composing' | 'preview' | 'done' | 'unavailable' | 'error'
|
|
1559
|
+
|
|
1560
|
+
/** The compose behavior's slice: `state.page` for `uses = { page: compose(...) }` */
|
|
1561
|
+
export interface ComposeState {
|
|
1562
|
+
prompt: string
|
|
1563
|
+
status: ComposeStatus
|
|
1564
|
+
/** the last finished layout (kept on an error and when nothing fits) */
|
|
1565
|
+
layout: Layout | null
|
|
1566
|
+
/** the layout being built (rendered instead while set) */
|
|
1567
|
+
preview: Layout | null
|
|
1568
|
+
/** the backend's step (plain JSON) */
|
|
1569
|
+
pending: unknown
|
|
1570
|
+
/** what the backend explains (decisions: its answers; LLM: its plan) */
|
|
1571
|
+
decisions: unknown
|
|
1572
|
+
error: string | null
|
|
1573
|
+
/** container state per node key (CONTROL) */
|
|
1574
|
+
ui: Record<string, Record<string, any>>
|
|
1575
|
+
/** `local` parts' state per node key */
|
|
1576
|
+
local: Record<string, any>
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
/** The compose behavior's actions, as `'<key>.<ACTION>'` on the host */
|
|
1580
|
+
export interface ComposeActions {
|
|
1581
|
+
/** compose (an edit of the current layout unless `fresh`): a prompt, `{ prompt, fresh }`, or the form's submit (the slice's prompt) */
|
|
1582
|
+
COMPOSE: string | { prompt: string; fresh?: boolean } | Event | undefined
|
|
1583
|
+
/** set the layout (finished first): restore, undo, the app's own edits; null clears */
|
|
1584
|
+
SET_LAYOUT: Layout | null
|
|
1585
|
+
/** remove a node (by key) and its subtree */
|
|
1586
|
+
REMOVE: string
|
|
1587
|
+
/** a container control: `ui[key][name] = value` */
|
|
1588
|
+
CONTROL: { key: string; name: string; value: unknown }
|
|
1589
|
+
/** a composition finished (a host entry `'page.DONE'` runs after the behavior's) */
|
|
1590
|
+
DONE: Layout
|
|
1591
|
+
/** Internal: the prompt field's input */
|
|
1592
|
+
PROMPT: string
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
export interface ComposeOptions {
|
|
1596
|
+
/** `compose.decisions({ … })`, `compose.llm({ … })`, or your own ComposeBackend */
|
|
1597
|
+
backend: ComposeBackend
|
|
1598
|
+
/** the form whose submit composes */
|
|
1599
|
+
form?: BehaviorTarget
|
|
1600
|
+
/** the prompt field (its input sets `prompt`) */
|
|
1601
|
+
prompt?: BehaviorTarget
|
|
1602
|
+
/** the request lane (default 'compose'): a new prompt aborts the step in flight */
|
|
1603
|
+
key?: string
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
/** A result part's PARENT value (`compose.answer(props, value)`) */
|
|
1607
|
+
export interface ComposeAnswer<V = unknown> { scope: string | undefined; node: string | undefined; value: V }
|
|
1608
|
+
|
|
1609
|
+
/**
|
|
1610
|
+
* PLAN-9 C-1: compose a page from a catalog. `uses = { page: compose(catalog, { backend:
|
|
1611
|
+
* compose.decisions({ model }), form: '.ask', prompt: '.prompt' }) }`, rendered with
|
|
1612
|
+
* `catalog.render(state.page, state)`.
|
|
1613
|
+
*/
|
|
1614
|
+
export const compose: {
|
|
1615
|
+
(catalog: Catalog<any>, options: ComposeOptions): Behavior<ComposeState, ComposeActions, {}, ComposeOptions>
|
|
1616
|
+
/** a decision-model backend (decide() on HTTP) */
|
|
1617
|
+
decisions(options?: ComposeDecisionsOptions): ComposeBackend
|
|
1618
|
+
/** a chat-LLM backend (structured output on the LLM sink) */
|
|
1619
|
+
llm(options?: ComposeLlmOptions): ComposeBackend
|
|
1620
|
+
/** a result part's PARENT value: `{ scope, node, value }` */
|
|
1621
|
+
answer<V>(props: { scope?: string; node?: string }, value: V): ComposeAnswer<V>
|
|
1622
|
+
/**
|
|
1623
|
+
* PLAN-9 C-2: the `agent.actions` entry that lets an agent (a chat assistant) compose the page:
|
|
1624
|
+
* `actions: { ...compose.action('page') }` offers `<agent name>_page_compose({ prompt, fresh? })`;
|
|
1625
|
+
* its result waits (`until`) for the composition to finish, so its `state` (the `read`) shows the
|
|
1626
|
+
* new page (`read: (s) => ({ page: compose.summary(s.page) })`). `timeout` default 60 s
|
|
1627
|
+
*/
|
|
1628
|
+
action<K extends string = 'page'>(key?: K, options?: { description?: string; timeout?: number }): Record<`${K}.COMPOSE`, AgentAction<any, { prompt: string; fresh?: boolean }>>
|
|
1629
|
+
/** PLAN-9 C-2: a composition as an agent's `read` shows it: its status, the layout as a tree, the error */
|
|
1630
|
+
summary(slice: ComposeState | null | undefined): { status: ComposeStatus; page: ComposeTree; error?: string } | undefined
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
/**
|
|
1634
|
+
* PLAN-9 T-1: answer the pending composition of `catalog` so that it becomes `tree`, whatever the
|
|
1635
|
+
* backend (each step through the backend's fake reply). `stream: n` streams an LLM reply in n chunks.
|
|
1636
|
+
*/
|
|
1637
|
+
export function composeWith<D>(t: RenderResult<any>, catalog: Catalog<D>, tree: CatalogTree<D>, options?: { stream?: number | boolean }): Promise<void>
|
|
1638
|
+
|
|
1639
|
+
// PLAN-9 B: compose.decisions
|
|
1640
|
+
/** `compose.decisions(options)`: the decision-model backend (`decide()` on the HTTP sink) */
|
|
1641
|
+
export interface ComposeDecisionsOptions {
|
|
1642
|
+
/** the decision endpoint (default '/api/decide': the app's own route, which holds the API key) */
|
|
1643
|
+
url?: string
|
|
1644
|
+
/** the decision model ('jev-latest', 'nimble', …); without one: SYG681 (dev) */
|
|
1645
|
+
model?: string
|
|
1646
|
+
/** the request lane (`key` of every `decide()` request; default: the behavior's, 'compose') */
|
|
1647
|
+
key?: string
|
|
1648
|
+
/**
|
|
1649
|
+
* rows: the mean side-by-side probability two groups of parts need to join one row
|
|
1650
|
+
* (average-linkage clustering of the `beside_` answers; default 0.25, measured on Jev; 0.5 suits nimble)
|
|
1651
|
+
*/
|
|
1652
|
+
rowMean?: number
|
|
1653
|
+
/**
|
|
1654
|
+
* ask which part `categories` the request is about first, then only about their parts (one more
|
|
1655
|
+
* call, about half the input tokens; measured on Jev's 50-part ops console: the same accuracy).
|
|
1656
|
+
* 'auto' (default): on when the catalog has categories and more than 60 parts to choose from
|
|
1657
|
+
* (30 with `profile: 'small'`); `true`: always (with categories); `false`: never
|
|
1658
|
+
*/
|
|
1659
|
+
prefilter?: boolean | 'auto'
|
|
1660
|
+
/**
|
|
1661
|
+
* the most options one choice question may have (default 255; 25 when `url` looks like Ollama;
|
|
1662
|
+
* at least 4): every larger choice is split into chunks that repeat its head options
|
|
1663
|
+
*/
|
|
1664
|
+
maxOptions?: number
|
|
1665
|
+
/**
|
|
1666
|
+
* 'small': for a local ~9B decision model such as Ollama's nimble, which favours the first of two
|
|
1667
|
+
* options: pairwise order questions are asked in both orders, and near-ties follow the order in
|
|
1668
|
+
* which the request names the parts (G-755); at most 8 parts per composition (`maxParts`); the
|
|
1669
|
+
* category prefilter from 30 parts
|
|
1670
|
+
*/
|
|
1671
|
+
profile?: 'small'
|
|
1672
|
+
/**
|
|
1673
|
+
* the most parts one composition selects (the most probable kept; required parts and companions
|
|
1674
|
+
* come on top). Default: the catalog's `limits.maxParts` (12); 8 with `profile: 'small'`. The
|
|
1675
|
+
* catalog's limit still caps the page
|
|
1676
|
+
*/
|
|
1677
|
+
maxParts?: number
|
|
1678
|
+
/** passed to every `decide()` request (D472): see `DecideOptions`. Default 64 */
|
|
1679
|
+
maxQuestions?: number
|
|
1680
|
+
/** passed to every `decide()` request (D472): see `DecideOptions`. Default: none */
|
|
1681
|
+
maxPromptTokens?: number
|
|
1682
|
+
/** passed to every `decide()` request (D472): false sends each step as one request, never halved */
|
|
1683
|
+
split?: boolean
|
|
1684
|
+
}
|
|
1685
|
+
/**
|
|
1686
|
+
* the decisions backend: plain-JSON `pending` steps (`'categories'`, `'select'`, `'layout'`; for an
|
|
1687
|
+
* edit of a previous layout, B-4: `'edit'`, then `'where'` when something moves or is added)
|
|
1688
|
+
*/
|
|
1689
|
+
export interface ComposeDecisionsBackend {
|
|
1690
|
+
sink: 'HTTP'
|
|
1691
|
+
kind: 'decisions'
|
|
1692
|
+
start(a: { catalog: any; state: any; prompt: string; previous: any; names: { ok: string; error: string; delta?: string; key?: string } }): { request: DecideRequest<any>; pending: any }
|
|
1693
|
+
reply(a: { catalog: any; state: any; pending: any; reply: any; names: { ok: string; error: string; delta?: string; key?: string } }): {
|
|
1694
|
+
layout?: any; unavailable?: true; preview?: any; request?: DecideRequest<any>; pending?: any
|
|
1695
|
+
/** a reply that can't be read (no `answers`, or every answer outside its options: SYG682); the behavior's status 'error' */
|
|
1696
|
+
error?: string
|
|
1697
|
+
/**
|
|
1698
|
+
* the raw answers per step (`{ select, layout }`, plus `categories` with `prefilter`; an edit:
|
|
1699
|
+
* `{ edit, where }`), for an explain panel: question name → `{ choice, probabilities }`. Names
|
|
1700
|
+
* an explain panel can rely on (`<p>` a part id, `<k>` an element of the layout step):
|
|
1701
|
+
* select: `use_<p>` (omit / use), `count_<p>`, `pick_<group>`, `rec_<p>__<n>` (the n-th record
|
|
1702
|
+
* slot, n = 1..4: none or a record), `param_<p>__<param>` (not_stated = the default), `row`;
|
|
1703
|
+
* layout: `parent_<k>`, `beside_<k>`, `first_<a>_vs_<b>`, `left_<a>_vs_<b>`, `same_<a>_vs_<b>`,
|
|
1704
|
+
* `gap_<x>_in_<p>`; categories: `cat_<category>`; edit: `remove_<node>`, `set_<node>__<param>`,
|
|
1705
|
+
* `add_<p>`, `moved_<n>`; where: `where_<node>`. A choice split by `maxOptions` shows as its
|
|
1706
|
+
* chunks, `<name>__c<k>` (each repeats the head options: not_stated, keep, none, omit)
|
|
1707
|
+
*/
|
|
1708
|
+
decisions?: Record<string, Record<string, any>>
|
|
1709
|
+
/** an edit's layout and preview keep the previous layout's keys (the behavior uses them as they are) */
|
|
1710
|
+
keyed?: boolean
|
|
1711
|
+
}
|
|
1712
|
+
/** the decision reply that makes the pending step read as `target` (composeWith); throws when the encoding can't reach it */
|
|
1713
|
+
fake(a: { catalog: any; state: any; pending: any; target: any[] }): Decision<any>
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
// PLAN-9 C: compose.llm ------------------------------------------------------------------------
|
|
1717
|
+
|
|
1718
|
+
/**
|
|
1719
|
+
* `compose.llm(options?)`: the GenAI LLM backend of `compose` (one structured-output request on
|
|
1720
|
+
* a makeChatDriver() sink; a streamed preview). The request carries the catalog's JSON Schema
|
|
1721
|
+
* (`asked_for`, then the layout tree), the system prompt (the catalog as text, its `guidance`,
|
|
1722
|
+
* `examples` as few-shot turns) and `delta` when streaming. Other keys go on the request as is
|
|
1723
|
+
* (transport options such as `body: { temperature: 0 }`). Requests carry `parse: false`: the reply
|
|
1724
|
+
* text is read leniently here (prose or a code fence around the JSON), and text with no layout in
|
|
1725
|
+
* it is the behavior's status 'error' (SYG683), not the driver's FAILED.
|
|
1726
|
+
*
|
|
1727
|
+
* B-4: with a current layout, COMPOSE is an edit. The request sends the page (`n1: part params`,
|
|
1728
|
+
* one node per line) and the change; the model returns operations positioned by named places
|
|
1729
|
+
* (`add` / `remove` / `move` / `replace` / `setParams`; `top`, `bottom`, `above n2`, `left of n3`,
|
|
1730
|
+
* …), applied to the current layout itself: every node they don't replace or remove keeps its key
|
|
1731
|
+
* (a param change too; another record is another node), a replacement takes the old node's place,
|
|
1732
|
+
* and the node a place names never moves. No operation: `unavailable`, the layout stays.
|
|
1733
|
+
*
|
|
1734
|
+
* compose(catalog, { backend: compose.llm({ model: 'gpt-5-mini' }) })
|
|
1735
|
+
*/
|
|
1736
|
+
export interface ComposeLlmOptions {
|
|
1737
|
+
/** The model id (default: the transport's), or a function of the host's state that returns one, resolved per request */
|
|
1738
|
+
model?: string | ((state: any) => string | undefined)
|
|
1739
|
+
/** The chat driver's sink (default 'LLM') */
|
|
1740
|
+
sink?: string
|
|
1741
|
+
/** The request key (default: the behavior's key) */
|
|
1742
|
+
key?: string
|
|
1743
|
+
/** Stream a preview of the complete parts so far (default true) */
|
|
1744
|
+
stream?: boolean
|
|
1745
|
+
/** Ask for the `asked_for` plan before the tree (default true; false when there are examples) */
|
|
1746
|
+
plan?: boolean
|
|
1747
|
+
/** More instructions after the rules, or a function of the system prompt returning the one to send */
|
|
1748
|
+
instructions?: string | ((prompt: string) => string)
|
|
1749
|
+
/** Few-shot examples (default: the catalog's): `[prompt, tree, asked_for?]` or `{ prompt, tree, asked_for? }` */
|
|
1750
|
+
examples?: ReadonlyArray<readonly [string, ReadonlyArray<ComposeLlmTreeNode>, ReadonlyArray<string>?] | { prompt: string; tree: ReadonlyArray<ComposeLlmTreeNode>; asked_for?: ReadonlyArray<string> }>
|
|
1751
|
+
/** Transport options, sent on the request as is */
|
|
1752
|
+
[option: string]: unknown
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
/** A node of a layout tree as `examples` take it (the contract's Tree) */
|
|
1756
|
+
export type ComposeLlmTreeNode = { part: string; params?: Record<string, unknown>; children?: ReadonlyArray<ComposeLlmTreeNode> | Record<string, ReadonlyArray<ComposeLlmTreeNode>> }
|
|
1757
|
+
|
|
1758
|
+
/** What `compose.llm()` puts in the slice's `decisions`: the model's plan and what the repair fixed */
|
|
1759
|
+
export interface ComposeLlmDecisions {
|
|
1760
|
+
/** Each thing the request asks for, in the model's words (with `plan`) */
|
|
1761
|
+
asked_for?: string[]
|
|
1762
|
+
/** What the repair changed: `alias`, `duplicate`, `param`, `structure`, `unknownPart`, `required`, `invalid`; `op` for an edit's operations */
|
|
1763
|
+
repairs?: Array<{ kind: string; detail: string }>
|
|
1764
|
+
/** An edit: the operations as applied, with layout keys (an add's `node` is the new key) */
|
|
1765
|
+
ops?: Array<{ op: 'add' | 'remove' | 'move' | 'replace' | 'setParams'; node: string; part?: string; params?: Record<string, unknown>; where?: string; moved?: string }>
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
// ---- PLAN-9 H: chat client tools and ask ---------------------------------------------------------
|
|
1769
|
+
|
|
1770
|
+
/** PLAN-9 H-1: a tool the app answers (`chat({ clientTools: { name: tool } })`) */
|
|
1771
|
+
export interface ChatClientTool {
|
|
1772
|
+
description: string
|
|
1773
|
+
/** The input's JSON Schema, or a Standard Schema with a JSON Schema form (its input is validated: a failure closes the call with the issues, so the model can repair it) */
|
|
1774
|
+
inputSchema: JsonSchemaObject | AgentSchema<any>
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
/** PLAN-9 H-2: `chat({ ask })` */
|
|
1778
|
+
export interface ChatAskOptions {
|
|
1779
|
+
/** The parts a card is built from; parts with `result: true` answer it (none: SYG686) */
|
|
1780
|
+
catalog: Catalog<any>
|
|
1781
|
+
/** What composes the card from the call's `need` and the app state: `compose.llm()` (default, on the chat's sink) or `compose.decisions()` */
|
|
1782
|
+
backend?: ComposeBackend
|
|
1783
|
+
/** More guidance on when and how to ask, added to the `ask_user` tool's description */
|
|
1784
|
+
instructions?: string
|
|
1785
|
+
/** PLAN-9 H-3: the tool's input is the card (the layout schema; LLM only, for providers that enforce tool schemas): no compose round trip */
|
|
1786
|
+
direct?: boolean
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
/** 'composing' (queued, or its backend step is out) | 'open' (answerable) | 'answered' | 'failed' (the call closed with "could not show a card: …") | 'cancelled' (STOP, a new message) */
|
|
1790
|
+
export type ChatCardStatus = 'composing' | 'open' | 'answered' | 'failed' | 'cancelled'
|
|
1791
|
+
|
|
1792
|
+
/** PLAN-9 H-2: an `ask_user` call's card, on its tool part (`part.ui`); render it with `askCard()` */
|
|
1793
|
+
export interface ChatCard {
|
|
1794
|
+
status: ChatCardStatus
|
|
1795
|
+
/** The card (a decision backend's preview while it composes) */
|
|
1796
|
+
layout: Layout | null
|
|
1797
|
+
/** State of the card's `local` parts, per node key */
|
|
1798
|
+
local: Record<string, any>
|
|
1799
|
+
/** Container state per node key (CONTROL) */
|
|
1800
|
+
ui: Record<string, Record<string, any>>
|
|
1801
|
+
/** The answer: the result part's node, its part id, the value */
|
|
1802
|
+
answer: { node: string; part: string | null; value: unknown } | null
|
|
1803
|
+
/** The backend's step while its request is out */
|
|
1804
|
+
pending?: unknown
|
|
1805
|
+
/**
|
|
1806
|
+
* What the backend explains, as a compose slice's `decisions`: its last reply's (`compose.llm()`:
|
|
1807
|
+
* ComposeLlmDecisions; `compose.decisions()`: the answers per step, see ComposeDecisionsBackend).
|
|
1808
|
+
* Absent until a reply carries some; kept (and persisted) with the card, for an explain panel
|
|
1809
|
+
*/
|
|
1810
|
+
decisions?: unknown
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
/** A tool part of a chat message */
|
|
1814
|
+
export type ChatToolPart = Extract<MessagePart, { toolCallId: string }>
|
|
1815
|
+
|
|
1816
|
+
/**
|
|
1817
|
+
* PLAN-9 H-1: the `input-available` tool parts of the last assistant message: the calls waiting for
|
|
1818
|
+
* the app's answer (and agent calls while they run: status 'streaming')
|
|
1819
|
+
*/
|
|
1820
|
+
export function openToolCalls(state: ChatState): ChatToolPart[]
|
|
1821
|
+
|
|
1822
|
+
/**
|
|
1823
|
+
* PLAN-9 H-2: the card of an `ask_user` tool part, for the host's view: `catalog.render(part.ui,
|
|
1824
|
+
* state, { scope: part.toolCallId, frozen, answer })`, frozen unless the card is open, `local`
|
|
1825
|
+
* written through a lens to the part. `key`: the chat's key in `uses` (default 'assistant'). Null
|
|
1826
|
+
* while the card has no layout.
|
|
1827
|
+
*/
|
|
1828
|
+
export function askCard(catalog: Catalog<any>, state: any, part: ChatToolPart, options?: { key?: string }): any
|
|
1829
|
+
|
|
1830
|
+
/** PLAN-9 M-1: `<Composed>`'s props */
|
|
1831
|
+
export interface ComposedProps {
|
|
1832
|
+
/** the catalog (defineCatalog's result); missing: SYG451, nothing rendered */
|
|
1833
|
+
catalog: Catalog<any>
|
|
1834
|
+
/**
|
|
1835
|
+
* Where the composition is in the host's state: a key (default 'page') or a path
|
|
1836
|
+
* (['boards', 0, 'page']); with `card`, the chat's key (default 'assistant'). Not a composition
|
|
1837
|
+
* (an object with `layout`): SYG451, nothing rendered
|
|
1838
|
+
*/
|
|
1839
|
+
from?: string | ReadonlyArray<string | number>
|
|
1840
|
+
/** an `ask_user` tool part: its card, as askCard() renders it (scope, frozen and answer from the part) */
|
|
1841
|
+
card?: ChatToolPart
|
|
1842
|
+
/** as catalog.render's options (they override the card's) */
|
|
1843
|
+
scope?: string
|
|
1844
|
+
frozen?: boolean
|
|
1845
|
+
answer?: { node: string; value: unknown } | null
|
|
1846
|
+
key?: string | number
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
/**
|
|
1850
|
+
* PLAN-9 M-1 (D466): `<Composed catalog={catalog} from="page" />` renders the composition at `from`
|
|
1851
|
+
* in the host's state, as `{catalog.render(state.page, state)}` in the view does: the parts are the
|
|
1852
|
+
* host's children (same keys, CHILD.select, lenses). Differences: it reads the host's own state (a
|
|
1853
|
+
* composition computed in the view needs render()), and a container view or part `props()` that
|
|
1854
|
+
* throws replaces only the composition with the host's `onError` (SYG408), where render() shows an
|
|
1855
|
+
* empty `.compose-error` element (SYG406).
|
|
1856
|
+
*/
|
|
1857
|
+
export function Composed(props: ComposedProps): JSX.Element
|