Assembling the prompt
The system prompt is a list of blocks. A builder function makes each block, and your app joins them. For a chat endpoint:
import {
getCommonInstructionsPartialPrompt,
getComponentsPartialPrompt,
getExpressionsPartialPrompt,
getFunctionsPartialPrompt,
getScopePartialPrompt,
} from "@uicast/core/prompt";
import { getFencePartialPrompt } from "@uicast/streamdown/prompt";
import { defs } from "@uicast/shadcn-catalog/all/defs";
import { tools } from "./tools";
const system = [
getCommonInstructionsPartialPrompt(),
getScopePartialPrompt({ kind: "answer" }),
getComponentsPartialPrompt({ definitions: defs }),
getFunctionsPartialPrompt({ functions: tools }),
getExpressionsPartialPrompt(),
getFencePartialPrompt(),
].join("\n\n");A page builder uses kind: "page" and leaves out the fence block.
Build the prompt from the same components and functions your page uses. The prompt then lists only what your page has. A component or function the page lacks fails with an error.
Order
Put the fence block last: it changes the output format the first block sets.
Every block starts with its own # heading, so the blocks join with a blank
line.
Adding your own text
Every builder in The builders takes note: your text, added
at the end of its block as ## Note. Put it on the block it is about:
- an extra scope on the common instructions;
- a rule about your catalog on the components;
- a warning about your data on the functions.
The builders
All of them come from @uicast/core/prompt, except getFencePartialPrompt,
which comes from @uicast/streamdown/prompt.
getCommonInstructionsPartialPrompt
The rules every surface needs: the output format (JSONL entries, keys,
children), how to replace part of a page, and what an expression can read
(scopes, evt, currentValue, host functions).
It emits # Output Format, # Rules and # Expression Context.
| Option | Default | |
|---|---|---|
maxListItems | 100 | The prompt tells the model to show at most this many items in a list, and to add pages for the rest. The renderer has no limit of its own. |
note | none | Your text, added as ## Note. |
getScopePartialPrompt
What one response should be. It emits # Scope.
| Option | Default | |
|---|---|---|
kind | required | "page", "widget" or "answer", see below. |
approxEntries | none | A size hint: “around N entries”. Not a limit. |
note | none | Your text, added as ## Note. |
kind | |
|---|---|
"page" | A complete, working page. A short request still gets the whole page: stats, a table with real data, filters, row actions. |
"widget" | One widget for your layout. No page headers or navigation. |
"answer" | A small UI that answers a question in a chat: a stat, a chart, a table. |
getScopePartialPrompt({ kind: "answer", approxEntries: 10 }) returns:
# Scope
Answering question in chat. Build **compact UI that fully answers it**: stat, chart, table or small mix, not page.
- Fewest entries that answer it. No page chrome (headers, navigation, filter bars) unless asked.
- Real values from host functions, never placeholders.
Typical response here: around 10 entries. Hint about ambition, not quota.getComponentsPartialPrompt
The components the model may use, from your definitions.
It emits # Available Components and ## Component Details. Three more appear
only when needed: ## Common Events for a payload with a $id,
## Shared Types for a schema with an id, and ## URL Props for a URL prop.
| Option | Default | |
|---|---|---|
definitions | required | Your definitions. One with hidden: true is left out. |
urlPolicy | Pass the renderer’s urlPolicy. ## URL Props | |
note | none | Your text, added as ## Note. |
It throws when two definitions have the same name, or when two different
payloads have the same $id.
const StatDef = createComponentDefinition({
name: "Stat",
description: "A KPI display.",
props: z.strictObject({
label: z.string().meta({ description: "The metric label" }),
}),
});getComponentsPartialPrompt({ definitions: [StatDef] }) returns:
# Available Components
Stat
## Component Details
- Stat — A KPI display.
Props:
- label: string — The metric labelgetFunctionsPartialPrompt
The functions the model may call, from your tools. The format is described in Host functions.
It emits # Available Functions and ## Function Details, plus
## Shared Types for a schema with an id.
| Option | Default | |
|---|---|---|
functions | required | Your tools. An empty array adds nothing. |
note | none | Your text, added as ## Note. |
It throws when a name is used twice, is not a valid identifier, or is
scopes, evt, currentValue or a global such as Math.
getExpressionsPartialPrompt
The expression language: its rules, the globals and a few idioms. It emits
# JavaScript Expressions.
| Option | Default | |
|---|---|---|
maxLength | 1000 | The longest expression. Pass your evaluator’s maxSourceLength. |
note | none | Your text, added as ## Note. |
The globals come from the evaluator’s own list. A test fails if this block names a method the evaluator refuses.
getFencePartialPrompt
For chat replies in Markdown. The model puts entries in a ```uicast fence,
and the Streamdown plugin renders it. Add it last; a page
builder leaves it out. It emits # Emitting UI.
| Option | Default | |
|---|---|---|
note | none | Your text, added as ## Note. |
Edit and recovery turns
These two build a user message for one turn, not a block of the system prompt.
getEditRequestPrompt
Asks the model to change a page it made.
| Option | Default | |
|---|---|---|
request | required | The change the user asked for, in their words. |
missingKeys | [] | Keys the page refers to but never got, as after a cut-off response. The model sends them. |
Send the stored entries as an assistant message, then this as the user message:
const messages = [
{ role: "assistant", content: storedLines.map((l) => JSON.stringify(l)).join("\n") },
{ role: "user", content: getEditRequestPrompt({ request: "Make the total large." }) },
];The model answers with only what changes, here one line that reuses the key:
{"key":"total","component":"Typography","props":{"expr":"({ text: scopes.root.total, variant: 'large' })"}}The other elements stay as they are. A re-sent element keeps its state: its
seed does not run again once it has succeeded. New state comes from a new
element’s seed or from a callback.
getErrorRecoveryPrompt
Tells the model which elements failed, so it sends fixed versions.
| Option | Default | |
|---|---|---|
failures | required | One { key, message, reason? } per failed element. With reason, the line also says what that kind of failure means. |
On a page, send it through the edit flow above. In a chat, send it as a normal user message. See Error recovery.
Where to go next
- Component definition: where the component list comes from.
- Host functions: the format of the functions block.
- The expression evaluator: the language the expressions block teaches.
- Streamdown plugin: how the chat fence renders.