From d1bd1531b0bac788440893767c7cb5a21653d477 Mon Sep 17 00:00:00 2001 From: Abhin Rustagi Date: Wed, 2 Sep 2026 11:47:31 +0530 Subject: [PATCH 01/10] Add a cloud flag to generateSystemPrompt in lang-core OpenUI Cloud now gets its system prompt from lang-core via `{ cloud: true }`, so callers emit the muse config sentinel instead of a locally generated prompt. --- docs/app/api/openui-cloud/chat/route.ts | 4 +- .../openui-cloud/api/chat-completions.mdx | 6 +- .../docs/openui-cloud/api/conversations.mdx | 4 +- .../docs/openui-cloud/api/responses.mdx | 15 +- .../openui-cloud/build/component-library.mdx | 10 +- .../agent-frameworks/vercel-ai-sdk.mdx | 6 +- docs/content/docs/openui-lang/quickstart.mdx | 3 +- .../docs/openui-lang/system-prompts.mdx | 22 ++ packages/lang-core/README.md | 43 +++- packages/lang-core/package.json | 2 +- packages/lang-core/src/index.ts | 4 + .../src/parser/__tests__/prompt.test.ts | 188 +++++++++++++++ packages/lang-core/src/parser/cloud-config.ts | 66 ++++++ packages/lang-core/src/parser/prompt.ts | 58 ++++- .../lang-core/src/parser/validate-library.ts | 217 ++++++++++++++++++ 15 files changed, 606 insertions(+), 42 deletions(-) create mode 100644 packages/lang-core/src/parser/__tests__/prompt.test.ts create mode 100644 packages/lang-core/src/parser/cloud-config.ts create mode 100644 packages/lang-core/src/parser/validate-library.ts diff --git a/docs/app/api/openui-cloud/chat/route.ts b/docs/app/api/openui-cloud/chat/route.ts index e4e8797f6..4dfe8f064 100644 --- a/docs/app/api/openui-cloud/chat/route.ts +++ b/docs/app/api/openui-cloud/chat/route.ts @@ -2,7 +2,8 @@ import { readOpenuiCloudConfig } from "@/lib/openui-cloud/config"; import { unavailableResponse } from "@/lib/openui-cloud/errors"; import { resolveRequestedModel } from "@/lib/openui-cloud/models"; import { hasAllowedOrigin, hasJsonContentType, readLimitedJson } from "@/lib/openui-cloud/request"; -import { artifactTool, generateSystemPrompt } from "@openuidev/thesys-server"; +import { generateSystemPrompt } from "@openuidev/lang-core"; +import { artifactTool } from "@openuidev/thesys-server"; import OpenAI from "openai"; import type { ResponseInputItem } from "openai/resources/responses/responses"; @@ -56,6 +57,7 @@ export async function POST(request: Request): Promise { { type: "image_search" }, ], instructions: generateSystemPrompt({ + cloud: true, instructions: "When creating an artifact, actively use the available web and image search tools to ground its content and visual choices.", }), diff --git a/docs/content/docs/openui-cloud/api/chat-completions.mdx b/docs/content/docs/openui-cloud/api/chat-completions.mdx index 08c9f9224..7177de7ca 100644 --- a/docs/content/docs/openui-cloud/api/chat-completions.mdx +++ b/docs/content/docs/openui-cloud/api/chat-completions.mdx @@ -14,12 +14,12 @@ Use the `embedClient` from the [API overview](/docs/openui-cloud/api/overview), Use the server helper to have OpenUI Cloud assemble the system prompt for its built-in component library. ```ts title="server.ts" -import { generateSystemPrompt } from "@openuidev/thesys-server"; +import { generateSystemPrompt } from "@openuidev/lang-core"; const completion = await embedClient.chat.completions.create({ model: "openai/gpt-5", messages: [ - { role: "system", content: generateSystemPrompt() }, + { role: "system", content: generateSystemPrompt({ cloud: true }) }, { role: "user", content: "Compare quarterly revenue by region." }, ], }); @@ -75,7 +75,7 @@ Hosted `web_search`, `image_search`, remote MCP, and artifacts-as-tool are avail ### Plain text passthrough -Use a `{provider}/{model}` model ID without the managed `generateSystemPrompt()` sentinel. OpenUI Cloud forwards your messages and system prompt without injecting a generative UI prompt or component schema. +Use a `{provider}/{model}` model ID without the managed `generateSystemPrompt({ cloud: true })` sentinel. OpenUI Cloud forwards your messages and system prompt without injecting a generative UI prompt or component schema. ```ts const completion = await embedClient.chat.completions.create({ diff --git a/docs/content/docs/openui-cloud/api/conversations.mdx b/docs/content/docs/openui-cloud/api/conversations.mdx index 442ff7cf3..eb5d83cfd 100644 --- a/docs/content/docs/openui-cloud/api/conversations.mdx +++ b/docs/content/docs/openui-cloud/api/conversations.mdx @@ -27,13 +27,13 @@ const conversation = await conversationClient.conversations.create({ Then send only the new turn to Responses and set `store: true`: ```ts -import { generateSystemPrompt } from "@openuidev/thesys-server"; +import { generateSystemPrompt } from "@openuidev/lang-core"; const response = await embedClient.responses.create({ model: "openai/gpt-5", conversation: conversation.id, input: "Compare quarterly revenue by region.", - instructions: generateSystemPrompt(), + instructions: generateSystemPrompt({ cloud: true }), store: true, stream: true, }); diff --git a/docs/content/docs/openui-cloud/api/responses.mdx b/docs/content/docs/openui-cloud/api/responses.mdx index 98d898c72..89d648719 100644 --- a/docs/content/docs/openui-cloud/api/responses.mdx +++ b/docs/content/docs/openui-cloud/api/responses.mdx @@ -14,12 +14,12 @@ Use the `embedClient` from the [API overview](/docs/openui-cloud/api/overview), Use the server helper to have OpenUI Cloud assemble the system prompt for its built-in component library. ```ts title="server.ts" -import { generateSystemPrompt } from "@openuidev/thesys-server"; +import { generateSystemPrompt } from "@openuidev/lang-core"; const response = await embedClient.responses.create({ model: "openai/gpt-5", input: "Compare quarterly revenue by region.", - instructions: generateSystemPrompt(), + instructions: generateSystemPrompt({ cloud: true }), }); console.log(response.output_text); @@ -72,14 +72,14 @@ Chain a follow-up to an earlier response: const first = await embedClient.responses.create({ model: "openai/gpt-5", input: "Compare quarterly revenue by region.", - instructions: generateSystemPrompt(), + instructions: generateSystemPrompt({ cloud: true }), store: true, }); const followUp = await embedClient.responses.create({ model: "openai/gpt-5", input: "Focus on Europe and explain the change.", - instructions: generateSystemPrompt(), + instructions: generateSystemPrompt({ cloud: true }), previous_response_id: first.id, store: true, }); @@ -105,7 +105,7 @@ import type { Tool } from "openai/resources/responses/responses"; const response = await embedClient.responses.create({ model: "openai/gpt-5", input: "Research the market and summarize the most important changes.", - instructions: generateSystemPrompt(), + instructions: generateSystemPrompt({ cloud: true }), tools: [ { type: "web_search" }, { type: "image_search" } as unknown as Tool, @@ -129,14 +129,15 @@ For a `function` tool, execute each returned `function_call` on your server and Add `artifactTool()` to generate editable slides or reports inside the agent stream: ```ts -import { artifactTool, generateSystemPrompt } from "@openuidev/thesys-server"; +import { generateSystemPrompt } from "@openuidev/lang-core"; +import { artifactTool } from "@openuidev/thesys-server"; import type { Tool } from "openai/resources/responses/responses"; const response = await embedClient.responses.create({ model: "openai/gpt-5", conversation: threadId, input: "Create a three-slide deck on Q4 results.", - instructions: generateSystemPrompt(), + instructions: generateSystemPrompt({ cloud: true }), tools: [artifactTool({ artifacts: ["slides", "report"] }) as unknown as Tool], store: true, stream: true, diff --git a/docs/content/docs/openui-cloud/build/component-library.mdx b/docs/content/docs/openui-cloud/build/component-library.mdx index 376cfa548..9243cd481 100644 --- a/docs/content/docs/openui-cloud/build/component-library.mdx +++ b/docs/content/docs/openui-cloud/build/component-library.mdx @@ -15,18 +15,19 @@ import { chatLibrary } from "@openuidev/thesys"; ; ``` -On the backend, `generateSystemPrompt()` uses the built-in library by default. Use the `embedClient` from the [API overview](/docs/openui-cloud/api/overview); the placement of the generated instructions depends on the API: +On the backend, `generateSystemPrompt({ cloud: true })` uses the built-in library by default. Use the `embedClient` from the [API overview](/docs/openui-cloud/api/overview); the placement of the generated instructions depends on the API: ```ts -import { generateSystemPrompt } from "@openuidev/thesys-server"; +import { generateSystemPrompt } from "@openuidev/lang-core"; const response = await embedClient.responses.create({ model: "openai/gpt-5", input: "Show revenue by region as an interactive dashboard.", instructions: generateSystemPrompt({ + cloud: true, instructions: "Optional instructions for the model.", }), }); @@ -36,7 +37,7 @@ const response = await embedClient.responses.create({ ```ts -import { generateSystemPrompt } from "@openuidev/thesys-server"; +import { generateSystemPrompt } from "@openuidev/lang-core"; const completion = await embedClient.chat.completions.create({ model: "openai/gpt-5", @@ -44,6 +45,7 @@ const completion = await embedClient.chat.completions.create({ { role: "system", content: generateSystemPrompt({ + cloud: true, instructions: "Optional instructions for the model.", }), }, @@ -123,6 +125,7 @@ npx @openuidev/cli@latest generate --spec src/lib/chat-library.tsx --out ./gener model: "openai/gpt-5", input, instructions: generateSystemPrompt({ + cloud: true, instructions: "Optional instructions for the model.", + library: librarySpec, + promptOptions: { preamble: "You build dashboards for Acme operators." }, @@ -142,6 +145,7 @@ npx @openuidev/cli@latest generate --spec src/lib/chat-library.tsx --out ./gener { role: "system", content: generateSystemPrompt({ + cloud: true, instructions: "Optional instructions for the model.", + library: librarySpec, + promptOptions: { preamble: "You build dashboards for Acme operators." }, diff --git a/docs/content/docs/openui-lang/examples/agent-frameworks/vercel-ai-sdk.mdx b/docs/content/docs/openui-lang/examples/agent-frameworks/vercel-ai-sdk.mdx index ec539e69e..bd3ef3772 100644 --- a/docs/content/docs/openui-lang/examples/agent-frameworks/vercel-ai-sdk.mdx +++ b/docs/content/docs/openui-lang/examples/agent-frameworks/vercel-ai-sdk.mdx @@ -22,7 +22,7 @@ This example uses the [Vercel AI SDK](https://ai-sdk.dev/) (`streamText`, `tool( ## How it connects -The API route calls `streamText` with Cloud Completions, `generateSystemPrompt()`, and tools defined with `tool()`. When the model calls a tool, the AI SDK executes it server-side and continues — up to 5 steps. The response is a UIMessage stream. +The API route calls `streamText` with Cloud Completions, `generateSystemPrompt({ cloud: true })`, and tools defined with `tool()`. When the model calls a tool, the AI SDK executes it server-side and continues — up to 5 steps. The response is a UIMessage stream. The page passes that stream to ``: @@ -54,7 +54,7 @@ Storage is omitted, so AgentInterface uses its built-in in-memory default (wiped ```ts import { createOpenAI } from "@ai-sdk/openai"; -import { generateSystemPrompt } from "@openuidev/thesys-server"; +import { generateSystemPrompt } from "@openuidev/lang-core"; import { convertToModelMessages, stepCountIs, streamText } from "ai"; import { tools } from "@/lib/tools"; @@ -67,7 +67,7 @@ export async function POST(req: Request) { const { messages } = await req.json(); const result = streamText({ model: openai.chat("google/gemini-3.6-flash-free"), - system: generateSystemPrompt(), + system: generateSystemPrompt({ cloud: true }), messages: await convertToModelMessages(messages), tools, stopWhen: stepCountIs(5), diff --git a/docs/content/docs/openui-lang/quickstart.mdx b/docs/content/docs/openui-lang/quickstart.mdx index e1b0ec369..f3332ec0f 100644 --- a/docs/content/docs/openui-lang/quickstart.mdx +++ b/docs/content/docs/openui-lang/quickstart.mdx @@ -297,8 +297,9 @@ Never claim that a recipe is allergen-safe. Update the `instructions` field inside `createParams`: { + expect(emitted.startsWith(CONFIG_MARKER)).toBe(true); + const body = emitted.slice(CONFIG_MARKER.length); + const firstLine = body.split("\n", 1)[0]; + return JSON.parse(firstLine); +} + +const library: ChatLibrary = { + root: "Card", + schema: { + $defs: { + Card: { + properties: { + children: { + type: "array", + items: { anyOf: [{ $ref: "#/$defs/TextContent" }] }, + }, + title: { type: "string" }, + }, + required: ["children"], + description: "Top-level container.", + }, + TextContent: { + properties: { text: { type: "string" } }, + required: ["text"], + }, + }, + }, + componentGroups: [ + { + name: "Content", + components: ["TextContent"], + }, + ], +}; + +describe("generateSystemPrompt — self-hosted", () => { + const spec: LibrarySpec = { + root: "Card", + components: { + Card: { signature: "Card(children: Component[])", description: "Root" }, + }, + }; + + it("renders a local prompt from a library spec", () => { + const prompt = generateSystemPrompt({ library: spec }); + expect(prompt).toContain("openui-lang"); + expect(prompt).toContain("Card(children: Component[])"); + expect(prompt.startsWith(CONFIG_MARKER)).toBe(false); + }); + + it("appends instructions after the local prompt", () => { + const prompt = generateSystemPrompt({ + library: spec, + instructions: "Be terse.", + }); + expect(prompt.endsWith("\nBe terse.")).toBe(true); + }); +}); + +describe("generateSystemPrompt({ cloud: true }) — sentinel", () => { + it("starts with the byte-exact marker", () => { + const out = generateSystemPrompt({ cloud: true }); + expect(out.slice(0, CONFIG_MARKER.length)).toBe(CONFIG_MARKER); + }); + + it("no library → JSON with only libraryVersion", () => { + const config = parseBlock(generateSystemPrompt({ cloud: true })); + expect(Object.keys(config)).toEqual(["libraryVersion"]); + expect(config.libraryVersion).toBe(CLOUD_CHAT_LIBRARY_VERSION); + }); + + it("emitted keys are a subset of muse MANAGED_CONFIG_KEYS", () => { + const config = parseBlock(generateSystemPrompt({ cloud: true, instructions: "Be terse." })); + for (const key of Object.keys(config)) { + expect(MUSE_MANAGED_CONFIG_KEYS).toContain(key); + } + }); + + it("appends customer prose after the block", () => { + const out = generateSystemPrompt({ cloud: true, instructions: "Be terse." }); + const [sentinelLine, jsonLine, ...rest] = out.split("\n"); + expect(sentinelLine).toBe("]]>openui:config"); + expect(() => JSON.parse(jsonLine)).not.toThrow(); + expect(rest.join("\n")).toBe("Be terse."); + }); + + it("no instructions → sentinel + JSON only", () => { + expect(generateSystemPrompt({ cloud: true }).split("\n")).toHaveLength(2); + }); +}); + +describe("generateSystemPrompt({ cloud: true, library })", () => { + it("emits the library under chatLibrary, dropping components", () => { + const specJson = { + ...library, + components: { + Card: { signature: "Card(children: (TextContent)[], title?: string)" }, + }, + }; + const config = parseBlock(generateSystemPrompt({ cloud: true, library: specJson })); + expect((config.chatLibrary as Record).components).toBeUndefined(); + expect(config.chatLibrary).toEqual(library); + expect(Object.keys(config)).toEqual(["chatLibrary"]); + }); + + it("systemPromptOptions rides as a sibling key", () => { + const config = parseBlock( + generateSystemPrompt({ + cloud: true, + library, + promptOptions: { preamble: "You generate UI for Acme." }, + }), + ); + expect(Object.keys(config)).toEqual(["chatLibrary", "systemPromptOptions"]); + expect(config.systemPromptOptions).toEqual({ preamble: "You generate UI for Acme." }); + }); + + it("strips Cloud-unsupported prompt flags from the wire", () => { + const config = parseBlock( + generateSystemPrompt({ + cloud: true, + library, + promptOptions: { + preamble: "Acme.", + editMode: true, + tools: ["search"], + } as CloudPromptOptions, + }), + ); + expect(config.systemPromptOptions).toEqual({ preamble: "Acme." }); + }); + + it("promptOptions without a library throws", () => { + expect(() => + generateSystemPrompt({ + cloud: true, + promptOptions: { preamble: "Nope." }, + }), + ).toThrowError(/promptOptions requires a library/); + }); + + it("invalid library throws with every issue", () => { + const broken: ChatLibrary = { + root: "Missing", + schema: { + $defs: { + Card: { + properties: { body: { $ref: "#/$defs/Nope" } }, + required: ["ghost"], + }, + }, + }, + }; + expect(() => generateSystemPrompt({ cloud: true, library: broken })).toThrowError( + /Invalid library/, + ); + expect(() => generateSystemPrompt({ cloud: true, library: broken })).toThrowError( + /Root component "Missing"/, + ); + expect(() => generateSystemPrompt({ cloud: true, library: broken })).toThrowError( + /Unresolvable \$ref "#\/\$defs\/Nope"/, + ); + expect(() => generateSystemPrompt({ cloud: true, library: broken })).toThrowError( + /required property "ghost"/, + ); + }); +}); diff --git a/packages/lang-core/src/parser/cloud-config.ts b/packages/lang-core/src/parser/cloud-config.ts new file mode 100644 index 000000000..77e333efe --- /dev/null +++ b/packages/lang-core/src/parser/cloud-config.ts @@ -0,0 +1,66 @@ +import type { CloudPromptOptions, SystemPromptOptions } from "./prompt"; +import { type ChatLibrary, validateChatLibrary } from "./validate-library"; + +/** `]]>openui:config\n` — request-direction config block header. Trailing newline is part of the wire contract. */ +export const CLOUD_CONFIG_MARKER = "]]>openui:config\n"; + +/** + * Wire pin for OpenUI Cloud's built-in chat library when `generateSystemPrompt({ cloud: true })` + * is called without a custom library. Muse 400s on a non-numeric or too-old version. + */ +export const CLOUD_CHAT_LIBRARY_VERSION = "0.1.0"; + +type CloudConfig = + | { libraryVersion: string } + | { chatLibrary: CloudChatLibraryWire; systemPromptOptions?: CloudPromptOptions }; + +type CloudChatLibraryWire = { + schema?: ChatLibrary["schema"]; + root?: string; + componentGroups?: ChatLibrary["componentGroups"]; + id?: string; +}; + +function pickCloudPromptOptions( + options: SystemPromptOptions | CloudPromptOptions | undefined, +): CloudPromptOptions | undefined { + if (!options) return undefined; + const picked: CloudPromptOptions = {}; + if (options.examples) picked.examples = options.examples; + if (options.preamble) picked.preamble = options.preamble; + if (options.additionalRules) picked.additionalRules = options.additionalRules; + return Object.keys(picked).length > 0 ? picked : undefined; +} + +export function generateCloudConfig(spec: { + library?: ChatLibrary; + promptOptions?: SystemPromptOptions | CloudPromptOptions; + instructions?: string; +}): string { + let config: CloudConfig; + + if (spec.library) { + const issues = validateChatLibrary(spec.library); + if (issues.length > 0) { + throw new Error( + `[generateSystemPrompt] Invalid library: ${issues.map((i) => i.message).join(" ")}`, + ); + } + const { schema, root, componentGroups, id } = spec.library; + const promptOptions = pickCloudPromptOptions(spec.promptOptions); + config = { + chatLibrary: { schema, root, componentGroups, id }, + ...(promptOptions ? { systemPromptOptions: promptOptions } : {}), + }; + } else { + if (spec.promptOptions && pickCloudPromptOptions(spec.promptOptions)) { + throw new Error( + "[generateSystemPrompt] promptOptions requires a library — the built-in library ignores it.", + ); + } + config = { libraryVersion: CLOUD_CHAT_LIBRARY_VERSION }; + } + + const block = `${CLOUD_CONFIG_MARKER}${JSON.stringify(config)}`; + return spec.instructions ? `${block}\n${spec.instructions}` : block; +} diff --git a/packages/lang-core/src/parser/prompt.ts b/packages/lang-core/src/parser/prompt.ts index d6ea9a76d..4934bc66c 100644 --- a/packages/lang-core/src/parser/prompt.ts +++ b/packages/lang-core/src/parser/prompt.ts @@ -1,6 +1,8 @@ import { recordSystemPromptGeneration } from "../telemetry/runtime"; import { BUILTINS, LAZY_BUILTIN_DEFS } from "./builtins"; +import { generateCloudConfig } from "./cloud-config"; import type { LibraryJSONSchema } from "./types"; +import type { ChatLibrary } from "./validate-library"; // ─── PromptSpec types (JSON-serializable, no Zod deps) ────────────────────── @@ -699,18 +701,45 @@ export function generatePrompt(spec: PromptSpec): string { /** Prompt options for {@link generateSystemPrompt} */ export type SystemPromptOptions = Omit; -/** Object input for {@link generateSystemPrompt}. */ -export interface SystemPromptSpec { - library: LibrarySpec; - promptOptions?: SystemPromptOptions; -} +/** + * Prompt options allowed on the OpenUI Cloud wire. Extra flags (`tools`, + * `editMode`, …) are stripped — Cloud's built-in prompt assembler ignores them. + */ +export type CloudPromptOptions = Pick< + SystemPromptOptions, + "examples" | "preamble" | "additionalRules" +>; -/** Render the full system prompt for a library. */ +/** + * Object input for {@link generateSystemPrompt}. + * + * Pass `cloud: true` to emit OpenUI Cloud's managed `]]>openui:config` block + * instead of a locally generated prompt. In that mode `library` is optional — + * omit it to use Cloud's built-in chat library. + */ +export type SystemPromptSpec = + | { + library: LibrarySpec; + promptOptions?: SystemPromptOptions; + instructions?: string; + cloud?: false; + } + | { + cloud: true; + library?: ChatLibrary; + promptOptions?: CloudPromptOptions; + instructions?: string; + }; + +/** Render the full system prompt for a library, or Cloud's managed config block when `cloud: true`. */ export function generateSystemPrompt(spec: SystemPromptSpec): string; /** @deprecated Pass `{ library, promptOptions, instructions }` instead. Removed at the next major. */ export function generateSystemPrompt(spec: PromptSpec): string; export function generateSystemPrompt(spec: SystemPromptSpec | PromptSpec): string { - if (!isSystemPromptSpec(spec)) { + if (isCloudSpec(spec)) { + return generateCloudConfig(spec); + } + if (!isLocalSystemPromptSpec(spec)) { const prompt = generatePrompt(spec); recordSystemPromptGeneration(spec, "legacy_prompt_spec"); return prompt; @@ -718,10 +747,17 @@ export function generateSystemPrompt(spec: SystemPromptSpec | PromptSpec): strin const merged: PromptSpec = { ...spec.library, ...spec.promptOptions }; const prompt = generatePrompt(merged); recordSystemPromptGeneration(merged, "library_spec"); - return prompt; + return spec.instructions ? `${prompt}\n${spec.instructions}` : prompt; +} + +function isCloudSpec( + spec: SystemPromptSpec | PromptSpec, +): spec is Extract { + return "cloud" in spec && (spec as { cloud?: unknown }).cloud === true; } -// use `library` to discriminate SystemPromptSpec from the deprecated base-PromptSpec -function isSystemPromptSpec(spec: SystemPromptSpec | PromptSpec): spec is SystemPromptSpec { - return "library" in spec && typeof (spec as SystemPromptSpec).library === "object"; +function isLocalSystemPromptSpec( + spec: SystemPromptSpec | PromptSpec, +): spec is Extract { + return "library" in spec && typeof (spec as { library?: unknown }).library === "object"; } diff --git a/packages/lang-core/src/parser/validate-library.ts b/packages/lang-core/src/parser/validate-library.ts new file mode 100644 index 000000000..47c902b96 --- /dev/null +++ b/packages/lang-core/src/parser/validate-library.ts @@ -0,0 +1,217 @@ +import { z } from "zod/v4"; +import type { LibrarySpec } from "./prompt"; + +/** + * Wire shape of a serialized chat library (the `]]>openui:config` `chatLibrary` + * value). Same as {@link LibrarySpec} without prompt-side `components`. + */ +export type ChatLibrary = Omit; + +export interface ChatLibraryIssue { + code: + | "invalid-shape" + | "root-not-found" + | "unresolved-ref" + | "unknown-group-component" + | "invalid-required"; + message: string; + /** e.g. "$defs/Card/properties/children/items" */ + path?: string; +} + +const REF_PATTERN = /^#\/\$defs\/(.+)$/; + +function isPlainObject(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +const componentGroupSchema = z.object({ + name: z.string(), + components: z.array(z.string()), + notes: z.array(z.string()).optional(), +}); + +/** + * Structural + semantic schema for a customer-supplied chat library. + * Shape is Zod; `$ref` / root / required / group membership are refinements + * so every issue is reported (not fail-fast). + */ +export const chatLibrarySchema = z + .object({ + root: z + .string() + .min(1, "chatLibrary.root, when present, must be a non-empty string.") + .optional(), + id: z.string().optional(), + // generate-spec / toSpec() leftover — accepted, never required on the wire + components: z.unknown().optional(), + schema: z + .object({ + $defs: z.record(z.string(), z.unknown()).refine((defs) => Object.keys(defs).length > 0, { + message: "chatLibrary.schema.$defs must be a non-empty object keyed by component name.", + }), + }) + .passthrough(), + componentGroups: z.array(componentGroupSchema).optional(), + }) + .passthrough() + .superRefine((library, ctx) => { + const defs = library.schema.$defs; + const defNames = new Set(Object.keys(defs)); + + const add = (issue: ChatLibraryIssue) => { + ctx.addIssue({ + code: "custom", + message: issue.message, + path: issue.path ? issue.path.split("/") : [], + params: { issueCode: issue.code }, + }); + }; + + if (library.root && !defNames.has(library.root)) { + add({ + code: "root-not-found", + message: `Root component "${library.root}" was not found in schema.$defs. Available components: ${[...defNames].join(", ")}.`, + path: "root", + }); + } + + for (const [name, def] of Object.entries(defs)) { + const defPath = `$defs/${name}`; + if (!isPlainObject(def)) { + add({ + code: "invalid-shape", + message: `${defPath} must be an object component schema.`, + path: defPath, + }); + continue; + } + + const properties = def["properties"]; + if (properties !== undefined && !isPlainObject(properties)) { + add({ + code: "invalid-shape", + message: `${defPath}/properties must be an object.`, + path: `${defPath}/properties`, + }); + } + + const required = def["required"]; + if (required !== undefined) { + if (!Array.isArray(required) || required.some((r) => typeof r !== "string")) { + add({ + code: "invalid-required", + message: `${defPath}/required must be an array of property names.`, + path: `${defPath}/required`, + }); + } else { + const propKeys = new Set(isPlainObject(properties) ? Object.keys(properties) : []); + for (const r of required) { + if (!propKeys.has(r)) { + add({ + code: "invalid-required", + message: `${defPath} lists required property "${r}" that is not in properties.`, + path: `${defPath}/required`, + }); + } + } + } + } + + collectRefIssues(properties, `${defPath}/properties`, defNames, add); + } + + for (const [index, group] of (library.componentGroups ?? []).entries()) { + for (const comp of group.components) { + if (!defNames.has(comp)) { + add({ + code: "unknown-group-component", + message: `Component group "${group.name}" references unknown component "${comp}".`, + path: `componentGroups/${index}`, + }); + } + } + } + }); + +/** Recursively collect every `$ref` and check it resolves within `$defs`. */ +function collectRefIssues( + node: unknown, + path: string, + defNames: Set, + add: (issue: ChatLibraryIssue) => void, +): void { + if (Array.isArray(node)) { + node.forEach((item, i) => collectRefIssues(item, `${path}/${i}`, defNames, add)); + return; + } + if (!isPlainObject(node)) return; + + const ref = node["$ref"]; + if (typeof ref === "string") { + const match = REF_PATTERN.exec(ref); + if (!match || !defNames.has(match[1] as string)) { + add({ + code: "unresolved-ref", + message: `Unresolvable $ref "${ref}" at ${path} — refs must be "#/$defs/" pointing at a component in $defs.`, + path, + }); + } + } + + for (const [key, value] of Object.entries(node)) { + if (key === "$ref") continue; + collectRefIssues(value, `${path}/${key}`, defNames, add); + } +} + +function pathOf(issue: z.core.$ZodIssue): string | undefined { + return issue.path.length > 0 ? issue.path.map(String).join("/") : undefined; +} + +function shapeMessage(issue: z.core.$ZodIssue, path: string | undefined): string { + if (path === "root" || path?.startsWith("root")) { + return "chatLibrary.root, when present, must be a non-empty string."; + } + if (path === "schema" || path?.startsWith("schema")) { + if (path === "schema/$defs" || path === "schema.$defs") { + return "chatLibrary.schema.$defs must be a non-empty object keyed by component name."; + } + if (path === "schema" || issue.code === "invalid_type") { + return "chatLibrary.schema must be an object with a $defs map of component schemas."; + } + } + if (path?.startsWith("componentGroups")) { + return "Each componentGroups entry must be {name: string, components: string[], notes?: string[]}."; + } + return issue.message; +} + +/** + * Structural validation of a customer-supplied design-system library. + * Returns ALL issues found (empty array = valid). + */ +export function validateChatLibrary(library: ChatLibrary): ChatLibraryIssue[] { + if (!isPlainObject(library)) { + return [ + { + code: "invalid-shape", + message: "chatLibrary must be an object.", + }, + ]; + } + + const result = chatLibrarySchema.safeParse(library); + if (result.success) return []; + + return result.error.issues.map((issue) => { + const issueCode = (issue as { params?: { issueCode?: ChatLibraryIssue["code"] } }).params + ?.issueCode; + const path = pathOf(issue); + return { + code: issueCode ?? "invalid-shape", + message: issueCode ? issue.message : shapeMessage(issue, path), + path, + }; + }); +} From c3df6de4d63cb52d01e281ae2985238eceb15465 Mon Sep 17 00:00:00 2001 From: Abhin Rustagi Date: Wed, 2 Sep 2026 13:51:31 +0530 Subject: [PATCH 02/10] Validate chat libraries with the original walker instead of Zod Keep the same issue codes and messages, without a schema layer in between. --- packages/lang-core/src/index.ts | 2 +- packages/lang-core/src/parser/prompt.ts | 7 +- .../lang-core/src/parser/validate-library.ts | 269 ++++++++---------- 3 files changed, 124 insertions(+), 154 deletions(-) diff --git a/packages/lang-core/src/index.ts b/packages/lang-core/src/index.ts index 5dd45d692..f49db91a2 100644 --- a/packages/lang-core/src/index.ts +++ b/packages/lang-core/src/index.ts @@ -63,7 +63,7 @@ export type { ValidationError, ValidationErrorCode, } from "./parser/types"; -export { chatLibrarySchema, validateChatLibrary } from "./parser/validate-library"; +export { validateChatLibrary } from "./parser/validate-library"; export type { ChatLibrary, ChatLibraryIssue } from "./parser/validate-library"; // ── Reactive schema marker ── diff --git a/packages/lang-core/src/parser/prompt.ts b/packages/lang-core/src/parser/prompt.ts index 4934bc66c..e5cd703bd 100644 --- a/packages/lang-core/src/parser/prompt.ts +++ b/packages/lang-core/src/parser/prompt.ts @@ -2,7 +2,6 @@ import { recordSystemPromptGeneration } from "../telemetry/runtime"; import { BUILTINS, LAZY_BUILTIN_DEFS } from "./builtins"; import { generateCloudConfig } from "./cloud-config"; import type { LibraryJSONSchema } from "./types"; -import type { ChatLibrary } from "./validate-library"; // ─── PromptSpec types (JSON-serializable, no Zod deps) ────────────────────── @@ -726,7 +725,7 @@ export type SystemPromptSpec = } | { cloud: true; - library?: ChatLibrary; + library?: LibrarySpec; promptOptions?: CloudPromptOptions; instructions?: string; }; @@ -739,7 +738,7 @@ export function generateSystemPrompt(spec: SystemPromptSpec | PromptSpec): strin if (isCloudSpec(spec)) { return generateCloudConfig(spec); } - if (!isLocalSystemPromptSpec(spec)) { + if (!isSystemPromptSpec(spec)) { const prompt = generatePrompt(spec); recordSystemPromptGeneration(spec, "legacy_prompt_spec"); return prompt; @@ -756,7 +755,7 @@ function isCloudSpec( return "cloud" in spec && (spec as { cloud?: unknown }).cloud === true; } -function isLocalSystemPromptSpec( +function isSystemPromptSpec( spec: SystemPromptSpec | PromptSpec, ): spec is Extract { return "library" in spec && typeof (spec as { library?: unknown }).library === "object"; diff --git a/packages/lang-core/src/parser/validate-library.ts b/packages/lang-core/src/parser/validate-library.ts index 47c902b96..972ab1c13 100644 --- a/packages/lang-core/src/parser/validate-library.ts +++ b/packages/lang-core/src/parser/validate-library.ts @@ -1,4 +1,7 @@ -import { z } from "zod/v4"; +/** + * Structural validator for a customer-supplied design-system library. + * Hand-rolled walker — reports every issue in one pass. + */ import type { LibrarySpec } from "./prompt"; /** @@ -25,124 +28,15 @@ function isPlainObject(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } -const componentGroupSchema = z.object({ - name: z.string(), - components: z.array(z.string()), - notes: z.array(z.string()).optional(), -}); - -/** - * Structural + semantic schema for a customer-supplied chat library. - * Shape is Zod; `$ref` / root / required / group membership are refinements - * so every issue is reported (not fail-fast). - */ -export const chatLibrarySchema = z - .object({ - root: z - .string() - .min(1, "chatLibrary.root, when present, must be a non-empty string.") - .optional(), - id: z.string().optional(), - // generate-spec / toSpec() leftover — accepted, never required on the wire - components: z.unknown().optional(), - schema: z - .object({ - $defs: z.record(z.string(), z.unknown()).refine((defs) => Object.keys(defs).length > 0, { - message: "chatLibrary.schema.$defs must be a non-empty object keyed by component name.", - }), - }) - .passthrough(), - componentGroups: z.array(componentGroupSchema).optional(), - }) - .passthrough() - .superRefine((library, ctx) => { - const defs = library.schema.$defs; - const defNames = new Set(Object.keys(defs)); - - const add = (issue: ChatLibraryIssue) => { - ctx.addIssue({ - code: "custom", - message: issue.message, - path: issue.path ? issue.path.split("/") : [], - params: { issueCode: issue.code }, - }); - }; - - if (library.root && !defNames.has(library.root)) { - add({ - code: "root-not-found", - message: `Root component "${library.root}" was not found in schema.$defs. Available components: ${[...defNames].join(", ")}.`, - path: "root", - }); - } - - for (const [name, def] of Object.entries(defs)) { - const defPath = `$defs/${name}`; - if (!isPlainObject(def)) { - add({ - code: "invalid-shape", - message: `${defPath} must be an object component schema.`, - path: defPath, - }); - continue; - } - - const properties = def["properties"]; - if (properties !== undefined && !isPlainObject(properties)) { - add({ - code: "invalid-shape", - message: `${defPath}/properties must be an object.`, - path: `${defPath}/properties`, - }); - } - - const required = def["required"]; - if (required !== undefined) { - if (!Array.isArray(required) || required.some((r) => typeof r !== "string")) { - add({ - code: "invalid-required", - message: `${defPath}/required must be an array of property names.`, - path: `${defPath}/required`, - }); - } else { - const propKeys = new Set(isPlainObject(properties) ? Object.keys(properties) : []); - for (const r of required) { - if (!propKeys.has(r)) { - add({ - code: "invalid-required", - message: `${defPath} lists required property "${r}" that is not in properties.`, - path: `${defPath}/required`, - }); - } - } - } - } - - collectRefIssues(properties, `${defPath}/properties`, defNames, add); - } - - for (const [index, group] of (library.componentGroups ?? []).entries()) { - for (const comp of group.components) { - if (!defNames.has(comp)) { - add({ - code: "unknown-group-component", - message: `Component group "${group.name}" references unknown component "${comp}".`, - path: `componentGroups/${index}`, - }); - } - } - } - }); - /** Recursively collect every `$ref` and check it resolves within `$defs`. */ function collectRefIssues( node: unknown, path: string, defNames: Set, - add: (issue: ChatLibraryIssue) => void, + issues: ChatLibraryIssue[], ): void { if (Array.isArray(node)) { - node.forEach((item, i) => collectRefIssues(item, `${path}/${i}`, defNames, add)); + node.forEach((item, i) => collectRefIssues(item, `${path}/${i}`, defNames, issues)); return; } if (!isPlainObject(node)) return; @@ -151,7 +45,7 @@ function collectRefIssues( if (typeof ref === "string") { const match = REF_PATTERN.exec(ref); if (!match || !defNames.has(match[1] as string)) { - add({ + issues.push({ code: "unresolved-ref", message: `Unresolvable $ref "${ref}" at ${path} — refs must be "#/$defs/" pointing at a component in $defs.`, path, @@ -161,30 +55,8 @@ function collectRefIssues( for (const [key, value] of Object.entries(node)) { if (key === "$ref") continue; - collectRefIssues(value, `${path}/${key}`, defNames, add); - } -} - -function pathOf(issue: z.core.$ZodIssue): string | undefined { - return issue.path.length > 0 ? issue.path.map(String).join("/") : undefined; -} - -function shapeMessage(issue: z.core.$ZodIssue, path: string | undefined): string { - if (path === "root" || path?.startsWith("root")) { - return "chatLibrary.root, when present, must be a non-empty string."; - } - if (path === "schema" || path?.startsWith("schema")) { - if (path === "schema/$defs" || path === "schema.$defs") { - return "chatLibrary.schema.$defs must be a non-empty object keyed by component name."; - } - if (path === "schema" || issue.code === "invalid_type") { - return "chatLibrary.schema must be an object with a $defs map of component schemas."; - } - } - if (path?.startsWith("componentGroups")) { - return "Each componentGroups entry must be {name: string, components: string[], notes?: string[]}."; + collectRefIssues(value, `${path}/${key}`, defNames, issues); } - return issue.message; } /** @@ -192,6 +64,8 @@ function shapeMessage(issue: z.core.$ZodIssue, path: string | undefined): string * Returns ALL issues found (empty array = valid). */ export function validateChatLibrary(library: ChatLibrary): ChatLibraryIssue[] { + const issues: ChatLibraryIssue[] = []; + if (!isPlainObject(library)) { return [ { @@ -201,17 +75,114 @@ export function validateChatLibrary(library: ChatLibrary): ChatLibraryIssue[] { ]; } - const result = chatLibrarySchema.safeParse(library); - if (result.success) return []; - - return result.error.issues.map((issue) => { - const issueCode = (issue as { params?: { issueCode?: ChatLibraryIssue["code"] } }).params - ?.issueCode; - const path = pathOf(issue); - return { - code: issueCode ?? "invalid-shape", - message: issueCode ? issue.message : shapeMessage(issue, path), - path, - }; - }); + const { root, schema, componentGroups } = library; + + if (root !== undefined && (typeof root !== "string" || root.length === 0)) { + issues.push({ + code: "invalid-shape", + message: "chatLibrary.root, when present, must be a non-empty string.", + path: "root", + }); + } + + if (!isPlainObject(schema)) { + issues.push({ + code: "invalid-shape", + message: "chatLibrary.schema must be an object with a $defs map of component schemas.", + path: "schema", + }); + return issues; + } + + const defs = schema.$defs; + if (!isPlainObject(defs) || Object.keys(defs).length === 0) { + issues.push({ + code: "invalid-shape", + message: "chatLibrary.schema.$defs must be a non-empty object keyed by component name.", + path: "schema/$defs", + }); + return issues; + } + + const defNames = new Set(Object.keys(defs)); + + if (typeof root === "string" && root.length > 0 && !defNames.has(root)) { + issues.push({ + code: "root-not-found", + message: `Root component "${root}" was not found in schema.$defs. Available components: ${[...defNames].join(", ")}.`, + path: "root", + }); + } + + for (const [name, def] of Object.entries(defs)) { + const defPath = `$defs/${name}`; + if (!isPlainObject(def)) { + issues.push({ + code: "invalid-shape", + message: `${defPath} must be an object component schema.`, + path: defPath, + }); + continue; + } + + const properties = def["properties"]; + if (properties !== undefined && !isPlainObject(properties)) { + issues.push({ + code: "invalid-shape", + message: `${defPath}/properties must be an object.`, + path: `${defPath}/properties`, + }); + } + + const required = def["required"]; + if (required !== undefined) { + if (!Array.isArray(required) || required.some((r) => typeof r !== "string")) { + issues.push({ + code: "invalid-required", + message: `${defPath}/required must be an array of property names.`, + path: `${defPath}/required`, + }); + } else { + const propKeys = new Set(isPlainObject(properties) ? Object.keys(properties) : []); + for (const r of required) { + if (!propKeys.has(r)) { + issues.push({ + code: "invalid-required", + message: `${defPath} lists required property "${r}" that is not in properties.`, + path: `${defPath}/required`, + }); + } + } + } + } + + collectRefIssues(properties, `${defPath}/properties`, defNames, issues); + } + + for (const group of componentGroups ?? []) { + if ( + !isPlainObject(group) || + typeof group.name !== "string" || + !Array.isArray(group.components) + ) { + issues.push({ + code: "invalid-shape", + message: + "Each componentGroups entry must be {name: string, components: string[], notes?: string[]}.", + path: "componentGroups", + }); + continue; + } + for (const comp of group.components) { + if (typeof comp !== "string" || !defNames.has(comp)) { + issues.push({ + code: "unknown-group-component", + message: `Component group "${group.name}" references unknown component "${String(comp)}".`, + path: "componentGroups", + }); + } + } + } + + return issues; } From a3d55e506290e40df6264e10e6a02e12444f9955 Mon Sep 17 00:00:00 2001 From: Abhin Rustagi Date: Wed, 2 Sep 2026 13:52:28 +0530 Subject: [PATCH 03/10] Keep chat library validation internal to generateSystemPrompt Callers don't need validateChatLibrary; Cloud prompt generation already runs it. --- packages/lang-core/src/index.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/lang-core/src/index.ts b/packages/lang-core/src/index.ts index f49db91a2..f880435c2 100644 --- a/packages/lang-core/src/index.ts +++ b/packages/lang-core/src/index.ts @@ -63,8 +63,7 @@ export type { ValidationError, ValidationErrorCode, } from "./parser/types"; -export { validateChatLibrary } from "./parser/validate-library"; -export type { ChatLibrary, ChatLibraryIssue } from "./parser/validate-library"; +export type { ChatLibrary } from "./parser/validate-library"; // ── Reactive schema marker ── export { isReactiveSchema, markReactive } from "./reactive"; From 384c7769406a2ded7e10e76f0ad9f9f9b2936206 Mon Sep 17 00:00:00 2001 From: Abhin Rustagi Date: Wed, 2 Sep 2026 13:55:41 +0530 Subject: [PATCH 04/10] Reuse LibrarySpec for Cloud libraries and drop components on the wire MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit No separate ChatLibrary type — generateSystemPrompt accepts the same spec and omits `components` from the config block. --- packages/lang-core/src/index.ts | 1 - .../src/parser/__tests__/prompt.test.ts | 8 ++++---- packages/lang-core/src/parser/cloud-config.ts | 19 ++++++------------- .../lang-core/src/parser/validate-library.ts | 8 +------- 4 files changed, 11 insertions(+), 25 deletions(-) diff --git a/packages/lang-core/src/index.ts b/packages/lang-core/src/index.ts index f880435c2..b1f2f21c7 100644 --- a/packages/lang-core/src/index.ts +++ b/packages/lang-core/src/index.ts @@ -63,7 +63,6 @@ export type { ValidationError, ValidationErrorCode, } from "./parser/types"; -export type { ChatLibrary } from "./parser/validate-library"; // ── Reactive schema marker ── export { isReactiveSchema, markReactive } from "./reactive"; diff --git a/packages/lang-core/src/parser/__tests__/prompt.test.ts b/packages/lang-core/src/parser/__tests__/prompt.test.ts index e06f49bf6..fc19ae8b9 100644 --- a/packages/lang-core/src/parser/__tests__/prompt.test.ts +++ b/packages/lang-core/src/parser/__tests__/prompt.test.ts @@ -2,7 +2,6 @@ import { describe, expect, it } from "vitest"; import { CLOUD_CHAT_LIBRARY_VERSION, generateSystemPrompt, - type ChatLibrary, type CloudPromptOptions, type LibrarySpec, } from "../../index"; @@ -25,7 +24,7 @@ function parseBlock(emitted: string): Record { return JSON.parse(firstLine); } -const library: ChatLibrary = { +const library = { root: "Card", schema: { $defs: { @@ -52,7 +51,7 @@ const library: ChatLibrary = { components: ["TextContent"], }, ], -}; +} as LibrarySpec; describe("generateSystemPrompt — self-hosted", () => { const spec: LibrarySpec = { @@ -161,8 +160,9 @@ describe("generateSystemPrompt({ cloud: true, library })", () => { }); it("invalid library throws with every issue", () => { - const broken: ChatLibrary = { + const broken: LibrarySpec = { root: "Missing", + components: {}, schema: { $defs: { Card: { diff --git a/packages/lang-core/src/parser/cloud-config.ts b/packages/lang-core/src/parser/cloud-config.ts index 77e333efe..b722ad108 100644 --- a/packages/lang-core/src/parser/cloud-config.ts +++ b/packages/lang-core/src/parser/cloud-config.ts @@ -1,5 +1,5 @@ -import type { CloudPromptOptions, SystemPromptOptions } from "./prompt"; -import { type ChatLibrary, validateChatLibrary } from "./validate-library"; +import type { CloudPromptOptions, LibrarySpec, SystemPromptOptions } from "./prompt"; +import { validateChatLibrary } from "./validate-library"; /** `]]>openui:config\n` — request-direction config block header. Trailing newline is part of the wire contract. */ export const CLOUD_CONFIG_MARKER = "]]>openui:config\n"; @@ -12,14 +12,7 @@ export const CLOUD_CHAT_LIBRARY_VERSION = "0.1.0"; type CloudConfig = | { libraryVersion: string } - | { chatLibrary: CloudChatLibraryWire; systemPromptOptions?: CloudPromptOptions }; - -type CloudChatLibraryWire = { - schema?: ChatLibrary["schema"]; - root?: string; - componentGroups?: ChatLibrary["componentGroups"]; - id?: string; -}; + | { chatLibrary: Omit; systemPromptOptions?: CloudPromptOptions }; function pickCloudPromptOptions( options: SystemPromptOptions | CloudPromptOptions | undefined, @@ -33,7 +26,7 @@ function pickCloudPromptOptions( } export function generateCloudConfig(spec: { - library?: ChatLibrary; + library?: LibrarySpec; promptOptions?: SystemPromptOptions | CloudPromptOptions; instructions?: string; }): string { @@ -46,10 +39,10 @@ export function generateCloudConfig(spec: { `[generateSystemPrompt] Invalid library: ${issues.map((i) => i.message).join(" ")}`, ); } - const { schema, root, componentGroups, id } = spec.library; + const { components: _components, ...chatLibrary } = spec.library; const promptOptions = pickCloudPromptOptions(spec.promptOptions); config = { - chatLibrary: { schema, root, componentGroups, id }, + chatLibrary, ...(promptOptions ? { systemPromptOptions: promptOptions } : {}), }; } else { diff --git a/packages/lang-core/src/parser/validate-library.ts b/packages/lang-core/src/parser/validate-library.ts index 972ab1c13..21c1f3e46 100644 --- a/packages/lang-core/src/parser/validate-library.ts +++ b/packages/lang-core/src/parser/validate-library.ts @@ -4,12 +4,6 @@ */ import type { LibrarySpec } from "./prompt"; -/** - * Wire shape of a serialized chat library (the `]]>openui:config` `chatLibrary` - * value). Same as {@link LibrarySpec} without prompt-side `components`. - */ -export type ChatLibrary = Omit; - export interface ChatLibraryIssue { code: | "invalid-shape" @@ -63,7 +57,7 @@ function collectRefIssues( * Structural validation of a customer-supplied design-system library. * Returns ALL issues found (empty array = valid). */ -export function validateChatLibrary(library: ChatLibrary): ChatLibraryIssue[] { +export function validateChatLibrary(library: LibrarySpec): ChatLibraryIssue[] { const issues: ChatLibraryIssue[] = []; if (!isPlainObject(library)) { From 8b3236db571487a1bb747d5b75d562b84e063812 Mon Sep 17 00:00:00 2001 From: Abhin Rustagi Date: Wed, 2 Sep 2026 13:56:45 +0530 Subject: [PATCH 05/10] Keep Cloud sentinel constants internal to generateSystemPrompt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Callers only need the prompt string — the marker and library version are wire details. --- packages/lang-core/src/index.ts | 1 - packages/lang-core/src/parser/__tests__/prompt.test.ts | 9 ++------- packages/lang-core/src/parser/cloud-config.ts | 4 ++-- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/packages/lang-core/src/index.ts b/packages/lang-core/src/index.ts index b1f2f21c7..8fc38cf4f 100644 --- a/packages/lang-core/src/index.ts +++ b/packages/lang-core/src/index.ts @@ -29,7 +29,6 @@ export { toNumber, } from "./parser/builtins"; export type { BuiltinDef } from "./parser/builtins"; -export { CLOUD_CHAT_LIBRARY_VERSION, CLOUD_CONFIG_MARKER } from "./parser/cloud-config"; export { enrichErrors } from "./parser/enrich-errors"; export { parseExpression } from "./parser/expressions"; export { tokenize } from "./parser/lexer"; diff --git a/packages/lang-core/src/parser/__tests__/prompt.test.ts b/packages/lang-core/src/parser/__tests__/prompt.test.ts index fc19ae8b9..c7a2bea49 100644 --- a/packages/lang-core/src/parser/__tests__/prompt.test.ts +++ b/packages/lang-core/src/parser/__tests__/prompt.test.ts @@ -1,10 +1,5 @@ import { describe, expect, it } from "vitest"; -import { - CLOUD_CHAT_LIBRARY_VERSION, - generateSystemPrompt, - type CloudPromptOptions, - type LibrarySpec, -} from "../../index"; +import { generateSystemPrompt, type CloudPromptOptions, type LibrarySpec } from "../../index"; const CONFIG_MARKER = "]]>openui:config\n"; @@ -86,7 +81,7 @@ describe("generateSystemPrompt({ cloud: true }) — sentinel", () => { it("no library → JSON with only libraryVersion", () => { const config = parseBlock(generateSystemPrompt({ cloud: true })); expect(Object.keys(config)).toEqual(["libraryVersion"]); - expect(config.libraryVersion).toBe(CLOUD_CHAT_LIBRARY_VERSION); + expect(config.libraryVersion).toBe("0.1.0"); }); it("emitted keys are a subset of muse MANAGED_CONFIG_KEYS", () => { diff --git a/packages/lang-core/src/parser/cloud-config.ts b/packages/lang-core/src/parser/cloud-config.ts index b722ad108..e6d07d4e9 100644 --- a/packages/lang-core/src/parser/cloud-config.ts +++ b/packages/lang-core/src/parser/cloud-config.ts @@ -2,13 +2,13 @@ import type { CloudPromptOptions, LibrarySpec, SystemPromptOptions } from "./pro import { validateChatLibrary } from "./validate-library"; /** `]]>openui:config\n` — request-direction config block header. Trailing newline is part of the wire contract. */ -export const CLOUD_CONFIG_MARKER = "]]>openui:config\n"; +const CLOUD_CONFIG_MARKER = "]]>openui:config\n"; /** * Wire pin for OpenUI Cloud's built-in chat library when `generateSystemPrompt({ cloud: true })` * is called without a custom library. Muse 400s on a non-numeric or too-old version. */ -export const CLOUD_CHAT_LIBRARY_VERSION = "0.1.0"; +const CLOUD_CHAT_LIBRARY_VERSION = "0.1.0"; type CloudConfig = | { libraryVersion: string } From 7fc9a3b31b63bff6a5eaf5aeb415017c87868f94 Mon Sep 17 00:00:00 2001 From: Abhin Rustagi Date: Wed, 2 Sep 2026 13:57:43 +0530 Subject: [PATCH 06/10] Drop internal service names from Cloud prompt comments Public code should talk about OpenUI Cloud, not backend service names. --- packages/lang-core/src/parser/__tests__/prompt.test.ts | 6 +++--- packages/lang-core/src/parser/cloud-config.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/lang-core/src/parser/__tests__/prompt.test.ts b/packages/lang-core/src/parser/__tests__/prompt.test.ts index c7a2bea49..6b9b74d03 100644 --- a/packages/lang-core/src/parser/__tests__/prompt.test.ts +++ b/packages/lang-core/src/parser/__tests__/prompt.test.ts @@ -3,7 +3,7 @@ import { generateSystemPrompt, type CloudPromptOptions, type LibrarySpec } from const CONFIG_MARKER = "]]>openui:config\n"; -const MUSE_MANAGED_CONFIG_KEYS = [ +const CLOUD_CONFIG_KEYS = [ "libraryVersion", "customComponents", "customActions", @@ -84,10 +84,10 @@ describe("generateSystemPrompt({ cloud: true }) — sentinel", () => { expect(config.libraryVersion).toBe("0.1.0"); }); - it("emitted keys are a subset of muse MANAGED_CONFIG_KEYS", () => { + it("emitted keys are a subset of Cloud config keys", () => { const config = parseBlock(generateSystemPrompt({ cloud: true, instructions: "Be terse." })); for (const key of Object.keys(config)) { - expect(MUSE_MANAGED_CONFIG_KEYS).toContain(key); + expect(CLOUD_CONFIG_KEYS).toContain(key); } }); diff --git a/packages/lang-core/src/parser/cloud-config.ts b/packages/lang-core/src/parser/cloud-config.ts index e6d07d4e9..e30a9fbf0 100644 --- a/packages/lang-core/src/parser/cloud-config.ts +++ b/packages/lang-core/src/parser/cloud-config.ts @@ -6,7 +6,7 @@ const CLOUD_CONFIG_MARKER = "]]>openui:config\n"; /** * Wire pin for OpenUI Cloud's built-in chat library when `generateSystemPrompt({ cloud: true })` - * is called without a custom library. Muse 400s on a non-numeric or too-old version. + * is called without a custom library. Cloud rejects a non-numeric or too-old version. */ const CLOUD_CHAT_LIBRARY_VERSION = "0.1.0"; From ce202c12f04737145c49ce298a051d7c9fc45b81 Mon Sep 17 00:00:00 2001 From: Abhin Rustagi Date: Wed, 2 Sep 2026 14:01:21 +0530 Subject: [PATCH 07/10] Keep instructions as a Cloud-only generateSystemPrompt field Self-hosted prompts already have preamble and additionalRules; extra prose after the config block is Cloud-specific. --- docs/content/docs/openui-lang/system-prompts.mdx | 2 +- packages/lang-core/README.md | 3 ++- packages/lang-core/src/parser/__tests__/prompt.test.ts | 8 -------- packages/lang-core/src/parser/prompt.ts | 10 +++++----- 4 files changed, 8 insertions(+), 15 deletions(-) diff --git a/docs/content/docs/openui-lang/system-prompts.mdx b/docs/content/docs/openui-lang/system-prompts.mdx index a2bd3a0ab..0809fa528 100644 --- a/docs/content/docs/openui-lang/system-prompts.mdx +++ b/docs/content/docs/openui-lang/system-prompts.mdx @@ -87,7 +87,7 @@ const systemPrompt = generateSystemPrompt({ ### OpenUI Cloud -Pass `cloud: true` to emit Cloud's managed config block instead of a local prompt. OpenUI Cloud assembles the real system prompt on the server. +Pass `cloud: true` to emit Cloud's managed config block instead of a local prompt. OpenUI Cloud assembles the real system prompt on the server. `instructions` is Cloud-only extra prose appended after the config block. ```ts import { generateSystemPrompt } from "@openuidev/lang-core"; diff --git a/packages/lang-core/README.md b/packages/lang-core/README.md index 7e8ab8e96..49be83716 100644 --- a/packages/lang-core/README.md +++ b/packages/lang-core/README.md @@ -79,7 +79,8 @@ const prompt = generateSystemPrompt({ ### OpenUI Cloud Pass `cloud: true` to emit Cloud's managed config block instead of a local prompt. OpenUI Cloud -assembles the real system prompt on the server. +assembles the real system prompt on the server. `instructions` is Cloud-only extra prose +appended after the config block — self-hosted prompts use `preamble` / `additionalRules` instead. ```ts import { generateSystemPrompt } from "@openuidev/lang-core"; diff --git a/packages/lang-core/src/parser/__tests__/prompt.test.ts b/packages/lang-core/src/parser/__tests__/prompt.test.ts index 6b9b74d03..0ae5413cd 100644 --- a/packages/lang-core/src/parser/__tests__/prompt.test.ts +++ b/packages/lang-core/src/parser/__tests__/prompt.test.ts @@ -62,14 +62,6 @@ describe("generateSystemPrompt — self-hosted", () => { expect(prompt).toContain("Card(children: Component[])"); expect(prompt.startsWith(CONFIG_MARKER)).toBe(false); }); - - it("appends instructions after the local prompt", () => { - const prompt = generateSystemPrompt({ - library: spec, - instructions: "Be terse.", - }); - expect(prompt.endsWith("\nBe terse.")).toBe(true); - }); }); describe("generateSystemPrompt({ cloud: true }) — sentinel", () => { diff --git a/packages/lang-core/src/parser/prompt.ts b/packages/lang-core/src/parser/prompt.ts index e5cd703bd..28f8da017 100644 --- a/packages/lang-core/src/parser/prompt.ts +++ b/packages/lang-core/src/parser/prompt.ts @@ -695,7 +695,7 @@ export function generatePrompt(spec: PromptSpec): string { return parts.join("\n"); } -// ─── System prompt (library + options + instructions) ─────────────────────── +// ─── System prompt (library + options) ────────────────────────────────────── /** Prompt options for {@link generateSystemPrompt} */ export type SystemPromptOptions = Omit; @@ -714,13 +714,13 @@ export type CloudPromptOptions = Pick< * * Pass `cloud: true` to emit OpenUI Cloud's managed `]]>openui:config` block * instead of a locally generated prompt. In that mode `library` is optional — - * omit it to use Cloud's built-in chat library. + * omit it to use Cloud's built-in chat library. `instructions` is Cloud-only + * extra prose appended after the config block. */ export type SystemPromptSpec = | { library: LibrarySpec; promptOptions?: SystemPromptOptions; - instructions?: string; cloud?: false; } | { @@ -732,7 +732,7 @@ export type SystemPromptSpec = /** Render the full system prompt for a library, or Cloud's managed config block when `cloud: true`. */ export function generateSystemPrompt(spec: SystemPromptSpec): string; -/** @deprecated Pass `{ library, promptOptions, instructions }` instead. Removed at the next major. */ +/** @deprecated Pass `{ library, promptOptions }` instead. Removed at the next major. */ export function generateSystemPrompt(spec: PromptSpec): string; export function generateSystemPrompt(spec: SystemPromptSpec | PromptSpec): string { if (isCloudSpec(spec)) { @@ -746,7 +746,7 @@ export function generateSystemPrompt(spec: SystemPromptSpec | PromptSpec): strin const merged: PromptSpec = { ...spec.library, ...spec.promptOptions }; const prompt = generatePrompt(merged); recordSystemPromptGeneration(merged, "library_spec"); - return spec.instructions ? `${prompt}\n${spec.instructions}` : prompt; + return prompt; } function isCloudSpec( From d2ed5c2af8181a1c28a29c3b77fe5349da95d6a3 Mon Sep 17 00:00:00 2001 From: AB <55152006+AbhinRustagi@users.noreply.github.com> Date: Wed, 2 Sep 2026 14:03:22 +0530 Subject: [PATCH 08/10] Clarify OpenUI Cloud usage and deprecate generatePrompt Updated README to clarify usage of OpenUI Cloud and deprecated prompt generation method. --- packages/lang-core/README.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/lang-core/README.md b/packages/lang-core/README.md index 49be83716..6b2157527 100644 --- a/packages/lang-core/README.md +++ b/packages/lang-core/README.md @@ -79,8 +79,7 @@ const prompt = generateSystemPrompt({ ### OpenUI Cloud Pass `cloud: true` to emit Cloud's managed config block instead of a local prompt. OpenUI Cloud -assembles the real system prompt on the server. `instructions` is Cloud-only extra prose -appended after the config block — self-hosted prompts use `preamble` / `additionalRules` instead. +assembles the real system prompt on the server. ```ts import { generateSystemPrompt } from "@openuidev/lang-core"; @@ -89,7 +88,7 @@ import { generateSystemPrompt } from "@openuidev/lang-core"; const instructions = generateSystemPrompt({ cloud: true }); // Custom library (the `.spec.json` from `openui generate`) -const custom = generateSystemPrompt({ +const myLibraryPrompt = generateSystemPrompt({ cloud: true, library: librarySpec, promptOptions: { preamble: "You build dashboards for Acme." }, @@ -122,8 +121,8 @@ const merged = mergeStatements(original, patch); | Export | Description | | :--- | :--- | -| `generatePrompt(spec)` | Generate a system prompt from a `PromptSpec` | -| `generateSystemPrompt(spec)` | Generate a system prompt from `{ library, promptOptions }`. Pass `{ cloud: true }` for OpenUI Cloud's managed config block. | +| `generatePrompt(spec)` | Generate a system prompt from a `PromptSpec` (now deprecated) | +| `generateSystemPrompt(spec)` | Generate a system prompt from `{ library, promptOptions }`. Pass `{ cloud: true }` for OpenUI Cloud's managed config. | **`PromptSpec`** includes component signatures, tool definitions (`ToolSpec[]`), feature flags (`toolCalls`, `bindings`, `editMode`, `inlineMode`), examples, and custom rules. From d72902b2f0a872d76fbf4537986b69158eaa8d94 Mon Sep 17 00:00:00 2001 From: AB <55152006+AbhinRustagi@users.noreply.github.com> Date: Wed, 2 Sep 2026 14:03:46 +0530 Subject: [PATCH 09/10] Update README to clarify library usage instructions --- packages/lang-core/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lang-core/README.md b/packages/lang-core/README.md index 6b2157527..cbf42ced0 100644 --- a/packages/lang-core/README.md +++ b/packages/lang-core/README.md @@ -87,7 +87,7 @@ import { generateSystemPrompt } from "@openuidev/lang-core"; // Built-in Cloud chat library const instructions = generateSystemPrompt({ cloud: true }); -// Custom library (the `.spec.json` from `openui generate`) +// Using your own library (the `.spec.json` from `openui generate`) const myLibraryPrompt = generateSystemPrompt({ cloud: true, library: librarySpec, From aeb581213cb42c8a240358a5d508d343ce5c1f93 Mon Sep 17 00:00:00 2001 From: Abhin Rustagi Date: Wed, 2 Sep 2026 14:05:19 +0530 Subject: [PATCH 10/10] Say your own library instead of custom library Callers bring a library, they do not register a custom one. --- docs/content/docs/openui-cloud/api/chat-completions.mdx | 2 +- docs/content/docs/openui-cloud/api/responses.mdx | 2 +- docs/content/docs/openui-lang/system-prompts.mdx | 4 ++-- packages/lang-core/src/parser/__tests__/prompt.test.ts | 2 +- packages/lang-core/src/parser/cloud-config.ts | 2 +- packages/lang-core/src/parser/validate-library.ts | 4 ++-- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/content/docs/openui-cloud/api/chat-completions.mdx b/docs/content/docs/openui-cloud/api/chat-completions.mdx index 7177de7ca..8b44cf59b 100644 --- a/docs/content/docs/openui-cloud/api/chat-completions.mdx +++ b/docs/content/docs/openui-cloud/api/chat-completions.mdx @@ -27,7 +27,7 @@ const completion = await embedClient.chat.completions.create({ console.log(completion.choices[0].message.content); ``` -The returned message content is an OpenUI Lang program. See [Component Library](/docs/openui-cloud/build/component-library) for the built-in client library and custom component workflow. +The returned message content is an OpenUI Lang program. See [Component Library](/docs/openui-cloud/build/component-library) for the built-in client library and using your own library. ## Stream and render responses diff --git a/docs/content/docs/openui-cloud/api/responses.mdx b/docs/content/docs/openui-cloud/api/responses.mdx index 89d648719..61d03d9e2 100644 --- a/docs/content/docs/openui-cloud/api/responses.mdx +++ b/docs/content/docs/openui-cloud/api/responses.mdx @@ -25,7 +25,7 @@ const response = await embedClient.responses.create({ console.log(response.output_text); ``` -The returned `output_text` is an OpenUI Lang program. See [Component Library](/docs/openui-cloud/build/component-library) for the built-in client library and custom component workflow. +The returned `output_text` is an OpenUI Lang program. See [Component Library](/docs/openui-cloud/build/component-library) for the built-in client library and using your own library. ## Stream and render responses diff --git a/docs/content/docs/openui-lang/system-prompts.mdx b/docs/content/docs/openui-lang/system-prompts.mdx index 0809fa528..d2dbae0e7 100644 --- a/docs/content/docs/openui-lang/system-prompts.mdx +++ b/docs/content/docs/openui-lang/system-prompts.mdx @@ -71,7 +71,7 @@ const systemPrompt = generateSystemPrompt({ editMode: true, // Enable incremental editing (LLM outputs patches, not full regen) inlineMode: true, // Enable text + code responses (LLM can answer questions without code) - // Custom instructions + // Preamble and extra rules preamble: "You build dashboards using openui-lang.", additionalRules: ['Use @Reset after form submit, not @Set($var, "")'], }, @@ -95,7 +95,7 @@ import { generateSystemPrompt } from "@openuidev/lang-core"; const instructions = generateSystemPrompt({ cloud: true }); ``` -With a custom library, pass the generated spec: +With your own library, pass the generated spec: ```ts const instructions = generateSystemPrompt({ diff --git a/packages/lang-core/src/parser/__tests__/prompt.test.ts b/packages/lang-core/src/parser/__tests__/prompt.test.ts index 0ae5413cd..24c1ca94e 100644 --- a/packages/lang-core/src/parser/__tests__/prompt.test.ts +++ b/packages/lang-core/src/parser/__tests__/prompt.test.ts @@ -83,7 +83,7 @@ describe("generateSystemPrompt({ cloud: true }) — sentinel", () => { } }); - it("appends customer prose after the block", () => { + it("appends extra prose after the block", () => { const out = generateSystemPrompt({ cloud: true, instructions: "Be terse." }); const [sentinelLine, jsonLine, ...rest] = out.split("\n"); expect(sentinelLine).toBe("]]>openui:config"); diff --git a/packages/lang-core/src/parser/cloud-config.ts b/packages/lang-core/src/parser/cloud-config.ts index e30a9fbf0..7dfdff373 100644 --- a/packages/lang-core/src/parser/cloud-config.ts +++ b/packages/lang-core/src/parser/cloud-config.ts @@ -6,7 +6,7 @@ const CLOUD_CONFIG_MARKER = "]]>openui:config\n"; /** * Wire pin for OpenUI Cloud's built-in chat library when `generateSystemPrompt({ cloud: true })` - * is called without a custom library. Cloud rejects a non-numeric or too-old version. + * is called without your own library. Cloud rejects a non-numeric or too-old version. */ const CLOUD_CHAT_LIBRARY_VERSION = "0.1.0"; diff --git a/packages/lang-core/src/parser/validate-library.ts b/packages/lang-core/src/parser/validate-library.ts index 21c1f3e46..32af9aa6e 100644 --- a/packages/lang-core/src/parser/validate-library.ts +++ b/packages/lang-core/src/parser/validate-library.ts @@ -1,5 +1,5 @@ /** - * Structural validator for a customer-supplied design-system library. + * Structural validator for your own library. * Hand-rolled walker — reports every issue in one pass. */ import type { LibrarySpec } from "./prompt"; @@ -54,7 +54,7 @@ function collectRefIssues( } /** - * Structural validation of a customer-supplied design-system library. + * Structural validation of your own library. * Returns ALL issues found (empty array = valid). */ export function validateChatLibrary(library: LibrarySpec): ChatLibraryIssue[] {