From a906808e575fb876989859e441591a60055d99da Mon Sep 17 00:00:00 2001 From: Sahil Date: Tue, 31 Mar 2026 19:21:53 +0530 Subject: [PATCH 01/11] refactor(skills-demo): update skills and prompts for HR management - Replaced the existing system prompt to reflect the new focus on HR operations, including employee onboarding and performance reviews. - Removed outdated skills related to revenue intelligence, customer health, frontend design, and incident management. - Introduced new skills for employee onboarding and performance reviews with updated icons and configurations. - Adjusted loading behavior to prevent duplicate skill calls in the conversation history. - Updated initial welcome message to align with the new HR Copilot theme. --- examples/skills-demo/server/index.ts | 13 +- .../skills-demo/skills/customer-health.md | 54 -------- .../skills-demo/skills/frontend-design.md | 57 --------- .../skills-demo/skills/incident-runbook.md | 57 --------- .../skills/revenue-intelligence.md | 41 ------ examples/skills-demo/src/App.tsx | 118 +++++++++--------- 6 files changed, 67 insertions(+), 273 deletions(-) delete mode 100644 examples/skills-demo/skills/customer-health.md delete mode 100644 examples/skills-demo/skills/frontend-design.md delete mode 100644 examples/skills-demo/skills/incident-runbook.md delete mode 100644 examples/skills-demo/skills/revenue-intelligence.md diff --git a/examples/skills-demo/server/index.ts b/examples/skills-demo/server/index.ts index 565c225..bc75c3c 100644 --- a/examples/skills-demo/server/index.ts +++ b/examples/skills-demo/server/index.ts @@ -71,15 +71,14 @@ console.log(`Using model: ${model}`); // ============================================ const systemPrompt = buildSystemPrompt( - `You are the AI Copilot for Dash, a SaaS analytics and operations platform. -You assist the team with revenue analysis, customer health monitoring, and incident response. + `You are the HR Copilot for an HR management platform. +You assist HR teams and managers with people operations, employee lifecycle, and team communication. When a user asks about: -- Revenue, MRR, churn, growth, or financial metrics → load the "revenue-intelligence" skill -- Customer risk, health scores, at-risk accounts, or engagement → load the "customer-health" skill -- Incidents, outages, production issues, or on-call → load the "incident-runbook" skill +- New hire setup, onboarding checklists, Day 1 plans, buddy programs, or 30/60/90 milestones → load the "employee-onboarding" skill +- Performance reviews, self-assessments, calibration, ratings, feedback, or promotions → load the "performance-review" skill -Always load the relevant skill before responding.`, +Load the relevant skill before responding — but only if you have not already loaded it earlier in this conversation. If the skill result is already present in the conversation history, use it directly without calling load_skill again.`, ); const runtime = createRuntime({ @@ -97,7 +96,7 @@ runtime.registerTool({ inputSchema: tools.load_skill.parameters, handler: async (params: { name: string }) => { // Delay so the shimmer animation is visible in the UI - await new Promise((r) => setTimeout(r, 2200)); + await new Promise((r) => setTimeout(r, 1200)); // Check dynamicSkills first before falling back to file-based skills const dynamic = dynamicSkills.find((s) => s.name === params.name); if (dynamic) { diff --git a/examples/skills-demo/skills/customer-health.md b/examples/skills-demo/skills/customer-health.md deleted file mode 100644 index f4dfe59..0000000 --- a/examples/skills-demo/skills/customer-health.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -name: customer-health -description: Score account health, surface at-risk customers, and identify engagement drop-off patterns -strategy: auto -version: 1.0.0 ---- - -## Customer Health Scoring Protocol - -You are now operating in Customer Health mode. Apply this framework when asked about customer risk, churn signals, account health, NPS, or engagement. - -### Health Score Dimensions - -Each account is scored 0–100 across five dimensions: - -| Dimension | Weight | Signal | -|-----------|--------|--------| -| Product Engagement | 30% | DAU/MAU ratio, feature adoption depth | -| Support Sentiment | 20% | Ticket volume, CSAT score, escalations | -| Contract Health | 20% | Renewal proximity, payment history | -| Growth Trajectory | 15% | Seat growth, usage expansion | -| Champion Strength | 15% | Stakeholder seniority, internal advocates | - -**Score Tiers:** -- 🟢 **Healthy** (75–100): Expansion candidate -- 🟡 **Neutral** (50–74): Monitor closely -- 🔴 **At Risk** (0–49): Immediate intervention required - -### At-Risk Detection Patterns - -Flag accounts showing: -- Login frequency drop > 30% over 14 days -- No new features adopted in 30+ days -- Ticket escalations in last 7 days -- Key champion changed roles or left -- Usage below 40% of contracted capacity - -### Intervention Playbooks - -**Red Account Playbook:** -1. CSM outreach within 24 hours -2. Executive business review within 2 weeks -3. Success plan refresh with clear milestones -4. Executive sponsor engagement if needed - -**Yellow Account Playbook:** -1. Check-in call within 1 week -2. Feature adoption webinar invitation -3. QBR scheduling - -### Output Format -- **Risk Summary** — headline risk level with reason -- **Top At-Risk Accounts** — ranked list with scores and key risk factor -- **Recommended Interventions** — specific next steps per account tier diff --git a/examples/skills-demo/skills/frontend-design.md b/examples/skills-demo/skills/frontend-design.md deleted file mode 100644 index ac479b5..0000000 --- a/examples/skills-demo/skills/frontend-design.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -name: frontend-design -description: Design and render beautiful UI components — payment cards, dashboards, stat grids, forms — using Tailwind CSS via the render_ui tool -strategy: auto -version: 1.0.0 ---- - -This skill guides creation of distinctive, production-grade UI components rendered via the `render_ui` tool. Avoid generic aesthetics. Every component should have a clear visual identity. - -## Output Format - -Always use the `render_ui` tool with `type: "html"` for UI components. -- Tailwind CSS (Play CDN) is pre-loaded in the iframe — use any utility class freely -- Chart.js is also available for embedded charts -- Set `height` to fit the content: `"240px"` for cards, `"500px"` for dashboards - -## Design Thinking - -Before generating, commit to a BOLD aesthetic direction: -- **Tone**: Pick an extreme — luxury/refined, brutally minimal, glassmorphism, editorial, retro-futuristic, art deco. Never default to generic. -- **Typography**: Use Google Fonts via `` tag. Distinctive choices only — no Inter, Roboto, or Arial. -- **Color**: Dominant background with sharp accent. Dark, rich palettes outperform washed-out light themes for cards and dashboards. -- **Details**: Grain overlays, gradient meshes, subtle borders, layered shadows — atmosphere beats flatness. - -## Component Guidance - -### Payment Cards -- Deep, rich background: navy, dark slate, charcoal, or gradient (never plain white) -- Chip icon (SVG or CSS), masked card number `•••• •••• •••• 4242`, cardholder name, expiry -- Network logo area (VISA / Mastercard wordmark in text is fine) -- Glassmorphism with `backdrop-filter: blur` works well -- Add subtle noise texture via SVG `feTurbulence` filter or CSS `background-image` -- Height: ~220–260px - -### Dashboards -- Dark base (`#0a0d14` or similar), grid of stat cards + chart -- Stat cards: metric label (uppercase, muted), large mono value, colored delta badge -- Use Chart.js inline for any charts -- Height: 480–600px - -### Stat Grids -- 3–4 column grid, each card: icon, value, label, trend -- Subtle borders, hover lift effect with CSS transition -- Height: ~180–200px - -### Forms / Auth Screens -- Single-column centered layout, generous padding -- Input fields with clear focus rings, matching aesthetic -- Height: ~380–440px - -## Style Rules - -NEVER use: Inter, Roboto, Arial, system-ui as primary fonts. NEVER use purple gradients on white. NEVER produce cookie-cutter shadcn defaults without a distinct personality on top. - -DO use: unexpected font pairings, asymmetric layouts, deliberate negative space, micro-animations via CSS `@keyframes`, decorative borders, and color that feels intentional. - -Every component should be something the user would screenshot and share. diff --git a/examples/skills-demo/skills/incident-runbook.md b/examples/skills-demo/skills/incident-runbook.md deleted file mode 100644 index a381ec4..0000000 --- a/examples/skills-demo/skills/incident-runbook.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -name: incident-runbook -description: Production incident response protocol with severity classification, checklists, and communication templates -strategy: manual -version: 1.0.0 ---- - -## Incident Response Runbook - -You are now in Incident Commander mode. Follow this protocol precisely for all production incidents. Speed and clarity save SLA. - -### Severity Classification - -| Level | Criteria | Response SLA | Example | -|-------|----------|-------------|---------| -| **P0** | Full outage, data loss risk | 15 min | Payments down, DB unavailable | -| **P1** | Core feature broken, >20% users affected | 30 min | Login failures, API errors | -| **P2** | Degraded performance, workaround exists | 2 hours | Slow queries, non-critical API | -| **P3** | Minor issue, cosmetic, < 5% users | 24 hours | UI glitch, edge-case bug | - -### Immediate Response Checklist (First 15 Minutes) - -**[ ] 1. Declare the incident** — post to #incidents with: severity, what is broken, first seen time -**[ ] 2. Assign roles** — Incident Commander, Technical Lead, Communications Lead -**[ ] 3. Start a war room** — Zoom / Slack huddle, record the link in the incident thread -**[ ] 4. Initial diagnosis** — check dashboards: error rate, latency, infra health -**[ ] 5. Scope assessment** — how many users affected? What regions? Which services? -**[ ] 6. Initial customer communication** — post status page update within 15 min of declaration - -### Diagnosis Checklist - -- Recent deploys in last 2 hours? → Roll back as first mitigation if yes -- Infrastructure alerts firing? → Check cloud provider status page -- Dependency failures? → Third-party APIs, payment processors, CDN -- Database issues? → Query performance, connection pool, replication lag -- Memory / CPU spikes? → Check K8s pods, auto-scaling events - -### Communication Templates - -**Status Page Update (initial):** -> We are investigating reports of [brief description]. Our engineering team is actively working on a resolution. We will provide an update within [X] minutes. - -**Customer Notification (P0/P1):** -> We are currently experiencing [service impact] affecting [scope]. This has been active since approximately [time]. We have identified the cause and are deploying a fix. Estimated resolution: [ETA]. - -**All-Clear:** -> This incident has been resolved as of [time]. Affected service: [name]. Root cause: [1 sentence]. Duration: [X min]. A full post-mortem will be shared within 48 hours. - -### Post-Incident Requirements - -Within 48 hours of resolution: -1. Write post-mortem document (timeline, root cause, contributing factors) -2. 5 Whys analysis -3. Action items with owners and due dates -4. Update runbook if gaps were found - -Always lead with facts. Give clear, time-stamped guidance. Panic spreads when information is absent. diff --git a/examples/skills-demo/skills/revenue-intelligence.md b/examples/skills-demo/skills/revenue-intelligence.md deleted file mode 100644 index fe259ea..0000000 --- a/examples/skills-demo/skills/revenue-intelligence.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -name: revenue-intelligence -description: Analyze MRR trends, churn impact, and expansion revenue signals with structured insights -strategy: auto -version: 1.0.0 ---- - -## Revenue Intelligence Protocol - -You are now operating in Revenue Intelligence mode. Apply this protocol when the user asks about revenue, MRR, churn, growth, or financial metrics. - -### Analysis Framework - -**1. Trend Identification** -- Identify the direction and velocity of MRR change -- Segment by New MRR, Expansion MRR, Contraction MRR, and Churned MRR -- Flag month-over-month deviations greater than ±5% - -**2. Churn Impact Assessment** -- Quantify the revenue impact of churned accounts -- Identify the top churned segments (plan tier, industry, company size) -- Separate voluntary vs involuntary churn (failed payments) - -**3. Expansion Revenue Signals** -- Identify accounts trending toward a plan upgrade based on usage patterns -- Score accounts by expansion probability (High / Medium / Low) -- Recommend specific upsell timing based on usage milestones - -**4. Forecast Guidance** -- Project next-90-day MRR based on current growth rate and churn -- Highlight key assumptions and risks in the forecast -- Suggest growth levers ranked by expected impact - -### Output Format -Always structure your response as: -- **Summary** (2–3 sentences with the key insight) -- **Breakdown** (structured data or bullets) -- **Recommended Actions** (top 2–3, ranked by impact) -- **Watch List** (metrics or accounts to monitor) - -Be specific with numbers. Reference the user's actual data when available. diff --git a/examples/skills-demo/src/App.tsx b/examples/skills-demo/src/App.tsx index 6282381..0395924 100644 --- a/examples/skills-demo/src/App.tsx +++ b/examples/skills-demo/src/App.tsx @@ -37,48 +37,26 @@ const SkillActivityContext = createContext({ // ─── Skill Domain Icons ─────────────────────────────────────────────────────── -function RevenueIcon() { +function OnboardingIcon() { return ( - - - - ); -} - -function HealthIcon() { - return ( - - - - ); } @@ -142,20 +122,16 @@ interface SkillConfig { } const SKILL_CONFIGS: Record = { - "revenue-intelligence": { + "employee-onboarding": { color: "#0d9488", bg: "rgba(13, 148, 136, 0.08)", - Icon: RevenueIcon, - }, - "customer-health": { - color: "#f59e0b", - bg: "rgba(245, 158, 11, 0.08)", - Icon: HealthIcon, + Icon: OnboardingIcon, }, - "incident-runbook": { - color: "#ef4444", - bg: "rgba(239, 68, 68, 0.08)", - Icon: IncidentIcon, + + "performance-review": { + color: "#2563eb", + bg: "rgba(37, 99, 235, 0.08)", + Icon: ReviewIcon, }, }; @@ -217,7 +193,7 @@ function TextShimmer({ // ─── Tool Renderers ─────────────────────────────────────────────────────────── function SkillLoadedCard({ execution }: ToolRendererProps) { - const { setExecutingSkill, addLoadedSkill } = + const { setExecutingSkill, addLoadedSkill, loadedSkills } = useContext(SkillActivityContext); const skillName = (execution.args?.name ?? @@ -245,6 +221,14 @@ function SkillLoadedCard({ execution }: ToolRendererProps) { // Guard phantom completed-without-result double-fire from SDK if (execution.status === "completed" && !execution.result) return null; + // Deduplicate: if this skill is already loaded (from a prior execution), skip the shimmer + if ( + (execution.status === "pending" || execution.status === "executing") && + loadedSkills.has(skillName) + ) { + return null; + } + if (execution.status === "pending" || execution.status === "executing") { return (
@@ -330,7 +314,7 @@ function CustomInput() { className="custom-prompt-input" > @@ -359,7 +343,7 @@ const INITIAL_MESSAGES = [ id: "welcome-1", role: "assistant" as const, content: - "Hey! I'm **Dash Copilot** — your AI assistant for this analytics platform.\n\nI can help you with:\n- **Revenue & MRR** — trends, churn, growth metrics\n- **Customer health** — at-risk accounts, engagement scores\n- **Incidents** — response runbooks, severity triage\n- **UI design** — render payment cards, dashboards, stat grids\n\nJust ask me anything to get started.", + "Hey! I'm your **HR Copilot** — your AI assistant for people operations.\n\nI can help you with:\n- **Employee Onboarding** — checklists, Day 1 plans, 30/60/90 milestones\n- **Performance Reviews** — review cycles, calibration, feedback frameworks\n\nJust ask me anything to get started.", createdAt: new Date(), }, ]; @@ -388,15 +372,35 @@ function LogoAvatar() { ); } +// ─── Message Debug Logger ───────────────────────────────────────────────────── + +function MessageLogger() { + const { messages } = useCopilot(); + useEffect(() => { + console.log( + `[SDK messages] count=${messages.length}`, + messages.map((m) => ({ + role: m.role, + content: + typeof m.content === "string" + ? m.content.slice(0, 80) + : JSON.stringify(m.content).slice(0, 120), + })), + ); + }, [messages]); + return null; +} + // ─── Chat Inner ─────────────────────────────────────────────────────────────── function ChatInner() { return (
+ Date: Tue, 31 Mar 2026 19:42:34 +0530 Subject: [PATCH 02/11] chore(llm-sdk): bump version to 2.1.4-alpha.3 and exclude source maps from package files - Updated the version in package.json to 2.1.4-alpha.3. - Excluded source map files from the package distribution by adding "!dist/**/*.map" to the files array. --- packages/llm-sdk/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/llm-sdk/package.json b/packages/llm-sdk/package.json index d9a5b25..32c9b7f 100644 --- a/packages/llm-sdk/package.json +++ b/packages/llm-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@yourgpt/llm-sdk", - "version": "2.1.4-alpha.2", + "version": "2.1.4-alpha.3", "description": "AI SDK for building AI Agents with any LLM", "main": "./dist/index.js", "module": "./dist/index.mjs", @@ -65,6 +65,7 @@ "homepage": "https://copilot-sdk.yourgpt.ai", "files": [ "dist", + "!dist/**/*.map", "README.md" ], "scripts": { From 785d9e789e3b421ba10f8c81c2c9a625cb4152e1 Mon Sep 17 00:00:00 2001 From: Sahil Date: Tue, 31 Mar 2026 19:45:01 +0530 Subject: [PATCH 03/11] chore(llm-sdk): bump version to 2.1.5 in package.json --- packages/llm-sdk/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/llm-sdk/package.json b/packages/llm-sdk/package.json index 32c9b7f..f34a073 100644 --- a/packages/llm-sdk/package.json +++ b/packages/llm-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@yourgpt/llm-sdk", - "version": "2.1.4-alpha.3", + "version": "2.1.5", "description": "AI SDK for building AI Agents with any LLM", "main": "./dist/index.js", "module": "./dist/index.mjs", From e29007c5e62f97a0733a7ce10732ce53a18e222e Mon Sep 17 00:00:00 2001 From: Sahil Date: Wed, 1 Apr 2026 14:06:55 +0530 Subject: [PATCH 04/11] fix(runtime): execute server-side tools inline during stream for correct event ordering Previously, server-side tools were executed post-loop after the adapter's for-await finished, causing message:end to arrive before action:end(result). This broke client message splitting and rendered skill cards below the assistant response instead of above. Changes: - llm-sdk/runtime: execute server tools inline in case "action:end" before message:end arrives naturally from the adapter, removing all event-ordering hacks - copilot-sdk/AbstractChat: split message turn on toolResults.size > 0 (not just text) so tool-only turns are correctly finalized at message:end - copilot-sdk/AbstractChat: skip server-side assistantWithToolCalls in done handler to prevent duplicate tool card renders in the UI - examples/playground: use workspace:* deps and add transpilePackages for Turbopack Co-Authored-By: Claude Sonnet 4.6 --- examples/playground/next.config.ts | 1 + examples/playground/package.json | 4 +- .../src/chat/classes/AbstractChat.ts | 15 +- packages/llm-sdk/src/server/runtime.ts | 190 +++++++++--------- pnpm-lock.yaml | 135 +------------ 5 files changed, 119 insertions(+), 226 deletions(-) diff --git a/examples/playground/next.config.ts b/examples/playground/next.config.ts index c2810f7..73f03f7 100644 --- a/examples/playground/next.config.ts +++ b/examples/playground/next.config.ts @@ -3,6 +3,7 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { basePath: "/playground", allowedDevOrigins: ["*.trycloudflare.com"], + transpilePackages: ["@yourgpt/copilot-sdk", "@yourgpt/llm-sdk"], }; export default nextConfig; diff --git a/examples/playground/package.json b/examples/playground/package.json index 2cf8ef5..57d9aa9 100644 --- a/examples/playground/package.json +++ b/examples/playground/package.json @@ -27,8 +27,8 @@ "@radix-ui/react-switch": "^1.2.3", "@radix-ui/react-tabs": "^1.1.13", "@tailwindcss/typography": "^0.5.19", - "@yourgpt/copilot-sdk": "2.1.5-alpha.8", - "@yourgpt/llm-sdk": "2.1.4-alpha.2", + "@yourgpt/copilot-sdk": "workspace:*", + "@yourgpt/llm-sdk": "workspace:*", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", diff --git a/packages/copilot-sdk/src/chat/classes/AbstractChat.ts b/packages/copilot-sdk/src/chat/classes/AbstractChat.ts index 3fa471c..51a5fff 100644 --- a/packages/copilot-sdk/src/chat/classes/AbstractChat.ts +++ b/packages/copilot-sdk/src/chat/classes/AbstractChat.ts @@ -1109,7 +1109,13 @@ export class AbstractChat { // Handle message:end mid-stream (server-side agent loop turn completed) // This creates separate messages for each turn instead of combining them - if (chunk.type === "message:end" && this.streamState?.content) { + // Split on text content OR server-side tool executions (no text, tool-only turns) + if ( + chunk.type === "message:end" && + this.streamState !== null && + (this.streamState.content || + (this.streamState.toolResults?.size ?? 0) > 0) + ) { this.debug("message:end mid-stream", { messageId: this.streamState.messageId, contentLength: this.streamState.content.length, @@ -1236,6 +1242,13 @@ export class AbstractChat { } // Skip plain assistant text — already streamed if (msg.role === "assistant" && !msg.tool_calls?.length) continue; + // Skip server-side tool assistant messages — already represented in streamed toolExecutions + if ( + msg.role === "assistant" && + msg.tool_calls?.length && + pendingIds.size === 0 + ) + continue; // Everything else (server tool results) needs inserting messagesToInsert.push({ id: generateMessageId(), diff --git a/packages/llm-sdk/src/server/runtime.ts b/packages/llm-sdk/src/server/runtime.ts index 563259d..38b5c9e 100644 --- a/packages/llm-sdk/src/server/runtime.ts +++ b/packages/llm-sdk/src/server/runtime.ts @@ -1011,6 +1011,20 @@ export class Runtime { const toolCalls: ToolCallInfo[] = []; let currentToolCall: { id: string; name: string; args: string } | null = null; + + // Server-side tool results (populated inline during stream, before message:end) + const serverToolResults: Array<{ + id: string; + name: string; + args: Record; + result: unknown; + tool: ToolDefinition; + }> = []; + + // Tool context data for server-side tool handlers + const toolContextData = + "toolContext" in this.config ? this.config.toolContext : undefined; + // Capture usage from adapter for onFinish callback (server-side only) let adapterUsage: | { @@ -1044,10 +1058,13 @@ export class Runtime { for await (const event of stream) { switch (event.type) { case "message:start": - case "message:end": yield event; // Forward to client break; + case "message:end": + yield event; // Natural order — always arrives after action:end from every provider + break; + case "message:delta": accumulatedText += event.content; yield event; // Forward text to client @@ -1093,6 +1110,73 @@ export class Runtime { yield event; // Forward to client break; + case "action:end": { + const toolName = (event as StreamEvent & { name?: string }).name; + const tool = toolName ? selectedToolMap.get(toolName) : undefined; + + if (tool?.location === "server" && tool.handler) { + // Execute server-side tool inline — before message:end arrives naturally + // This preserves the correct event order: action:end(result) → message:end + if (debug) { + console.log( + `[Copilot SDK] Executing server-side tool: ${toolName}`, + ); + } + const tc = toolCalls.find((t) => t.id === event.id); + const args = tc?.args ?? {}; + const toolContext = buildToolContext( + event.id, + signal, + request.threadId, + _httpRequest, + toolContextData, + ); + try { + const result = await tool.handler(args, toolContext); + serverToolResults.push({ + id: event.id, + name: toolName!, + args, + result, + tool, + }); + yield { + type: "action:end", + id: event.id, + name: toolName, + result, + } as StreamEvent; + } catch (error) { + const errorResult = { + success: false, + error: + error instanceof Error + ? error.message + : "Tool execution failed", + }; + serverToolResults.push({ + id: event.id, + name: toolName!, + args, + result: errorResult, + tool, + }); + yield { + type: "action:end", + id: event.id, + name: toolName, + error: + error instanceof Error + ? error.message + : "Tool execution failed", + } as StreamEvent; + } + } else { + yield event; // Client-side tool — forward as-is + } + break; + } + case "citation": // Forward web search citations to client yield event; @@ -1124,92 +1208,11 @@ export class Runtime { ); } - // Separate server-side and client-side tool calls - const serverToolCalls: ToolCallInfo[] = []; - const clientToolCalls: ToolCallInfo[] = []; - - for (const tc of toolCalls) { - const tool = selectedToolMap.get(tc.name); - if (tool?.location === "server" && tool.handler) { - serverToolCalls.push(tc); - } else { - clientToolCalls.push(tc); - } - } - - // Execute server-side tools - const serverToolResults: Array<{ - id: string; - name: string; - args: Record; - result: unknown; - tool: ToolDefinition; - }> = []; - - // Get toolContext from config (if available) - const toolContextData = - "toolContext" in this.config ? this.config.toolContext : undefined; - - for (const tc of serverToolCalls) { - const tool = selectedToolMap.get(tc.name); - if (tool?.handler) { - if (debug) { - console.log(`[Copilot SDK] Executing server-side tool: ${tc.name}`); - } - - // Build rich context for the tool handler - const toolContext = buildToolContext( - tc.id, - signal, - request.threadId, - _httpRequest, - toolContextData, - ); - - try { - const result = await tool.handler(tc.args, toolContext); - serverToolResults.push({ - id: tc.id, - name: tc.name, - args: tc.args, - result, - tool, - }); - - yield { - type: "action:end", - id: tc.id, - name: tc.name, - result, - } as StreamEvent; - } catch (error) { - const errorResult = { - success: false, - error: - error instanceof Error - ? error.message - : "Tool execution failed", - }; - serverToolResults.push({ - id: tc.id, - name: tc.name, - args: tc.args, - result: errorResult, - tool, - }); - - yield { - type: "action:end", - id: tc.id, - name: tc.name, - error: - error instanceof Error - ? error.message - : "Tool execution failed", - } as StreamEvent; - } - } - } + // Client-side tool calls = those not executed server-side inline + const serverToolIds = new Set(serverToolResults.map((r) => r.id)); + const clientToolCalls = toolCalls.filter( + (tc) => !serverToolIds.has(tc.id), + ); // If there are server-side tools executed, continue the loop by making another LLM call if (serverToolResults.length > 0) { @@ -1223,12 +1226,12 @@ export class Runtime { const assistantWithToolCalls: DoneEventMessage = { role: "assistant", content: accumulatedText || null, - tool_calls: serverToolCalls.map((tc) => ({ - id: tc.id, + tool_calls: serverToolResults.map((tr) => ({ + id: tr.id, type: "function" as const, function: { - name: tc.name, - arguments: JSON.stringify(tc.args), + name: tr.name, + arguments: JSON.stringify(tr.args), }, })), }; @@ -1274,11 +1277,6 @@ export class Runtime { })), ); - // Signal end of current message turn before continuing - // This tells the client to finalize the current assistant message - // The recursive call will emit a new message:start for the next turn - yield { type: "message:end" } as StreamEvent; - // Continue the agent loop - pass accumulated messages and HTTP request for await (const event of this.processChatWithLoop( nextRequest, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f5c0b7c..70d951d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -842,11 +842,11 @@ importers: specifier: ^0.5.19 version: 0.5.19(tailwindcss@4.1.18) '@yourgpt/copilot-sdk': - specifier: 2.1.5-alpha.8 - version: 2.1.5-alpha.8(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + specifier: workspace:* + version: link:../../packages/copilot-sdk '@yourgpt/llm-sdk': - specifier: 2.1.4-alpha.2 - version: 2.1.4-alpha.2(@anthropic-ai/sdk@0.71.2(zod@3.25.76))(@google/generative-ai@0.24.1)(openai@6.16.0(ws@8.18.0)(zod@3.25.76)) + specifier: workspace:* + version: link:../../packages/llm-sdk class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -4554,33 +4554,6 @@ packages: babel-plugin-react-compiler: optional: true - '@yourgpt/copilot-sdk@2.1.5-alpha.8': - resolution: {integrity: sha512-5dtH/F8rmlv+V78xTnMoEBAMQYAr1YGCxNWyX+2V004xTHzgqTsdgZCNaYlFkNbv0PMNCwTfH9hfabhBEviVkQ==} - engines: {node: '>=18'} - peerDependencies: - react: ^18.0.0 || ^19.0.0 - react-dom: ^18.0.0 || ^19.0.0 - peerDependenciesMeta: - react: - optional: true - react-dom: - optional: true - - '@yourgpt/llm-sdk@2.1.4-alpha.2': - resolution: {integrity: sha512-0m9ZtwSOxxJdG3Bx0S81StoDqVTqzKw7wTg7Lgnp4ofk388ZqtAdaAMUhNz6E5Y2DH8w3eCDx9M63MgUUWOO9Q==} - engines: {node: '>=18'} - peerDependencies: - '@anthropic-ai/sdk': '>=0.20.0' - '@google/generative-ai': '>=0.21.0' - openai: '>=4.0.0' - peerDependenciesMeta: - '@anthropic-ai/sdk': - optional: true - '@google/generative-ai': - optional: true - openai: - optional: true - abort-controller@3.0.0: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} @@ -8886,20 +8859,6 @@ snapshots: optionalDependencies: '@types/react': 18.3.27 - '@base-ui/react@1.0.0(@types/react@18.3.27)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': - dependencies: - '@babel/runtime': 7.28.4 - '@base-ui/utils': 0.2.3(@types/react@18.3.27)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@floating-ui/react-dom': 2.1.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@floating-ui/utils': 0.2.10 - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - reselect: 5.1.1 - tabbable: 6.3.0 - use-sync-external-store: 1.6.0(react@19.2.3) - optionalDependencies: - '@types/react': 18.3.27 - '@base-ui/utils@0.2.3(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.28.4 @@ -8911,17 +8870,6 @@ snapshots: optionalDependencies: '@types/react': 18.3.27 - '@base-ui/utils@0.2.3(@types/react@18.3.27)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': - dependencies: - '@babel/runtime': 7.28.4 - '@floating-ui/utils': 0.2.10 - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - reselect: 5.1.1 - use-sync-external-store: 1.6.0(react@19.2.3) - optionalDependencies: - '@types/react': 18.3.27 - '@changesets/apply-release-plan@7.0.14': dependencies: '@changesets/config': 3.1.2 @@ -11556,11 +11504,6 @@ snapshots: react: 18.3.1 shiki: 3.20.0 - '@streamdown/code@1.0.1(react@19.2.3)': - dependencies: - react: 19.2.3 - shiki: 3.20.0 - '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 @@ -12066,40 +12009,6 @@ snapshots: '@rolldown/pluginutils': 1.0.0-rc.7 vite: 8.0.3(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@20.19.27)(esbuild@0.27.1)(jiti@2.6.1)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2) - '@yourgpt/copilot-sdk@2.1.5-alpha.8(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': - dependencies: - '@base-ui/react': 1.0.0(@types/react@18.3.27)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-avatar': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-hover-card': 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-slot': 1.2.4(@types/react@18.3.27)(react@19.2.3) - '@radix-ui/react-tooltip': 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@streamdown/code': 1.0.1(react@19.2.3) - class-variance-authority: 0.7.1 - clsx: 2.1.1 - html-to-image: 1.11.13 - html2canvas: 1.4.1 - lucide-react: 0.561.0(react@19.2.3) - streamdown: 2.1.0(react@19.2.3) - tailwind-merge: 3.4.0 - use-stick-to-bottom: 1.1.1(react@19.2.3) - zod: 3.25.76 - optionalDependencies: - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - transitivePeerDependencies: - - '@types/react' - - '@types/react-dom' - - supports-color - - '@yourgpt/llm-sdk@2.1.4-alpha.2(@anthropic-ai/sdk@0.71.2(zod@3.25.76))(@google/generative-ai@0.24.1)(openai@6.16.0(ws@8.18.0)(zod@3.25.76))': - dependencies: - hono: 4.11.0 - zod: 3.25.76 - optionalDependencies: - '@anthropic-ai/sdk': 0.71.2(zod@3.25.76) - '@google/generative-ai': 0.24.1 - openai: 6.16.0(ws@8.18.0)(zod@3.25.76) - abort-controller@3.0.0: dependencies: event-target-shim: 5.0.1 @@ -12973,7 +12882,7 @@ snapshots: eslint: 9.39.2(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) - eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-react-hooks: 7.0.1(eslint@9.39.2(jiti@2.6.1)) @@ -12993,7 +12902,7 @@ snapshots: eslint: 9.39.2(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) - eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-react-hooks: 7.0.1(eslint@9.39.2(jiti@2.6.1)) @@ -13026,7 +12935,7 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -13095,7 +13004,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)): + eslint-plugin-import@2.32.0(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -14459,10 +14368,6 @@ snapshots: dependencies: react: 18.3.1 - lucide-react@0.561.0(react@19.2.3): - dependencies: - react: 19.2.3 - lucide-react@0.562.0(react@19.2.1): dependencies: react: 19.2.1 @@ -16339,26 +16244,6 @@ snapshots: transitivePeerDependencies: - supports-color - streamdown@2.1.0(react@19.2.3): - dependencies: - clsx: 2.1.1 - hast-util-to-jsx-runtime: 2.3.6 - html-url-attributes: 3.0.1 - marked: 17.0.1 - react: 19.2.3 - rehype-harden: 1.1.7 - rehype-raw: 7.0.0 - rehype-sanitize: 6.0.0 - remark-gfm: 4.0.1 - remark-parse: 11.0.0 - remark-rehype: 11.1.2 - remend: 1.1.0 - tailwind-merge: 3.4.0 - unified: 11.0.5 - unist-util-visit: 5.0.0 - transitivePeerDependencies: - - supports-color - strict-event-emitter@0.5.1: {} string-argv@0.3.2: {} @@ -16862,10 +16747,6 @@ snapshots: dependencies: react: 18.3.1 - use-stick-to-bottom@1.1.1(react@19.2.3): - dependencies: - react: 19.2.3 - use-sync-external-store@1.6.0(react@18.3.1): dependencies: react: 18.3.1 From 9d35b6e270f84ff6dd5bea46fb527d3cbc97b14f Mon Sep 17 00:00:00 2001 From: Sahil Date: Wed, 1 Apr 2026 14:08:13 +0530 Subject: [PATCH 05/11] chore(copilot-sdk): bump version to 2.1.6 in package.json --- packages/copilot-sdk/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/copilot-sdk/package.json b/packages/copilot-sdk/package.json index 1d6891d..c748d8c 100644 --- a/packages/copilot-sdk/package.json +++ b/packages/copilot-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@yourgpt/copilot-sdk", - "version": "2.1.5-alpha.8", + "version": "2.1.6", "description": "Copilot SDK for building Production-ready AI Copilots for any product. Connect any LLM, deploy on your infrastructure, own your data.", "type": "module", "types": "./dist/core/index.d.ts", From 5d1e4dc976dfbb9cd46e4de34e2961311b51e372 Mon Sep 17 00:00:00 2001 From: Sahil Date: Thu, 2 Apr 2026 12:04:58 +0530 Subject: [PATCH 06/11] feat(skills-demo): add current date tool and enhance tool rendering - Introduced a new tool for retrieving the current date, including its day of the week. - Updated tool rendering logic to handle the new date tool and improve UI feedback during execution. - Adjusted the handling of assistant messages to prevent duplicate rendering of tool results in the chat interface. --- examples/skills-demo/src/App.tsx | 57 ++++++++++++++++++- .../src/chat/classes/AbstractChat.ts | 21 ++++--- 2 files changed, 69 insertions(+), 9 deletions(-) diff --git a/examples/skills-demo/src/App.tsx b/examples/skills-demo/src/App.tsx index 0395924..6df4f3b 100644 --- a/examples/skills-demo/src/App.tsx +++ b/examples/skills-demo/src/App.tsx @@ -6,7 +6,11 @@ import { useCallback, } from "react"; import { Drawer } from "vaul"; -import { CopilotProvider, useCopilot } from "@yourgpt/copilot-sdk/react"; +import { + CopilotProvider, + useCopilot, + useTool, +} from "@yourgpt/copilot-sdk/react"; import { CopilotChat, PromptInput, @@ -287,7 +291,32 @@ function FallbackToolCard({ execution }: ToolRendererProps) { ); } -const toolRenderers = { load_skill: SkillLoadedCard }; +function DateToolCard({ execution }: ToolRendererProps) { + if (execution.status === "pending" || execution.status === "executing") { + return ( +
+ + Checking current date… +
+ ); + } + if (execution.status === "error" || execution.status === "failed") + return null; + const date = (execution.result as { date?: string })?.date ?? ""; + return ( +
+ +

+ Date: {date} +

+
+ ); +} + +const toolRenderers = { + load_skill: SkillLoadedCard, + get_current_date: DateToolCard, +}; // ─── Custom Fixed Input ─────────────────────────────────────────────────────── // Uses useCopilot() (CopilotProvider-level) instead of useCopilotChatContext() @@ -394,6 +423,30 @@ function MessageLogger() { // ─── Chat Inner ─────────────────────────────────────────────────────────────── function ChatInner() { + useTool({ + name: "get_current_date", + description: + "Returns today's date and day of week from the client. Use this when the user asks about deadlines, timelines, or scheduling relative to today.", + inputSchema: { + type: "object", + properties: {}, + required: [], + }, + handler: async () => { + const now = new Date(); + return { + date: now.toLocaleDateString("en-US", { + weekday: "long", + year: "numeric", + month: "long", + day: "numeric", + }), + iso: now.toISOString().split("T")[0], + timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, + }; + }, + }); + return (
diff --git a/packages/copilot-sdk/src/chat/classes/AbstractChat.ts b/packages/copilot-sdk/src/chat/classes/AbstractChat.ts index 51a5fff..23e3c2d 100644 --- a/packages/copilot-sdk/src/chat/classes/AbstractChat.ts +++ b/packages/copilot-sdk/src/chat/classes/AbstractChat.ts @@ -1242,11 +1242,21 @@ export class AbstractChat { } // Skip plain assistant text — already streamed if (msg.role === "assistant" && !msg.tool_calls?.length) continue; - // Skip server-side tool assistant messages — already represented in streamed toolExecutions + // Skip assistant messages whose tool_calls are all server-side (not in pendingIds) + // These are already represented in streamed toolExecutions — inserting would duplicate the card if ( msg.role === "assistant" && msg.tool_calls?.length && - pendingIds.size === 0 + (msg.tool_calls as Array<{ id?: string }>).every( + (tc) => !pendingIds.has(tc?.id ?? ""), + ) + ) + continue; + // Skip tool result messages for client-side tools — client already executed them + if ( + msg.role === "tool" && + msg.tool_call_id && + pendingIds.has(msg.tool_call_id) ) continue; // Everything else (server tool results) needs inserting @@ -1414,11 +1424,8 @@ export class AbstractChat { }); // Adopt threadId from server storage adapter (if present) - if ( - chunk.type === "done" && - (chunk as { threadId?: string }).threadId - ) { - const serverThreadId = (chunk as { threadId?: string }).threadId!; + if (chunk.type === "done" && chunk.threadId) { + const serverThreadId = chunk.threadId; if ( !this.config.threadId || this.config.threadId !== serverThreadId From 4890f266d8a5b269d064393462ed40bd5e946ac9 Mon Sep 17 00:00:00 2001 From: Sahil Date: Mon, 6 Apr 2026 13:31:56 +0530 Subject: [PATCH 07/11] feat(playground): add yourgpt-server integration and enhance API key handling - Introduced a new provider configuration for "yourgpt-server" in constants and types. - Added a proxy route for yourgpt-server to handle streaming and non-streaming requests. - Updated PlaygroundPage to conditionally check for API keys based on the selected provider. - Bumped versions for copilot-sdk and llm-sdk to reflect recent changes. --- .../app/api/yourgpt-server/route.ts | 37 +++ examples/playground/app/page.tsx | 3 +- examples/playground/lib/constants.ts | 13 + examples/playground/lib/types.ts | 4 +- packages/copilot-sdk/package.json | 2 +- .../src/chat/classes/AbstractChat.ts | 11 + .../src/chat/interfaces/ChatTransport.ts | 5 + packages/copilot-sdk/src/ui/styles/base.css | 21 ++ packages/llm-sdk/package.json | 2 +- packages/llm-sdk/src/adapters/base.ts | 14 +- packages/llm-sdk/src/adapters/index.ts | 2 +- packages/llm-sdk/src/adapters/openai.ts | 55 +++- packages/llm-sdk/src/adapters/xai.ts | 304 +----------------- packages/llm-sdk/src/core/stream-events.ts | 21 ++ .../llm-sdk/src/providers/google/index.ts | 31 +- packages/llm-sdk/src/server/runtime.ts | 59 +++- 16 files changed, 267 insertions(+), 317 deletions(-) create mode 100644 examples/playground/app/api/yourgpt-server/route.ts diff --git a/examples/playground/app/api/yourgpt-server/route.ts b/examples/playground/app/api/yourgpt-server/route.ts new file mode 100644 index 0000000..683b478 --- /dev/null +++ b/examples/playground/app/api/yourgpt-server/route.ts @@ -0,0 +1,37 @@ +/** + * Proxy to local yourgpt-server-demo for testing SDK stream/non-stream endpoints. + * + * Routes based on `streaming` field in the request body: + * streaming: true → /api/copilot/stream (SSE) + * streaming: false → /api/copilot/chat (JSON) + * + * Set YOURGPT_SERVER_URL in .env.local to point at your local server. + * Default: http://localhost:3001 + */ + +const SERVER_URL = process.env.YOURGPT_SERVER_URL || "http://localhost:3001"; + +export async function POST(request: Request) { + const body = await request.json(); + const isStreaming = body.streaming !== false; + const endpoint = isStreaming ? "/api/copilot/stream" : "/api/copilot/chat"; + const targetUrl = `${SERVER_URL}${endpoint}`; + + const upstream = await fetch(targetUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + + // Pass the response body (streamed or JSON) straight through + return new Response(upstream.body, { + status: upstream.status, + headers: { + "Content-Type": + upstream.headers.get("Content-Type") ?? "application/json", + // Forward cache-control so SSE isn't buffered + "Cache-Control": "no-cache", + "X-Accel-Buffering": "no", + }, + }); +} diff --git a/examples/playground/app/page.tsx b/examples/playground/app/page.tsx index ba2d863..5d6dc05 100644 --- a/examples/playground/app/page.tsx +++ b/examples/playground/app/page.tsx @@ -119,7 +119,8 @@ export default function PlaygroundPage() { }, [actions]); // Derived state - const hasApiKey = !!apiKeys[selectedProvider]; + const hasApiKey = + selectedProvider === "yourgpt-server" || !!apiKeys[selectedProvider]; // Don't render until mounted (avoid hydration issues) if (!mounted) return null; diff --git a/examples/playground/lib/constants.ts b/examples/playground/lib/constants.ts index 4d4f1b8..71ee2f5 100644 --- a/examples/playground/lib/constants.ts +++ b/examples/playground/lib/constants.ts @@ -102,6 +102,18 @@ export const providers: ProviderConfig[] = [ createProvider: "createOpenRouter", importPath: "@yourgpt/llm-sdk/openrouter", }, + { + id: "yourgpt-server", + name: "YourGPT Server", + model: "local demo", + color: "#f59e0b", + keyPlaceholder: "", + keyLink: "", + keyLinkText: "", + envVar: "", + createProvider: "", + importPath: "", + }, ]; // Sample person data for useAIContext demo @@ -161,6 +173,7 @@ export const INITIAL_API_KEYS: ApiKeys = { google: "", xai: "", openrouter: "", + "yourgpt-server": "", }; // OpenRouter model options for the model selector (static fallback) diff --git a/examples/playground/lib/types.ts b/examples/playground/lib/types.ts index 5129216..a31fd21 100644 --- a/examples/playground/lib/types.ts +++ b/examples/playground/lib/types.ts @@ -35,6 +35,7 @@ export interface ApiKeys { google: string; xai: string; openrouter: string; + "yourgpt-server"?: string; } export type ProviderId = @@ -42,7 +43,8 @@ export type ProviderId = | "anthropic" | "google" | "xai" - | "openrouter"; + | "openrouter" + | "yourgpt-server"; export interface ProviderConfig { id: ProviderId; diff --git a/packages/copilot-sdk/package.json b/packages/copilot-sdk/package.json index c748d8c..35d01c6 100644 --- a/packages/copilot-sdk/package.json +++ b/packages/copilot-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@yourgpt/copilot-sdk", - "version": "2.1.6", + "version": "2.1.7", "description": "Copilot SDK for building Production-ready AI Copilots for any product. Connect any LLM, deploy on your infrastructure, own your data.", "type": "module", "types": "./dist/core/index.d.ts", diff --git a/packages/copilot-sdk/src/chat/classes/AbstractChat.ts b/packages/copilot-sdk/src/chat/classes/AbstractChat.ts index 23e3c2d..fd2c05f 100644 --- a/packages/copilot-sdk/src/chat/classes/AbstractChat.ts +++ b/packages/copilot-sdk/src/chat/classes/AbstractChat.ts @@ -1410,6 +1410,17 @@ export class AbstractChat { this.callbacks.onMessageDelta?.(assistantMessage.id, chunk.content); } + // Adopt threadId early — emitted by server before any message events + if (chunk.type === "thread:created") { + const serverThreadId = chunk.threadId; + if (!this.config.threadId || this.config.threadId !== serverThreadId) { + this.config.threadId = serverThreadId; + this.sessionInitPromise = null; + this.setSessionStatus("ready"); + this.callbacks.onThreadChange?.(serverThreadId); + } + } + // Check for completion if (isStreamDone(chunk)) { this.debug("streamDone", { diff --git a/packages/copilot-sdk/src/chat/interfaces/ChatTransport.ts b/packages/copilot-sdk/src/chat/interfaces/ChatTransport.ts index 17f5fa4..6babaea 100644 --- a/packages/copilot-sdk/src/chat/interfaces/ChatTransport.ts +++ b/packages/copilot-sdk/src/chat/interfaces/ChatTransport.ts @@ -108,6 +108,11 @@ export type StreamChunk = | { type: "tool_calls"; toolCalls: unknown[]; assistantMessage: unknown } | { type: "source:add"; source: unknown } | { type: "error"; message: string } + | { + /** Emitted early in the stream (before message events) when server storage creates a session */ + type: "thread:created"; + threadId: string; + } | { type: "done"; messages?: Array<{ diff --git a/packages/copilot-sdk/src/ui/styles/base.css b/packages/copilot-sdk/src/ui/styles/base.css index baea8cc..5841221 100644 --- a/packages/copilot-sdk/src/ui/styles/base.css +++ b/packages/copilot-sdk/src/ui/styles/base.css @@ -301,3 +301,24 @@ color: hsl(var(--muted-foreground)); } +/* ── Streamdown code block overrides ────────────────────────────────────────── + * @streamdown/code injects hardcoded Tailwind classes (bg-sidebar, bg-background) + * which can conflict with the host app's theme. We override them here using + * SDK-scoped CSS variables so consumers can customise via --csdk-code-* vars. + * --------------------------------------------------------------------------- */ + +.csdk-message-content [data-streamdown="code-block"] { + background: var(--csdk-code-bg, hsl(var(--muted))); + border-color: var(--csdk-code-border, hsl(var(--border))); + border-radius: var(--csdk-code-radius, 0.5rem); +} + +.csdk-message-content [data-streamdown="code-block-body"] { + background: var(--csdk-code-body-bg, hsl(var(--card))); + border-color: var(--csdk-code-border, hsl(var(--border))); +} + +.csdk-message-content [data-streamdown="code-block-header"] { + color: hsl(var(--muted-foreground)); +} + diff --git a/packages/llm-sdk/package.json b/packages/llm-sdk/package.json index f34a073..08fe24d 100644 --- a/packages/llm-sdk/package.json +++ b/packages/llm-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@yourgpt/llm-sdk", - "version": "2.1.5", + "version": "2.1.6", "description": "AI SDK for building AI Agents with any LLM", "main": "./dist/index.js", "module": "./dist/index.mjs", diff --git a/packages/llm-sdk/src/adapters/base.ts b/packages/llm-sdk/src/adapters/base.ts index e788b67..dca81e2 100644 --- a/packages/llm-sdk/src/adapters/base.ts +++ b/packages/llm-sdk/src/adapters/base.ts @@ -59,7 +59,13 @@ export interface CompletionResult { /** Text content */ content: string; /** Tool calls */ - toolCalls: Array<{ id: string; name: string; args: Record }>; + toolCalls: Array<{ + id: string; + name: string; + args: Record; + /** Provider-specific metadata (e.g. Gemini 3 thought_signature in extra_content.google) */ + extra_content?: Record; + }>; /** Thinking content (if extended thinking enabled) */ thinking?: string; /** Token usage for billing/tracking */ @@ -767,11 +773,13 @@ export function formatMessagesForOpenAI( content: messageToOpenAIContent(msg), }); } else if (msg.role === "assistant") { + const hasToolCalls = msg.tool_calls && msg.tool_calls.length > 0; const assistantMsg: OpenAIMessage = { role: "assistant", - content: msg.content, + // Gemini/xAI (OpenAI-compatible) reject content: "" on assistant messages with tool_calls + content: hasToolCalls ? msg.content || null : msg.content, }; - if (msg.tool_calls && msg.tool_calls.length > 0) { + if (hasToolCalls) { (assistantMsg as { tool_calls: typeof msg.tool_calls }).tool_calls = msg.tool_calls; } diff --git a/packages/llm-sdk/src/adapters/index.ts b/packages/llm-sdk/src/adapters/index.ts index af040f0..8832b15 100644 --- a/packages/llm-sdk/src/adapters/index.ts +++ b/packages/llm-sdk/src/adapters/index.ts @@ -51,7 +51,7 @@ export { } from "./google"; // xAI Grok -export { XAIAdapter, createXAIAdapter, type XAIAdapterConfig } from "./xai"; +export { createXAIAdapter, type XAIAdapterConfig } from "./xai"; // Azure OpenAI export { diff --git a/packages/llm-sdk/src/adapters/openai.ts b/packages/llm-sdk/src/adapters/openai.ts index 36d96fd..d134de8 100644 --- a/packages/llm-sdk/src/adapters/openai.ts +++ b/packages/llm-sdk/src/adapters/openai.ts @@ -40,7 +40,7 @@ export interface OpenAIAdapterConfig { * Supports: GPT-4, GPT-4o, GPT-3.5-turbo, etc. */ export class OpenAIAdapter implements LLMAdapter { - readonly provider = "openai"; + readonly provider: string; readonly model: string; private client: any; // OpenAI client (lazy loaded) @@ -49,6 +49,15 @@ export class OpenAIAdapter implements LLMAdapter { constructor(config: OpenAIAdapterConfig) { this.config = config; this.model = config.model || "gpt-4o"; + this.provider = OpenAIAdapter.resolveProviderName(config.baseUrl); + } + + private static resolveProviderName(baseUrl?: string): string { + if (!baseUrl) return "openai"; + if (baseUrl.includes("generativelanguage.googleapis.com")) return "google"; + if (baseUrl.includes("x.ai")) return "xai"; + if (baseUrl.includes("azure")) return "azure"; + return "openai"; } private async getClient() { @@ -294,6 +303,17 @@ export class OpenAIAdapter implements LLMAdapter { if (request.rawMessages && request.rawMessages.length > 0) { // Process raw messages - convert any attachments to OpenAI vision format const processedMessages = request.rawMessages.map((msg) => { + // Normalize assistant messages with tool_calls: empty string content → null + // Gemini/xAI (OpenAI-compatible) reject content: "" on assistant messages with tool_calls + if ( + msg.role === "assistant" && + Array.isArray(msg.tool_calls) && + msg.tool_calls.length > 0 && + msg.content === "" + ) { + return { ...msg, content: null }; + } + // Check if message has attachments (images) const hasAttachments = msg.attachments && @@ -426,6 +446,7 @@ export class OpenAIAdapter implements LLMAdapter { id: string; name: string; arguments: string; + extra_content?: Record; } | null = null; // Track citations from web search @@ -501,16 +522,24 @@ export class OpenAIAdapter implements LLMAdapter { }; } + const tcExtraContent = (toolCall as any).extra_content as + | Record + | undefined; + currentToolCall = { id: toolCall.id, name: toolCall.function?.name || "", arguments: toolCall.function?.arguments || "", + ...(tcExtraContent ? { extra_content: tcExtraContent } : {}), }; yield { type: "action:start", id: currentToolCall.id, name: currentToolCall.name, + ...(currentToolCall.extra_content + ? { extra_content: currentToolCall.extra_content } + : {}), }; } else if (currentToolCall && toolCall.function?.arguments) { // Append to current tool call arguments @@ -554,7 +583,7 @@ export class OpenAIAdapter implements LLMAdapter { yield { type: "error", message: error instanceof Error ? error.message : "Unknown error", - code: "OPENAI_ERROR", + code: `${this.provider.toUpperCase()}_ERROR`, }; } } @@ -568,15 +597,28 @@ export class OpenAIAdapter implements LLMAdapter { let messages: Array>; if (request.rawMessages && request.rawMessages.length > 0) { - messages = request.rawMessages; + const sanitized = request.rawMessages.map((msg) => { + // Gemini/xAI (OpenAI-compatible) reject content: "" on assistant messages with tool_calls + if ( + msg.role === "assistant" && + Array.isArray(msg.tool_calls) && + msg.tool_calls.length > 0 && + msg.content === "" + ) { + return { ...msg, content: null }; + } + return msg; + }); if ( request.systemPrompt && - !messages.some((message) => message.role === "system") + !sanitized.some((message) => message.role === "system") ) { messages = [ { role: "system", content: request.systemPrompt }, - ...messages, + ...sanitized, ]; + } else { + messages = sanitized; } } else { messages = formatMessagesForOpenAI( @@ -632,6 +674,9 @@ export class OpenAIAdapter implements LLMAdapter { return {}; } })(), + ...(toolCall.extra_content + ? { extra_content: toolCall.extra_content } + : {}), })) ?? [], usage: response.usage ? { diff --git a/packages/llm-sdk/src/adapters/xai.ts b/packages/llm-sdk/src/adapters/xai.ts index e176fbe..e101f55 100644 --- a/packages/llm-sdk/src/adapters/xai.ts +++ b/packages/llm-sdk/src/adapters/xai.ts @@ -1,33 +1,16 @@ /** - * xAI Grok LLM Adapter + * xAI Grok Adapter * - * xAI uses an OpenAI-compatible API, so this adapter extends OpenAIAdapter - * with a different base URL. - * - * Supports: Grok-2, Grok-2-mini, Grok-beta - * Features: Vision, Tools/Function Calling + * xAI uses an OpenAI-compatible API — this is a thin factory + * that creates an OpenAIAdapter with the xAI endpoint baked in. + * No separate class needed. */ -import type { LLMConfig, StreamEvent } from "../core/stream-events"; -import { generateMessageId, generateToolCallId } from "../core/utils"; -import type { - LLMAdapter, - ChatCompletionRequest, - CompletionResult, -} from "./base"; -import { - formatMessagesForOpenAI, - formatTools, - logProviderPayload, -} from "./base"; +import { createOpenAIAdapter } from "./openai"; +import type { OpenAIAdapterConfig } from "./openai"; -// ============================================ -// Types -// ============================================ +const XAI_BASE_URL = "https://api.x.ai/v1"; -/** - * xAI adapter configuration - */ export interface XAIAdapterConfig { apiKey: string; model?: string; @@ -36,269 +19,12 @@ export interface XAIAdapterConfig { maxTokens?: number; } -// Default xAI API endpoint -const XAI_BASE_URL = "https://api.x.ai/v1"; - -// ============================================ -// Adapter Implementation -// ============================================ - -/** - * xAI Grok LLM Adapter - * - * Uses OpenAI-compatible API with xAI's endpoint - */ -export class XAIAdapter implements LLMAdapter { - readonly provider = "xai"; - readonly model: string; - - private client: any; // OpenAI client (lazy loaded) - private config: XAIAdapterConfig; - - constructor(config: XAIAdapterConfig) { - this.config = config; - this.model = config.model || "grok-2"; - } - - private async getClient() { - if (!this.client) { - // Use OpenAI SDK with xAI base URL - const { default: OpenAI } = await import("openai"); - this.client = new OpenAI({ - apiKey: this.config.apiKey, - baseURL: this.config.baseUrl || XAI_BASE_URL, - }); - } - return this.client; - } - - async *stream(request: ChatCompletionRequest): AsyncGenerator { - const client = await this.getClient(); - - // Use raw messages if provided (for agent loop with tool calls), otherwise format from Message[] - let messages: Array>; - if (request.rawMessages && request.rawMessages.length > 0) { - // Process raw messages - convert any attachments to OpenAI vision format - const processedMessages = request.rawMessages.map((msg) => { - // Check if message has attachments (images) - const hasAttachments = - msg.attachments && - Array.isArray(msg.attachments) && - msg.attachments.length > 0; - - if (hasAttachments) { - // Convert to OpenAI multimodal content format - const content: Array> = []; - - // Add text content if present - if (msg.content) { - content.push({ type: "text", text: msg.content }); - } - - // Add image attachments - for (const attachment of msg.attachments as Array<{ - type: string; - data: string; - mimeType?: string; - }>) { - if (attachment.type === "image") { - // Convert to OpenAI image_url format - let imageUrl = attachment.data; - if (!imageUrl.startsWith("data:")) { - imageUrl = `data:${attachment.mimeType || "image/png"};base64,${attachment.data}`; - } - content.push({ - type: "image_url", - image_url: { url: imageUrl, detail: "auto" }, - }); - } - } - - return { ...msg, content, attachments: undefined }; - } - return msg; - }); - - // Add system prompt at the start if provided and not already present - if (request.systemPrompt) { - const hasSystem = processedMessages.some((m) => m.role === "system"); - if (!hasSystem) { - messages = [ - { role: "system", content: request.systemPrompt }, - ...processedMessages, - ]; - } else { - messages = processedMessages; - } - } else { - messages = processedMessages; - } - } else { - // Format from Message[] with multimodal support (images, attachments) - messages = formatMessagesForOpenAI( - request.messages, - request.systemPrompt, - ) as Array>; - } - - const tools = request.actions?.length - ? formatTools(request.actions) - : undefined; - - const messageId = generateMessageId(); - - // Emit message start - yield { type: "message:start", id: messageId }; - - try { - const payload = { - model: request.config?.model || this.model, - messages, - tools, - temperature: request.config?.temperature ?? this.config.temperature, - max_tokens: request.config?.maxTokens ?? this.config.maxTokens, - stream: true, - }; - logProviderPayload("xai", "request payload", payload, request.debug); - const stream = await client.chat.completions.create(payload); - - let currentToolCall: { - id: string; - name: string; - arguments: string; - } | null = null; - - for await (const chunk of stream) { - logProviderPayload("xai", "stream chunk", chunk, request.debug); - // Check for abort - if (request.signal?.aborted) { - break; - } - - const delta = chunk.choices[0]?.delta; - - // Handle content - if (delta?.content) { - yield { type: "message:delta", content: delta.content }; - } - - // Handle tool calls - if (delta?.tool_calls) { - for (const toolCall of delta.tool_calls) { - // New tool call - if (toolCall.id) { - // End previous tool call if any - if (currentToolCall) { - yield { - type: "action:args", - id: currentToolCall.id, - args: currentToolCall.arguments, - }; - } - - currentToolCall = { - id: toolCall.id, - name: toolCall.function?.name || "", - arguments: toolCall.function?.arguments || "", - }; - - yield { - type: "action:start", - id: currentToolCall.id, - name: currentToolCall.name, - }; - } else if (currentToolCall && toolCall.function?.arguments) { - // Append to current tool call arguments - currentToolCall.arguments += toolCall.function.arguments; - } - } - } - - // Check for finish - if (chunk.choices[0]?.finish_reason) { - // Complete any pending tool call - if (currentToolCall) { - yield { - type: "action:args", - id: currentToolCall.id, - args: currentToolCall.arguments, - }; - } - } - } - - // Emit message end - yield { type: "message:end" }; - yield { type: "done" }; - } catch (error) { - yield { - type: "error", - message: error instanceof Error ? error.message : "Unknown error", - code: "XAI_ERROR", - }; - } - } - - /** - * Non-streaming completion (optional, for debugging) - */ - async complete(request: ChatCompletionRequest): Promise { - const client = await this.getClient(); - - let messages: Array>; - if (request.rawMessages && request.rawMessages.length > 0) { - messages = request.rawMessages as Array>; - if (request.systemPrompt) { - const hasSystem = messages.some((m) => m.role === "system"); - if (!hasSystem) { - messages = [ - { role: "system", content: request.systemPrompt }, - ...messages, - ]; - } - } - } else { - messages = formatMessagesForOpenAI( - request.messages, - request.systemPrompt, - ) as Array>; - } - - const tools = request.actions?.length - ? formatTools(request.actions) - : undefined; - - const payload = { - model: request.config?.model || this.model, - messages, - tools, - temperature: request.config?.temperature ?? this.config.temperature, - max_tokens: request.config?.maxTokens ?? this.config.maxTokens, - }; - logProviderPayload("xai", "request payload", payload, request.debug); - const response = await client.chat.completions.create(payload); - logProviderPayload("xai", "response payload", response, request.debug); - - const choice = response.choices[0]; - const message = choice?.message; - - const toolCalls = (message?.tool_calls || []).map((tc: any) => ({ - id: tc.id, - name: tc.function.name, - args: JSON.parse(tc.function.arguments || "{}"), - })); - - return { - content: message?.content || "", - toolCalls, - rawResponse: response as Record, - }; - } -} - -/** - * Create xAI Grok adapter - */ -export function createXAIAdapter(config: XAIAdapterConfig): XAIAdapter { - return new XAIAdapter(config); +export function createXAIAdapter(config: XAIAdapterConfig) { + return createOpenAIAdapter({ + apiKey: config.apiKey, + model: config.model || "grok-3", + baseUrl: config.baseUrl || XAI_BASE_URL, + temperature: config.temperature, + maxTokens: config.maxTokens, + } satisfies OpenAIAdapterConfig); } diff --git a/packages/llm-sdk/src/core/stream-events.ts b/packages/llm-sdk/src/core/stream-events.ts index c3d0b12..808fb87 100644 --- a/packages/llm-sdk/src/core/stream-events.ts +++ b/packages/llm-sdk/src/core/stream-events.ts @@ -22,6 +22,7 @@ export type StreamEventType = | "loop:iteration" | "loop:complete" | "error" + | "thread:created" | "done"; /** @@ -85,6 +86,8 @@ export interface ActionStartEvent extends BaseEvent { name: string; /** Whether this tool should be hidden from UI */ hidden?: boolean; + /** Provider-specific metadata (e.g. Gemini 3 thought_signature) */ + extra_content?: Record; } /** @@ -125,6 +128,8 @@ export interface ToolCallInfo { args: Record; /** Whether this tool should be hidden from UI */ hidden?: boolean; + /** Provider-specific metadata (e.g. Gemini 3 thought_signature) */ + extra_content?: Record; } /** @@ -140,6 +145,8 @@ export interface AssistantToolMessage { name: string; arguments: string; }; + /** Provider-specific metadata (e.g. Gemini 3 thought_signature) */ + extra_content?: Record; }>; } @@ -220,6 +227,8 @@ export interface DoneEventMessage { name: string; arguments: string; }; + /** Provider-specific metadata (e.g. Gemini 3 thought_signature) */ + extra_content?: Record; }>; tool_call_id?: string; } @@ -233,6 +242,15 @@ export interface TokenUsageRaw { total_tokens?: number; } +/** + * Thread/session created — emitted early in the stream, before any message events, + * so the client can adopt the threadId without waiting for the done chunk. + */ +export interface ThreadCreatedEvent extends BaseEvent { + type: "thread:created"; + threadId: string; +} + /** * Stream completed */ @@ -265,6 +283,7 @@ export type StreamEvent = | LoopIterationEvent | LoopCompleteEvent | ErrorEvent + | ThreadCreatedEvent | DoneEvent; /** @@ -285,6 +304,8 @@ export interface ToolCall { name: string; arguments: string; }; + /** Provider-specific metadata (e.g. Gemini 3 thought_signature in extra_content.google) */ + extra_content?: Record; } /** diff --git a/packages/llm-sdk/src/providers/google/index.ts b/packages/llm-sdk/src/providers/google/index.ts index 5fcf141..1e9f767 100644 --- a/packages/llm-sdk/src/providers/google/index.ts +++ b/packages/llm-sdk/src/providers/google/index.ts @@ -17,7 +17,7 @@ export { google, createGoogle as createGoogleModel } from "./provider"; export type { GoogleProviderOptions } from "./provider"; -import { createGoogleAdapter } from "../../adapters/google"; +import { createOpenAIAdapter } from "../../adapters/openai"; import { createCallableProvider, type AIProvider, @@ -25,6 +25,10 @@ import { type GoogleProviderConfig, } from "../types"; +// Gemini OpenAI-compatible endpoint +const GEMINI_BASE_URL = + "https://generativelanguage.googleapis.com/v1beta/openai/"; + // ============================================ // Model Definitions // ============================================ @@ -127,6 +131,26 @@ const GOOGLE_MODELS: Record = { outputTokens: 8192, }, + // Gemini 3 series (thinking models) + "gemini-3.1-flash-lite-preview": { + vision: true, + tools: true, + audio: false, + video: false, + pdf: true, + maxTokens: 1000000, + outputTokens: 32768, + }, + "gemini-3.1-flash-preview": { + vision: true, + tools: true, + audio: false, + video: false, + pdf: true, + maxTokens: 1000000, + outputTokens: 32768, + }, + // Gemini 1.0 series (legacy) "gemini-1.0-pro": { vision: false, @@ -168,11 +192,10 @@ export function createGoogle(config: GoogleProviderConfig = {}): AIProvider { // Create the callable function const providerFn = (modelId: string) => { - return createGoogleAdapter({ + return createOpenAIAdapter({ apiKey, model: modelId, - baseUrl: config.baseUrl, - safetySettings: config.safetySettings, + baseUrl: config.baseUrl || GEMINI_BASE_URL, }); }; diff --git a/packages/llm-sdk/src/server/runtime.ts b/packages/llm-sdk/src/server/runtime.ts index 38b5c9e..635e8a3 100644 --- a/packages/llm-sdk/src/server/runtime.ts +++ b/packages/llm-sdk/src/server/runtime.ts @@ -1009,8 +1009,12 @@ export class Runtime { // Accumulate data from stream let accumulatedText = ""; const toolCalls: ToolCallInfo[] = []; - let currentToolCall: { id: string; name: string; args: string } | null = - null; + let currentToolCall: { + id: string; + name: string; + args: string; + extra_content?: Record; + } | null = null; // Server-side tool results (populated inline during stream, before message:end) const serverToolResults: Array<{ @@ -1071,7 +1075,14 @@ export class Runtime { break; case "action:start": - currentToolCall = { id: event.id, name: event.name, args: "" }; + currentToolCall = { + id: event.id, + name: event.name, + args: "", + ...(event.extra_content + ? { extra_content: event.extra_content } + : {}), + }; if (debug) { console.log(`[Copilot SDK] Tool call started: ${event.name}`); } @@ -1092,6 +1103,9 @@ export class Runtime { id: currentToolCall.id, name: currentToolCall.name, args: parsedArgs, + ...(currentToolCall.extra_content + ? { extra_content: currentToolCall.extra_content } + : {}), }); } catch (e) { console.error( @@ -1103,6 +1117,9 @@ export class Runtime { id: currentToolCall.id, name: currentToolCall.name, args: {}, + ...(currentToolCall.extra_content + ? { extra_content: currentToolCall.extra_content } + : {}), }); } currentToolCall = null; @@ -1226,14 +1243,18 @@ export class Runtime { const assistantWithToolCalls: DoneEventMessage = { role: "assistant", content: accumulatedText || null, - tool_calls: serverToolResults.map((tr) => ({ - id: tr.id, - type: "function" as const, - function: { - name: tr.name, - arguments: JSON.stringify(tr.args), - }, - })), + tool_calls: serverToolResults.map((tr) => { + const tc = toolCalls.find((t) => t.id === tr.id); + return { + id: tr.id, + type: "function" as const, + function: { + name: tr.name, + arguments: JSON.stringify(tr.args), + }, + ...(tc?.extra_content ? { extra_content: tc.extra_content } : {}), + }; + }), }; // Create tool result messages (using buildToolResultForAI for AI response control) @@ -1304,6 +1325,7 @@ export class Runtime { name: tc.name, arguments: JSON.stringify(tc.args), }, + ...(tc.extra_content ? { extra_content: tc.extra_content } : {}), })), }; @@ -1621,6 +1643,9 @@ export class Runtime { name: tc.name, arguments: JSON.stringify(tc.args), }, + ...(tc.extra_content + ? { extra_content: tc.extra_content } + : {}), })), }; @@ -1679,6 +1704,9 @@ export class Runtime { name: tc.name, arguments: JSON.stringify(tc.args), }, + ...(tc.extra_content + ? { extra_content: tc.extra_content } + : {}), })), }; @@ -1922,6 +1950,15 @@ export class Runtime { } } + // Emit threadId early — before any message events — so the client can + // adopt it immediately without waiting for the done chunk + if (resolvedThreadId) { + yield { + type: "thread:created", + threadId: resolvedThreadId, + } as StreamEvent; + } + // Save input messages (user message / tool results) if (resolvedThreadId && storageHealthy) { try { From f11ca83837d3f4cd6040b6b0c3f316833763cc53 Mon Sep 17 00:00:00 2001 From: Sahil Date: Wed, 8 Apr 2026 15:22:58 +0530 Subject: [PATCH 08/11] =?UTF-8?q?docs:=20major=20restructuring=20=E2=80=94?= =?UTF-8?q?=20icons,=20generative=20UI,=20redirects,=20sidebar=20cleanup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace all sidebar icons with Hugeicons duotone-rounded (AiMagic, AiBook, MagicWand, Puzzle, BubbleChat, SlidersHorizontal, FileCode, ServerStack, AiChip1) - Move Generative UI to root sidebar; rewrite with two-approach structure (toolRenderers vs AI-generated iframe via useGenerativeUI/HtmlRenderer) - Move branching from chat/ to advanced/; merge message-actions into chat/ui - Collapse skills/ folder to flat skills.mdx (fixes fumadocs dropdown bug) - Move context/ pages into advanced/; move headless into customizations/ - Add 18 permanent redirects in next.config.mjs for all moved/deleted routes - Add AiMagic, AiBook, BubbleChat, FileCode, MagicWand, Puzzle, ServerStack, SlidersHorizontal icon components Co-Authored-By: Claude Sonnet 4.6 --- apps/docs/alpha-docs/BETA-FEATURES.md | 303 ++++++++++++++ apps/docs/components/icons/ai-book.tsx | 45 +++ apps/docs/components/icons/ai-chip1.tsx | 49 ++- apps/docs/components/icons/ai-magic.tsx | 40 ++ apps/docs/components/icons/bubble-chat.tsx | 34 ++ apps/docs/components/icons/file-code.tsx | 41 ++ apps/docs/components/icons/index.ts | 8 + apps/docs/components/icons/magic-wand.tsx | 49 +++ apps/docs/components/icons/puzzle.tsx | 26 ++ apps/docs/components/icons/server-stack.tsx | 67 +++ .../components/icons/sliders-horizontal.tsx | 54 +++ .../docs/{chat => advanced}/branching.mdx | 61 +-- .../docs/{context => advanced}/compaction.mdx | 0 apps/docs/content/docs/advanced/meta.json | 5 + .../{context => advanced}/token-tracking.mdx | 0 .../{ => frameworks}/angular.mdx | 0 .../docs/api-reference/frameworks/meta.json | 4 + .../api-reference/{ => frameworks}/vue.mdx | 0 .../docs/content/docs/api-reference/meta.json | 2 +- .../content/docs/{ => chat}/attachments.mdx | 1 - apps/docs/content/docs/chat/index.mdx | 1 - .../content/docs/chat/message-actions.mdx | 158 -------- apps/docs/content/docs/chat/meta.json | 4 +- .../docs/{ => chat/storage}/chat-history.mdx | 1 - apps/docs/content/docs/chat/storage/meta.json | 4 + .../{context => chat/storage}/session.mdx | 0 apps/docs/content/docs/{ => chat}/ui.mdx | 81 +++- apps/docs/content/docs/context/index.mdx | 251 ------------ apps/docs/content/docs/context/meta.json | 5 - .../docs/{ => customizations}/headless.mdx | 1 - .../content/docs/customizations/index.mdx | 1 - .../content/docs/customizations/meta.json | 4 +- apps/docs/content/docs/deploy.mdx | 1 - apps/docs/content/docs/generative-ui.mdx | 380 +++++++++--------- apps/docs/content/docs/llm-sdk/meta.json | 2 +- apps/docs/content/docs/mcp/index.mdx | 213 ---------- apps/docs/content/docs/mcp/meta.json | 5 - apps/docs/content/docs/mcp/ui.mdx | 216 ---------- apps/docs/content/docs/mcp/usage.mdx | 334 --------------- apps/docs/content/docs/meta.json | 15 +- apps/docs/content/docs/server/index.mdx | 1 - apps/docs/content/docs/server/meta.json | 2 +- apps/docs/content/docs/skills.mdx | 174 ++++++++ apps/docs/content/docs/skills/client.mdx | 172 -------- apps/docs/content/docs/skills/index.mdx | 130 ------ apps/docs/content/docs/skills/meta.json | 5 - apps/docs/content/docs/skills/server.mdx | 183 --------- apps/docs/content/docs/tools/agentic-loop.mdx | 3 +- .../docs/content/docs/tools/backend-tools.mdx | 3 +- .../content/docs/tools/built-in/meta.json | 1 - .../content/docs/tools/deferred-tools.mdx | 171 -------- .../content/docs/tools/frontend-tools.mdx | 47 ++- apps/docs/content/docs/tools/hidden-tools.mdx | 70 ---- apps/docs/content/docs/tools/mcp.mdx | 183 +++++++++ apps/docs/content/docs/tools/meta.json | 7 +- apps/docs/lib/source.ts | 31 +- apps/docs/next.config.mjs | 86 ++++ 57 files changed, 1512 insertions(+), 2223 deletions(-) create mode 100644 apps/docs/alpha-docs/BETA-FEATURES.md create mode 100644 apps/docs/components/icons/ai-book.tsx create mode 100644 apps/docs/components/icons/ai-magic.tsx create mode 100644 apps/docs/components/icons/bubble-chat.tsx create mode 100644 apps/docs/components/icons/file-code.tsx create mode 100644 apps/docs/components/icons/magic-wand.tsx create mode 100644 apps/docs/components/icons/puzzle.tsx create mode 100644 apps/docs/components/icons/server-stack.tsx create mode 100644 apps/docs/components/icons/sliders-horizontal.tsx rename apps/docs/content/docs/{chat => advanced}/branching.mdx (72%) rename apps/docs/content/docs/{context => advanced}/compaction.mdx (100%) create mode 100644 apps/docs/content/docs/advanced/meta.json rename apps/docs/content/docs/{context => advanced}/token-tracking.mdx (100%) rename apps/docs/content/docs/api-reference/{ => frameworks}/angular.mdx (100%) create mode 100644 apps/docs/content/docs/api-reference/frameworks/meta.json rename apps/docs/content/docs/api-reference/{ => frameworks}/vue.mdx (100%) rename apps/docs/content/docs/{ => chat}/attachments.mdx (99%) delete mode 100644 apps/docs/content/docs/chat/message-actions.mdx rename apps/docs/content/docs/{ => chat/storage}/chat-history.mdx (99%) create mode 100644 apps/docs/content/docs/chat/storage/meta.json rename apps/docs/content/docs/{context => chat/storage}/session.mdx (100%) rename apps/docs/content/docs/{ => chat}/ui.mdx (80%) delete mode 100644 apps/docs/content/docs/context/index.mdx delete mode 100644 apps/docs/content/docs/context/meta.json rename apps/docs/content/docs/{ => customizations}/headless.mdx (99%) delete mode 100644 apps/docs/content/docs/mcp/index.mdx delete mode 100644 apps/docs/content/docs/mcp/meta.json delete mode 100644 apps/docs/content/docs/mcp/ui.mdx delete mode 100644 apps/docs/content/docs/mcp/usage.mdx create mode 100644 apps/docs/content/docs/skills.mdx delete mode 100644 apps/docs/content/docs/skills/client.mdx delete mode 100644 apps/docs/content/docs/skills/index.mdx delete mode 100644 apps/docs/content/docs/skills/meta.json delete mode 100644 apps/docs/content/docs/skills/server.mdx delete mode 100644 apps/docs/content/docs/tools/deferred-tools.mdx delete mode 100644 apps/docs/content/docs/tools/hidden-tools.mdx create mode 100644 apps/docs/content/docs/tools/mcp.mdx diff --git a/apps/docs/alpha-docs/BETA-FEATURES.md b/apps/docs/alpha-docs/BETA-FEATURES.md new file mode 100644 index 0000000..e39eb55 --- /dev/null +++ b/apps/docs/alpha-docs/BETA-FEATURES.md @@ -0,0 +1,303 @@ +# Beta Features — Complete List + +> All features present in `beta` branch but **not yet in `main`** (production). +> Organised by SDK package. Docs/alpha-doc status noted per feature. + +--- + +## Copilot SDK (`@yourgpt/copilot-sdk`) + +### 1. Conversation Branching + +Edit any user message to fork a new parallel conversation path. Each edit spawns a sibling branch; `← N/M →` arrows let the user navigate between variants — same UX pattern as ChatGPT, Claude.ai, and Gemini. + +- **API:** `allowEdit` prop on `` · `switchBranch()` · `getAllMessages()` · `BranchNavigator` component +- **Package:** `@yourgpt/copilot-sdk/react` + `@yourgpt/copilot-sdk/ui` +- **Docs page:** `content/docs/chat/branching.mdx` +- **Alpha-doc:** `alpha-docs/BRANCHING.md` + +--- + +### 2. Message History Compaction + +Automatic context-window management. When token usage crosses a configurable threshold the SDK compacts old messages using a chosen strategy, without ever shrinking the display history the user sees. + +- **Strategies:** `none` (default) · `sliding-window` · `selective-prune` · `summary-buffer` (recommended) +- **API:** `messageHistory` prop on `` · `useMessageHistory()` hook · `compactSession(instructions?)` for manual trigger +- **Package:** `@yourgpt/copilot-sdk/react` +- **Docs page:** `content/docs/context/compaction.mdx` +- **Alpha-doc:** `alpha-docs/message-history-compaction.md` + +--- + +### 3. Skills System + +On-demand instruction playbooks the AI loads at runtime. Skills are Markdown files or inline strings. Keeps the system prompt lean by deferring behaviour descriptions until the AI actually needs them. + +- **Strategies:** `eager` (always injected) · `auto` (AI calls `load_skill` tool on demand) · `manual` (accessible but not advertised) +- **API:** `` · `defineSkill()` · `useSkill()` · `useSkillStatus()` +- **Package:** `@yourgpt/copilot-sdk/react` +- **Docs page:** `content/docs/skills/client.mdx` + `content/docs/skills/server.mdx` +- **Alpha-doc:** `alpha-docs/SKILLS.md` + `alpha-docs/skills-system.md` + +--- + +### 4. Chat Primitives (`ChatPrimitives` namespace) + +A complete set of low-level building blocks for composing fully custom chat UIs. Every primitive reads state from SDK context internally — no manual wiring needed. + +- **Primitives:** `MessageList` · `DefaultMessage` · `Header` · `Welcome` · `Input` · `ScrollAnchor` · `Message` · `MessageAvatar` · `MessageContent` · `MessageActions` · `MessageAction` · `Loader` +- **API:** `import { ChatPrimitives as Chat } from "@yourgpt/copilot-sdk/ui"` or via `CopilotChat.*` compound extensions +- **Package:** `@yourgpt/copilot-sdk/ui` +- **Docs page:** `content/docs/customizations/chat-primitives.mdx` +- **Alpha-doc:** `alpha-docs/CHAT-PRIMITIVES.md` + +--- + +### 5. Custom Message View (`messageView` prop) + +Intercepts the message-list render loop and hands both raw messages and pre-rendered SDK elements to a render-prop callback. Use it to inject custom UI, conditionally replace messages by type, or build entirely custom layouts. + +- **API:** ` ReactNode }} />` +- **Package:** `@yourgpt/copilot-sdk/ui` +- **Docs page:** `content/docs/customizations/custom-message-view.mdx` +- **Alpha-doc:** `alpha-docs/CUSTOM-MESSAGE-VIEW.md` + +--- + +### 6. Message Actions (Compound Components) + +Declarative floating action buttons on chat messages — copy, inline edit, thumbs up/down, or fully custom actions. Role-based configuration (user vs assistant). Appears on hover; same compound-component pattern as shadcn/Radix. + +- **API:** `CopilotChat.MessageActions` · `CopilotChat.CopyAction` · `CopilotChat.EditAction` · `CopilotChat.FeedbackAction` · `CopilotChat.Action` +- **Package:** `@yourgpt/copilot-sdk/ui` +- **Docs page:** `content/docs/chat/message-actions.mdx` +- **Alpha-doc:** `alpha-docs/MESSAGE-ACTIONS.md` + +--- + +### 7. File Attachments + +Drag-and-drop file and media attachments in chat. Includes an `AttachmentStrip` thumbnail strip, a drop-zone overlay, upload error handling, and automatic forwarding to the server runtime. + +- **API:** Auto-enabled when `runtimeUrl` is set · `useAttachments()` for headless access +- **Package:** `@yourgpt/copilot-sdk/ui` +- **Docs page:** `content/docs/attachments.mdx` +- **Alpha-doc:** ✗ not covered + +--- + +### 8. Headless Primitives (`useCopilotEvent` + `useMessageMeta`) + +Subscribe to raw stream events and read per-message metadata without using any SDK UI components. The foundation for building fully headless integrations (Slack bots, custom renderers, etc.). + +- **Events:** `thinking:delta` · `action:start` · `action:end` · `loop:iteration` · `*` (catch-all) +- **API:** `useCopilotEvent(event, handler)` · `useMessageMeta(messageId)` → `{ tokenUsage, metadata }` +- **Package:** `@yourgpt/copilot-sdk/react` +- **Docs page:** `content/docs/headless.mdx` +- **Alpha-doc:** ✗ not covered + +--- + +### 9. Context Stats (`useContextStats`) + +Real-time reactive context window usage. Returns token counts and percentages updated after every LLM call, plus a convenient chars-based estimate before the first send. + +- **API:** `const { contextUsage, totalTokens, usagePercent } = useContextStats()` +- **Package:** `@yourgpt/copilot-sdk/react` +- **Docs page:** `content/docs/context/token-tracking.mdx` +- **Alpha-doc:** `alpha-docs/CONTEXT-MANAGEMENT.md` §5 + +--- + +### 10. Thread Management + Thread Picker + +Enhanced multi-thread support with `activeLeafId` (tracks the active conversation leaf), a `ThreadPicker` UI component for switching between saved threads, and a localStorage adapter for zero-config thread persistence. + +- **API:** `` from `@yourgpt/copilot-sdk/ui` · `useThread()` hook · `localStorageAdapter` +- **Package:** `@yourgpt/copilot-sdk/react` + `@yourgpt/copilot-sdk/ui` +- **Docs page:** `content/docs/context/session.mdx` (partial) +- **Alpha-doc:** ✗ not covered + +--- + +### 11. Agent Iteration Tracking + +Track how many tool-use loops the agent has run in a single turn. Enforce a `maxAgentIterations` cap to prevent runaway loops. Exposes checkpoint exports for state inspection. + +- **API:** `agentIteration` value from `useContextStats()` · `maxAgentIterations` on `` · `allowEdit` prop on `` +- **Package:** `@yourgpt/copilot-sdk/react` +- **Docs page:** `content/docs/tools/agentic-loop.mdx` +- **Alpha-doc:** `alpha-docs/CONTEXT-MANAGEMENT.md` §6 + +--- + +### 12. Deferred Tools + +Tools not eagerly sent to the LLM. Instead they sit in a registry and are only included in the request when the user message semantically matches them via BM25 search. Reduces tokens significantly for large tool sets. + +- **API:** `useTool({ name: "...", deferred: true, description: "...", ... })` +- **Package:** `@yourgpt/copilot-sdk/react` +- **Docs page:** `content/docs/tools/deferred-tools.mdx` +- **Alpha-doc:** `alpha-docs/CONTEXT-MANAGEMENT.md` §7 + +--- + +### 13. Hidden Tools + +Tools registered with the SDK but never sent to the LLM. Used for client-side execution the AI triggers indirectly, or for internal state mutations that should never appear in the model's tool list. + +- **API:** `useTool({ name: "...", hidden: true, ... })` +- **Package:** `@yourgpt/copilot-sdk/react` +- **Docs page:** `content/docs/tools/hidden-tools.mdx` +- **Alpha-doc:** `alpha-docs/CONTEXT-MANAGEMENT.md` §7 + +--- + +### 14. Fallback Tool Renderer + +A catch-all render function for tool calls that have no registered renderer. Prevents unknown or dynamically-named tool calls from showing a blank area in the chat. + +- **API:** ` } />` +- **Package:** `@yourgpt/copilot-sdk/ui` +- **Docs page:** partial — `content/docs/tools/` section +- **Alpha-doc:** `alpha-docs/CONTEXT-MANAGEMENT.md` §7 + +--- + +### 15. Message Grouping + +Consecutive tool call + tool result pairs are visually grouped in the chat UI instead of rendering as separate message bubbles. Keeps the conversation thread readable during multi-step agent runs. + +- **API:** Automatic — internal `groupConsecutiveToolMessages` config +- **Package:** `@yourgpt/copilot-sdk/ui` +- **Docs page:** ✗ no dedicated page +- **Alpha-doc:** `alpha-docs/CONTEXT-MANAGEMENT.md` §8 + +--- + +### 16. CSS Class Reference + +Comprehensive reference for all `csdk-*` CSS classes applied to every chat UI element. Enables deterministic custom theming without touching component internals. + +- **API:** Target `.csdk-chat` · `.csdk-message` · `.csdk-input` · `.csdk-overlay` etc. in your stylesheets +- **Package:** `@yourgpt/copilot-sdk/ui` +- **Docs page:** `content/docs/customizations/css-classes.mdx` +- **Alpha-doc:** ✗ not covered + +--- + +### 17. Generative UI — Experimental + +The AI can render structured UI components (cards, tables, charts, stat tiles) inline inside the chat, generated from tool call results. Ships with four built-in renderers plus a hook for custom ones. + +- **Renderers:** `CardRenderer` · `TableRenderer` · `StatRenderer` · `HtmlRenderer` +- **API:** `import { generativeUITool, useGenerativeUI } from "@yourgpt/copilot-sdk/experimental"` — register `generativeUITool` as a tool, use `useGenerativeUI()` to render results +- **Package:** `@yourgpt/copilot-sdk/experimental` +- **Docs page:** `content/docs/generative-ui.mdx` (page existed on main; full demo + renderers added in beta) +- **Alpha-doc:** ✗ not covered + +--- + +### 18. Streaming Enhancements (Pre-created Message IDs) + +Messages receive stable IDs before streaming begins, enabling optimistic UI updates, correct server-side event ordering, and `useMessageMeta()` access from the very first chunk. + +- **API:** Internal — no breaking API change. Consumed transparently by `useMessageMeta(id)`. +- **Package:** `@yourgpt/copilot-sdk` (core) +- **Docs page:** ✗ not documented +- **Alpha-doc:** ✗ not covered + +--- + +## LLM SDK (`@yourgpt/llm-sdk`) + +### 19. Fallback Chain & Routing Strategies + +Chain multiple LLM providers or models with automatic failover. Supports `priority` (try in order) and `round-robin` routing. Per-model retry with exponential or fixed backoff. Pluggable `RoutingStore` for serverless environments (Redis, Upstash, Cloudflare KV). + +- **API:** `createFallbackChain([provider1, provider2], { routing: "round-robin", retries: 3, backoff: "exponential", onFallback, onRetry })` → pass result to `createRuntime({ provider: chain })` +- **Package:** `@yourgpt/llm-sdk` — `fallback` module +- **Docs page:** `content/docs/providers/fallback.mdx` +- **Alpha-doc:** ✗ not covered + +--- + +### 20. Tool Profiles + BM25 Tool Search + +For large tool sets (50+ tools), tools are grouped into named profiles and ranked per-request using BM25. Only the most relevant tools are forwarded to the LLM, keeping per-request token cost low. Also supports native provider search (Anthropic / OpenAI). + +- **API:** `createRuntime({ toolProfiles: { defaultProfile: "core", profiles: { ... } }, maxEagerTools: 20, exposeWhenExceeds: 40 })` +- **Package:** `@yourgpt/llm-sdk/server` +- **Docs page:** ✗ no dedicated page yet +- **Alpha-doc:** `alpha-docs/CONTEXT-MANAGEMENT.md` §7 + +--- + +### 21. YourGPT Provider (`createYourGPT`) + +First-party storage adapter for the YourGPT backend. Plugging it into `createRuntime({ storage })` auto-creates sessions on first message, persists every user message + assistant response + tool pair, and provides a one-liner file upload endpoint. + +- **API:** `import { createYourGPT } from "@yourgpt/llm-sdk/yourgpt"` → `createRuntime({ provider, model, storage: createYourGPT({ apiKey, widgetUid }) })` +- **Package:** `@yourgpt/llm-sdk/yourgpt` +- **Docs page:** `content/docs/server/storage.mdx` +- **Alpha-doc:** `alpha-docs/STORAGE-ADAPTER.md` + +--- + +### 22. Resolvable Pattern + +Any runtime config value — system prompt, model ID, tools list, max tokens — can be a static value, a synchronous function, or an async function. Evaluated fresh on every request, enabling per-user or per-tenant dynamic configuration. + +- **API:** `createRuntime({ systemPrompt: async (req) => getPromptFor(req.userId), model: (req) => req.body.model ?? "gpt-4o" })` +- **Package:** `@yourgpt/llm-sdk` + `@yourgpt/copilot-sdk` +- **Docs page:** ✗ no dedicated page +- **Alpha-doc:** ✗ not covered + +--- + +### 23. New Provider Adapters (Azure, xAI, Ollama) + +Official adapters for Azure OpenAI, xAI (Grok), and Ollama (local models) added alongside the existing OpenAI, Anthropic, and Google adapters. Drop-in compatible with `createRuntime` and the fallback chain. + +- **API:** `import { createAzure } from "@yourgpt/llm-sdk/azure"` · `import { createXAI } from "@yourgpt/llm-sdk/xai"` · `import { createOllama } from "@yourgpt/llm-sdk/ollama"` +- **Package:** `@yourgpt/llm-sdk` +- **Docs page:** `content/docs/providers/xai.mdx` + `content/docs/providers/ollama.mdx` (existed on main) +- **Alpha-doc:** ✗ not covered + +--- + +### 24. Context Budget / Tool Result Truncation + +Automatically truncates tool results that exceed `toolResultMaxChars` before they are included in the LLM context, preventing a single large tool response from blowing the context window. + +- **API:** `` (client) · runtime enforces server-side +- **Package:** `@yourgpt/llm-sdk/server` + `@yourgpt/copilot-sdk` +- **Docs page:** `content/docs/context/compaction.mdx` (partial) +- **Alpha-doc:** `alpha-docs/CONTEXT-MANAGEMENT.md` + `alpha-docs/message-history-compaction.md` + +--- + +### 25. Server-side Compaction Endpoint + +A `compactSession()` function on the server runtime that summarises a stored conversation before the next LLM call. Pairs with the client-side compaction system and can be triggered via a dedicated API route. + +- **API:** `runtime.compactSession(threadId, instructions?)` +- **Package:** `@yourgpt/llm-sdk/server` +- **Docs page:** `content/docs/context/compaction.mdx` +- **Alpha-doc:** `alpha-docs/message-history-compaction.md` + +--- + +## Missing Docs Summary + +Features that are **in beta code** but have **no proper docs page** yet: + +| Feature | SDK | Coverage | +| ---------------------------------------- | ------------------------- | ---------------------- | +| Tool Profiles + BM25 Tool Search | `llm-sdk` | Alpha-doc only | +| Message Grouping | `copilot-sdk` | Alpha-doc only (brief) | +| Thread Picker (full guide) | `copilot-sdk` | Not documented | +| Resolvable Pattern | `llm-sdk` + `copilot-sdk` | Not documented | +| Fallback Tool Renderer | `copilot-sdk` | Alpha-doc only | +| Streaming Enhancements (pre-created IDs) | `copilot-sdk` | Not documented | diff --git a/apps/docs/components/icons/ai-book.tsx b/apps/docs/components/icons/ai-book.tsx new file mode 100644 index 0000000..59c5a8f --- /dev/null +++ b/apps/docs/components/icons/ai-book.tsx @@ -0,0 +1,45 @@ +import type { SVGProps } from "react"; + +export function AiBookIcon(props: SVGProps) { + return ( + + + + + + + + ); +} diff --git a/apps/docs/components/icons/ai-chip1.tsx b/apps/docs/components/icons/ai-chip1.tsx index 408b6a5..cbb867b 100644 --- a/apps/docs/components/icons/ai-chip1.tsx +++ b/apps/docs/components/icons/ai-chip1.tsx @@ -1,41 +1,40 @@ -interface AiChip1Props { - width?: number | string; - height?: number | string; - className?: string; - color?: string; -} +import type { SVGProps } from "react"; -export function AiChip1({ - width = 24, - height = 24, - className, - color = "currentColor", -}: AiChip1Props) { +export function AiChip1(props: SVGProps) { return ( + ); diff --git a/apps/docs/components/icons/ai-magic.tsx b/apps/docs/components/icons/ai-magic.tsx new file mode 100644 index 0000000..36ea77f --- /dev/null +++ b/apps/docs/components/icons/ai-magic.tsx @@ -0,0 +1,40 @@ +import * as React from "react"; +import type { SVGProps } from "react"; + +export function AiMagicIcon(props: SVGProps) { + return ( + + {/* Duotone fill layer */} + + + {/* Stroke outline layer */} + + + + ); +} diff --git a/apps/docs/components/icons/bubble-chat.tsx b/apps/docs/components/icons/bubble-chat.tsx new file mode 100644 index 0000000..cccea31 --- /dev/null +++ b/apps/docs/components/icons/bubble-chat.tsx @@ -0,0 +1,34 @@ +import type { SVGProps } from "react"; + +export function BubbleChatIcon(props: SVGProps) { + return ( + + + + + + ); +} diff --git a/apps/docs/components/icons/file-code.tsx b/apps/docs/components/icons/file-code.tsx new file mode 100644 index 0000000..5cfbb20 --- /dev/null +++ b/apps/docs/components/icons/file-code.tsx @@ -0,0 +1,41 @@ +import type { SVGProps } from "react"; + +export function FileCodeIcon(props: SVGProps) { + return ( + + + + + + + ); +} diff --git a/apps/docs/components/icons/index.ts b/apps/docs/components/icons/index.ts index 7842af0..6227922 100644 --- a/apps/docs/components/icons/index.ts +++ b/apps/docs/components/icons/index.ts @@ -5,3 +5,11 @@ export { RocketIcon } from "./rocket"; export { MessageQuestionIcon } from "./message-question"; export { AiChip1 } from "./ai-chip1"; export { Grid1 } from "./grid1"; +export { SlidersHorizontalIcon } from "./sliders-horizontal"; +export { AiBookIcon } from "./ai-book"; +export { MagicWandIcon } from "./magic-wand"; +export { PuzzleIcon } from "./puzzle"; +export { BubbleChatIcon } from "./bubble-chat"; +export { FileCodeIcon } from "./file-code"; +export { ServerStackIcon } from "./server-stack"; +export { AiMagicIcon } from "./ai-magic"; diff --git a/apps/docs/components/icons/magic-wand.tsx b/apps/docs/components/icons/magic-wand.tsx new file mode 100644 index 0000000..4b29f40 --- /dev/null +++ b/apps/docs/components/icons/magic-wand.tsx @@ -0,0 +1,49 @@ +import type { SVGProps } from "react"; + +export function MagicWandIcon(props: SVGProps) { + return ( + + + + + + + + + ); +} diff --git a/apps/docs/components/icons/puzzle.tsx b/apps/docs/components/icons/puzzle.tsx new file mode 100644 index 0000000..1224728 --- /dev/null +++ b/apps/docs/components/icons/puzzle.tsx @@ -0,0 +1,26 @@ +import type { SVGProps } from "react"; + +export function PuzzleIcon(props: SVGProps) { + return ( + + + + + ); +} diff --git a/apps/docs/components/icons/server-stack.tsx b/apps/docs/components/icons/server-stack.tsx new file mode 100644 index 0000000..fe47c2c --- /dev/null +++ b/apps/docs/components/icons/server-stack.tsx @@ -0,0 +1,67 @@ +import type { SVGProps } from "react"; + +export function ServerStackIcon(props: SVGProps) { + return ( + + + + + + + + + + + ); +} diff --git a/apps/docs/components/icons/sliders-horizontal.tsx b/apps/docs/components/icons/sliders-horizontal.tsx new file mode 100644 index 0000000..0f29760 --- /dev/null +++ b/apps/docs/components/icons/sliders-horizontal.tsx @@ -0,0 +1,54 @@ +import type { SVGProps } from "react"; + +export function SlidersHorizontalIcon(props: SVGProps) { + return ( + + + + + + + + + ); +} diff --git a/apps/docs/content/docs/chat/branching.mdx b/apps/docs/content/docs/advanced/branching.mdx similarity index 72% rename from apps/docs/content/docs/chat/branching.mdx rename to apps/docs/content/docs/advanced/branching.mdx index 9c0f3d5..406dd49 100644 --- a/apps/docs/content/docs/chat/branching.mdx +++ b/apps/docs/content/docs/advanced/branching.mdx @@ -1,15 +1,9 @@ --- title: Conversation Branching description: Edit messages to create parallel conversation paths, just like ChatGPT and Claude.ai -icon: GitBranch --- import { Callout } from 'fumadocs-ui/components/callout'; -import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; - - -**Beta** — This feature is in **alpha**. APIs may change before stable release. - Edit any user message to create a parallel conversation path, preserving the original. Navigate between variants with `← N/M →` — the same UX as ChatGPT, Claude.ai, and Gemini. @@ -30,9 +24,9 @@ If you use ``, branching is **already active**. No code changes n --- -## New APIs +## API -### `useCopilot()` / `useCopilotProvider` +### `useCopilot()` ```typescript const { @@ -44,7 +38,7 @@ const { } = useCopilot(); ``` -### `BranchInfo` type +### `BranchInfo` ```typescript interface BranchInfo { @@ -76,7 +70,7 @@ import { BranchNavigator } from "@yourgpt/copilot-sdk/ui"; ### `MessageTree` (framework-agnostic) ```typescript -import { MessageTree, type BranchInfo } from "@yourgpt/copilot-sdk"; +import { MessageTree } from "@yourgpt/copilot-sdk"; const tree = new MessageTree(messages); tree.getVisibleMessages(); // active path only (sent to AI) @@ -88,26 +82,6 @@ tree.hasBranches; // boolean --- -## Manual Wiring (`` users) - -Wire the three props from `useCopilot()`: - -```tsx -function MyChat() { - const { switchBranch, getBranchInfo, editMessage } = useCopilot(); - - return ( - - ); -} -``` - ---- - ## Custom Message Renderers Use `getBranchInfo` + `BranchNavigator` in your own message components: @@ -158,9 +132,7 @@ await saveToServer(allMessages); ## Persistence -### New DB columns (optional) - -Two new optional columns on your messages table: +### Optional DB columns ```sql ALTER TABLE messages @@ -169,12 +141,10 @@ ALTER TABLE messages ``` -These columns are **optional**. Existing rows without them are auto-migrated to a linear tree on load — no data loss, no required migration script. +These columns are **optional**. Existing rows without them are auto-migrated to a linear tree on load — no data loss, no migration required. -### What gets saved - -When `onMessagesChange` fires, the payload now contains **all messages across all branches**. Each message carries: +When `onMessagesChange` fires, the payload contains **all messages across all branches**: ```json { @@ -186,25 +156,12 @@ When `onMessagesChange` fires, the payload now contains **all messages across al } ``` -### Upsert strategy (recommended) +Use upsert-by-ID when saving — a simple overwrite will lose inactive branches: ```typescript -// ✅ Safe for branching — upsert by ID +// ✅ Safe for branching await db.messages.upsert({ id: msg.id, ...msg }); // ⚠️ Loses inactive branches await db.threads.update({ messages: visibleMessages }); ``` - ---- - -## Breaking Changes - -**None.** All new fields and methods are optional. Existing usage is untouched. - -| Scenario | Behavior | -|----------|----------| -| Messages with no `parentId` | Falls back to insertion order (legacy linear) | -| `regenerate()` with no args | Identical to before | -| `sendMessage()` with no options | Identical to before | -| `onMessagesChange` consumers | Payload now includes all branches — shape unchanged | diff --git a/apps/docs/content/docs/context/compaction.mdx b/apps/docs/content/docs/advanced/compaction.mdx similarity index 100% rename from apps/docs/content/docs/context/compaction.mdx rename to apps/docs/content/docs/advanced/compaction.mdx diff --git a/apps/docs/content/docs/advanced/meta.json b/apps/docs/content/docs/advanced/meta.json new file mode 100644 index 0000000..c2cf730 --- /dev/null +++ b/apps/docs/content/docs/advanced/meta.json @@ -0,0 +1,5 @@ +{ + "title": "Advanced", + "icon": "SlidersHorizontal", + "pages": ["compaction", "token-tracking", "branching"] +} diff --git a/apps/docs/content/docs/context/token-tracking.mdx b/apps/docs/content/docs/advanced/token-tracking.mdx similarity index 100% rename from apps/docs/content/docs/context/token-tracking.mdx rename to apps/docs/content/docs/advanced/token-tracking.mdx diff --git a/apps/docs/content/docs/api-reference/angular.mdx b/apps/docs/content/docs/api-reference/frameworks/angular.mdx similarity index 100% rename from apps/docs/content/docs/api-reference/angular.mdx rename to apps/docs/content/docs/api-reference/frameworks/angular.mdx diff --git a/apps/docs/content/docs/api-reference/frameworks/meta.json b/apps/docs/content/docs/api-reference/frameworks/meta.json new file mode 100644 index 0000000..c1c72cb --- /dev/null +++ b/apps/docs/content/docs/api-reference/frameworks/meta.json @@ -0,0 +1,4 @@ +{ + "title": "Other Frameworks", + "pages": ["vue", "angular"] +} diff --git a/apps/docs/content/docs/api-reference/vue.mdx b/apps/docs/content/docs/api-reference/frameworks/vue.mdx similarity index 100% rename from apps/docs/content/docs/api-reference/vue.mdx rename to apps/docs/content/docs/api-reference/frameworks/vue.mdx diff --git a/apps/docs/content/docs/api-reference/meta.json b/apps/docs/content/docs/api-reference/meta.json index ed618ed..aabc09a 100644 --- a/apps/docs/content/docs/api-reference/meta.json +++ b/apps/docs/content/docs/api-reference/meta.json @@ -1,5 +1,5 @@ { "title": "API Reference", "icon": "FileCode", - "pages": ["core", "chat", "react", "vue", "angular"] + "pages": ["core", "chat", "react", "frameworks"] } diff --git a/apps/docs/content/docs/attachments.mdx b/apps/docs/content/docs/chat/attachments.mdx similarity index 99% rename from apps/docs/content/docs/attachments.mdx rename to apps/docs/content/docs/chat/attachments.mdx index 4edd12d..bf2ba81 100644 --- a/apps/docs/content/docs/attachments.mdx +++ b/apps/docs/content/docs/chat/attachments.mdx @@ -1,7 +1,6 @@ --- title: Attachments description: Send images, PDFs, and files alongside chat messages -icon: Paperclip --- import { Callout } from 'fumadocs-ui/components/callout'; diff --git a/apps/docs/content/docs/chat/index.mdx b/apps/docs/content/docs/chat/index.mdx index 2d68742..a3a7c51 100644 --- a/apps/docs/content/docs/chat/index.mdx +++ b/apps/docs/content/docs/chat/index.mdx @@ -1,7 +1,6 @@ --- title: Chat description: Pre-built chat component -icon: MessageSquare --- import { Callout } from 'fumadocs-ui/components/callout'; diff --git a/apps/docs/content/docs/chat/message-actions.mdx b/apps/docs/content/docs/chat/message-actions.mdx deleted file mode 100644 index 2ee635e..0000000 --- a/apps/docs/content/docs/chat/message-actions.mdx +++ /dev/null @@ -1,158 +0,0 @@ ---- -title: Message Actions -description: Add floating copy, edit, feedback, and custom action buttons to chat messages -icon: MousePointerClick ---- - -import { Callout } from 'fumadocs-ui/components/callout'; - - -**Beta** — This feature is in **alpha**. APIs may change before stable release. - - -A compound component API for registering floating action buttons on chat messages — copy, edit, feedback, or fully custom actions. Actions appear on hover, floating below the message bubble. Declarative, role-based, fully composable. - ---- - -## Quick Start - -```tsx - - - - sendFeedback({ messageId: message.id, type })} - /> - - - - - - -``` - - -If no `` children are declared, nothing changes — existing chat UI looks and behaves identically. - - ---- - -## Compound Components - -| Component | Description | -|-----------|-------------| -| `CopilotChat.MessageActions` | Registers actions for a role (`user` or `assistant`) | -| `CopilotChat.CopyAction` | Copy message to clipboard (with ✓ feedback) | -| `CopilotChat.EditAction` | Inline edit for user messages (wired to branching) | -| `CopilotChat.FeedbackAction` | Thumbs up / down | -| `CopilotChat.Action` | Fully custom action button | - ---- - -## Props Reference - -```tsx -// MessageActions -role: "user" | "assistant" - -// CopyAction -tooltip?: string -className?: string - -// EditAction -tooltip?: string -className?: string - -// FeedbackAction -onFeedback?: (message: ChatMessage, type: "helpful" | "not-helpful") => void -tooltip?: string -className?: string - -// Action (custom) -id?: string -icon: ReactNode -tooltip: string -onClick: (props: { message: ChatMessage }) => void -hidden?: boolean | ((props: { message: ChatMessage }) => boolean) -className?: string -``` - ---- - -## Examples - -### Copy + Feedback on assistant - -```tsx - - - - { - sendFeedback({ messageId: message.id, type }); - }} - /> - - -``` - -### Custom action - -```tsx - - - - } - tooltip="Share" - onClick={({ message }) => share(message.content)} - /> - - -``` - -### Conditional action (hide based on message content) - -```tsx - - - } - tooltip="Report" - hidden={({ message }) => !message.content} - onClick={({ message }) => report(message.id)} - /> - - -``` - -### Full setup — both roles - -```tsx - - - - log(msg.id, type)} /> - } - tooltip="Save" - onClick={({ message }) => save(message)} - /> - - - - - } - tooltip="Delete" - onClick={({ message }) => deleteMessage(message.id)} - /> - - -``` - ---- - -## Breaking Changes - -**None.** Purely additive. If no `MessageActions` children are declared, the chat UI is identical to before. diff --git a/apps/docs/content/docs/chat/meta.json b/apps/docs/content/docs/chat/meta.json index edc9c09..7709c7a 100644 --- a/apps/docs/content/docs/chat/meta.json +++ b/apps/docs/content/docs/chat/meta.json @@ -1,5 +1,5 @@ { "title": "Chat", - "icon": "MessageSquare", - "pages": ["index", "branching", "message-actions"] + "icon": "BubbleChat", + "pages": ["ui", "attachments", "storage"] } diff --git a/apps/docs/content/docs/chat-history.mdx b/apps/docs/content/docs/chat/storage/chat-history.mdx similarity index 99% rename from apps/docs/content/docs/chat-history.mdx rename to apps/docs/content/docs/chat/storage/chat-history.mdx index 9061439..c243dd7 100644 --- a/apps/docs/content/docs/chat-history.mdx +++ b/apps/docs/content/docs/chat/storage/chat-history.mdx @@ -1,7 +1,6 @@ --- title: Chat History description: Save and restore chat conversations across sessions -icon: Database --- import { Callout } from 'fumadocs-ui/components/callout'; diff --git a/apps/docs/content/docs/chat/storage/meta.json b/apps/docs/content/docs/chat/storage/meta.json new file mode 100644 index 0000000..a4b2f0e --- /dev/null +++ b/apps/docs/content/docs/chat/storage/meta.json @@ -0,0 +1,4 @@ +{ + "title": "Storage", + "pages": ["session", "chat-history"] +} diff --git a/apps/docs/content/docs/context/session.mdx b/apps/docs/content/docs/chat/storage/session.mdx similarity index 100% rename from apps/docs/content/docs/context/session.mdx rename to apps/docs/content/docs/chat/storage/session.mdx diff --git a/apps/docs/content/docs/ui.mdx b/apps/docs/content/docs/chat/ui.mdx similarity index 80% rename from apps/docs/content/docs/ui.mdx rename to apps/docs/content/docs/chat/ui.mdx index 6d9e314..c30ad4e 100644 --- a/apps/docs/content/docs/ui.mdx +++ b/apps/docs/content/docs/chat/ui.mdx @@ -1,7 +1,6 @@ --- title: Copilot UI description: Pre-built chat components and styling setup -icon: Palette --- import { Callout } from 'fumadocs-ui/components/callout'; @@ -332,8 +331,84 @@ import { --- +--- + +## Message Actions + +Add floating copy, edit, feedback, and custom buttons to messages. Actions appear on hover below each message bubble. + +```tsx + + + + sendFeedback({ messageId: message.id, type })} + /> + + + + + + +``` + +| Component | Description | +|-----------|-------------| +| `CopilotChat.MessageActions` | Registers actions for a role (`user` or `assistant`) | +| `CopilotChat.CopyAction` | Copy message to clipboard | +| `CopilotChat.EditAction` | Inline edit for user messages | +| `CopilotChat.FeedbackAction` | Thumbs up / down | +| `CopilotChat.Action` | Fully custom action button | + +```tsx +// Custom action + + + } + tooltip="Share" + onClick={({ message }) => share(message.content)} + /> + + +// Conditional — hide based on message content +} + tooltip="Report" + hidden={({ message }) => !message.content} + onClick={({ message }) => report(message.id)} +/> +``` + +### Props + +```tsx +// MessageActions +role: "user" | "assistant" + +// CopyAction / EditAction +tooltip?: string +className?: string + +// FeedbackAction +onFeedback?: (message: ChatMessage, type: "helpful" | "not-helpful") => void +tooltip?: string + +// Action (custom) +icon: ReactNode +tooltip: string +onClick: (props: { message: ChatMessage }) => void +hidden?: boolean | ((props: { message: ChatMessage }) => boolean) +``` + + +If no `MessageActions` children are declared, the chat UI is identical to before — purely additive. + + +--- + ## Next Steps - [Customizations](/docs/customizations) - Create custom themes, CSS classes, branding -- [Chat](/docs/chat) - Chat component props and configuration -- [Generative UI](/docs/generative-ui) - Render custom components from AI +- [Generative UI](/docs/chat/generative-ui) - Render custom components from AI +- [Branching](/docs/advanced/branching) - Conversation branching and edit history diff --git a/apps/docs/content/docs/context/index.mdx b/apps/docs/content/docs/context/index.mdx deleted file mode 100644 index 2e5df51..0000000 --- a/apps/docs/content/docs/context/index.mdx +++ /dev/null @@ -1,251 +0,0 @@ ---- -title: Context Management -description: Make AI aware of your application state and manage the context window -icon: Lightbulb ---- - -import { Callout } from 'fumadocs-ui/components/callout'; - -Give the AI awareness of your application state so it can provide relevant, contextual responses. The SDK also provides advanced context window management for long conversations. - ---- - -## Application Context - -Inject your app state into the AI's context so it can answer questions about what's happening in your app. - -### Without context - -``` -User: "Is this in stock?" -AI: "I don't know what product you're referring to." -``` - -### With context - -```tsx -useAIContext({ - key: 'current-product', - data: { name: 'Wireless Headphones', stock: 42, price: 79.99 }, -}); -``` - -``` -User: "Is this in stock?" -AI: "Yes! The Wireless Headphones are in stock with 42 units available at $79.99." -``` - ---- - -## useAIContext - -Register a single context: - -```tsx -import { useAIContext } from '@yourgpt/copilot-sdk/react'; - -function ProductPage({ product }) { - useAIContext({ - key: 'current-product', - data: { - id: product.id, - name: product.name, - price: product.price, - description: product.description, - inStock: product.inventory > 0, - category: product.category, - }, - description: 'The product the user is currently viewing', - }); - - return ; -} -``` - -### Parameters - -| Parameter | Type | Required | Description | -|-----------|------|----------|-------------| -| `key` | `string` | Yes | Unique identifier for this context | -| `data` | `any` | Yes | The data to expose to AI | -| `description` | `string` | No | Helps AI understand when to use this context | -| `parentId` | `string` | No | For hierarchical contexts | - -### Returns - -Returns a context ID string that can be used as `parentId` for nested contexts. - ---- - -## useAIContexts - -Register multiple contexts at once: - -```tsx -import { useAIContexts } from '@yourgpt/copilot-sdk/react'; - -function AppContext() { - useAIContexts([ - { - key: 'user', - data: { - name: user.name, - email: user.email, - plan: user.subscription, - role: user.role, - }, - description: 'Current logged-in user information', - }, - { - key: 'cart', - data: { - items: cart.items, - total: cart.total, - itemCount: cart.items.length, - }, - description: 'Shopping cart contents', - }, - { - key: 'page', - data: { - route: router.pathname, - params: router.query, - }, - description: 'Current page location', - }, - ]); - - return null; -} -``` - ---- - -## Hierarchical Contexts - -Create parent-child relationships for complex data: - -```tsx -function TeamDashboard({ team }) { - const teamContextId = useAIContext({ - key: 'team', - data: { name: team.name, memberCount: team.members.length }, - description: 'The team being viewed', - }); - - return ( -
-

{team.name}

- {team.members.map(member => ( - - ))} -
- ); -} - -function MemberCard({ member, parentId }) { - useAIContext({ - key: `member-${member.id}`, - data: { name: member.name, role: member.role, tasks: member.tasks }, - description: `Team member: ${member.name}`, - parentId, - }); - - return
{member.name}
; -} -``` - - -Hierarchical contexts help AI understand relationships. When a user asks about "John's tasks", AI knows John is part of the team context. - - ---- - -## Dynamic Context Updates - -Context updates automatically when data changes: - -```tsx -function LiveDashboard() { - const [metrics, setMetrics] = useState(null); - - useEffect(() => { - const interval = setInterval(async () => { - const data = await fetchMetrics(); - setMetrics(data); - }, 5000); - return () => clearInterval(interval); - }, []); - - // Context auto-updates when metrics change - useAIContext({ - key: 'live-metrics', - data: metrics, - description: 'Real-time dashboard metrics (updates every 5s)', - }); - - return ; -} -``` - ---- - -## Context Cleanup - -Contexts are automatically removed when components unmount: - -```tsx -function ConditionalContext({ showDetails }) { - if (showDetails) { - return ; - } - return null; -} - -function DetailedContext() { - useAIContext({ - key: 'details', - data: { /* ... */ }, - }); - // Context removed when this component unmounts - return
; -} -``` - ---- - -## What to Include in Context - -```tsx -// ✅ Good — relevant, scoped data -useAIContext({ - key: 'order-details', - data: { - orderId: order.id, - status: order.status, - items: order.items.map(i => ({ name: i.name, qty: i.qty })), - total: order.total, - }, -}); -``` - - -Never include sensitive data (passwords, API keys, credit cards) in AI context. The context is sent to the LLM provider. - - ---- - -## Advanced Context Window Management - -For long conversations, the SDK provides tools to control what the AI sees and how history is managed. - -- **[Compaction](/docs/context/compaction)** — auto-summarize old messages to stay within token limits -- **[Token Tracking](/docs/context/token-tracking)** — monitor context window usage with `useContextStats` -- **[Session Persistence](/docs/context/session)** — survive page reloads and compact on the server - ---- - -## Next Steps - -- [Custom Tools](/docs/tools) - Build tools that use context -- [Compaction](/docs/context/compaction) - Manage long conversation history diff --git a/apps/docs/content/docs/context/meta.json b/apps/docs/content/docs/context/meta.json deleted file mode 100644 index 00cb426..0000000 --- a/apps/docs/content/docs/context/meta.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "title": "Context Management", - "icon": "Lightbulb", - "pages": ["compaction", "token-tracking", "session"] -} diff --git a/apps/docs/content/docs/headless.mdx b/apps/docs/content/docs/customizations/headless.mdx similarity index 99% rename from apps/docs/content/docs/headless.mdx rename to apps/docs/content/docs/customizations/headless.mdx index 32eed7d..6873a73 100644 --- a/apps/docs/content/docs/headless.mdx +++ b/apps/docs/content/docs/customizations/headless.mdx @@ -1,7 +1,6 @@ --- title: Headless Copilot description: Build fully custom chat UIs using raw SDK primitives — no built-in components required -icon: Layers --- import { Callout } from 'fumadocs-ui/components/callout'; diff --git a/apps/docs/content/docs/customizations/index.mdx b/apps/docs/content/docs/customizations/index.mdx index e236ed7..78de9c6 100644 --- a/apps/docs/content/docs/customizations/index.mdx +++ b/apps/docs/content/docs/customizations/index.mdx @@ -1,7 +1,6 @@ --- title: Customizations description: Create custom themes, extend components, and brand your copilot -icon: Paintbrush --- import { Callout } from 'fumadocs-ui/components/callout'; diff --git a/apps/docs/content/docs/customizations/meta.json b/apps/docs/content/docs/customizations/meta.json index df28bea..9fde28f 100644 --- a/apps/docs/content/docs/customizations/meta.json +++ b/apps/docs/content/docs/customizations/meta.json @@ -1,5 +1,5 @@ { "title": "Customizations", - "icon": "Paintbrush", - "pages": ["css-classes", "chat-primitives", "custom-message-view"] + "icon": "MagicWand", + "pages": ["headless", "css-classes", "chat-primitives", "custom-message-view"] } diff --git a/apps/docs/content/docs/deploy.mdx b/apps/docs/content/docs/deploy.mdx index 82f8171..f8c195f 100644 --- a/apps/docs/content/docs/deploy.mdx +++ b/apps/docs/content/docs/deploy.mdx @@ -1,7 +1,6 @@ --- title: Deploy description: Deploy your Copilot backend to any platform -icon: Rocket --- import { Callout } from 'fumadocs-ui/components/callout'; diff --git a/apps/docs/content/docs/generative-ui.mdx b/apps/docs/content/docs/generative-ui.mdx index 32c418f..b277df2 100644 --- a/apps/docs/content/docs/generative-ui.mdx +++ b/apps/docs/content/docs/generative-ui.mdx @@ -1,44 +1,41 @@ --- title: Generative UI -description: Render custom React components from tool results -icon: Blocks +description: Render rich React components from AI tool results — per-tool custom renderers or AI-driven built-in components +icon: AiMagic --- import { Callout } from 'fumadocs-ui/components/callout'; +import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; -Transform tool results into rich, interactive React components instead of plain text. +Instead of showing raw JSON or plain text from tool calls, render interactive UI directly inside the chat — from your own branded React components per tool, to fully AI-generated dashboards, charts, and layouts running in a sandboxed iframe. --- -## Overview +## Two Approaches -When AI calls a tool, instead of showing raw JSON, you can render custom UI: - -``` -User: "What's the weather in Miami?" - ↓ -AI calls: get_weather({ city: "Miami" }) - ↓ -Tool returns: { temp: 82, conditions: "Sunny" } - ↓ -UI renders: [Beautiful weather card component] -``` +| | `toolRenderers` | `useGenerativeUI` (experimental) | +|---|---|---| +| **What it does** | Your React component renders per tool result | AI writes full HTML + Tailwind + Chart.js, runs in a sandboxed iframe — or picks a typed renderer (table, stat, card, chart) | +| **Who decides the UI** | You — one renderer per tool | The AI — generates or selects based on the data | +| **Setup** | Pass `toolRenderers` to `` | One `useGenerativeUI()` call + backend `generativeUITool()` | +| **Best for** | Domain-specific, branded components | Dashboards, charts, tables, any data layout you haven't pre-built | +| **Customization** | Full control | Override any built-in renderer | --- -## Basic Setup +## Approach 1 — `toolRenderers` + +Map tool names to React components. Each component receives the tool's args and result as props. -### 1. Define Tool Renderers +### Basic example ```tsx -import { CopilotChat } from '@yourgpt/copilot-sdk/ui'; +import { CopilotChat } from "@yourgpt/copilot-sdk/ui"; -// Custom component for weather results function WeatherCard({ data, status }) { - if (status === 'executing') { + if (status === "executing") { return
Loading weather...
; } - return (

{data.city}

@@ -48,74 +45,35 @@ function WeatherCard({ data, status }) { ); } -// Pass to CopilotChat ``` -### 2. Register the Tool - -```tsx -useTools({ - get_weather: { - description: 'Get current weather for a city', - parameters: z.object({ - city: z.string(), - }), - handler: async ({ city }) => { - const weather = await fetchWeather(city); - return { - success: true, - data: { - city, - temp: weather.temperature, - conditions: weather.conditions, - }, - }; - }, - }, -}); -``` - ---- - -## ToolRendererProps +### ToolRendererProps -Every tool renderer receives these props: +Every renderer receives these props: ```typescript interface ToolRendererProps { - // Current execution status - status: 'pending' | 'executing' | 'completed' | 'error' | 'failed' | 'rejected'; - - // Arguments passed to the tool - args: Record; - - // Result data (when completed) - data?: unknown; - - // Error message (when failed) - error?: string; - - // Unique execution ID + status: "pending" | "executing" | "completed" | "error" | "failed" | "rejected"; + args: Record; // arguments passed to the tool + data?: unknown; // result (when completed) + error?: string; // error message (when failed) executionId: string; - - // Tool name toolName: string; } ``` ---- - -## Handling All States +### Handling all states ```tsx function ChartCard({ status, data, error, args }: ToolRendererProps) { - // Loading state - if (status === 'pending' || status === 'executing') { + if (status === "pending" || status === "executing") { return (
@@ -126,8 +84,7 @@ function ChartCard({ status, data, error, args }: ToolRendererProps) { ); } - // Error state - if (status === 'error' || status === 'failed') { + if (status === "error" || status === "failed") { return (

Failed to load chart

@@ -136,8 +93,7 @@ function ChartCard({ status, data, error, args }: ToolRendererProps) { ); } - // Rejected (user denied approval) - if (status === 'rejected') { + if (status === "rejected") { return (

Chart request was declined

@@ -145,7 +101,6 @@ function ChartCard({ status, data, error, args }: ToolRendererProps) { ); } - // Success state return (

{data.title}

@@ -161,44 +116,20 @@ function ChartCard({ status, data, error, args }: ToolRendererProps) { } ``` ---- - -## Multiple Tool Renderers - -```tsx - -``` - ---- - -## Interactive Components +### Interactive components -Tool renderers can be fully interactive: +Renderers can be fully interactive and call back into the chat: ```tsx function ProductCard({ data }: ToolRendererProps) { const [quantity, setQuantity] = useState(1); const { sendMessage } = useCopilot(); - const handleAddToCart = () => { - // Trigger AI to call add_to_cart tool - sendMessage(`Add ${quantity} of ${data.name} to my cart`); - }; - return (
{data.name}

{data.name}

${data.price}

-
setQuantity(Number(e.target.value))} className="w-16 border rounded px-2 py-1" /> -
@@ -215,111 +149,193 @@ function ProductCard({ data }: ToolRendererProps) { } ``` ---- - -## With AI Response Control +### Control AI response verbosity -Combine with `_aiResponseMode` to control AI behavior: +Return `_aiResponseMode: "brief"` from your tool handler to prevent the AI from describing what the UI already shows: ```tsx -useTools({ - show_dashboard: { - description: 'Display analytics dashboard', - parameters: z.object({ timeRange: z.string() }), - handler: async ({ timeRange }) => { - const data = await fetchDashboardData(timeRange); - return { - success: true, - data, - // Tell AI to be brief - UI speaks for itself - _aiResponseMode: 'brief', - _aiContext: `Dashboard displayed for ${timeRange}`, - }; - }, - }, -}); +handler: async ({ timeRange }) => { + const data = await fetchDashboardData(timeRange); + return { + success: true, + data, + _aiResponseMode: "brief", + _aiContext: `Dashboard for ${timeRange}`, + }; +}, ``` -Use `_aiResponseMode: 'brief'` when your UI component is self-explanatory. The AI will give a short acknowledgment instead of describing the data. +Use `_aiResponseMode: "brief"` when your UI component is self-explanatory. The AI gives a short acknowledgment instead of narrating the data. --- -## Best Practices +## Approach 2 — AI-Generated UI (Experimental) -1. **Handle all states** - Show loading, error, and success states -2. **Keep it focused** - One component per tool, single responsibility -3. **Make it responsive** - Components appear inline with chat messages -4. **Use AI Response Control** - Prevent AI from redundantly describing visual data -5. **Add interactivity** - Let users take actions directly from the UI + +`@yourgpt/copilot-sdk/experimental` — APIs may change without a semver major bump. + ---- +The AI calls a single `render_ui` tool and generates the UI itself. The standout capability is `type: "html"` — the AI writes full HTML with Tailwind CSS and Chart.js, rendered in a sandboxed iframe. No pre-built component needed. For structured data it can also pick typed renderers (`table`, `stat`, `card`, `chart`) automatically. -## Example: Complete Weather Tool +``` +User: "Show Q1 revenue by region" + ↓ +AI calls: render_ui({ type: "chart", chartType: "bar", labels: ["NA","EU","APAC"], datasets: [...] }) + ↓ +UI renders: [Bar chart] -```tsx -// Tool definition -useTools({ - get_weather: { - description: 'Get current weather for any city', - parameters: z.object({ - city: z.string().describe('City name'), - units: z.enum(['celsius', 'fahrenheit']).optional(), - }), - handler: async ({ city, units = 'fahrenheit' }) => { - const weather = await fetchWeatherAPI(city, units); - return { - success: true, - data: { - city, - temp: weather.temperature, - conditions: weather.conditions, - humidity: weather.humidity, - wind: weather.wind, - units, - }, - _aiResponseMode: 'brief', - _aiContext: `Weather: ${weather.temperature}° ${weather.conditions} in ${city}`, - }; +User: "Build an analytics dashboard" + ↓ +AI calls: render_ui({ type: "html", html: "
...
", height: "600px" }) + ↓ +UI renders: [Full dashboard in sandboxed iframe with Tailwind + Chart.js] +``` + +### Setup + + + + +Register `generativeUITool()` in your route. The key becomes the tool name. + +```typescript +import { generativeUITool } from "@yourgpt/copilot-sdk/experimental"; +import { streamText } from "@yourgpt/llm-sdk"; + +export async function POST(req: Request) { + const { messages } = await req.json(); + + const result = await streamText({ + model: openai("gpt-4o"), + system: "Use render_ui for any data, charts, or structured results.", + messages, + tools: { + render_ui: generativeUITool(), }, - }, -}); + }); -// Renderer component -function WeatherCard({ data, status }: ToolRendererProps) { - if (status !== 'completed') { - return ; - } + return result.toDataStreamResponse(); +} +``` - const { city, temp, conditions, humidity, wind, units } = data; - const tempUnit = units === 'celsius' ? '°C' : '°F'; + + - return ( -
-
-
-

{city}

-

{temp}{tempUnit}

-
- -
+Call `useGenerativeUI()` once in your component tree — it registers the renderer automatically. + +```tsx +import { useGenerativeUI } from "@yourgpt/copilot-sdk/experimental"; +import { CopilotChat } from "@yourgpt/copilot-sdk/ui"; + +function App() { + useGenerativeUI({ + chartRenderer: MyChartComponent, // required for chart type + }); + + return ; +} +``` + + + -

{conditions}

+### Built-in component types -
- 💧 {humidity}% - 💨 {wind} mph +| Type | When the AI uses it | Renderer | +|------|-------------------|----------| +| `html` | Dashboards, custom layouts, anything freeform | `HtmlRenderer` — sandboxed `