= {
+ AAPL: 189.84,
+ GOOGL: 141.8,
+ TSLA: 248.42,
+ MSFT: 378.91,
+ AMZN: 178.25,
+ NVDA: 875.28,
+ META: 485.58,
+ };
+ const price = knownPrices[s] ?? Math.floor(Math.random() * 500 + 20);
+ const change = parseFloat((Math.random() * 8 - 4).toFixed(2));
+ return {
+ symbol: s,
+ price: parseFloat((price + change).toFixed(2)),
+ change,
+ change_percent: parseFloat(((change / price) * 100).toFixed(2)),
+ volume: `${(Math.random() * 50 + 10).toFixed(1)}M`,
+ day_high: parseFloat((price + Math.abs(change) + 1.5).toFixed(2)),
+ day_low: parseFloat((price - Math.abs(change) - 1.2).toFixed(2)),
+ };
+ },
+ }),
+
+ calculate: tool({
+ description: "Evaluate a math expression.",
+ inputSchema: z.object({
+ expression: z.string().describe("Math expression to evaluate"),
+ }),
+ execute: async ({ expression }) => {
+ await new Promise((r) => setTimeout(r, 300));
+ try {
+ const sanitized = expression.replace(
+ /[^0-9+\-*/().%\s,Math.sqrtpowabsceilfloorround]/g,
+ "",
+ );
+ const result = new Function(`return (${sanitized})`)();
+ return { expression, result: Number(result) };
+ } catch {
+ return { expression, error: "Invalid expression" };
+ }
+ },
+ }),
+
+ search_web: tool({
+ description: "Search the web for information.",
+ inputSchema: z.object({
+ query: z.string().describe("Search query"),
+ }),
+ execute: async ({ query }) => {
+ await new Promise((r) => setTimeout(r, 1000));
+ return {
+ query,
+ results: [
+ {
+ title: `Top result for "${query}"`,
+ snippet: `Comprehensive overview of ${query} with the latest information.`,
+ },
+ {
+ title: `${query} - Latest News`,
+ snippet: `Recent developments and updates related to ${query}.`,
+ },
+ {
+ title: `Understanding ${query}`,
+ snippet: `An in-depth guide explaining everything about ${query}.`,
+ },
+ ],
+ };
+ },
+ }),
+};
diff --git a/examples/svelte-chat/src/routes/+layout.svelte b/examples/svelte-chat/src/routes/+layout.svelte
new file mode 100644
index 000000000..d701e67c8
--- /dev/null
+++ b/examples/svelte-chat/src/routes/+layout.svelte
@@ -0,0 +1,7 @@
+
+
+{@render children()}
diff --git a/examples/svelte-chat/src/routes/+layout.ts b/examples/svelte-chat/src/routes/+layout.ts
new file mode 100644
index 000000000..a3d15781a
--- /dev/null
+++ b/examples/svelte-chat/src/routes/+layout.ts
@@ -0,0 +1 @@
+export const ssr = false;
diff --git a/examples/svelte-chat/src/routes/+page.svelte b/examples/svelte-chat/src/routes/+page.svelte
new file mode 100644
index 000000000..a8af7d126
--- /dev/null
+++ b/examples/svelte-chat/src/routes/+page.svelte
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+ {#if chat.messages.length === 0}
+
+ {:else}
+
+ {#each chat.messages as message, i}
+ {#if message.role === "user"}
+
+ {:else if message.role === "assistant"}
+
+ {/if}
+ {/each}
+
+ {#if isLoading && (chat.messages.length === 0 || chat.messages[chat.messages.length - 1]?.role === "user")}
+
+ {/if}
+
+
+
+ {/if}
+
+
+
chat.stop()} />
+
diff --git a/examples/svelte-chat/src/routes/AssistantMessage.svelte b/examples/svelte-chat/src/routes/AssistantMessage.svelte
new file mode 100644
index 000000000..57a8b068f
--- /dev/null
+++ b/examples/svelte-chat/src/routes/AssistantMessage.svelte
@@ -0,0 +1,71 @@
+
+
+
+
+ AI
+
+
+ {#each toolParts as tp}
+ {@const state = (tp as any).state}
+ {@const done = state === "output-available"}
+
+ {#if done}
+
+ {:else}
+
+ {/if}
+
{getToolName(tp)}
+
+ {/each}
+ {#if textContent}
+
+ {/if}
+
+
diff --git a/examples/svelte-chat/src/routes/ChatHeader.svelte b/examples/svelte-chat/src/routes/ChatHeader.svelte
new file mode 100644
index 000000000..86707b508
--- /dev/null
+++ b/examples/svelte-chat/src/routes/ChatHeader.svelte
@@ -0,0 +1,11 @@
+
+
+
diff --git a/examples/svelte-chat/src/routes/ChatInput.svelte b/examples/svelte-chat/src/routes/ChatInput.svelte
new file mode 100644
index 000000000..2e93f3b4b
--- /dev/null
+++ b/examples/svelte-chat/src/routes/ChatInput.svelte
@@ -0,0 +1,55 @@
+
+
+
+
+
diff --git a/examples/svelte-chat/src/routes/LoadingIndicator.svelte b/examples/svelte-chat/src/routes/LoadingIndicator.svelte
new file mode 100644
index 000000000..07d1ab66c
--- /dev/null
+++ b/examples/svelte-chat/src/routes/LoadingIndicator.svelte
@@ -0,0 +1,19 @@
+
+
+
diff --git a/examples/svelte-chat/src/routes/UserMessage.svelte b/examples/svelte-chat/src/routes/UserMessage.svelte
new file mode 100644
index 000000000..bb999345b
--- /dev/null
+++ b/examples/svelte-chat/src/routes/UserMessage.svelte
@@ -0,0 +1,19 @@
+
+
+
+
+ {#each parts as part}
+ {#if part.type === "text"}
+
{part.text}
+ {/if}
+ {/each}
+
+
diff --git a/examples/svelte-chat/src/routes/WelcomeScreen.svelte b/examples/svelte-chat/src/routes/WelcomeScreen.svelte
new file mode 100644
index 000000000..c3bb760a6
--- /dev/null
+++ b/examples/svelte-chat/src/routes/WelcomeScreen.svelte
@@ -0,0 +1,29 @@
+
+
+
+
+
+ Welcome to OpenUI Chat
+
+
+ Ask anything — responses are rendered as structured UI components.
+
+
+
+ {#each starters as starter}
+
+ {/each}
+
+
diff --git a/examples/svelte-chat/src/routes/api/chat/+server.ts b/examples/svelte-chat/src/routes/api/chat/+server.ts
new file mode 100644
index 000000000..6c0dc1620
--- /dev/null
+++ b/examples/svelte-chat/src/routes/api/chat/+server.ts
@@ -0,0 +1,23 @@
+import { env } from "$env/dynamic/private";
+import { library, promptOptions } from "$lib/library";
+import { tools } from "$lib/tools";
+import { createOpenAI } from "@ai-sdk/openai";
+import { convertToModelMessages, stepCountIs, streamText } from "ai";
+
+const openai = createOpenAI({ apiKey: env.OPENAI_API_KEY ?? "" });
+
+const systemPrompt = library.prompt(promptOptions);
+
+export async function POST({ request }: { request: Request }) {
+ const { messages } = await request.json();
+
+ const result = streamText({
+ model: openai("gpt-5.4"),
+ system: systemPrompt,
+ messages: await convertToModelMessages(messages),
+ tools,
+ stopWhen: stepCountIs(5),
+ });
+
+ return result.toUIMessageStreamResponse();
+}
diff --git a/examples/svelte-chat/svelte.config.js b/examples/svelte-chat/svelte.config.js
new file mode 100644
index 000000000..f21eeff58
--- /dev/null
+++ b/examples/svelte-chat/svelte.config.js
@@ -0,0 +1,9 @@
+import adapter from "@sveltejs/adapter-auto";
+import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
+
+export default {
+ preprocess: vitePreprocess(),
+ kit: {
+ adapter: adapter(),
+ },
+};
diff --git a/examples/svelte-chat/tsconfig.json b/examples/svelte-chat/tsconfig.json
new file mode 100644
index 000000000..e2018a00b
--- /dev/null
+++ b/examples/svelte-chat/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "extends": "./.svelte-kit/tsconfig.json",
+ "compilerOptions": {
+ "strict": true,
+ "moduleResolution": "bundler"
+ }
+}
diff --git a/examples/svelte-chat/vite.config.ts b/examples/svelte-chat/vite.config.ts
new file mode 100644
index 000000000..a23082f45
--- /dev/null
+++ b/examples/svelte-chat/vite.config.ts
@@ -0,0 +1,10 @@
+import { sveltekit } from "@sveltejs/kit/vite";
+import tailwindcss from "@tailwindcss/vite";
+import { defineConfig } from "vite";
+
+export default defineConfig({
+ plugins: [tailwindcss(), sveltekit()],
+ ssr: {
+ noExternal: ["@openuidev/svelte-lang"],
+ },
+});
diff --git a/packages/lang-core/eslint.config.cjs b/packages/lang-core/eslint.config.cjs
new file mode 100644
index 000000000..3f9a9e7c4
--- /dev/null
+++ b/packages/lang-core/eslint.config.cjs
@@ -0,0 +1,72 @@
+const tseslint = require("@typescript-eslint/eslint-plugin");
+const typescript = require("@typescript-eslint/parser");
+const prettier = require("eslint-config-prettier");
+const unusedImports = require("eslint-plugin-unused-imports");
+const eslintPluginPrettier = require("eslint-plugin-prettier");
+
+module.exports = [
+ {
+ files: ["**/__tests__/**/*.{ts,tsx}", "**/*.test.{ts,tsx}", "**/*.spec.{ts,tsx}"],
+ languageOptions: {
+ parser: typescript,
+ parserOptions: {
+ project: "./tsconfig.test.json",
+ sourceType: "module",
+ },
+ },
+ },
+ {
+ files: ["**/*.{ts,tsx}"],
+ ignores: [
+ "**/*.stories.tsx",
+ "**/__tests__/**/*.{ts,tsx}",
+ "**/*.test.{ts,tsx}",
+ "**/*.spec.{ts,tsx}",
+ ],
+ languageOptions: {
+ parser: typescript,
+ parserOptions: {
+ project: "./tsconfig.json",
+ sourceType: "module",
+ },
+ },
+ plugins: {
+ "@typescript-eslint": tseslint,
+ "unused-imports": unusedImports,
+ prettier: eslintPluginPrettier,
+ },
+ rules: {
+ "@typescript-eslint/interface-name-prefix": "off",
+ "@typescript-eslint/explicit-function-return-type": "off",
+ "@typescript-eslint/explicit-module-boundary-types": "off",
+ "@typescript-eslint/no-explicit-any": "off",
+ "no-undefined": "off",
+ "@typescript-eslint/no-unused-vars": [
+ "error",
+ {
+ vars: "all",
+ varsIgnorePattern: "^_",
+ args: "after-used",
+ argsIgnorePattern: "^_",
+ },
+ ],
+ "@typescript-eslint/no-use-before-define": [
+ "error",
+ {
+ functions: false,
+ classes: false,
+ variables: false,
+ },
+ ],
+ "unused-imports/no-unused-imports": "error",
+ "no-console": [
+ "error",
+ {
+ allow: ["error", "warn", "info"],
+ },
+ ],
+ ...eslintPluginPrettier.configs.recommended.rules,
+ },
+ },
+ prettier,
+];
diff --git a/packages/lang-core/package.json b/packages/lang-core/package.json
new file mode 100644
index 000000000..ee5e500b9
--- /dev/null
+++ b/packages/lang-core/package.json
@@ -0,0 +1,58 @@
+{
+ "name": "@openuidev/lang-core",
+ "version": "0.1.0",
+ "description": "Framework-agnostic core for OpenUI Lang: parser, prompt generation, validation, and type definitions",
+ "license": "MIT",
+ "type": "module",
+ "main": "dist/index.js",
+ "types": "dist/index.d.ts",
+ "files": [
+ "dist",
+ "README.md"
+ ],
+ "exports": {
+ ".": {
+ "types": "./dist/index.d.ts",
+ "import": "./dist/index.js",
+ "default": "./dist/index.js"
+ }
+ },
+ "scripts": {
+ "test": "vitest run",
+ "build": "tsc -p .",
+ "watch": "tsc -p . --watch",
+ "lint:check": "eslint ./src",
+ "lint:fix": "eslint ./src --fix",
+ "format:fix": "prettier --write ./src",
+ "format:check": "prettier --check ./src",
+ "prepare": "pnpm run build",
+ "ci": "pnpm run lint:check && pnpm run format:check"
+ },
+ "dependencies": {
+ "zod": "^4.0.0"
+ },
+ "keywords": [
+ "openui",
+ "openui-lang",
+ "parser",
+ "prompt-generation",
+ "validation",
+ "zod",
+ "llm",
+ "generative-ui",
+ "framework-agnostic"
+ ],
+ "homepage": "https://openui.com",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/thesysdev/openui.git",
+ "directory": "packages/lang-core"
+ },
+ "bugs": {
+ "url": "https://github.com/thesysdev/openui/issues"
+ },
+ "author": "engineering@thesys.dev",
+ "devDependencies": {
+ "vitest": "^4.0.18"
+ }
+}
diff --git a/packages/lang-core/src/index.ts b/packages/lang-core/src/index.ts
new file mode 100644
index 000000000..f0696373d
--- /dev/null
+++ b/packages/lang-core/src/index.ts
@@ -0,0 +1,22 @@
+// ── Library (framework-generic) ──
+export { createLibrary, defineComponent } from "./library";
+export type {
+ ComponentGroup,
+ ComponentRenderProps,
+ DefinedComponent,
+ Library,
+ LibraryDefinition,
+ PromptOptions,
+ SubComponentOf,
+} from "./library";
+
+// ── Parser ──
+export { createParser, createStreamingParser, parse } from "./parser";
+export type { LibraryJSONSchema, Parser, StreamParser } from "./parser";
+export { generatePrompt } from "./parser/prompt";
+export { BuiltinActionType } from "./parser/types";
+export type { ActionEvent, ElementNode, ParseResult, ValidationErrorCode } from "./parser/types";
+
+// ── Validation ──
+export { builtInValidators, parseRules, parseStructuredRules, validate } from "./utils/validation";
+export type { ParsedRule, ValidatorFn } from "./utils/validation";
diff --git a/packages/lang-core/src/library.ts b/packages/lang-core/src/library.ts
new file mode 100644
index 000000000..8d0a4b18c
--- /dev/null
+++ b/packages/lang-core/src/library.ts
@@ -0,0 +1,133 @@
+import { z } from "zod";
+import { generatePrompt } from "./parser/prompt";
+
+// ─── Sub-component type ──────────────────────────────────────────────────────
+
+/**
+ * Runtime shape of a parsed sub-component element as seen by parent renderers.
+ */
+export type SubComponentOf = {
+ type: "element";
+ typeName: string;
+ props: P;
+ partial: boolean;
+};
+
+// ─── Renderer types (framework-generic) ──────────────────────────────────────
+
+/**
+ * The props passed to every component renderer.
+ *
+ * Framework adapters narrow `RenderNode`:
+ * - React: RenderNode = ReactNode
+ * - Svelte: RenderNode = Snippet<[unknown]>
+ * - Vue: RenderNode = VNode
+ */
+export interface ComponentRenderProps
, RenderNode = unknown> {
+ props: P;
+ renderNode: (value: unknown) => RenderNode;
+}
+
+// ─── DefinedComponent (framework-generic) ────────────────────────────────────
+
+/**
+ * A fully defined component. The `C` parameter represents the
+ * framework-specific component/renderer type. lang-core never
+ * inspects this value — it is stored opaquely and consumed
+ * by the framework adapter's Renderer.
+ */
+export interface DefinedComponent = z.ZodObject, C = unknown> {
+ name: string;
+ props: T;
+ description: string;
+ component: C;
+ /** Use in parent schemas: `z.array(ChildComponent.ref)` */
+ ref: z.ZodType>>;
+}
+
+/**
+ * Define a component with name, schema, description, and renderer.
+ * Registers the Zod schema globally and returns a `.ref` for parent schemas.
+ */
+export function defineComponent, C>(config: {
+ name: string;
+ props: T;
+ description: string;
+ component: C;
+}): DefinedComponent {
+ (config.props as any).register(z.globalRegistry, { id: config.name });
+ return {
+ ...config,
+ ref: config.props as unknown as z.ZodType>>,
+ };
+}
+
+// ─── Groups & Prompt ──────────────────────────────────────────────────────────
+
+export interface ComponentGroup {
+ name: string;
+ components: string[];
+ notes?: string[];
+}
+
+export interface PromptOptions {
+ preamble?: string;
+ additionalRules?: string[];
+ examples?: string[];
+}
+
+// ─── Library ──────────────────────────────────────────────────────────────────
+
+export interface Library {
+ readonly components: Record>;
+ readonly componentGroups: ComponentGroup[] | undefined;
+ readonly root: string | undefined;
+
+ prompt(options?: PromptOptions): string;
+ toJSONSchema(): object;
+}
+
+export interface LibraryDefinition {
+ components: DefinedComponent[];
+ componentGroups?: ComponentGroup[];
+ root?: string;
+}
+
+/**
+ * Create a component library from an array of defined components.
+ */
+export function createLibrary(input: LibraryDefinition): Library {
+ const componentsRecord: Record> = {};
+ for (const comp of input.components) {
+ if (!z.globalRegistry.has(comp.props)) {
+ comp.props.register(z.globalRegistry, { id: comp.name });
+ }
+ componentsRecord[comp.name] = comp;
+ }
+
+ if (input.root && !componentsRecord[input.root]) {
+ const available = Object.keys(componentsRecord).join(", ");
+ throw new Error(
+ `[createLibrary] Root component "${input.root}" was not found in components. Available components: ${available}`,
+ );
+ }
+
+ const library: Library = {
+ components: componentsRecord,
+ componentGroups: input.componentGroups,
+ root: input.root,
+
+ prompt(options?: PromptOptions): string {
+ return generatePrompt(library, options);
+ },
+
+ toJSONSchema(): object {
+ const combinedSchema = z.object(
+ Object.fromEntries(Object.entries(componentsRecord).map(([k, v]) => [k, v.props])) as any,
+ );
+ return z.toJSONSchema(combinedSchema);
+ },
+ };
+
+ return library;
+}
diff --git a/packages/react-lang/src/parser/__tests__/parser.test.ts b/packages/lang-core/src/parser/__tests__/parser.test.ts
similarity index 98%
rename from packages/react-lang/src/parser/__tests__/parser.test.ts
rename to packages/lang-core/src/parser/__tests__/parser.test.ts
index bd91ede82..1ea6cfb7d 100644
--- a/packages/react-lang/src/parser/__tests__/parser.test.ts
+++ b/packages/lang-core/src/parser/__tests__/parser.test.ts
@@ -27,7 +27,7 @@ const schema: ParamMap = new Map([
// ── Helpers ───────────────────────────────────────────────────────────────────
const errors = (input: string) => parse(input, schema).meta.errors;
-const codes = (input: string) => errors(input).map((e) => e.code);
+const codes = (input: string) => errors(input).map((e: { code: string }) => e.code);
// ── unknown-component ────────────────────────────────────────────────────────
diff --git a/packages/react-lang/src/parser/index.ts b/packages/lang-core/src/parser/index.ts
similarity index 100%
rename from packages/react-lang/src/parser/index.ts
rename to packages/lang-core/src/parser/index.ts
diff --git a/packages/react-lang/src/parser/parser.ts b/packages/lang-core/src/parser/parser.ts
similarity index 100%
rename from packages/react-lang/src/parser/parser.ts
rename to packages/lang-core/src/parser/parser.ts
diff --git a/packages/react-lang/src/parser/prompt.ts b/packages/lang-core/src/parser/prompt.ts
similarity index 100%
rename from packages/react-lang/src/parser/prompt.ts
rename to packages/lang-core/src/parser/prompt.ts
diff --git a/packages/react-lang/src/parser/types.ts b/packages/lang-core/src/parser/types.ts
similarity index 100%
rename from packages/react-lang/src/parser/types.ts
rename to packages/lang-core/src/parser/types.ts
diff --git a/packages/react-lang/src/utils/validation.ts b/packages/lang-core/src/utils/validation.ts
similarity index 100%
rename from packages/react-lang/src/utils/validation.ts
rename to packages/lang-core/src/utils/validation.ts
diff --git a/packages/lang-core/tsconfig.json b/packages/lang-core/tsconfig.json
new file mode 100644
index 000000000..00124d5b9
--- /dev/null
+++ b/packages/lang-core/tsconfig.json
@@ -0,0 +1,17 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "extends": "../../tsconfig.json",
+ "include": ["src/**/*"],
+ "exclude": ["src/**/__tests__/**", "src/**/*.test.ts"],
+ "compilerOptions": {
+ "moduleResolution": "bundler",
+ "module": "ESNext",
+ "outDir": "./dist",
+ "rootDir": "./src",
+ "declaration": true,
+ "noPropertyAccessFromIndexSignature": false,
+ "noUncheckedIndexedAccess": false,
+ "noImplicitReturns": false,
+ "noImplicitOverride": false
+ }
+}
diff --git a/packages/lang-core/tsconfig.test.json b/packages/lang-core/tsconfig.test.json
new file mode 100644
index 000000000..60b3001d5
--- /dev/null
+++ b/packages/lang-core/tsconfig.test.json
@@ -0,0 +1,9 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "noEmit": true
+ },
+ "include": ["src/**/*"],
+ "exclude": ["dist", "node_modules"]
+}
diff --git a/packages/react-lang/package.json b/packages/react-lang/package.json
index 08c1c5f5c..09904e67e 100644
--- a/packages/react-lang/package.json
+++ b/packages/react-lang/package.json
@@ -55,6 +55,7 @@
},
"author": "engineering@thesys.dev",
"dependencies": {
+ "@openuidev/lang-core": "workspace:^",
"zod": "^4.0.0"
},
"peerDependencies": {
diff --git a/packages/react-lang/src/Renderer.tsx b/packages/react-lang/src/Renderer.tsx
index ef5997e48..fdd8c6eb9 100644
--- a/packages/react-lang/src/Renderer.tsx
+++ b/packages/react-lang/src/Renderer.tsx
@@ -1,8 +1,8 @@
+import type { ActionEvent, ElementNode, ParseResult } from "@openuidev/lang-core";
import React, { Component, Fragment, useEffect } from "react";
import { OpenUIContext, useOpenUI, useRenderNode } from "./context";
import { useOpenUIState } from "./hooks/useOpenUIState";
import type { ComponentRenderer, Library } from "./library";
-import type { ActionEvent, ElementNode, ParseResult } from "./parser/types";
export interface RendererProps {
/** Raw response text (openui-lang code). */
diff --git a/packages/react-lang/src/hooks/useFormValidation.ts b/packages/react-lang/src/hooks/useFormValidation.ts
index dfaea6708..073448b36 100644
--- a/packages/react-lang/src/hooks/useFormValidation.ts
+++ b/packages/react-lang/src/hooks/useFormValidation.ts
@@ -1,5 +1,5 @@
+import { validate, type ParsedRule } from "@openuidev/lang-core";
import { createContext, useCallback, useContext, useMemo, useRef, useState } from "react";
-import { validate, type ParsedRule } from "../utils/validation";
export interface FormValidationContextValue {
errors: Record;
diff --git a/packages/react-lang/src/hooks/useOpenUIState.ts b/packages/react-lang/src/hooks/useOpenUIState.ts
index 59099fa9a..f10875088 100644
--- a/packages/react-lang/src/hooks/useOpenUIState.ts
+++ b/packages/react-lang/src/hooks/useOpenUIState.ts
@@ -1,10 +1,13 @@
+import {
+ BuiltinActionType,
+ createParser,
+ type ActionEvent,
+ type ParseResult,
+} from "@openuidev/lang-core";
import type React from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import type { OpenUIContextValue } from "../context";
import type { Library } from "../library";
-import { createParser } from "../parser/parser";
-import type { ActionEvent, ParseResult } from "../parser/types";
-import { BuiltinActionType } from "../parser/types";
export interface UseOpenUIStateOptions {
response: string | null;
diff --git a/packages/react-lang/src/index.ts b/packages/react-lang/src/index.ts
index 662d02475..9ef8bdce5 100644
--- a/packages/react-lang/src/index.ts
+++ b/packages/react-lang/src/index.ts
@@ -16,11 +16,11 @@ export { Renderer } from "./Renderer";
export type { RendererProps } from "./Renderer";
// openui-lang action types
-export { BuiltinActionType } from "./parser/types";
-export type { ActionEvent, ElementNode, ParseResult } from "./parser/types";
+export { BuiltinActionType } from "@openuidev/lang-core";
+export type { ActionEvent, ElementNode, ParseResult } from "@openuidev/lang-core";
// openui-lang parser (server-side use)
-export { createParser, createStreamingParser, type LibraryJSONSchema } from "./parser";
+export { createParser, createStreamingParser, type LibraryJSONSchema } from "@openuidev/lang-core";
// openui-lang context hooks (for use inside component renderers)
export {
@@ -42,5 +42,10 @@ export {
} from "./hooks/useFormValidation";
export type { FormValidationContextValue } from "./hooks/useFormValidation";
-export { builtInValidators, parseRules, parseStructuredRules, validate } from "./utils/validation";
-export type { ParsedRule, ValidatorFn } from "./utils/validation";
+export {
+ builtInValidators,
+ parseRules,
+ parseStructuredRules,
+ validate,
+} from "@openuidev/lang-core";
+export type { ParsedRule, ValidatorFn } from "@openuidev/lang-core";
diff --git a/packages/react-lang/src/library.ts b/packages/react-lang/src/library.ts
index 7c877adb7..e0cdc8109 100644
--- a/packages/react-lang/src/library.ts
+++ b/packages/react-lang/src/library.ts
@@ -1,42 +1,34 @@
+import {
+ createLibrary as coreCreateLibrary,
+ defineComponent as coreDefineComponent,
+ type DefinedComponent as CoreDefinedComponent,
+ type Library as CoreLibrary,
+ type LibraryDefinition as CoreLibraryDefinition,
+ type ComponentRenderProps as CoreRenderProps,
+} from "@openuidev/lang-core";
import type { ReactNode } from "react";
import { z } from "zod";
-import { generatePrompt } from "./parser/prompt";
-// ─── Sub-component type ──────────────────────────────────────────────────────
+// Re-export framework-agnostic types unchanged
+export type { ComponentGroup, PromptOptions, SubComponentOf } from "@openuidev/lang-core";
-/**
- * Runtime shape of a parsed sub-component element as seen by parent renderers.
- */
-export type SubComponentOf = {
- type: "element";
- typeName: string;
- props: P;
- partial: boolean;
-};
+// ─── React-specific types ───────────────────────────────────────────────────
-// ─── Renderer types ───────────────────────────────────────────────────────────
-
-export interface ComponentRenderProps
> {
- props: P;
- renderNode: (value: unknown) => ReactNode;
-}
+export interface ComponentRenderProps
>
+ extends CoreRenderProps
{}
export type ComponentRenderer
> = React.FC>;
-// ─── DefinedComponent ─────────────────────────────────────────────────────────
+export type DefinedComponent = z.ZodObject> = CoreDefinedComponent<
+ T,
+ ComponentRenderer>
+>;
-/**
- * A fully defined component with name, schema, description, renderer,
- * and a `.ref` for type-safe cross-referencing in parent schemas.
- */
-export interface DefinedComponent = z.ZodObject> {
- name: string;
- props: T;
- description: string;
- component: ComponentRenderer>;
- /** Use in parent schemas: `z.array(ChildComponent.ref)` */
- ref: z.ZodType>>;
-}
+export type Library = CoreLibrary>;
+
+export type LibraryDefinition = CoreLibraryDefinition>;
+
+// ─── defineComponent (React) ────────────────────────────────────────────────
/**
* Define a component with name, schema, description, and renderer.
@@ -67,56 +59,10 @@ export function defineComponent>(config: {
description: string;
component: ComponentRenderer>;
}): DefinedComponent {
- (config.props as any).register(z.globalRegistry, { id: config.name });
- return {
- ...config,
- ref: config.props as unknown as z.ZodType>>,
- };
+ return coreDefineComponent>>(config);
}
-// ─── Groups & Prompt ──────────────────────────────────────────────────────────
-
-export interface ComponentGroup {
- name: string;
- components: string[];
- notes?: string[];
-}
-
-export interface PromptOptions {
- preamble?: string;
- additionalRules?: string[];
- examples?: string[];
-}
-
-// ─── Library ──────────────────────────────────────────────────────────────────
-
-export interface Library {
- readonly components: Record;
- readonly componentGroups: ComponentGroup[] | undefined;
- readonly root: string | undefined;
-
- prompt(options?: PromptOptions): string;
- /**
- * Returns a single, valid JSON Schema document for the entire library.
- * All component schemas are in `$defs`, keyed by component name.
- * Sub-schemas shared across components (e.g. `Series`, `CardHeader`) are
- * emitted once and referenced via `$ref` — no repetition.
- *
- * @example
- * ```ts
- * const schema = library.toJSONSchema();
- * // schema.$defs["Card"] → { properties: {...}, required: [...] }
- * // schema.$defs["Series"] → { properties: {...}, required: [...] }
- * ```
- */
- toJSONSchema(): object;
-}
-
-export interface LibraryDefinition {
- components: DefinedComponent[];
- componentGroups?: ComponentGroup[];
- root?: string;
-}
+// ─── createLibrary (React) ──────────────────────────────────────────────────
/**
* Create a component library from an array of defined components.
@@ -130,40 +76,5 @@ export interface LibraryDefinition {
* ```
*/
export function createLibrary(input: LibraryDefinition): Library {
- const componentsRecord: Record = {};
- for (const comp of input.components) {
- if (!z.globalRegistry.has(comp.props)) {
- comp.props.register(z.globalRegistry, { id: comp.name });
- }
- componentsRecord[comp.name] = comp;
- }
-
- if (input.root && !componentsRecord[input.root]) {
- const available = Object.keys(componentsRecord).join(", ");
- throw new Error(
- `[createLibrary] Root component "${input.root}" was not found in components. Available components: ${available}`,
- );
- }
-
- const library: Library = {
- components: componentsRecord,
- componentGroups: input.componentGroups,
- root: input.root,
-
- prompt(options?: PromptOptions): string {
- return generatePrompt(library, options);
- },
-
- toJSONSchema(): object {
- // Build one combined z.object so z.toJSONSchema emits all component
- // schemas into a shared $defs block — sub-schemas like CardHeader or
- // Series are defined once and referenced via $ref everywhere else.
- const combinedSchema = z.object(
- Object.fromEntries(Object.entries(componentsRecord).map(([k, v]) => [k, v.props])) as any,
- );
- return z.toJSONSchema(combinedSchema);
- },
- };
-
- return library;
+ return coreCreateLibrary>(input) as Library;
}
diff --git a/packages/react-lang/src/utils/index.ts b/packages/react-lang/src/utils/index.ts
deleted file mode 100644
index 6495f6152..000000000
--- a/packages/react-lang/src/utils/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { builtInValidators, parseRules, parseStructuredRules, validate } from "./validation";
-export type { ValidatorFn } from "./validation";
diff --git a/packages/svelte-lang/README.md b/packages/svelte-lang/README.md
new file mode 100644
index 000000000..8fbee62e5
--- /dev/null
+++ b/packages/svelte-lang/README.md
@@ -0,0 +1,258 @@
+# @openuidev/svelte-lang
+
+Svelte 5 runtime for [OpenUI](https://openui.com) — define component libraries, generate model prompts, and render structured UI from streaming LLM output.
+
+[](https://github.com/thesysdev/openui/blob/main/LICENSE)
+
+## Install
+
+```bash
+npm install @openuidev/svelte-lang
+# or
+pnpm add @openuidev/svelte-lang
+```
+
+**Peer dependencies:** `svelte >=5.0.0`
+
+## Overview
+
+`@openuidev/svelte-lang` provides three core capabilities:
+
+1. **Define components** — Use `defineComponent` and `createLibrary` to declare what the model is allowed to generate, with typed props via Zod schemas.
+2. **Generate prompts** — Call `library.prompt()` to produce a system prompt that instructs the model how to emit OpenUI Lang output.
+3. **Render output** — Use `` to parse and progressively render streamed OpenUI Lang into Svelte components.
+
+## Quick Start
+
+### 1. Define a component
+
+```svelte
+
+
+
+
+ Hello, {props.name}!
+
+```
+
+```ts
+import { defineComponent } from "@openuidev/svelte-lang";
+import { z } from "zod";
+import Greeting from "./Greeting.svelte";
+
+const GreetingDef = defineComponent({
+ name: "Greeting",
+ description: "Displays a greeting message",
+ props: z.object({
+ name: z.string().describe("The person's name"),
+ mood: z.enum(["happy", "excited"]).optional().describe("Tone of the greeting"),
+ }),
+ component: Greeting,
+});
+```
+
+### 2. Create a library
+
+```ts
+import { createLibrary } from "@openuidev/svelte-lang";
+
+const library = createLibrary({
+ components: [GreetingDef, CardDef, TableDef /* ... */],
+ root: "Card", // optional default root component
+});
+```
+
+### 3. Generate a system prompt
+
+```ts
+const systemPrompt = library.prompt({
+ preamble: "You are a helpful assistant.",
+ additionalRules: ["Always greet the user by name."],
+ examples: ['User: Hi\n\nroot = Greeting("Alice", "happy")'],
+});
+```
+
+### 4. Render streamed output
+
+```svelte
+
+
+ console.log("Action:", event)}
+/>
+```
+
+## API Reference
+
+### Component Definition
+
+| Export | Description |
+| :-------------------------- | :----------------------------------------------------------------------------------------- |
+| `defineComponent(config)` | Define a single component with a name, Zod props schema, description, and Svelte component |
+| `createLibrary(definition)` | Create a library from an array of defined components |
+
+### Rendering
+
+| Export | Description |
+| :--------- | :---------------------------------------------------------- |
+| `Renderer` | Svelte component that parses and renders OpenUI Lang output |
+
+**`RendererProps`:**
+
+| Prop | Type | Description |
+| :-------------- | :-------------------------------------- | :---------------------------------------------------------------- |
+| `response` | `string \| null` | Raw OpenUI Lang text from the model |
+| `library` | `Library` | Component library from `createLibrary()` |
+| `isStreaming` | `boolean` | Whether the model is still streaming (disables form interactions) |
+| `onAction` | `(event: ActionEvent) => void` | Callback when a component triggers an action |
+| `onStateUpdate` | `(state: Record) => void` | Callback when form field values change |
+| `initialState` | `Record` | Initial form state for hydration |
+| `onParseResult` | `(result: ParseResult \| null) => void` | Callback when the parse result changes |
+
+### Children Rendering
+
+Svelte components receive `renderNode` as a **snippet prop** (not via context). Use it to render child element nodes:
+
+```svelte
+
+
+
+ {#if props.children}
+ {@render renderNode(props.children)}
+ {/if}
+
+```
+
+### Parser (Server-Side)
+
+| Export | Description |
+| :------------------------------ | :----------------------------------------------------- |
+| `createParser(schema)` | Create a one-shot parser for complete OpenUI Lang text |
+| `createStreamingParser(schema)` | Create an incremental parser for streaming input |
+
+The streaming parser exposes two methods:
+
+| Method | Description |
+| :------------ | :---------------------------------------------------- |
+| `push(chunk)` | Feed the next chunk; returns the latest `ParseResult` |
+| `getResult()` | Get the latest result without consuming new data |
+
+After the stream ends, check `meta.unresolved` for any identifiers that were referenced but never defined. During streaming these are expected (forward refs) and are not treated as errors.
+
+#### Errors
+
+`ParseResult.meta.errors` contains structured `OpenUIError` objects. Each error has a `type` discriminant (currently always `"validation"`) and a `code` for consumer-side filtering:
+
+| Code | Meaning |
+| :------------------ | :-------------------------------------------------- |
+| `missing-required` | Required prop absent with no default |
+| `null-required` | Required prop explicitly null with no default |
+| `unknown-component` | Component name not found in the library schema |
+| `excess-args` | More positional args passed than the schema defines |
+
+Errors do not affect rendering — the parser stays permissive and renders what it can:
+
+```ts
+const result = parser.parse(output);
+const critical = result.meta.errors.filter((e) => e.code === "unknown-component");
+```
+
+### Context Getters
+
+Use these inside component renderers to interact with the rendering context:
+
+| Function | Returns | Description |
+| :------------------------- | :-------------------- | :----------------------------------------------------------------------------- |
+| `getOpenUIContext()` | `OpenUIContextValue` | Access the full context object (library, streaming state, field accessors) |
+| `getIsStreaming()` | `() => boolean` | Returns a getter for the streaming state — call it reactively: `isStreaming()` |
+| `getTriggerAction()` | `Function` | Trigger an action event |
+| `getGetFieldValue()` | `Function` | Get a form field's current value |
+| `getSetFieldValue()` | `Function` | Set a form field's value |
+| `getFormName()` | `string \| undefined` | Get the current form's name |
+| `useSetDefaultValue(opts)` | `void` | Set a field's default value once streaming completes |
+
+### Form Validation
+
+| Export | Description |
+| :--------------------------- | :---------------------------------------------------- |
+| `getFormValidation()` | Access form validation state |
+| `createFormValidation()` | Create a form validation context |
+| `setFormValidationContext()` | Provide validation context to child components |
+| `validate(value, rules)` | Run validation rules against a value |
+| `builtInValidators` | Built-in validators (required, email, min, max, etc.) |
+| `parseRules(rules)` | Parse a rules config object into `ParsedRule[]` |
+
+### Types
+
+```ts
+import type {
+ // Component definition
+ Library,
+ LibraryDefinition,
+ DefinedComponent,
+ ComponentRenderer,
+ ComponentRenderProps,
+ ComponentGroup,
+ SubComponentOf,
+ PromptOptions,
+
+ // Rendering
+ RendererProps,
+ OpenUIContextValue,
+ ActionConfig,
+
+ // Parser & core
+ ActionEvent,
+ ElementNode,
+ ParseResult,
+ LibraryJSONSchema,
+
+ // Validation
+ FormValidationContextValue,
+ ParsedRule,
+ ValidatorFn,
+} from "@openuidev/svelte-lang";
+```
+
+## JSON Schema Output
+
+Libraries can also produce a JSON Schema representation of their components:
+
+```ts
+const schema = library.toJSONSchema();
+// schema.$defs["Card"] → { properties: {...}, required: [...] }
+// schema.$defs["Greeting"] → { properties: {...}, required: [...] }
+```
+
+## Differences from React
+
+| Concern | `react-lang` | `svelte-lang` |
+| :----------------- | :----------------------------------------------- | :----------------------------------------------- |
+| Children rendering | `renderNode` function prop returning `ReactNode` | `renderNode` **snippet** (`Snippet<[unknown]>`) |
+| Context access | Hooks (`useIsStreaming()`, etc.) | Getters (`getIsStreaming()`, etc.) |
+| Error boundaries | Class-based, preserves last valid render | `svelte:boundary` with auto-retry on prop change |
+| Reactivity | Hooks re-run on every render | Runes (`$state`, `$derived`, `$effect`) |
+
+## Documentation
+
+Full documentation, guides, and the language specification are available at **[openui.com](https://openui.com)**.
+
+## License
+
+[MIT](https://github.com/thesysdev/openui/blob/main/LICENSE)
diff --git a/packages/svelte-lang/package.json b/packages/svelte-lang/package.json
new file mode 100644
index 000000000..12ec22803
--- /dev/null
+++ b/packages/svelte-lang/package.json
@@ -0,0 +1,78 @@
+{
+ "name": "@openuidev/svelte-lang",
+ "version": "0.1.0",
+ "description": "Define component libraries, generate LLM system prompts, and render streaming OpenUI Lang output in Svelte 5 — the Svelte runtime for OpenUI generative UI",
+ "license": "MIT",
+ "type": "module",
+ "main": "dist/index.js",
+ "types": "dist/index.d.ts",
+ "svelte": "./dist/index.js",
+ "files": [
+ "dist",
+ "README.md"
+ ],
+ "exports": {
+ ".": {
+ "svelte": "./dist/index.js",
+ "import": "./dist/index.js",
+ "types": "./dist/index.d.ts"
+ }
+ },
+ "scripts": {
+ "build": "svelte-package",
+ "watch": "svelte-package --watch",
+ "lint:check": "eslint ./src",
+ "lint:fix": "eslint ./src --fix",
+ "format:fix": "prettier --write ./src",
+ "format:check": "prettier --check ./src",
+ "prepare": "pnpm run build",
+ "check": "svelte-check --tsconfig ./tsconfig.json",
+ "test": "vitest run",
+ "ci": "pnpm run lint:check && pnpm run format:check"
+ },
+ "keywords": [
+ "openui",
+ "generative-ui",
+ "svelte",
+ "svelte5",
+ "llm",
+ "streaming",
+ "renderer",
+ "parser",
+ "ai",
+ "components",
+ "prompt-generation",
+ "zod",
+ "ui-generation",
+ "model-driven-ui",
+ "openui-lang"
+ ],
+ "homepage": "https://openui.com",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/thesysdev/openui.git",
+ "directory": "packages/svelte-lang"
+ },
+ "bugs": {
+ "url": "https://github.com/thesysdev/openui/issues"
+ },
+ "author": "engineering@thesys.dev",
+ "dependencies": {
+ "@openuidev/lang-core": "workspace:^",
+ "zod": "^4.0.0"
+ },
+ "peerDependencies": {
+ "svelte": ">=5.0.0"
+ },
+ "devDependencies": {
+ "@sveltejs/package": "^2.3.0",
+ "@sveltejs/vite-plugin-svelte": "^5.0.0",
+ "@testing-library/svelte": "^5.2.0",
+ "jsdom": "^26.1.0",
+ "svelte": "^5.0.0",
+ "svelte-check": "^4.0.0",
+ "typescript": "^5.0.0",
+ "vite": "^6.0.0",
+ "vitest": "^3.0.0"
+ }
+}
diff --git a/packages/svelte-lang/src/__tests__/Renderer.test.ts b/packages/svelte-lang/src/__tests__/Renderer.test.ts
new file mode 100644
index 000000000..846eb9d82
--- /dev/null
+++ b/packages/svelte-lang/src/__tests__/Renderer.test.ts
@@ -0,0 +1,127 @@
+import { render } from "@testing-library/svelte";
+import { tick } from "svelte";
+import { describe, expect, it, vi } from "vitest";
+import { z } from "zod";
+import Renderer from "../lib/Renderer.svelte";
+import { createLibrary, defineComponent } from "../lib/library.js";
+
+// Dummy renderer — never actually renders DOM, used for parser/callback tests
+const DummyComponent = (() => null) as any;
+
+const TextContent = defineComponent({
+ name: "TextContent",
+ props: z.object({ text: z.string() }),
+ description: "Displays text content",
+ component: DummyComponent,
+});
+
+const library = createLibrary({
+ components: [TextContent],
+ root: "TextContent",
+});
+
+// openui-lang uses assignment syntax: `identifier = Component(args)`
+const VALID_RESPONSE = 'root = TextContent("Hello world")';
+
+// ─── Renderer ───────────────────────────────────────────────────────────────
+
+describe("Renderer", () => {
+ it("renders without errors when response is null", () => {
+ const { container } = render(Renderer, {
+ props: {
+ response: null,
+ library,
+ },
+ });
+
+ // Should render an empty container (no crash)
+ expect(container).toBeDefined();
+ });
+
+ it("renders without errors when response is empty string", () => {
+ const { container } = render(Renderer, {
+ props: {
+ response: "",
+ library,
+ },
+ });
+
+ expect(container).toBeDefined();
+ });
+
+ it("calls onParseResult with null when response is null", async () => {
+ const onParseResult = vi.fn();
+
+ render(Renderer, {
+ props: {
+ response: null,
+ library,
+ onParseResult,
+ },
+ });
+
+ // $effect runs asynchronously — flush microtasks
+ await tick();
+
+ expect(onParseResult).toHaveBeenCalledWith(null);
+ });
+
+ it("calls onParseResult with a ParseResult when given valid openui-lang", async () => {
+ const onParseResult = vi.fn();
+
+ render(Renderer, {
+ props: {
+ response: VALID_RESPONSE,
+ library,
+ onParseResult,
+ },
+ });
+
+ await tick();
+
+ expect(onParseResult).toHaveBeenCalled();
+ const result = onParseResult.mock.calls[onParseResult.mock.calls.length - 1]![0];
+ expect(result).not.toBeNull();
+ expect(result.root).toBeDefined();
+ expect(result.root).not.toBeNull();
+ });
+
+ it("parse result contains the correct component typeName", async () => {
+ const onParseResult = vi.fn();
+
+ render(Renderer, {
+ props: {
+ response: VALID_RESPONSE,
+ library,
+ onParseResult,
+ },
+ });
+
+ await tick();
+
+ const result = onParseResult.mock.calls[onParseResult.mock.calls.length - 1]![0];
+ expect(result?.root?.typeName).toBe("TextContent");
+ });
+
+ it("defaults isStreaming to false", () => {
+ // Should not throw when isStreaming is omitted
+ const { container } = render(Renderer, {
+ props: {
+ response: null,
+ library,
+ },
+ });
+ expect(container).toBeDefined();
+ });
+
+ it("accepts isStreaming prop without errors", () => {
+ const { container } = render(Renderer, {
+ props: {
+ response: null,
+ library,
+ isStreaming: true,
+ },
+ });
+ expect(container).toBeDefined();
+ });
+});
diff --git a/packages/svelte-lang/src/__tests__/library.test.ts b/packages/svelte-lang/src/__tests__/library.test.ts
new file mode 100644
index 000000000..085abaf61
--- /dev/null
+++ b/packages/svelte-lang/src/__tests__/library.test.ts
@@ -0,0 +1,127 @@
+import { describe, expect, it } from "vitest";
+import { z } from "zod";
+import { createLibrary, defineComponent } from "../lib/library.js";
+
+// Dummy renderer — never actually called in these tests
+const DummyComponent = (() => null) as any;
+
+function makeComponent(name: string, schema: z.ZodObject, description: string) {
+ return defineComponent({
+ name,
+ props: schema,
+ description,
+ component: DummyComponent,
+ });
+}
+
+// ─── defineComponent ────────────────────────────────────────────────────────
+
+describe("defineComponent", () => {
+ it("returns an object with name, props, description, component, and ref", () => {
+ const schema = z.object({ label: z.string() });
+ const result = defineComponent({
+ name: "Badge",
+ props: schema,
+ description: "A simple badge",
+ component: DummyComponent,
+ });
+
+ expect(result.name).toBe("Badge");
+ expect(result.props).toBe(schema);
+ expect(result.description).toBe("A simple badge");
+ expect(result.component).toBe(DummyComponent);
+ expect(result.ref).toBeDefined();
+ });
+
+ it("registers the Zod schema in the global registry", () => {
+ const schema = z.object({ title: z.string() });
+ const comp = defineComponent({
+ name: "Heading",
+ props: schema,
+ description: "A heading element",
+ component: DummyComponent,
+ });
+
+ // After defineComponent, the schema should be in the global registry
+ expect(z.globalRegistry.has(comp.props)).toBe(true);
+ });
+});
+
+// ─── createLibrary ──────────────────────────────────────────────────────────
+
+describe("createLibrary", () => {
+ const TextContent = makeComponent(
+ "TextContent",
+ z.object({ text: z.string() }),
+ "Displays text content",
+ );
+
+ const Container = makeComponent(
+ "Container",
+ z.object({ title: z.string() }),
+ "A container with a title",
+ );
+
+ it("creates a library with a components record", () => {
+ const lib = createLibrary({ components: [TextContent, Container] });
+
+ expect(lib.components.TextContent).toBe(TextContent);
+ expect(lib.components.Container).toBe(Container);
+ expect(Object.keys(lib.components)).toHaveLength(2);
+ });
+
+ it("stores root and componentGroups", () => {
+ const lib = createLibrary({
+ components: [TextContent],
+ root: "TextContent",
+ componentGroups: [{ name: "Display", components: ["TextContent"] }],
+ });
+
+ expect(lib.root).toBe("TextContent");
+ expect(lib.componentGroups).toEqual([{ name: "Display", components: ["TextContent"] }]);
+ });
+
+ it("throws if root component is not found in components", () => {
+ expect(() =>
+ createLibrary({
+ components: [TextContent],
+ root: "NonExistent",
+ }),
+ ).toThrow(/Root component "NonExistent" was not found/);
+ });
+
+ it("prompt() returns a string containing component descriptions", () => {
+ const lib = createLibrary({
+ components: [TextContent, Container],
+ root: "TextContent",
+ });
+
+ const prompt = lib.prompt();
+ expect(typeof prompt).toBe("string");
+ expect(prompt.length).toBeGreaterThan(0);
+ // The prompt should mention at least one component name
+ expect(prompt).toContain("TextContent");
+ });
+
+ it("toJSONSchema() returns an object with $defs", () => {
+ const lib = createLibrary({
+ components: [TextContent],
+ root: "TextContent",
+ });
+
+ const schema = lib.toJSONSchema() as Record;
+ expect(schema).toBeDefined();
+ expect(typeof schema).toBe("object");
+ expect(schema["$defs"]).toBeDefined();
+ expect(typeof schema["$defs"]).toBe("object");
+ });
+
+ it("works without a root component", () => {
+ const lib = createLibrary({ components: [TextContent] });
+
+ expect(lib.root).toBeUndefined();
+ // prompt/schema should still work
+ expect(typeof lib.prompt()).toBe("string");
+ expect(lib.toJSONSchema()).toBeDefined();
+ });
+});
diff --git a/packages/svelte-lang/src/__tests__/validation.test.ts b/packages/svelte-lang/src/__tests__/validation.test.ts
new file mode 100644
index 000000000..4b319dac0
--- /dev/null
+++ b/packages/svelte-lang/src/__tests__/validation.test.ts
@@ -0,0 +1,127 @@
+import { describe, expect, it } from "vitest";
+import {
+ builtInValidators,
+ parseRules,
+ parseStructuredRules,
+ validate,
+} from "../lib/validation.svelte.js";
+
+// ─── builtInValidators ──────────────────────────────────────────────────────
+
+describe("builtInValidators", () => {
+ it("has all expected validators", () => {
+ const expected = [
+ "required",
+ "email",
+ "url",
+ "numeric",
+ "min",
+ "max",
+ "minLength",
+ "maxLength",
+ "pattern",
+ ];
+ for (const name of expected) {
+ expect(builtInValidators[name]).toBeDefined();
+ expect(typeof builtInValidators[name]).toBe("function");
+ }
+ });
+});
+
+// ─── parseRules ─────────────────────────────────────────────────────────────
+
+describe("parseRules", () => {
+ it("parses simple rule strings into ParsedRule objects", () => {
+ const result = parseRules(["required", "email"]);
+ expect(result).toEqual([{ type: "required" }, { type: "email" }]);
+ });
+
+ it("parses rules with numeric arguments", () => {
+ const result = parseRules(["min:8", "maxLength:100"]);
+ expect(result).toEqual([
+ { type: "min", arg: 8 },
+ { type: "maxLength", arg: 100 },
+ ]);
+ });
+
+ it("parses rules with string arguments", () => {
+ const result = parseRules(["pattern:^[a-z]+"]);
+ expect(result).toEqual([{ type: "pattern", arg: "^[a-z]+" }]);
+ });
+
+ it("returns an empty array for non-array input", () => {
+ expect(parseRules(null)).toEqual([]);
+ expect(parseRules(undefined)).toEqual([]);
+ expect(parseRules("required")).toEqual([]);
+ });
+
+ it("filters out non-string entries", () => {
+ const result = parseRules(["required", 42, null, "email"]);
+ expect(result).toEqual([{ type: "required" }, { type: "email" }]);
+ });
+});
+
+// ─── validate ───────────────────────────────────────────────────────────────
+
+describe("validate", () => {
+ it("returns error string for required on empty value", () => {
+ const rules = [{ type: "required" }];
+ const error = validate("", rules);
+ expect(error).toBe("This field is required");
+ });
+
+ it("returns undefined when valid value passes required", () => {
+ const rules = [{ type: "required" }];
+ expect(validate("hello", rules)).toBeUndefined();
+ });
+
+ it("validates email format", () => {
+ const rules = [{ type: "email" }];
+ expect(validate("bad-email", rules)).toBe("Please enter a valid email");
+ expect(validate("test@email.com", rules)).toBeUndefined();
+ });
+
+ it("validates min/max with numeric arguments", () => {
+ expect(validate(3, [{ type: "min", arg: 5 }])).toBe("Must be at least 5");
+ expect(validate(10, [{ type: "min", arg: 5 }])).toBeUndefined();
+ expect(validate(20, [{ type: "max", arg: 10 }])).toBe("Must be no more than 10");
+ expect(validate(5, [{ type: "max", arg: 10 }])).toBeUndefined();
+ });
+
+ it("stops on first error with multiple rules", () => {
+ const rules = [{ type: "required" }, { type: "email" }];
+ // Empty string triggers "required" first, not "email"
+ expect(validate("", rules)).toBe("This field is required");
+ });
+
+ it("returns undefined when no rules match", () => {
+ expect(validate("anything", [{ type: "nonExistentRule" }])).toBeUndefined();
+ });
+});
+
+// ─── parseStructuredRules ───────────────────────────────────────────────────
+
+describe("parseStructuredRules", () => {
+ it("parses an object of rules into ParsedRule array", () => {
+ const result = parseStructuredRules({ required: true, minLength: 5 });
+ expect(result).toContainEqual({ type: "required" });
+ expect(result).toContainEqual({ type: "minLength", arg: 5 });
+ });
+
+ it("skips false/undefined/null values", () => {
+ const result = parseStructuredRules({
+ required: true,
+ email: false,
+ max: undefined,
+ min: null,
+ });
+ expect(result).toEqual([{ type: "required" }]);
+ });
+
+ it("returns empty array for non-object input", () => {
+ expect(parseStructuredRules(null)).toEqual([]);
+ expect(parseStructuredRules(undefined)).toEqual([]);
+ expect(parseStructuredRules([])).toEqual([]);
+ expect(parseStructuredRules("string")).toEqual([]);
+ });
+});
diff --git a/packages/svelte-lang/src/lib/RenderNode.svelte b/packages/svelte-lang/src/lib/RenderNode.svelte
new file mode 100644
index 000000000..5517c2026
--- /dev/null
+++ b/packages/svelte-lang/src/lib/RenderNode.svelte
@@ -0,0 +1,66 @@
+
+
+{#if node && Comp}
+
+
+
+ {#snippet failed()}{/snippet}
+
+{/if}
diff --git a/packages/svelte-lang/src/lib/Renderer.svelte b/packages/svelte-lang/src/lib/Renderer.svelte
new file mode 100644
index 000000000..32275a7d0
--- /dev/null
+++ b/packages/svelte-lang/src/lib/Renderer.svelte
@@ -0,0 +1,162 @@
+
+
+{#snippet renderNode(value: unknown)}
+ {#if value == null}
+
+ {:else if typeof value === "string"}
+ {value}
+ {:else if typeof value === "number" || typeof value === "boolean"}
+ {String(value)}
+ {:else if Array.isArray(value)}
+ {#each value as item}
+ {@render renderNode(item)}
+ {/each}
+ {:else if typeof value === "object" && (value as any).type === "element"}
+
+ {/if}
+{/snippet}
+
+{#if result?.root}
+
+{/if}
diff --git a/packages/svelte-lang/src/lib/context.svelte.ts b/packages/svelte-lang/src/lib/context.svelte.ts
new file mode 100644
index 000000000..858b7a4f9
--- /dev/null
+++ b/packages/svelte-lang/src/lib/context.svelte.ts
@@ -0,0 +1,153 @@
+import { getContext, setContext } from "svelte";
+import type { Library } from "./library.js";
+
+// ─── Action config ───
+
+export interface ActionConfig {
+ type?: string;
+ params?: Record;
+}
+
+// ─── OpenUI context ───
+
+/**
+ * Shared context provided by to all rendered components.
+ *
+ * Note: `renderNode` is passed as a snippet prop, NOT via context.
+ * This avoids the stale-closure problem and matches Svelte's snippet model.
+ */
+export interface OpenUIContextValue {
+ /** The active component library (schema + renderers). */
+ library: Library;
+
+ /**
+ * Trigger an action. Components call this to fire structured ActionEvents.
+ *
+ * @param userMessage Human-readable label ("Submit Application")
+ * @param formName Optional form name — if provided, form state for this form is included
+ * @param action Optional custom action config { type, params }
+ */
+ triggerAction: (userMessage: string, formName?: string, action?: ActionConfig) => void;
+
+ /** Whether the LLM is currently streaming content. */
+ isStreaming: boolean;
+
+ /** Get a form field value. Returns undefined if not set. */
+ getFieldValue: (formName: string | undefined, name: string) => any;
+
+ /**
+ * Set a form field value.
+ *
+ * @param formName The form's name prop
+ * @param componentType The component type (e.g. "Input", "Select", "RadioGroup")
+ * @param name The field's name prop
+ * @param value The new value
+ * @param shouldTriggerSaveCallback When true, persists the updated state via updateMessage.
+ * Text inputs should pass `false` on change and `true` on blur.
+ * Discrete inputs (Select, RadioGroup, etc.) should always pass `true`.
+ */
+ setFieldValue: (
+ formName: string | undefined,
+ componentType: string | undefined,
+ name: string,
+ value: any,
+ shouldTriggerSaveCallback?: boolean,
+ ) => void;
+}
+
+const OPENUI_CONTEXT_KEY = Symbol("openui-context");
+const FORM_NAME_CONTEXT_KEY = Symbol("openui-form-name");
+
+// ─── Context setters ───
+
+export function setOpenUIContext(value: OpenUIContextValue): void {
+ setContext(OPENUI_CONTEXT_KEY, value);
+}
+
+export function setFormNameContext(formName: string | undefined): void {
+ setContext(FORM_NAME_CONTEXT_KEY, formName);
+}
+
+// ─── Context getters ───
+
+/**
+ * Access the full OpenUI context. Throws if used outside a .
+ */
+export function getOpenUIContext(): OpenUIContextValue {
+ const ctx = getContext(OPENUI_CONTEXT_KEY);
+ if (!ctx) {
+ throw new Error("getOpenUIContext must be used within a component.");
+ }
+ return ctx;
+}
+
+/**
+ * Get the triggerAction function for firing structured action events.
+ */
+export function getTriggerAction() {
+ return getOpenUIContext().triggerAction;
+}
+
+/**
+ * Returns a getter for the streaming state.
+ * Use as: `const isStreaming = getIsStreaming(); ... disabled={isStreaming()}`
+ */
+export function getIsStreaming(): () => boolean {
+ const ctx = getOpenUIContext();
+ return () => ctx.isStreaming;
+}
+
+/**
+ * Get a form field value from the form state context.
+ */
+export function getGetFieldValue() {
+ return getOpenUIContext().getFieldValue;
+}
+
+/**
+ * Get the setFieldValue function for updating form field values.
+ */
+export function getSetFieldValue() {
+ return getOpenUIContext().setFieldValue;
+}
+
+/**
+ * Get the current form name (set by the nearest parent Form component).
+ * Returns undefined if not inside a Form.
+ */
+export function getFormName(): string | undefined {
+ return getContext(FORM_NAME_CONTEXT_KEY);
+}
+
+// ─── Default value helper ───
+
+/**
+ * Persists a component's default/initial value into form state once streaming
+ * finishes — but only if the user hasn't already set a value.
+ *
+ * Reads the current field value directly from form state inside the effect
+ * so that Svelte tracks it as a reactive dependency (unlike a snapshot
+ * parameter that would be captured once at call time).
+ */
+export function useSetDefaultValue({
+ formName,
+ componentType,
+ name,
+ defaultValue,
+ shouldTriggerSaveCallback = false,
+}: {
+ formName?: string;
+ componentType: string;
+ name: string;
+ defaultValue: any;
+ shouldTriggerSaveCallback?: boolean;
+}): void {
+ const ctx = getOpenUIContext();
+
+ $effect(() => {
+ const existing = ctx.getFieldValue(formName, name);
+ if (!ctx.isStreaming && existing === undefined && defaultValue !== undefined) {
+ ctx.setFieldValue(formName, componentType, name, defaultValue, shouldTriggerSaveCallback);
+ }
+ });
+}
diff --git a/packages/svelte-lang/src/lib/index.ts b/packages/svelte-lang/src/lib/index.ts
new file mode 100644
index 000000000..c413e6a91
--- /dev/null
+++ b/packages/svelte-lang/src/lib/index.ts
@@ -0,0 +1,70 @@
+// ─── Component definition ───
+
+export { createLibrary, defineComponent } from "./library.js";
+export type {
+ ComponentGroup,
+ ComponentRenderer,
+ ComponentRenderProps,
+ DefinedComponent,
+ Library,
+ LibraryDefinition,
+ PromptOptions,
+ SubComponentOf,
+} from "./library.js";
+
+// ─── Renderer ───
+
+import type { ActionEvent, ParseResult } from "@openuidev/lang-core";
+import type { Library } from "./library.js";
+
+export { default as Renderer } from "./Renderer.svelte";
+
+/** Props accepted by the Renderer component. */
+export interface RendererProps {
+ response: string | null;
+ library: Library;
+ isStreaming?: boolean;
+ onAction?: (event: ActionEvent) => void;
+ onStateUpdate?: (state: Record) => void;
+ initialState?: Record;
+ onParseResult?: (result: ParseResult | null) => void;
+}
+
+// ─── Context (for use inside component renderers) ───
+
+export {
+ getFormName,
+ getGetFieldValue,
+ getIsStreaming,
+ getOpenUIContext,
+ getSetFieldValue,
+ getTriggerAction,
+ setFormNameContext,
+ setOpenUIContext,
+ useSetDefaultValue,
+} from "./context.svelte.js";
+export type { ActionConfig, OpenUIContextValue } from "./context.svelte.js";
+
+// ─── Form validation ───
+
+export {
+ createFormValidation,
+ getFormValidation,
+ setFormValidationContext,
+} from "./validation.svelte.js";
+export type { FormValidationContextValue } from "./validation.svelte.js";
+
+export {
+ builtInValidators,
+ parseRules,
+ parseStructuredRules,
+ validate,
+} from "./validation.svelte.js";
+export type { ParsedRule, ValidatorFn } from "./validation.svelte.js";
+
+// ─── Re-exports from lang-core (parser, types) ───
+
+export { BuiltinActionType } from "@openuidev/lang-core";
+export type { ActionEvent, ElementNode, ParseResult } from "@openuidev/lang-core";
+
+export { createParser, createStreamingParser, type LibraryJSONSchema } from "@openuidev/lang-core";
diff --git a/packages/svelte-lang/src/lib/library.ts b/packages/svelte-lang/src/lib/library.ts
new file mode 100644
index 000000000..93c7f4e31
--- /dev/null
+++ b/packages/svelte-lang/src/lib/library.ts
@@ -0,0 +1,72 @@
+import {
+ createLibrary as coreCreateLibrary,
+ defineComponent as coreDefineComponent,
+ type DefinedComponent as CoreDefinedComponent,
+ type Library as CoreLibrary,
+ type LibraryDefinition as CoreLibraryDefinition,
+} from "@openuidev/lang-core";
+import type { Component, Snippet } from "svelte";
+import { z } from "zod";
+
+// Re-export framework-agnostic types unchanged
+export type { ComponentGroup, PromptOptions, SubComponentOf } from "@openuidev/lang-core";
+
+// ─── Svelte-specific types ──────────────────────────────────────────────────
+
+export interface ComponentRenderProps> {
+ props: P;
+ renderNode: Snippet<[unknown]>;
+}
+
+export type ComponentRenderer
> = Component>;
+
+export type DefinedComponent = z.ZodObject> = CoreDefinedComponent<
+ T,
+ ComponentRenderer>
+>;
+
+export type Library = CoreLibrary>;
+
+export type LibraryDefinition = CoreLibraryDefinition>;
+
+// ─── defineComponent (Svelte) ───────────────────────────────────────────────
+
+/**
+ * Define a component with name, schema, description, and renderer.
+ * Registers the Zod schema globally and returns a `.ref` for parent schemas.
+ *
+ * @example
+ * ```ts
+ * const TabItem = defineComponent({
+ * name: "TabItem",
+ * props: z.object({ value: z.string(), trigger: z.string(), content: z.array(ContentChildUnion) }),
+ * description: "Tab panel",
+ * component: TabItemRenderer,
+ * });
+ * ```
+ */
+export function defineComponent>(config: {
+ name: string;
+ props: T;
+ description: string;
+ component: ComponentRenderer>;
+}): DefinedComponent {
+ return coreDefineComponent>>(config);
+}
+
+// ─── createLibrary (Svelte) ─────────────────────────────────────────────────
+
+/**
+ * Create a component library from an array of defined components.
+ *
+ * @example
+ * ```ts
+ * const library = createLibrary({
+ * components: [TabItem, Tabs, Card],
+ * root: "Card",
+ * });
+ * ```
+ */
+export function createLibrary(input: LibraryDefinition): Library {
+ return coreCreateLibrary>(input) as Library;
+}
diff --git a/packages/svelte-lang/src/lib/validation.svelte.ts b/packages/svelte-lang/src/lib/validation.svelte.ts
new file mode 100644
index 000000000..a05153ddc
--- /dev/null
+++ b/packages/svelte-lang/src/lib/validation.svelte.ts
@@ -0,0 +1,101 @@
+import {
+ builtInValidators,
+ parseRules,
+ parseStructuredRules,
+ validate,
+ type ParsedRule,
+ type ValidatorFn,
+} from "@openuidev/lang-core";
+import { getContext, setContext } from "svelte";
+
+// ─── Re-exports from lang-core ───
+
+export { builtInValidators, parseRules, parseStructuredRules, validate };
+export type { ParsedRule, ValidatorFn };
+
+// ─── Form validation context ───
+
+export interface FormValidationContextValue {
+ errors: Record;
+ validateField: (name: string, value: unknown, rules: ParsedRule[]) => boolean;
+ registerField: (name: string, rules: ParsedRule[], getValue: () => unknown) => void;
+ unregisterField: (name: string) => void;
+ validateForm: () => boolean;
+ clearFieldError: (name: string) => void;
+}
+
+const FORM_VALIDATION_CONTEXT_KEY = Symbol("openui-form-validation");
+
+/**
+ * Create a form validation instance backed by Svelte 5 $state.
+ *
+ * Call this in the Form component's `