diff --git a/examples/05-browser/README.md b/examples/05-browser/README.md new file mode 100644 index 0000000..21d08af --- /dev/null +++ b/examples/05-browser/README.md @@ -0,0 +1,31 @@ +# Browser examples + +A Vite + vanilla TypeScript app whose pages each run the agent loop entirely in the browser. +Start the dev server from the repository root (after `pnpm install` and `pnpm -r build`): + +```bash +pnpm --filter example-05-browser dev +``` + +Open the printed URL, paste an API key into the page, and try the pages — they link to each other: + +| Page | What it demonstrates | +| --- | --- | +| [`/`](index.html) — Chat | Streaming into the DOM, client-side tools (`set_theme`, `get_local_time`), and session persistence in `localStorage`: the session is plain JSON, so persisting it is `JSON.stringify(session)` and resuming after a reload is `agent.deserializeSession(...)` | +| [`/canvas.html`](canvas.html) — Canvas | An agent that paints: every drawing primitive is a `tool()` executing against a 2D canvas context, so one instruction becomes a series of tool calls rendered as they run | +| [`/structured.html`](structured.html) — Structured output | A schema passed as `responseFormat`; the typed, validated `response.value` fills the page's fields directly | +| [`/anthropic.html`](anthropic.html) — Anthropic | The chat page with `AnthropicChatClient` swapped in — the agent, tools and streaming code carry over unchanged, which is the point (it skips the chat page's persistence to stay small) | + +Suggested first prompts: + +> Switch the page to dark mode, then tell me my time zone. + +> Draw a snowman on a blue background. + +## A note on API keys + +The key you paste stays in the tab's memory; the pages never store it. But any key that reaches a +browser is readable by whoever uses that browser, which is why both the OpenAI and Anthropic SDKs +require the explicit `dangerouslyAllowBrowser` opt-in these pages set. For anything beyond a local +demo, run agents server-side and keep provider credentials there — or point the **Base URL** field +at a proxy that holds the real key. diff --git a/examples/05-browser/anthropic.html b/examples/05-browser/anthropic.html new file mode 100644 index 0000000..71dd750 --- /dev/null +++ b/examples/05-browser/anthropic.html @@ -0,0 +1,53 @@ + + + + + + Agent Framework — Anthropic example + + + +
+
+ +

Same page, different provider

+

+ This page is the chat example with AnthropicChatClient swapped in — the + agent, tools and streaming code are unchanged. Keys pasted here stay in memory — in + production, run agents server-side. +

+
+ + + +
+
+
+
+ + +
+
+ + + diff --git a/examples/05-browser/canvas.html b/examples/05-browser/canvas.html new file mode 100644 index 0000000..cf24754 --- /dev/null +++ b/examples/05-browser/canvas.html @@ -0,0 +1,53 @@ + + + + + + Agent Framework — canvas example + + + +
+
+ +

An agent that paints

+

+ Each drawing primitive is a client-side tool, so the function-calling loop renders on the + canvas as it runs. Keys pasted here stay in memory — in production, run agents server-side. +

+
+ + + +
+
+ +
+
+ + +
+
+ + + diff --git a/examples/05-browser/index.html b/examples/05-browser/index.html new file mode 100644 index 0000000..7c0e674 --- /dev/null +++ b/examples/05-browser/index.html @@ -0,0 +1,54 @@ + + + + + + Agent Framework — browser example + + + +
+
+ +

Agent in the browser

+

+ The whole agent loop runs in this page. The key below stays in memory, but anything shipped to + a browser is readable by its user — in production, run agents server-side and keep provider + credentials there. +

+
+ + + +
+
+
+
+ + + +
+
+ + + diff --git a/examples/05-browser/package.json b/examples/05-browser/package.json new file mode 100644 index 0000000..b93a879 --- /dev/null +++ b/examples/05-browser/package.json @@ -0,0 +1,26 @@ +{ + "name": "example-05-browser", + "version": "0.0.0", + "private": true, + "type": "module", + "engines": { + "node": ">=24" + }, + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "typecheck": "tsc -p tsconfig.json --noEmit" + }, + "dependencies": { + "@anthropic-ai/sdk": "^0.120.0", + "@polymind-inc/agent-framework": "workspace:^", + "openai": "catalog:", + "zod": "^4.4.3" + }, + "devDependencies": { + "@types/node": "catalog:", + "typescript": "catalog:", + "vite": "^8.2.2" + } +} diff --git a/examples/05-browser/src/anthropic.ts b/examples/05-browser/src/anthropic.ts new file mode 100644 index 0000000..3980c0e --- /dev/null +++ b/examples/05-browser/src/anthropic.ts @@ -0,0 +1,112 @@ +/** + * Anthropic — the same browser chat, different provider. + * + * `AnthropicChatClient` is a `ChatClient` like any other, so the agent, tools and streaming code + * carry over from the OpenAI chat page unchanged — only the client construction differs. (The + * chat page's localStorage persistence is left out to keep the comparison small.) The Anthropic + * SDK has the same explicit browser opt-in as OpenAI's, and CORS on the Anthropic API allows + * direct calls from a page. + * + * Run: `pnpm --filter example-05-browser dev`, then open /anthropic.html + */ +import Anthropic from '@anthropic-ai/sdk'; +import { Agent, type AgentSession, tool } from '@polymind-inc/agent-framework'; +import { AnthropicChatClient } from '@polymind-inc/agent-framework/anthropic'; +import { z } from 'zod'; +import { bubble, chip, element, errorText, readSettings, streamingBubble } from './ui.js'; + +const setTheme = tool({ + name: 'set_theme', + description: 'Switch the page between the light and dark theme', + parameters: z.object({ theme: z.enum(['light', 'dark']) }), + execute: ({ theme }) => { + document.documentElement.dataset.theme = theme; + return `The page is now in ${theme} mode.`; + }, +}); + +const getLocalTime = tool({ + name: 'get_local_time', + description: "Read the visitor's current local time and time zone from the browser", + parameters: z.object({}), + execute: () => ({ + timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone, + localTime: new Date().toString(), + }), +}); + +const log = element('#log'); +const composer = element('#composer'); +const promptInput = element('#prompt'); +const sendButton = element('#send'); + +let agent: Agent | undefined; +let session: AgentSession | undefined; +let agentSettings = ''; + +function currentAgent(): Agent { + const settings = readSettings('claude-sonnet-4-5'); + const fingerprint = JSON.stringify(settings); + if (agent === undefined || fingerprint !== agentSettings) { + agent = new Agent({ + client: new AnthropicChatClient({ + model: settings.model, + client: new Anthropic({ + apiKey: settings.apiKey, + dangerouslyAllowBrowser: true, + ...(settings.baseURL === '' ? {} : { baseURL: settings.baseURL }), + }), + }), + name: 'BrowserAssistant', + instructions: + 'You are a cheerful assistant living inside a web page. Your tools can restyle the page ' + + "and read the visitor's clock. Answer briefly.", + tools: [setTheme, getLocalTime], + }); + agentSettings = fingerprint; + } + return agent; +} + +async function send(): Promise { + const text = promptInput.value.trim(); + if (text === '' || sendButton.disabled) { + return; + } + let active: Agent; + try { + active = currentAgent(); + } catch (error) { + bubble(log, 'error', errorText(error)); + return; + } + promptInput.value = ''; + sendButton.disabled = true; + bubble(log, 'user', text); + const reply = streamingBubble(log); + try { + session ??= active.createSession(); + const stream = active.run(text, { session }); + for await (const update of stream) { + for (const content of update.contents) { + if (content.type === 'function_call') { + chip(log, `tool: ${content.name}`); + } + } + if (update.text !== '') { + reply.append(update.text); + } + } + } catch (error) { + reply.remove(); + bubble(log, 'error', errorText(error)); + } finally { + sendButton.disabled = false; + promptInput.focus(); + } +} + +composer.addEventListener('submit', (event) => { + event.preventDefault(); + void send(); +}); diff --git a/examples/05-browser/src/canvas.ts b/examples/05-browser/src/canvas.ts new file mode 100644 index 0000000..3e13979 --- /dev/null +++ b/examples/05-browser/src/canvas.ts @@ -0,0 +1,162 @@ +/** + * Canvas — an agent that paints with client-side tools. + * + * Every drawing primitive is a `tool()` whose `execute` runs in the page against a 2D canvas + * context. The function-calling loop turns one instruction ("draw a snowman") into a series of + * tool calls, each rendered the moment it executes. The session lives in memory, so follow-up + * instructions ("now add a hat") keep building on the same picture. + * + * Run: `pnpm --filter example-05-browser dev`, then open /canvas.html + */ +import { Agent, type AgentSession, tool } from '@polymind-inc/agent-framework'; +import { OpenAIChatClient } from '@polymind-inc/agent-framework/openai'; +import OpenAI from 'openai'; +import { z } from 'zod'; +import { bubble, chip, element, errorText, readSettings, streamingBubble } from './ui.js'; + +const canvas = element('#canvas'); +const context = canvas.getContext('2d'); +if (context === null) { + throw new Error('This browser does not provide a 2D canvas context.'); +} + +const color = z.string().describe('Any CSS color, e.g. "tomato" or "#1d76db"').default('#1d76db'); + +const drawCircle = tool({ + name: 'draw_circle', + description: 'Draw a filled circle on the canvas', + parameters: z.object({ x: z.number(), y: z.number(), radius: z.number().positive(), color }), + execute: (args) => { + context.fillStyle = args.color; + context.beginPath(); + context.arc(args.x, args.y, args.radius, 0, 2 * Math.PI); + context.fill(); + return `Circle at (${args.x}, ${args.y}).`; + }, +}); + +const drawRect = tool({ + name: 'draw_rect', + description: 'Draw a filled rectangle on the canvas', + parameters: z.object({ + x: z.number(), + y: z.number(), + width: z.number().positive(), + height: z.number().positive(), + color, + }), + execute: (args) => { + context.fillStyle = args.color; + context.fillRect(args.x, args.y, args.width, args.height); + return `Rectangle at (${args.x}, ${args.y}).`; + }, +}); + +const drawLine = tool({ + name: 'draw_line', + description: 'Draw a straight line on the canvas', + parameters: z.object({ + x1: z.number(), + y1: z.number(), + x2: z.number(), + y2: z.number(), + color, + lineWidth: z.number().positive().default(2), + }), + execute: (args) => { + context.strokeStyle = args.color; + context.lineWidth = args.lineWidth; + context.beginPath(); + context.moveTo(args.x1, args.y1); + context.lineTo(args.x2, args.y2); + context.stroke(); + return `Line from (${args.x1}, ${args.y1}) to (${args.x2}, ${args.y2}).`; + }, +}); + +const clearCanvas = tool({ + name: 'clear_canvas', + description: 'Erase everything on the canvas', + parameters: z.object({}), + execute: () => { + context.clearRect(0, 0, canvas.width, canvas.height); + return 'The canvas is blank.'; + }, +}); + +const log = element('#log'); +const composer = element('#composer'); +const promptInput = element('#prompt'); +const sendButton = element('#send'); + +let agent: Agent | undefined; +let session: AgentSession | undefined; +let agentSettings = ''; + +function currentAgent(): Agent { + const settings = readSettings('gpt-4o-mini'); + const fingerprint = JSON.stringify(settings); + if (agent === undefined || fingerprint !== agentSettings) { + agent = new Agent({ + client: new OpenAIChatClient({ + model: settings.model, + client: new OpenAI({ + apiKey: settings.apiKey, + dangerouslyAllowBrowser: true, + ...(settings.baseURL === '' ? {} : { baseURL: settings.baseURL }), + }), + }), + name: 'CanvasPainter', + instructions: + `You paint on a ${canvas.width}×${canvas.height} canvas whose origin is the top-left ` + + 'corner. Compose pictures from your drawing tools — several calls per request is normal. ' + + 'After drawing, describe what you made in one short sentence.', + tools: [drawCircle, drawRect, drawLine, clearCanvas], + }); + agentSettings = fingerprint; + } + return agent; +} + +async function send(): Promise { + const text = promptInput.value.trim(); + if (text === '' || sendButton.disabled) { + return; + } + let active: Agent; + try { + active = currentAgent(); + } catch (error) { + bubble(log, 'error', errorText(error)); + return; + } + promptInput.value = ''; + sendButton.disabled = true; + bubble(log, 'user', text); + const reply = streamingBubble(log); + try { + session ??= active.createSession(); + const stream = active.run(text, { session }); + for await (const update of stream) { + for (const content of update.contents) { + if (content.type === 'function_call') { + chip(log, `tool: ${content.name}`); + } + } + if (update.text !== '') { + reply.append(update.text); + } + } + } catch (error) { + reply.remove(); + bubble(log, 'error', errorText(error)); + } finally { + sendButton.disabled = false; + promptInput.focus(); + } +} + +composer.addEventListener('submit', (event) => { + event.preventDefault(); + void send(); +}); diff --git a/examples/05-browser/src/main.ts b/examples/05-browser/src/main.ts new file mode 100644 index 0000000..5e0c5e4 --- /dev/null +++ b/examples/05-browser/src/main.ts @@ -0,0 +1,196 @@ +/** + * Chat — the agent loop running in the browser. + * + * The whole loop — model calls, function calling, streaming — runs in the page. Tools can + * therefore touch browser APIs directly: one restyles the page, another reads the visitor's + * clock. The session is plain JSON, persisted to `localStorage` across reloads. + * + * The API key entered in the page stays in this tab's memory, but anything shipped to a browser + * is readable by its user — in production, run agents server-side and keep credentials there. + * + * Run: `pnpm --filter example-05-browser dev` + */ +import { Agent, type AgentSession, tool } from '@polymind-inc/agent-framework'; +import { OpenAIChatClient } from '@polymind-inc/agent-framework/openai'; +import OpenAI from 'openai'; +import { z } from 'zod'; +import { + bubble, + chip, + element, + errorText, + loadJson, + readSettings, + removeStored, + saveJson, + streamingBubble, +} from './ui.js'; + +const SESSION_KEY = 'agent-framework-example.session'; +const TRANSCRIPT_KEY = 'agent-framework-example.transcript'; + +// These tools run inside the page, so they can reach browser APIs the model cannot. +const setTheme = tool({ + name: 'set_theme', + description: 'Switch the page between the light and dark theme', + parameters: z.object({ theme: z.enum(['light', 'dark']) }), + execute: ({ theme }) => { + document.documentElement.dataset.theme = theme; + return `The page is now in ${theme} mode.`; + }, +}); + +const getLocalTime = tool({ + name: 'get_local_time', + description: "Read the visitor's current local time and time zone from the browser", + parameters: z.object({}), + execute: () => ({ + timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone, + localTime: new Date().toString(), + }), +}); + +const log = element('#log'); +const composer = element('#composer'); +const promptInput = element('#prompt'); +const sendButton = element('#send'); +const clearButton = element('#clear'); + +type TranscriptEntry = { role: 'user' | 'assistant'; text: string }; + +function isTranscriptEntry(value: unknown): value is TranscriptEntry { + if (typeof value !== 'object' || value === null) { + return false; + } + const entry = value as Record; + return (entry.role === 'user' || entry.role === 'assistant') && typeof entry.text === 'string'; +} + +function restoreTranscript(): TranscriptEntry[] { + const stored = loadJson(TRANSCRIPT_KEY); + if (stored === undefined) { + return []; + } + if (Array.isArray(stored) && stored.every(isTranscriptEntry)) { + return stored; + } + // Anything else parses but is not what this page wrote; drop it rather than render garbage. + removeStored(TRANSCRIPT_KEY); + return []; +} + +// The session is the source of truth the model sees; the transcript only redraws past bubbles. +const transcript: TranscriptEntry[] = restoreTranscript(); +for (const entry of transcript) { + bubble(log, entry.role, entry.text); +} +if (transcript.length > 0) { + chip(log, 'conversation restored from localStorage'); +} + +let agent: Agent | undefined; +let session: AgentSession | undefined; +let agentSettings = ''; + +function currentAgent(): Agent { + const settings = readSettings('gpt-4o-mini'); + const fingerprint = JSON.stringify(settings); + if (agent === undefined || fingerprint !== agentSettings) { + agent = new Agent({ + client: new OpenAIChatClient({ + model: settings.model, + // The OpenAI SDK refuses to run in a browser unless the risk of exposing the key is + // acknowledged explicitly. Here the key is typed into the page and kept in memory only. + client: new OpenAI({ + apiKey: settings.apiKey, + dangerouslyAllowBrowser: true, + ...(settings.baseURL === '' ? {} : { baseURL: settings.baseURL }), + }), + }), + name: 'BrowserAssistant', + instructions: + 'You are a cheerful assistant living inside a web page. Your tools can restyle the page ' + + "and read the visitor's clock. Answer briefly.", + tools: [setTheme, getLocalTime], + }); + agentSettings = fingerprint; + } + return agent; +} + +function currentSession(active: Agent): AgentSession { + if (session === undefined) { + const saved = loadJson(SESSION_KEY); + if (saved !== undefined) { + try { + session = active.deserializeSession(saved); + } catch { + // Corrupted or incompatible saved state would fail every send; drop it and start over. + removeStored(SESSION_KEY); + chip(log, 'saved session could not be restored; starting fresh'); + } + } + session ??= active.createSession(); + } + return session; +} + +async function send(): Promise { + const text = promptInput.value.trim(); + if (text === '' || sendButton.disabled) { + return; + } + let active: Agent; + try { + active = currentAgent(); + } catch (error) { + bubble(log, 'error', errorText(error)); + return; + } + promptInput.value = ''; + sendButton.disabled = true; + // Clearing mid-run would race the in-flight loop, which repopulates the log and storage. + clearButton.disabled = true; + bubble(log, 'user', text); + transcript.push({ role: 'user', text }); + const reply = streamingBubble(log); + try { + const stream = active.run(text, { session: currentSession(active) }); + for await (const update of stream) { + for (const content of update.contents) { + if (content.type === 'function_call') { + chip(log, `tool: ${content.name}`); + } + } + if (update.text !== '') { + reply.append(update.text); + } + } + const replyText = reply.text(); + if (replyText !== '') { + transcript.push({ role: 'assistant', text: replyText }); + } + saveJson(SESSION_KEY, session); + saveJson(TRANSCRIPT_KEY, transcript); + } catch (error) { + reply.remove(); + bubble(log, 'error', errorText(error)); + } finally { + sendButton.disabled = false; + clearButton.disabled = false; + promptInput.focus(); + } +} + +composer.addEventListener('submit', (event) => { + event.preventDefault(); + void send(); +}); + +clearButton.addEventListener('click', () => { + session = undefined; + transcript.length = 0; + log.replaceChildren(); + removeStored(SESSION_KEY); + removeStored(TRANSCRIPT_KEY); +}); diff --git a/examples/05-browser/src/structured.ts b/examples/05-browser/src/structured.ts new file mode 100644 index 0000000..2ac1503 --- /dev/null +++ b/examples/05-browser/src/structured.ts @@ -0,0 +1,88 @@ +/** + * Structured output — typed extraction rendered straight into the page. + * + * Pass a schema as `responseFormat` and the parsed, validated value lands on `response.value`, + * typed from the schema — no JSON.parse, no manual checks. Here the value fills a definition + * list, which is exactly the kind of UI code that benefits from the value already being typed. + * + * Run: `pnpm --filter example-05-browser dev`, then open /structured.html + */ +import { Agent } from '@polymind-inc/agent-framework'; +import { OpenAIChatClient } from '@polymind-inc/agent-framework/openai'; +import OpenAI from 'openai'; +import { z } from 'zod'; +import { bubble, element, errorText, readSettings } from './ui.js'; + +const EventInfo = z.object({ + title: z.string(), + date: z.string().describe('ISO 8601 date'), + location: z.string(), + attendees: z.array(z.string()), +}); + +const log = element('#log'); +const form = element('#composer'); +const input = element('#source'); +const extractButton = element('#extract'); +const result = element('#result'); + +function renderValue(value: z.infer): void { + const rows: [string, string][] = [ + ['Title', value.title], + ['Date', value.date], + ['Location', value.location], + ['Attendees', value.attendees.join(', ')], + ]; + const dl = document.createElement('dl'); + for (const [term, detail] of rows) { + const dt = document.createElement('dt'); + dt.textContent = term; + const dd = document.createElement('dd'); + dd.textContent = detail; + dl.append(dt, dd); + } + result.replaceChildren(dl); +} + +async function extract(): Promise { + const text = input.value.trim(); + if (text === '' || extractButton.disabled) { + return; + } + let agent: Agent; + try { + const settings = readSettings('gpt-4o-mini'); + agent = new Agent({ + client: new OpenAIChatClient({ + model: settings.model, + client: new OpenAI({ + apiKey: settings.apiKey, + dangerouslyAllowBrowser: true, + ...(settings.baseURL === '' ? {} : { baseURL: settings.baseURL }), + }), + }), + instructions: 'Extract the event described in the user text.', + }); + } catch (error) { + bubble(log, 'error', errorText(error)); + return; + } + extractButton.disabled = true; + try { + const response = await agent.run(text, { responseFormat: EventInfo }); + // A suspended response has no value yet, so narrow it before use. + if (response.value === undefined) { + throw new Error('The run stopped before producing its structured output.'); + } + renderValue(response.value); + } catch (error) { + bubble(log, 'error', errorText(error)); + } finally { + extractButton.disabled = false; + } +} + +form.addEventListener('submit', (event) => { + event.preventDefault(); + void extract(); +}); diff --git a/examples/05-browser/src/style.css b/examples/05-browser/src/style.css new file mode 100644 index 0000000..1adbdd6 --- /dev/null +++ b/examples/05-browser/src/style.css @@ -0,0 +1,215 @@ +:root { + color-scheme: light; + --bg: #f6f7f9; + --panel: #ffffff; + --text: #1f2328; + --muted: #59636e; + --accent: #0969da; + --border: #d1d9e0; +} + +:root[data-theme="dark"] { + color-scheme: dark; + --bg: #0d1117; + --panel: #161b22; + --text: #e6edf3; + --muted: #9198a1; + --accent: #4493f8; + --border: #30363d; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + font-family: system-ui, sans-serif; + background: var(--bg); + color: var(--text); +} + +main { + max-width: 720px; + min-height: 100dvh; + margin: 0 auto; + padding: 1rem; + display: flex; + flex-direction: column; + gap: 1rem; +} + +h1 { + margin: 0 0 0.5rem; + font-size: 1.4rem; +} + +nav { + display: flex; + gap: 1rem; + margin-bottom: 0.75rem; + font-size: 0.85rem; +} + +nav a { + color: var(--muted); + text-decoration: none; +} + +nav a[aria-current="page"] { + color: var(--accent); + font-weight: 600; +} + +nav a:hover { + color: var(--accent); +} + +.warning { + margin: 0 0 0.75rem; + padding: 0.5rem 0.75rem; + border: 1px solid var(--border); + border-left: 3px solid var(--accent); + border-radius: 6px; + background: var(--panel); + color: var(--muted); + font-size: 0.85rem; +} + +.settings { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0.5rem; +} + +.settings label { + display: flex; + flex-direction: column; + gap: 0.25rem; + font-size: 0.8rem; + color: var(--muted); +} + +input, +button { + font: inherit; + padding: 0.5rem 0.65rem; + border: 1px solid var(--border); + border-radius: 6px; + background: var(--panel); + color: var(--text); +} + +input:focus { + outline: 2px solid var(--accent); + outline-offset: -1px; +} + +#log { + flex: 1; + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0.25rem 0; + overflow-y: auto; +} + +.bubble { + max-width: 85%; + padding: 0.5rem 0.75rem; + border-radius: 10px; + white-space: pre-wrap; + overflow-wrap: anywhere; +} + +.bubble.user { + align-self: flex-end; + background: var(--accent); + color: #ffffff; +} + +.bubble.assistant { + align-self: flex-start; + background: var(--panel); + border: 1px solid var(--border); +} + +.bubble.error { + align-self: stretch; + border: 1px solid #d1242f; + color: #d1242f; + background: var(--panel); +} + +.chip { + align-self: flex-start; + padding: 0.15rem 0.6rem; + border: 1px dashed var(--border); + border-radius: 999px; + color: var(--muted); + font-size: 0.75rem; +} + +#canvas { + align-self: center; + border: 1px solid var(--border); + border-radius: 6px; + background: #ffffff; + max-width: 100%; +} + +textarea { + font: inherit; + width: 100%; + padding: 0.5rem 0.65rem; + border: 1px solid var(--border); + border-radius: 6px; + background: var(--panel); + color: var(--text); + resize: vertical; +} + +#result dl { + display: grid; + grid-template-columns: auto 1fr; + gap: 0.35rem 1rem; + margin: 0; + padding: 0.75rem 1rem; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--panel); +} + +#result dt { + color: var(--muted); +} + +#result dd { + margin: 0; +} + +#composer { + display: flex; + gap: 0.5rem; +} + +#composer:has(textarea) { + flex-direction: column; + align-items: flex-start; +} + +#prompt { + flex: 1; +} + +#send, +#extract { + background: var(--accent); + border-color: var(--accent); + color: #ffffff; +} + +#send:disabled, +#extract:disabled { + opacity: 0.6; +} diff --git a/examples/05-browser/src/ui.ts b/examples/05-browser/src/ui.ts new file mode 100644 index 0000000..b90750b --- /dev/null +++ b/examples/05-browser/src/ui.ts @@ -0,0 +1,104 @@ +/** Small DOM and storage helpers shared by every page of this example. */ + +export function element(selector: string): T { + const found = document.querySelector(selector); + if (found === null) { + throw new Error(`Missing element: ${selector}`); + } + return found; +} + +export type BubbleKind = 'user' | 'assistant' | 'error'; + +export function bubble(log: HTMLElement, kind: BubbleKind, text = ''): HTMLElement { + const el = document.createElement('div'); + el.className = `bubble ${kind}`; + el.textContent = text; + log.append(el); + el.scrollIntoView({ block: 'end' }); + return el; +} + +/** + * An assistant bubble that appears on the first streamed chunk and grows by mutating a single + * text node, so a long response stays one DOM node instead of one per chunk. + */ +export function streamingBubble(log: HTMLElement): { + append(chunk: string): void; + text(): string; + remove(): void; +} { + let el: HTMLElement | undefined; + let node: Text | undefined; + return { + append(chunk: string): void { + if (el === undefined || node === undefined) { + el = bubble(log, 'assistant'); + node = document.createTextNode(''); + el.append(node); + } + node.appendData(chunk); + el.scrollIntoView({ block: 'end' }); + }, + text: (): string => node?.data ?? '', + remove: (): void => el?.remove(), + }; +} + +export function chip(log: HTMLElement, text: string): void { + const el = document.createElement('div'); + el.className = 'chip'; + el.textContent = text; + log.append(el); + el.scrollIntoView({ block: 'end' }); +} + +export function loadJson(key: string): T | undefined { + try { + const raw = localStorage.getItem(key); + return raw === null ? undefined : (JSON.parse(raw) as T); + } catch { + return undefined; + } +} + +export function saveJson(key: string, value: unknown): void { + try { + localStorage.setItem(key, JSON.stringify(value)); + } catch { + // Storage can be unavailable (private windows, quota); the page works without persistence. + } +} + +export function removeStored(key: string): void { + try { + localStorage.removeItem(key); + } catch { + // Storage can be unavailable; there is then nothing to remove. + } +} + +export interface Settings { + apiKey: string; + baseURL: string; + model: string; +} + +/** Read the API key / base URL / model row every page shares; throws until a key is entered. */ +export function readSettings(defaultModel: string): Settings { + const apiKeyInput = element('#api-key'); + const apiKey = apiKeyInput.value.trim(); + if (apiKey === '') { + apiKeyInput.focus(); + throw new Error('Enter an API key first.'); + } + return { + apiKey, + baseURL: element('#base-url').value.trim(), + model: element('#model').value.trim() || defaultModel, + }; +} + +export function errorText(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} diff --git a/examples/05-browser/structured.html b/examples/05-browser/structured.html new file mode 100644 index 0000000..0f19043 --- /dev/null +++ b/examples/05-browser/structured.html @@ -0,0 +1,49 @@ + + + + + + Agent Framework — structured output example + + + +
+
+ +

Typed extraction into the page

+

+ The schema-validated response.value fills the fields below directly — no + JSON.parse in sight. Keys pasted here stay in memory — in production, run agents server-side. +

+
+ + + +
+
+
+ + +
+
+
+
+ + + diff --git a/examples/05-browser/tsconfig.json b/examples/05-browser/tsconfig.json new file mode 100644 index 0000000..41386a2 --- /dev/null +++ b/examples/05-browser/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.neutral.json", + "compilerOptions": { + "isolatedDeclarations": false, + "noEmit": true + }, + "include": ["src/**/*.ts"] +} diff --git a/examples/05-browser/vite.config.ts b/examples/05-browser/vite.config.ts new file mode 100644 index 0000000..8e44e8d --- /dev/null +++ b/examples/05-browser/vite.config.ts @@ -0,0 +1,15 @@ +import { resolve } from 'node:path'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + build: { + rollupOptions: { + input: { + index: resolve(import.meta.dirname, 'index.html'), + canvas: resolve(import.meta.dirname, 'canvas.html'), + structured: resolve(import.meta.dirname, 'structured.html'), + anthropic: resolve(import.meta.dirname, 'anthropic.html'), + }, + }, + }, +}); diff --git a/examples/README.md b/examples/README.md index 3fb1fe4..8e091e7 100644 --- a/examples/README.md +++ b/examples/README.md @@ -73,3 +73,17 @@ pnpm --filter example-04-a2a server | Multi-turn | A question from the agent, task linking, and session persistence | `pnpm --filter example-04-a2a multi-turn` | | Background | Continuation tokens: resuming a task, and re-subscribing to a live one | `pnpm --filter example-04-a2a background` | | Authentication | A static token, and one that expires and is refreshed on a 401 | Start the agent with `A2A_TOKEN=invoice-secret`, then run `A2A_TOKEN=invoice-secret pnpm --filter example-04-a2a authentication` | + +## Browser + +One Vite app, four pages, each running the agent loop entirely in the browser. API keys are +pasted into the page and kept in memory — see [`05-browser/README.md`](05-browser/README.md) for +why production agents belong server-side. Start with `pnpm --filter example-05-browser dev`, then +open the page: + +| Page | What it demonstrates | +| --- | --- | +| `/` | Chat: streaming into the DOM, client-side tools, session persistence in `localStorage` | +| `/canvas.html` | An agent that paints — every drawing primitive is a tool executing against a canvas | +| `/structured.html` | Typed extraction: the schema-validated `response.value` fills the page's fields | +| `/anthropic.html` | The chat page with `AnthropicChatClient` swapped in; the agent, tools and streaming code are unchanged | diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d6e70ec..80379a0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -52,7 +52,7 @@ importers: version: 7.0.2 vitest: specifier: 'catalog:' - version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.11)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(tsx@4.23.12)) + version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.11)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.1)(tsx@4.23.12)) examples/01-get-started: dependencies: @@ -139,6 +139,31 @@ importers: specifier: 'catalog:' version: 7.0.2 + examples/05-browser: + dependencies: + '@anthropic-ai/sdk': + specifier: ^0.120.0 + version: 0.120.0(zod@4.4.3) + '@polymind-inc/agent-framework': + specifier: workspace:^ + version: link:../../packages/meta + openai: + specifier: 'catalog:' + version: 7.5.0(zod@4.4.3) + zod: + specifier: ^4.4.3 + version: 4.4.3 + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 24.13.3 + typescript: + specifier: 'catalog:' + version: 7.0.2 + vite: + specifier: ^8.2.2 + version: 8.2.2(@types/node@24.13.3)(esbuild@0.28.1)(tsx@4.23.12) + packages/a2a: dependencies: '@a2a-js/sdk': @@ -156,7 +181,7 @@ importers: version: 7.0.2 vitest: specifier: 'catalog:' - version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.11)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(tsx@4.23.12)) + version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.11)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.1)(tsx@4.23.12)) packages/agentserver: dependencies: @@ -208,7 +233,7 @@ importers: version: 7.0.2 vitest: specifier: 'catalog:' - version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.11)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(tsx@4.23.12)) + version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.11)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.1)(tsx@4.23.12)) packages/anthropic: dependencies: @@ -227,7 +252,7 @@ importers: version: 7.0.2 vitest: specifier: 'catalog:' - version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.11)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(tsx@4.23.12)) + version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.11)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.1)(tsx@4.23.12)) packages/core: dependencies: @@ -258,7 +283,7 @@ importers: version: 7.0.2 vitest: specifier: 'catalog:' - version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.11)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(tsx@4.23.12)) + version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.11)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.1)(tsx@4.23.12)) packages/foundry: dependencies: @@ -301,7 +326,7 @@ importers: version: 7.0.2 vitest: specifier: 'catalog:' - version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.11)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(tsx@4.23.12)) + version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.11)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.1)(tsx@4.23.12)) packages/mcp: dependencies: @@ -329,7 +354,7 @@ importers: version: 7.0.2 vitest: specifier: 'catalog:' - version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.11)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(tsx@4.23.12)) + version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.11)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.1)(tsx@4.23.12)) packages/meta: dependencies: @@ -366,7 +391,7 @@ importers: version: 7.0.2 vitest: specifier: 'catalog:' - version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.11)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(tsx@4.23.12)) + version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.11)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.1)(tsx@4.23.12)) packages/openai: dependencies: @@ -385,7 +410,7 @@ importers: version: 7.0.2 vitest: specifier: 'catalog:' - version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.11)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(tsx@4.23.12)) + version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.11)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.1)(tsx@4.23.12)) scripts/type-resolution: dependencies: @@ -582,21 +607,12 @@ packages: cpu: [x64] os: [win32] - '@emnapi/core@1.11.1': - resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} - '@emnapi/core@2.0.0-alpha.3': resolution: {integrity: sha512-AZypUeJ/yByuxyS7BlSNRDOMLMlROYtjYdIAuBmJssVz1UJDSeYxLrdizhXCFYhedC5bqd/ASy8EuNXbVVXp9g==} - '@emnapi/runtime@1.11.1': - resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} - '@emnapi/runtime@2.0.0-alpha.3': resolution: {integrity: sha512-hFPAhMUjJD9BSyCANEISPOogeXC9Zo9ZQl7L6vKnaVsMkCtzznaW/naYypeyl0Gv5rYfWYsZbpixTMpjDJzQeA==} - '@emnapi/wasi-threads@1.2.2': - resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} - '@emnapi/wasi-threads@2.0.1': resolution: {integrity: sha512-9DsSk+o5NBX0CCJT8s0EROGSGxjR/tKu6aBTaVyq+SjAEQH4XcdcRxPBRzsBLizTTJ49MJjF+jgu3qnO9GLQcQ==} @@ -929,12 +945,12 @@ packages: resolution: {integrity: sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==} engines: {node: '>=14'} - '@oxc-project/types@0.139.0': - resolution: {integrity: sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==} - '@oxc-project/types@0.142.0': resolution: {integrity: sha512-7W+2q5AKQVU36fkaryontrHn3YDt1RyUYXatw9i5H8ocYe2sPKSFB6eS8WNPeRKiN1qAWWZUPm7gwFzJGrccqQ==} + '@oxc-project/types@0.147.0': + resolution: {integrity: sha512-IJ3s6ltHLp45S0bh7phkX+gJO7A1Wuz2EaqpAhb8WjqDwbzMiWKHhyyT42tskaWjEYXtHtVCPpnBJVT9+dcRLg==} + '@protobufjs/aspromise@1.1.2': resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} @@ -965,10 +981,10 @@ packages: '@quansync/fs@1.0.0': resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==} - '@rolldown/binding-android-arm64@1.1.5': - resolution: {integrity: sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==} + '@rolldown/binding-android-arm-eabi@1.2.6': + resolution: {integrity: sha512-b+jTcARdTiFLI6jB4a5XjTm0RWd6KcRfQj/I2356fxUZemiho9zQLxo0RtCuMDAyKcLo6cEltkgbQp6d1+sjjQ==} engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] + cpu: [arm] os: [android] '@rolldown/binding-android-arm64@1.2.1': @@ -977,11 +993,11 @@ packages: cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.1.5': - resolution: {integrity: sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==} + '@rolldown/binding-android-arm64@1.2.6': + resolution: {integrity: sha512-lkWU8ZJaRk9q3CIEY1Tc7vIFALp3Xw5NfGJo2hQg5oIqNgxWi1zI+IiDEK3r70BF5Dzol1tcXsnzsRc8NLhG+Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] - os: [darwin] + os: [android] '@rolldown/binding-darwin-arm64@1.2.1': resolution: {integrity: sha512-fMsTOnN0OjFm3CyppWPitKnc8UlliVARUULW6cfU6AIqjdtgmSFWSk9vecHzZduv/yMWIHDlRhM1e8Iff9uAfA==} @@ -989,10 +1005,10 @@ packages: cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.1.5': - resolution: {integrity: sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==} + '@rolldown/binding-darwin-arm64@1.2.6': + resolution: {integrity: sha512-dgR56NYnvAszm7Ob1B2/Vn0e8bUQYZH2UjVaMMtMVOCKFSfjhfLmuA/9+O+F+ajUdG6B/bSssrKW6JJYASa8jA==} engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] + cpu: [arm64] os: [darwin] '@rolldown/binding-darwin-x64@1.2.1': @@ -1001,11 +1017,11 @@ packages: cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.1.5': - resolution: {integrity: sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==} + '@rolldown/binding-darwin-x64@1.2.6': + resolution: {integrity: sha512-vpVxFvUCFioJqug7OTvqptkc4yb8UX0AwfDmJpaR/0sWz+BUmqSVAf7c8JkUgnN8YLspb4a/N6NhTyMAmdyQ7Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] - os: [freebsd] + os: [darwin] '@rolldown/binding-freebsd-x64@1.2.1': resolution: {integrity: sha512-Fa0jHR07E7YBN4vOEsbVf2briYNsuOowfLJaXULZM0ldMlaCaj2LJgLMbMe4iacRyZmvR8efFhgR9wKuGclQUg==} @@ -1013,11 +1029,11 @@ packages: cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.1.5': - resolution: {integrity: sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==} + '@rolldown/binding-freebsd-x64@1.2.6': + resolution: {integrity: sha512-h1wG6Y6K3JlRswxsI64qQJqBAy4vrLuHgRbc8CZMGSWTOFRY6ghMApM1NKzB2I0n5xV1fjkE18SuVl2QpLeNpA==} engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [linux] + cpu: [x64] + os: [freebsd] '@rolldown/binding-linux-arm-gnueabihf@1.2.1': resolution: {integrity: sha512-pzkgu1SSHGgRRyRZ4fbmSgmajbVt+epaLP99NDjFft69v/ypfTi6swBMiVdh2EkQ0OSnHE1lZDM7DRGkyAzUpA==} @@ -1025,12 +1041,11 @@ packages: cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.1.5': - resolution: {integrity: sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==} + '@rolldown/binding-linux-arm-gnueabihf@1.2.6': + resolution: {integrity: sha512-tbCiqub0q2MVWJKgF5PoAlNWCtQydiOYSLIkd8sByqK/6MMYLJRcSXSYodqYtd0O+Fw7QaVmKKlS4oL94YRZ0w==} engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] + cpu: [arm] os: [linux] - libc: [glibc] '@rolldown/binding-linux-arm64-gnu@1.2.1': resolution: {integrity: sha512-QI5SEDY8cbiYWHx0VO4vIc3UlS6a32vXHjU8Qy/17adEmZIPuByJg13UEvo9c/UCiUkdcVWY83C+b+JrwnNyUg==} @@ -1039,12 +1054,12 @@ packages: os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.1.5': - resolution: {integrity: sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==} + '@rolldown/binding-linux-arm64-gnu@1.2.6': + resolution: {integrity: sha512-oxK9+baEBPhZG5HB4URY+uU04zJWeZlH6Tb9rB5DK4DF9XR1uXNLXt5Q5ZsugTKayNCNLhkcwz/ye74hRI98dg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [musl] + libc: [glibc] '@rolldown/binding-linux-arm64-musl@1.2.1': resolution: {integrity: sha512-Sm41FyCeXqmYcERoYOCbGIL5hNfd8w9LQ7Y61Bev48HkcjaJqV/iiVOaiDxjVTRMS+QKrZmD8cfPt4uMVnvM+A==} @@ -1053,12 +1068,12 @@ packages: os: [linux] libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.1.5': - resolution: {integrity: sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==} + '@rolldown/binding-linux-arm64-musl@1.2.6': + resolution: {integrity: sha512-muWCk27FVBEZtv0MsK8gnfSmgczA8KQ0uRVJbTABKhkRfQc38aUrcb7fhi3BNiyseFmgcRsoMfQsSNJ+DbZdSw==} engines: {node: ^20.19.0 || >=22.12.0} - cpu: [ppc64] + cpu: [arm64] os: [linux] - libc: [glibc] + libc: [musl] '@rolldown/binding-linux-ppc64-gnu@1.2.1': resolution: {integrity: sha512-2x+WhXTGl9yJYPbltW/BSEPTVz9OIWQyER4N+gJEDWkkn904eRcBzELqh/Hf7K0w/ubGbKNMv0ZC+94QK/IFEg==} @@ -1067,10 +1082,10 @@ packages: os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.1.5': - resolution: {integrity: sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==} + '@rolldown/binding-linux-ppc64-gnu@1.2.6': + resolution: {integrity: sha512-eWDoSfU7Co2qj3vgB3Dt4lj1mG6CoWbcJQkRMP3XJplyCMtuaq3LHvPFjS9QIPvMGWVadJC04Xiy0IdcVPtnwQ==} engines: {node: ^20.19.0 || >=22.12.0} - cpu: [s390x] + cpu: [ppc64] os: [linux] libc: [glibc] @@ -1081,10 +1096,10 @@ packages: os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.1.5': - resolution: {integrity: sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==} + '@rolldown/binding-linux-s390x-gnu@1.2.6': + resolution: {integrity: sha512-2bWNjRSIayvupRKxXUY2tWG9fYdoUlTqWywHRvE8Eq3GvuQ+f2HeIkve697fIt+IQs/PV8yFsdWuhp1aJ1PdnA==} engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] + cpu: [s390x] os: [linux] libc: [glibc] @@ -1095,12 +1110,12 @@ packages: os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.1.5': - resolution: {integrity: sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==} + '@rolldown/binding-linux-x64-gnu@1.2.6': + resolution: {integrity: sha512-KekI0gS0wLxe1UBSQSjenBVwou/JkcQPDzBPICGZjxUv9k3RteHDPBQaiOicZUFKRIH2wKEimGwVpnJsbPzu7w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [musl] + libc: [glibc] '@rolldown/binding-linux-x64-musl@1.2.1': resolution: {integrity: sha512-xxBJRL+0q0Kce7orznGWLuylHDY65vuARXZRpX+hPdv+DqK2c3NlCsVA98tlWzWNEE7yPqA/1NQ5nnCrj49Y5A==} @@ -1109,11 +1124,12 @@ packages: os: [linux] libc: [musl] - '@rolldown/binding-openharmony-arm64@1.1.5': - resolution: {integrity: sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==} + '@rolldown/binding-linux-x64-musl@1.2.6': + resolution: {integrity: sha512-TvtPnfVr+HtyGiDmPK4VWmlNm7QhNNAcK5Q9A7aOXsI8545yCyaoMaicXrFZ72JzeYjaUVk7yT243zT0jzjFKQ==} engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [openharmony] + cpu: [x64] + os: [linux] + libc: [musl] '@rolldown/binding-openharmony-arm64@1.2.1': resolution: {integrity: sha512-M6AdXIXw3s+/8XpKMzdGDEXGS1S7kwUsy+rcTIUIOx5Ge4nXKCtAFHFV9YKkXvGcC5WMoTjAteLzlsQROVI0Yw==} @@ -1121,35 +1137,36 @@ packages: cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.1.5': - resolution: {integrity: sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==} + '@rolldown/binding-openharmony-arm64@1.2.6': + resolution: {integrity: sha512-iOo0VEay2XFhaCcH0sps5XIimkSuOnNaZrf6+ZkoSOQBJPKNU48RkmJv0/lSpipexu5P+ouFgafe5IGr/DiQfg==} engines: {node: ^20.19.0 || >=22.12.0} - cpu: [wasm32] + cpu: [arm64] + os: [openharmony] '@rolldown/binding-wasm32-wasi@1.2.1': resolution: {integrity: sha512-/TX0SoRGojHzSAHpfVBbavRVSazg5U3h3Y3VXfcc0cdugq6kxdqw8LPGFiPr+/7gE/60zRcsOY2Vi9b9eT0jww==} engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} - '@rolldown/binding-win32-arm64-msvc@1.1.5': - resolution: {integrity: sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==} + '@rolldown/binding-win32-arm64-msvc@1.2.1': + resolution: {integrity: sha512-EvRrivJieyHG+AO9lleZWgq+g0+S7oV2C51yuqlcyU/R9net+sI4Pj0F+lUoP2bEr6TWX3SqFaaS0SzfLxSzkw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-arm64-msvc@1.2.1': - resolution: {integrity: sha512-EvRrivJieyHG+AO9lleZWgq+g0+S7oV2C51yuqlcyU/R9net+sI4Pj0F+lUoP2bEr6TWX3SqFaaS0SzfLxSzkw==} + '@rolldown/binding-win32-arm64-msvc@1.2.6': + resolution: {integrity: sha512-y5NTmmasMS455JlOCO4ZM9krIchv3Mvm1crL1iUPGOPgEzSkves9n0SdC5Sjz6+qWDFhd8/JpfWMH8NSWNHe+A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.1.5': - resolution: {integrity: sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==} + '@rolldown/binding-win32-x64-msvc@1.2.1': + resolution: {integrity: sha512-Z4eCmn5QJ/5+azF9knpLWKfVd9aidn0mAe9TpJgvBLId9Ax3t0+JVxBmT25Bv7NBbVW1TZyKjQjQReouMeH5UQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.2.1': - resolution: {integrity: sha512-Z4eCmn5QJ/5+azF9knpLWKfVd9aidn0mAe9TpJgvBLId9Ax3t0+JVxBmT25Bv7NBbVW1TZyKjQjQReouMeH5UQ==} + '@rolldown/binding-win32-x64-msvc@1.2.6': + resolution: {integrity: sha512-np8iZSLfXlAD4kWhiyq/u0Yt8oZDtRQ8lGhQaCXo2rl37KNjeU0GjJuwr4P3oeZ++ROfofsKNBqR5LTO8aXyWQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -2074,8 +2091,8 @@ packages: resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} engines: {node: '>=16.20.0'} - postcss@8.5.25: - resolution: {integrity: sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==} + postcss@8.5.26: + resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} engines: {node: ^10 || ^12 || >=14} protobufjs@7.6.5: @@ -2131,13 +2148,13 @@ packages: vue-tsc: optional: true - rolldown@1.1.5: - resolution: {integrity: sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==} + rolldown@1.2.1: + resolution: {integrity: sha512-4FKJhg8d3OiyQOA6Q1Q0hoFFpW9/OoX+VsHzpECsdsIZoOArrAK90gl59YK/Z+gnDel45bgJZK03ozH/9bCqEw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true - rolldown@1.2.1: - resolution: {integrity: sha512-4FKJhg8d3OiyQOA6Q1Q0hoFFpW9/OoX+VsHzpECsdsIZoOArrAK90gl59YK/Z+gnDel45bgJZK03ozH/9bCqEw==} + rolldown@1.2.6: + resolution: {integrity: sha512-vMM4q3aixf46GiF1Kok8jDPFsEpXgFWGjUHXNkNHNm+Y2adXAG2dbX91jkti3i0ZRsOlcmbuzAz1poObSHCmUA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -2330,13 +2347,13 @@ packages: resolution: {integrity: sha512-w2Eo8LSIIoW7qxNBzT7/17k+bh8plXo7G3dHjEIDqPlnluhzaxr9JX8F28VSYEtDvc1/a3WBDih6xNUZseebXg==} engines: {node: '>=18.12.0'} - vite@8.1.5: - resolution: {integrity: sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==} + vite@8.2.2: + resolution: {integrity: sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.3.0 + '@vitejs/devtools': ^0.4.0 || ^0.5.0 esbuild: ^0.27.0 || ^0.28.0 jiti: '>=1.21.0' less: ^4.0.0 @@ -2643,33 +2660,17 @@ snapshots: '@biomejs/cli-win32-x64@2.5.9': optional: true - '@emnapi/core@1.11.1': - dependencies: - '@emnapi/wasi-threads': 1.2.2 - tslib: 2.8.1 - optional: true - '@emnapi/core@2.0.0-alpha.3': dependencies: '@emnapi/wasi-threads': 2.0.1 tslib: 2.8.1 optional: true - '@emnapi/runtime@1.11.1': - dependencies: - tslib: 2.8.1 - optional: true - '@emnapi/runtime@2.0.0-alpha.3': dependencies: tslib: 2.8.1 optional: true - '@emnapi/wasi-threads@1.2.2': - dependencies: - tslib: 2.8.1 - optional: true - '@emnapi/wasi-threads@2.0.1': dependencies: tslib: 2.8.1 @@ -2790,13 +2791,6 @@ snapshots: dependencies: zod: 4.4.3 - '@napi-rs/wasm-runtime@1.2.1(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': - dependencies: - '@emnapi/core': 1.11.1 - '@emnapi/runtime': 1.11.1 - '@tybys/wasm-util': 0.10.3 - optional: true - '@napi-rs/wasm-runtime@1.2.1(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)': dependencies: '@emnapi/core': 2.0.0-alpha.3 @@ -2959,10 +2953,10 @@ snapshots: '@opentelemetry/semantic-conventions@1.43.0': {} - '@oxc-project/types@0.139.0': {} - '@oxc-project/types@0.142.0': {} + '@oxc-project/types@0.147.0': {} + '@protobufjs/aspromise@1.1.2': {} '@protobufjs/base64@1.1.2': {} @@ -2987,83 +2981,79 @@ snapshots: dependencies: quansync: 1.0.0 - '@rolldown/binding-android-arm64@1.1.5': + '@rolldown/binding-android-arm-eabi@1.2.6': optional: true '@rolldown/binding-android-arm64@1.2.1': optional: true - '@rolldown/binding-darwin-arm64@1.1.5': + '@rolldown/binding-android-arm64@1.2.6': optional: true '@rolldown/binding-darwin-arm64@1.2.1': optional: true - '@rolldown/binding-darwin-x64@1.1.5': + '@rolldown/binding-darwin-arm64@1.2.6': optional: true '@rolldown/binding-darwin-x64@1.2.1': optional: true - '@rolldown/binding-freebsd-x64@1.1.5': + '@rolldown/binding-darwin-x64@1.2.6': optional: true '@rolldown/binding-freebsd-x64@1.2.1': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.1.5': + '@rolldown/binding-freebsd-x64@1.2.6': optional: true '@rolldown/binding-linux-arm-gnueabihf@1.2.1': optional: true - '@rolldown/binding-linux-arm64-gnu@1.1.5': + '@rolldown/binding-linux-arm-gnueabihf@1.2.6': optional: true '@rolldown/binding-linux-arm64-gnu@1.2.1': optional: true - '@rolldown/binding-linux-arm64-musl@1.1.5': + '@rolldown/binding-linux-arm64-gnu@1.2.6': optional: true '@rolldown/binding-linux-arm64-musl@1.2.1': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.1.5': + '@rolldown/binding-linux-arm64-musl@1.2.6': optional: true '@rolldown/binding-linux-ppc64-gnu@1.2.1': optional: true - '@rolldown/binding-linux-s390x-gnu@1.1.5': + '@rolldown/binding-linux-ppc64-gnu@1.2.6': optional: true '@rolldown/binding-linux-s390x-gnu@1.2.1': optional: true - '@rolldown/binding-linux-x64-gnu@1.1.5': + '@rolldown/binding-linux-s390x-gnu@1.2.6': optional: true '@rolldown/binding-linux-x64-gnu@1.2.1': optional: true - '@rolldown/binding-linux-x64-musl@1.1.5': + '@rolldown/binding-linux-x64-gnu@1.2.6': optional: true '@rolldown/binding-linux-x64-musl@1.2.1': optional: true - '@rolldown/binding-openharmony-arm64@1.1.5': + '@rolldown/binding-linux-x64-musl@1.2.6': optional: true '@rolldown/binding-openharmony-arm64@1.2.1': optional: true - '@rolldown/binding-wasm32-wasi@1.1.5': - dependencies: - '@emnapi/core': 1.11.1 - '@emnapi/runtime': 1.11.1 - '@napi-rs/wasm-runtime': 1.2.1(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + '@rolldown/binding-openharmony-arm64@1.2.6': optional: true '@rolldown/binding-wasm32-wasi@1.2.1': @@ -3073,18 +3063,18 @@ snapshots: '@napi-rs/wasm-runtime': 1.2.1(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3) optional: true - '@rolldown/binding-win32-arm64-msvc@1.1.5': - optional: true - '@rolldown/binding-win32-arm64-msvc@1.2.1': optional: true - '@rolldown/binding-win32-x64-msvc@1.1.5': + '@rolldown/binding-win32-arm64-msvc@1.2.6': optional: true '@rolldown/binding-win32-x64-msvc@1.2.1': optional: true + '@rolldown/binding-win32-x64-msvc@1.2.6': + optional: true + '@rolldown/pluginutils@1.0.1': {} '@stablelib/base64@1.0.1': {} @@ -3226,7 +3216,7 @@ snapshots: obug: 2.1.4 std-env: 4.2.0 tinyrainbow: 3.1.1 - vitest: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.11)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(tsx@4.23.12)) + vitest: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.11)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.1)(tsx@4.23.12)) '@vitest/expect@4.1.11': dependencies: @@ -3237,13 +3227,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.1 - '@vitest/mocker@4.1.11(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(tsx@4.23.12))': + '@vitest/mocker@4.1.11(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.1)(tsx@4.23.12))': dependencies: '@vitest/spy': 4.1.11 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(tsx@4.23.12) + vite: 8.2.2(@types/node@24.13.3)(esbuild@0.28.1)(tsx@4.23.12) '@vitest/pretty-format@4.1.11': dependencies: @@ -3863,7 +3853,7 @@ snapshots: pkce-challenge@5.0.1: {} - postcss@8.5.25: + postcss@8.5.26: dependencies: nanoid: 3.3.18 picocolors: 1.1.1 @@ -3929,27 +3919,6 @@ snapshots: transitivePeerDependencies: - oxc-resolver - rolldown@1.1.5: - dependencies: - '@oxc-project/types': 0.139.0 - '@rolldown/pluginutils': 1.0.1 - optionalDependencies: - '@rolldown/binding-android-arm64': 1.1.5 - '@rolldown/binding-darwin-arm64': 1.1.5 - '@rolldown/binding-darwin-x64': 1.1.5 - '@rolldown/binding-freebsd-x64': 1.1.5 - '@rolldown/binding-linux-arm-gnueabihf': 1.1.5 - '@rolldown/binding-linux-arm64-gnu': 1.1.5 - '@rolldown/binding-linux-arm64-musl': 1.1.5 - '@rolldown/binding-linux-ppc64-gnu': 1.1.5 - '@rolldown/binding-linux-s390x-gnu': 1.1.5 - '@rolldown/binding-linux-x64-gnu': 1.1.5 - '@rolldown/binding-linux-x64-musl': 1.1.5 - '@rolldown/binding-openharmony-arm64': 1.1.5 - '@rolldown/binding-wasm32-wasi': 1.1.5 - '@rolldown/binding-win32-arm64-msvc': 1.1.5 - '@rolldown/binding-win32-x64-msvc': 1.1.5 - rolldown@1.2.1: dependencies: '@oxc-project/types': 0.142.0 @@ -3971,6 +3940,27 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.2.1 '@rolldown/binding-win32-x64-msvc': 1.2.1 + rolldown@1.2.6: + dependencies: + '@oxc-project/types': 0.147.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm-eabi': 1.2.6 + '@rolldown/binding-android-arm64': 1.2.6 + '@rolldown/binding-darwin-arm64': 1.2.6 + '@rolldown/binding-darwin-x64': 1.2.6 + '@rolldown/binding-freebsd-x64': 1.2.6 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.6 + '@rolldown/binding-linux-arm64-gnu': 1.2.6 + '@rolldown/binding-linux-arm64-musl': 1.2.6 + '@rolldown/binding-linux-ppc64-gnu': 1.2.6 + '@rolldown/binding-linux-s390x-gnu': 1.2.6 + '@rolldown/binding-linux-x64-gnu': 1.2.6 + '@rolldown/binding-linux-x64-musl': 1.2.6 + '@rolldown/binding-openharmony-arm64': 1.2.6 + '@rolldown/binding-win32-arm64-msvc': 1.2.6 + '@rolldown/binding-win32-x64-msvc': 1.2.6 + router@2.2.0(supports-color@7.2.0): dependencies: debug: 4.4.3(supports-color@7.2.0) @@ -4176,12 +4166,12 @@ snapshots: verkit@0.3.1: {} - vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(tsx@4.23.12): + vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.1)(tsx@4.23.12): dependencies: lightningcss: 1.33.0 picomatch: 4.0.5 - postcss: 8.5.25 - rolldown: 1.1.5 + postcss: 8.5.26 + rolldown: 1.2.6 tinyglobby: 0.2.17 optionalDependencies: '@types/node': 24.13.3 @@ -4189,10 +4179,10 @@ snapshots: fsevents: 2.3.3 tsx: 4.23.12 - vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.11)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(tsx@4.23.12)): + vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.11)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.1)(tsx@4.23.12)): dependencies: '@vitest/expect': 4.1.11 - '@vitest/mocker': 4.1.11(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(tsx@4.23.12)) + '@vitest/mocker': 4.1.11(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.1)(tsx@4.23.12)) '@vitest/pretty-format': 4.1.11 '@vitest/runner': 4.1.11 '@vitest/snapshot': 4.1.11 @@ -4209,7 +4199,7 @@ snapshots: tinyexec: 1.3.0 tinyglobby: 0.2.17 tinyrainbow: 3.1.1 - vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(tsx@4.23.12) + vite: 8.2.2(@types/node@24.13.3)(esbuild@0.28.1)(tsx@4.23.12) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.9.1