Skip to Content
Assembling the prompt

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.

OptionDefaultWhat it does
maxListItems100The 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.
notenoneYour text, added as ## Note.

getScopePartialPrompt

What one response should be. It emits # Scope.

OptionDefaultWhat it does
kindrequired"page", "widget" or "answer", see below.
approxEntriesnoneA size hint: “around N entries”. Not a limit.
notenoneYour text, added as ## Note.
kindThe model builds
"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.

OptionDefaultWhat it does
definitionsrequiredYour definitions. One with hidden: true is left out.
urlPolicythe renderer’s defaultsPass the renderer’s urlPolicy. ## URL Props then tells the model which URLs load.
notenoneYour 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 label

getFunctionsPartialPrompt

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.

OptionDefaultWhat it does
functionsrequiredYour tools. An empty array adds nothing.
notenoneYour 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.

OptionDefaultWhat it does
maxLength1000The longest expression. Pass your evaluator’s maxSourceLength.
notenoneYour 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.

OptionDefaultWhat it does
notenoneYour 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.

OptionDefaultWhat it does
requestrequiredThe 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.

OptionDefaultWhat it does
failuresrequiredOne { 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

Last updated on