From 7a2240e52ad150b4660f2ab3d6e26b496bc32580 Mon Sep 17 00:00:00 2001 From: ankushchhabradelta4infotech-ai Date: Fri, 20 Mar 2026 19:38:05 +0530 Subject: [PATCH 01/39] fix --- packages/copilot-sdk/package.json | 1 + .../src/chat/classes/AbstractChat.ts | 142 +++++++----- .../src/ui/components/ui/markdown.tsx | 3 +- pnpm-lock.yaml | 219 +++++++++++++----- 4 files changed, 249 insertions(+), 116 deletions(-) diff --git a/packages/copilot-sdk/package.json b/packages/copilot-sdk/package.json index 3ece582..6320a8c 100644 --- a/packages/copilot-sdk/package.json +++ b/packages/copilot-sdk/package.json @@ -103,6 +103,7 @@ "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-tooltip": "^1.2.8", "@streamdown/code": "^1.0.1", + "@streamdown/math": "^1.0.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.0", "html-to-image": "^1.11.13", diff --git a/packages/copilot-sdk/src/chat/classes/AbstractChat.ts b/packages/copilot-sdk/src/chat/classes/AbstractChat.ts index 43b3d1e..a013879 100644 --- a/packages/copilot-sdk/src/chat/classes/AbstractChat.ts +++ b/packages/copilot-sdk/src/chat/classes/AbstractChat.ts @@ -799,11 +799,25 @@ export class AbstractChat { if (existing) { assistantMessage = existing; } else { - assistantMessage = createEmptyAssistantMessage() as T; + const visibleMessages = this.state.messages; + const currentLeafId = + visibleMessages.length > 0 + ? visibleMessages[visibleMessages.length - 1].id + : undefined; + assistantMessage = createEmptyAssistantMessage(undefined, { + parentId: currentLeafId, + }) as T; this.state.pushMessage(assistantMessage); } } else { - assistantMessage = createEmptyAssistantMessage() as T; + const visibleMessages = this.state.messages; + const currentLeafId = + visibleMessages.length > 0 + ? visibleMessages[visibleMessages.length - 1].id + : undefined; + assistantMessage = createEmptyAssistantMessage(undefined, { + parentId: currentLeafId, + }) as T; this.state.pushMessage(assistantMessage); } @@ -836,61 +850,47 @@ export class AbstractChat { return; } - // 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) { - this.debug("message:end mid-stream", { + // Handle message:end mid-stream (server-side agent loop turn completed). + // Do NOT create a separate message for each turn — keep accumulating into + // the same message so the user sees one assistant bubble, not three. + // Just skip message:end entirely and let content continue flowing. + if (chunk.type === "message:end" && this.streamState) { + this.debug("message:end mid-stream (keeping streamState alive)", { messageId: this.streamState.messageId, contentLength: this.streamState.content.length, toolCallsInState: this.streamState.toolCalls?.length ?? 0, chunkCount, }); + // Don't reset streamState — next message:start will be ignored and + // subsequent deltas will append to the same message. + continue; + } - // Finalize current message with its content and tool calls - const turnMessage = streamStateToMessage(this.streamState) as T; - - // Add toolCallsHidden metadata if applicable - const toolCallsHidden: Record = {}; - for (const [id, result] of this.streamState.toolResults) { - if (result.hidden !== undefined) { - toolCallsHidden[id] = result.hidden; - } - } - if ( - turnMessage.toolCalls?.length && - Object.keys(toolCallsHidden).length > 0 - ) { - (turnMessage as T & { metadata?: Record }).metadata = - { - ...(turnMessage as T & { metadata?: Record }) - .metadata, - toolCallsHidden, - }; - } - - this.state.updateMessageById( - this.streamState.messageId, - (existing) => ({ - ...turnMessage, - ...(existing.parentId !== undefined - ? { parentId: existing.parentId } - : {}), - ...(existing.childrenIds !== undefined - ? { childrenIds: existing.childrenIds } - : {}), - }), + // Handle message:start after a mid-stream message:end. + // Since we keep streamState alive above, this only fires if streamState + // was null for another reason. Just skip it — deltas will flow into + // the existing streamState. + if (chunk.type === "message:start" && this.streamState !== null) { + this.debug( + "message:start mid-stream (streamState already active, skipping)", ); - this.callbacks.onMessageFinish?.(turnMessage); - - // Reset stream state for next turn - will be initialized on next message:start - this.streamState = null; continue; } - // Handle message:start after a mid-stream finalization + // Handle message:start when streamState is null (shouldn't happen in + // normal flow, but handle gracefully by creating a new message). if (chunk.type === "message:start" && this.streamState === null) { - this.debug("message:start after mid-stream end - creating new message"); - const newMessage = createEmptyAssistantMessage() as T; + this.debug( + "message:start with null streamState - creating new message", + ); + const visibleMessages = this.state.messages; + const currentLeafId = + visibleMessages.length > 0 + ? visibleMessages[visibleMessages.length - 1].id + : undefined; + const newMessage = createEmptyAssistantMessage(undefined, { + parentId: currentLeafId, + }) as T; this.state.pushMessage(newMessage); this.streamState = createStreamState(newMessage.id); this.callbacks.onMessageStart?.(newMessage.id); @@ -936,6 +936,14 @@ export class AbstractChat { const messagesToInsert: T[] = []; let clientAssistantToolCalls: unknown[] | undefined; + // Track parent chain for inserted messages so they don't become + // orphan root children in the MessageTree. + const lastVisibleMsgs = this.state.messages; + let postEndInsertParentId: string | undefined = + lastVisibleMsgs.length > 0 + ? lastVisibleMsgs[lastVisibleMsgs.length - 1].id + : undefined; + for (const msg of chunk.messages) { // This is the client-tool assistant message already in state // (finalized by message:end but without toolCalls). @@ -954,14 +962,19 @@ export class AbstractChat { // Skip plain assistant text — already streamed if (msg.role === "assistant" && !msg.tool_calls?.length) continue; // Everything else (server tool results) needs inserting - messagesToInsert.push({ + const insertedMsg = { id: generateMessageId(), role: msg.role as T["role"], content: msg.content ?? "", toolCalls: msg.tool_calls as T["toolCalls"], toolCallId: msg.tool_call_id, createdAt: new Date(), - } as T); + ...(postEndInsertParentId + ? { parentId: postEndInsertParentId } + : {}), + } as T; + postEndInsertParentId = insertedMsg.id; + messagesToInsert.push(insertedMsg); } // Merge OpenAI-format tool_calls into the existing last assistant message @@ -983,8 +996,9 @@ export class AbstractChat { } if (messagesToInsert.length > 0) { - // Insert server tool results before the last assistant message - const currentMessages = this.state.messages; + // Insert server tool results before the last assistant message. + // Use _allMessages() to preserve inactive branch messages. + const currentMessages = this._allMessages(); let insertIdx = currentMessages.length; for (let i = currentMessages.length - 1; i >= 0; i--) { if (currentMessages[i].role === "assistant") { @@ -1106,11 +1120,25 @@ export class AbstractChat { ), }); - const currentStreamToolCallIds = new Set( - this.streamState?.toolCalls?.map((toolCall) => toolCall.id) ?? [], - ); + const currentStreamToolCallIds = new Set([ + ...(this.streamState?.toolCalls?.map((toolCall) => toolCall.id) ?? + []), + // Also include IDs from toolResults (populated by action:start/args/end + // chunks for server-side tools). Without this, assistant messages with + // tool_calls from done.messages are treated as "new" and inserted as + // duplicates even though the tools were already executed in-stream. + ...(this.streamState?.toolResults + ? Array.from(this.streamState.toolResults.keys()) + : []), + ]); const messagesToInsert: T[] = []; + // Track parent chain for inserted messages so they don't become + // orphan root children in the MessageTree (which would redirect + // the active path and blank the UI). + let insertChainParentId: string | undefined = + this.streamState?.messageId; + // Build hidden map from stream state's toolResults const toolCallsHidden: Record = {}; if (this.streamState?.toolResults) { @@ -1163,13 +1191,19 @@ export class AbstractChat { toolCallId: msg.tool_call_id, createdAt: new Date(), metadata, + ...(insertChainParentId ? { parentId: insertChainParentId } : {}), } as T; + insertChainParentId = message.id; messagesToInsert.push(message); } if (messagesToInsert.length > 0) { - const currentMessages = this.state.messages; + // Use _allMessages() to preserve inactive branch messages. + // this.state.messages only returns the visible path; calling + // setMessages() with just that would destroy all other branches + // when tree.reset() rebuilds. + const currentMessages = this._allMessages(); const currentStreamIndex = this.streamState ? currentMessages.findIndex( (message) => message.id === this.streamState!.messageId, diff --git a/packages/copilot-sdk/src/ui/components/ui/markdown.tsx b/packages/copilot-sdk/src/ui/components/ui/markdown.tsx index b640a54..b586e39 100644 --- a/packages/copilot-sdk/src/ui/components/ui/markdown.tsx +++ b/packages/copilot-sdk/src/ui/components/ui/markdown.tsx @@ -1,6 +1,7 @@ import { memo, ComponentProps } from "react"; import { Streamdown, LinkSafetyConfig } from "streamdown"; import { code } from "@streamdown/code"; +import { math } from "@streamdown/math"; export type MarkdownProps = { children: string; @@ -45,7 +46,7 @@ function MarkdownComponent({ return (
= 6'} + commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + compute-scroll-into-view@3.1.1: resolution: {integrity: sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==} @@ -5504,9 +5491,21 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hast-util-from-dom@5.0.1: + resolution: {integrity: sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q==} + + hast-util-from-html-isomorphic@2.0.0: + resolution: {integrity: sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw==} + + hast-util-from-html@2.0.3: + resolution: {integrity: sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==} + hast-util-from-parse5@8.0.3: resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==} + hast-util-is-element@3.0.0: + resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==} + hast-util-parse-selector@4.0.0: resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} @@ -5531,6 +5530,9 @@ packages: hast-util-to-string@3.0.1: resolution: {integrity: sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==} + hast-util-to-text@4.0.2: + resolution: {integrity: sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==} + hast-util-whitespace@3.0.0: resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} @@ -5916,6 +5918,10 @@ packages: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} + katex@0.16.39: + resolution: {integrity: sha512-FR2f6y85+81ZLO0GPhyQ+EJl/E5ILNWltJhpAeOTzRny952Z13x2867lTFDmvMZix//Ux3CuMQ2VkLXRbUwOFg==} + hasBin: true + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -6194,6 +6200,9 @@ packages: mdast-util-gfm@3.1.0: resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} + mdast-util-math@3.0.0: + resolution: {integrity: sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w==} + mdast-util-mdx-expression@2.0.1: resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} @@ -6268,6 +6277,9 @@ packages: micromark-extension-gfm@3.0.0: resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + micromark-extension-math@3.1.0: + resolution: {integrity: sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==} + micromark-extension-mdx-expression@3.0.1: resolution: {integrity: sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==} @@ -7064,6 +7076,9 @@ packages: rehype-harden@1.1.7: resolution: {integrity: sha512-j5DY0YSK2YavvNGV+qBHma15J9m0WZmRe8posT5AtKDS6TNWtMVTo6RiqF8SidfcASYz8f3k2J/1RWmq5zTXUw==} + rehype-katex@7.0.1: + resolution: {integrity: sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA==} + rehype-raw@7.0.0: resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==} @@ -7076,6 +7091,9 @@ packages: remark-gfm@4.0.1: resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} + remark-math@6.0.0: + resolution: {integrity: sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA==} + remark-mdx@3.1.1: resolution: {integrity: sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==} @@ -7711,6 +7729,9 @@ packages: unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + unist-util-find-after@5.0.0: + resolution: {integrity: sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==} + unist-util-is@6.0.1: resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} @@ -10909,6 +10930,15 @@ snapshots: react: 18.3.1 shiki: 3.20.0 + '@streamdown/math@1.0.2(react@18.3.1)': + dependencies: + katex: 0.16.39 + react: 18.3.1 + rehype-katex: 7.0.1 + remark-math: 6.0.0 + transitivePeerDependencies: + - supports-color + '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 @@ -11170,6 +11200,8 @@ snapshots: dependencies: '@types/node': 20.19.27 + '@types/katex@0.16.8': {} + '@types/mdast@4.0.4': dependencies: '@types/unist': 3.0.3 @@ -11772,6 +11804,8 @@ snapshots: commander@4.1.1: {} + commander@8.3.0: {} + compute-scroll-into-view@3.1.1: {} concat-map@0.0.1: {} @@ -12240,13 +12274,13 @@ snapshots: escape-string-regexp@5.0.0: {} - eslint-config-next@16.0.10(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3): + eslint-config-next@16.0.10(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3): dependencies: '@next/eslint-plugin-next': 16.0.10 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-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.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)) @@ -12280,32 +12314,12 @@ snapshots: - eslint-plugin-import-x - supports-color - eslint-config-next@16.1.5(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3): - dependencies: - '@next/eslint-plugin-next': 16.1.5 - 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-plugin-import: 2.32.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.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)) - globals: 16.4.0 - typescript-eslint: 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 - transitivePeerDependencies: - - '@typescript-eslint/parser' - - eslint-import-resolver-webpack - - eslint-plugin-import-x - - supports-color - eslint-config-next@16.1.5(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3): dependencies: '@next/eslint-plugin-next': 16.1.5 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-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-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)) @@ -12328,7 +12342,7 @@ snapshots: transitivePeerDependencies: - supports-color - 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-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 @@ -12339,11 +12353,11 @@ 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(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)): + 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)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 @@ -12354,18 +12368,18 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(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@9.39.2(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(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: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) 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-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -12390,7 +12404,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.2(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -13087,6 +13101,28 @@ snapshots: dependencies: function-bind: 1.1.2 + hast-util-from-dom@5.0.1: + dependencies: + '@types/hast': 3.0.4 + hastscript: 9.0.1 + web-namespaces: 2.0.1 + + hast-util-from-html-isomorphic@2.0.0: + dependencies: + '@types/hast': 3.0.4 + hast-util-from-dom: 5.0.1 + hast-util-from-html: 2.0.3 + unist-util-remove-position: 5.0.0 + + hast-util-from-html@2.0.3: + dependencies: + '@types/hast': 3.0.4 + devlop: 1.1.0 + hast-util-from-parse5: 8.0.3 + parse5: 7.3.0 + vfile: 6.0.3 + vfile-message: 4.0.3 + hast-util-from-parse5@8.0.3: dependencies: '@types/hast': 3.0.4 @@ -13098,6 +13134,10 @@ snapshots: vfile-location: 5.0.3 web-namespaces: 2.0.1 + hast-util-is-element@3.0.0: + dependencies: + '@types/hast': 3.0.4 + hast-util-parse-selector@4.0.0: dependencies: '@types/hast': 3.0.4 @@ -13193,6 +13233,13 @@ snapshots: dependencies: '@types/hast': 3.0.4 + hast-util-to-text@4.0.2: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + hast-util-is-element: 3.0.0 + unist-util-find-after: 5.0.0 + hast-util-whitespace@3.0.0: dependencies: '@types/hast': 3.0.4 @@ -13537,6 +13584,10 @@ snapshots: object.assign: 4.1.7 object.values: 1.2.1 + katex@0.16.39: + dependencies: + commander: 8.3.0 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -13829,6 +13880,18 @@ snapshots: transitivePeerDependencies: - supports-color + mdast-util-math@3.0.0: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + longest-streak: 3.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + unist-util-remove-position: 5.0.0 + transitivePeerDependencies: + - supports-color + mdast-util-mdx-expression@2.0.1: dependencies: '@types/estree-jsx': 1.0.5 @@ -14002,6 +14065,16 @@ snapshots: micromark-util-combine-extensions: 2.0.1 micromark-util-types: 2.0.2 + micromark-extension-math@3.1.0: + dependencies: + '@types/katex': 0.16.8 + devlop: 1.1.0 + katex: 0.16.39 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + micromark-extension-mdx-expression@3.0.1: dependencies: '@types/estree': 1.0.8 @@ -15014,6 +15087,16 @@ snapshots: dependencies: unist-util-visit: 5.0.0 + rehype-katex@7.0.1: + dependencies: + '@types/hast': 3.0.4 + '@types/katex': 0.16.8 + hast-util-from-html-isomorphic: 2.0.0 + hast-util-to-text: 4.0.2 + katex: 0.16.39 + unist-util-visit-parents: 6.0.2 + vfile: 6.0.3 + rehype-raw@7.0.0: dependencies: '@types/hast': 3.0.4 @@ -15044,6 +15127,15 @@ snapshots: transitivePeerDependencies: - supports-color + remark-math@6.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-math: 3.0.0 + micromark-extension-math: 3.1.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + remark-mdx@3.1.1: dependencies: mdast-util-mdx: 3.0.0 @@ -15927,6 +16019,11 @@ snapshots: trough: 2.2.0 vfile: 6.0.3 + unist-util-find-after@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-is@6.0.1: dependencies: '@types/unist': 3.0.3 From 25cee8928e72c5da90e0f4777ae20a9625c3e99d Mon Sep 17 00:00:00 2001 From: ankushchhabradelta4infotech-ai Date: Wed, 25 Mar 2026 19:40:35 +0530 Subject: [PATCH 02/39] fix: continue with tool execution --- packages/copilot-sdk/src/chat/AbstractAgentLoop.ts | 4 +++- packages/copilot-sdk/src/chat/ChatWithTools.ts | 6 ++++++ packages/copilot-sdk/src/chat/classes/AbstractChat.ts | 9 +++++++++ packages/copilot-sdk/src/react/hooks/useTool.ts | 9 ++++++--- 4 files changed, 24 insertions(+), 4 deletions(-) diff --git a/packages/copilot-sdk/src/chat/AbstractAgentLoop.ts b/packages/copilot-sdk/src/chat/AbstractAgentLoop.ts index c621214..e66170b 100644 --- a/packages/copilot-sdk/src/chat/AbstractAgentLoop.ts +++ b/packages/copilot-sdk/src/chat/AbstractAgentLoop.ts @@ -272,8 +272,10 @@ export class AbstractAgentLoop implements AgentLoopActions { } // Create new abort controller for this batch + // Do NOT reset _isCancelled here — if stop() was called between the + // iteration check above and this line, we must not wipe that signal. + // _isCancelled is only reset in resetIterations() (called by sendMessage). this.abortController = new AbortController(); - this._isCancelled = false; this._isProcessing = true; this.setIteration(this._iteration + 1); diff --git a/packages/copilot-sdk/src/chat/ChatWithTools.ts b/packages/copilot-sdk/src/chat/ChatWithTools.ts index a858854..8f807bb 100644 --- a/packages/copilot-sdk/src/chat/ChatWithTools.ts +++ b/packages/copilot-sdk/src/chat/ChatWithTools.ts @@ -265,6 +265,12 @@ export class ChatWithTools { const results = await this.agentLoop.executeToolCalls(toolCallInfos); this.debug("Tool results:", results); + // If stop() was called while tools were executing, don't restart the loop + if (this.agentLoop.isCancelled) { + this.debug("Skipping continueWithToolResults — loop was cancelled"); + return; + } + // Continue chat with tool results if (results.length > 0) { const toolResults = results.map((r) => ({ diff --git a/packages/copilot-sdk/src/chat/classes/AbstractChat.ts b/packages/copilot-sdk/src/chat/classes/AbstractChat.ts index e3e6943..90c3bbd 100644 --- a/packages/copilot-sdk/src/chat/classes/AbstractChat.ts +++ b/packages/copilot-sdk/src/chat/classes/AbstractChat.ts @@ -428,6 +428,15 @@ export class AbstractChat { // is not enough for React 18 to render the loading state. await new Promise((resolve) => setTimeout(resolve, 0)); + // If stop() was called during the macrotask yield, status will have been + // reset to "ready" — don't restart the loop in that case. + if (this.status === "ready" || this.status === "error") { + this.debug( + "Skipping processRequest — status reset during yield (stop was called)", + ); + return; + } + // Continue request await this.processRequest(); } catch (error) { diff --git a/packages/copilot-sdk/src/react/hooks/useTool.ts b/packages/copilot-sdk/src/react/hooks/useTool.ts index f8c1c88..4ff1a96 100644 --- a/packages/copilot-sdk/src/react/hooks/useTool.ts +++ b/packages/copilot-sdk/src/react/hooks/useTool.ts @@ -240,8 +240,11 @@ export function useTools(tools: ToolSet): void { // Update ref when tools change toolsRef.current = tools; - // Create a stable key from tool names to detect actual changes - const toolsKey = Object.keys(tools).sort().join(","); + // Create a stable key from tool names + availability flags to detect actual changes + const toolsKey = Object.entries(tools) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([name, def]) => `${name}:${def.available ?? true}`) + .join(","); useEffect(() => { const currentTools = toolsRef.current; @@ -269,7 +272,7 @@ export function useTools(tools: ToolSet): void { registeredToolsRef.current = []; }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [toolsKey]); // Only re-run when tool names change, not on every render + }, [toolsKey]); // Re-run when tool names or availability flags change } /** From 82d1fbfc15cf73bf513b2431dca4d15df34449ec Mon Sep 17 00:00:00 2001 From: Sahil Date: Sat, 28 Mar 2026 21:40:24 +0530 Subject: [PATCH 03/39] feat(examples): add generative-ui-demo and skills-demo as standalone Vite 8 examples - generative-ui-demo: port of experimental generative UI to Vite+Express, recharts, dark UI with prompt suggestions sidebar - skills-demo: new demo for server-side skills system with loadSkills(), 3 skill files (code-review/concise-mode/customer-support), branching toggle via allowEdit prop Co-Authored-By: Claude Sonnet 4.6 --- examples/generative-ui-demo/.env.example | 3 + examples/generative-ui-demo/index.html | 12 + examples/generative-ui-demo/package.json | 38 +++ .../generative-ui-demo/postcss.config.mjs | 5 + examples/generative-ui-demo/server/index.ts | 109 +++++++ examples/generative-ui-demo/src/App.tsx | 201 ++++++++++++ .../generative-ui-demo/src/ChartRenderer.tsx | 176 +++++++++++ examples/generative-ui-demo/src/index.css | 1 + examples/generative-ui-demo/src/main.tsx | 10 + examples/generative-ui-demo/tsconfig.app.json | 22 ++ examples/generative-ui-demo/tsconfig.json | 7 + .../generative-ui-demo/tsconfig.node.json | 15 + examples/generative-ui-demo/vite.config.ts | 18 ++ examples/skills-demo/.env.example | 3 + examples/skills-demo/index.html | 13 + examples/skills-demo/package.json | 38 +++ examples/skills-demo/postcss.config.mjs | 5 + examples/skills-demo/server/index.ts | 149 +++++++++ examples/skills-demo/src/App.tsx | 295 ++++++++++++++++++ examples/skills-demo/src/index.css | 1 + examples/skills-demo/src/main.tsx | 10 + examples/skills-demo/tsconfig.json | 22 ++ examples/skills-demo/tsconfig.node.json | 15 + examples/skills-demo/vite.config.ts | 18 ++ 24 files changed, 1186 insertions(+) create mode 100644 examples/generative-ui-demo/.env.example create mode 100644 examples/generative-ui-demo/index.html create mode 100644 examples/generative-ui-demo/package.json create mode 100644 examples/generative-ui-demo/postcss.config.mjs create mode 100644 examples/generative-ui-demo/server/index.ts create mode 100644 examples/generative-ui-demo/src/App.tsx create mode 100644 examples/generative-ui-demo/src/ChartRenderer.tsx create mode 100644 examples/generative-ui-demo/src/index.css create mode 100644 examples/generative-ui-demo/src/main.tsx create mode 100644 examples/generative-ui-demo/tsconfig.app.json create mode 100644 examples/generative-ui-demo/tsconfig.json create mode 100644 examples/generative-ui-demo/tsconfig.node.json create mode 100644 examples/generative-ui-demo/vite.config.ts create mode 100644 examples/skills-demo/.env.example create mode 100644 examples/skills-demo/index.html create mode 100644 examples/skills-demo/package.json create mode 100644 examples/skills-demo/postcss.config.mjs create mode 100644 examples/skills-demo/server/index.ts create mode 100644 examples/skills-demo/src/App.tsx create mode 100644 examples/skills-demo/src/index.css create mode 100644 examples/skills-demo/src/main.tsx create mode 100644 examples/skills-demo/tsconfig.json create mode 100644 examples/skills-demo/tsconfig.node.json create mode 100644 examples/skills-demo/vite.config.ts diff --git a/examples/generative-ui-demo/.env.example b/examples/generative-ui-demo/.env.example new file mode 100644 index 0000000..afe6c89 --- /dev/null +++ b/examples/generative-ui-demo/.env.example @@ -0,0 +1,3 @@ +ANTHROPIC_API_KEY=sk-ant-... +# or +OPENAI_API_KEY=sk-... diff --git a/examples/generative-ui-demo/index.html b/examples/generative-ui-demo/index.html new file mode 100644 index 0000000..163a1d2 --- /dev/null +++ b/examples/generative-ui-demo/index.html @@ -0,0 +1,12 @@ + + + + + + Generative UI Demo + + +
+ + + diff --git a/examples/generative-ui-demo/package.json b/examples/generative-ui-demo/package.json new file mode 100644 index 0000000..988bfca --- /dev/null +++ b/examples/generative-ui-demo/package.json @@ -0,0 +1,38 @@ +{ + "name": "generative-ui-demo", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "concurrently \"pnpm dev:server\" \"pnpm dev:client\"", + "dev:client": "vite --port 3031", + "dev:server": "tsx watch server/index.ts", + "build": "tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@yourgpt/copilot-sdk": "workspace:*", + "@yourgpt/llm-sdk": "workspace:*", + "cors": "^2.8.5", + "dotenv": "^16.4.0", + "express": "^4.21.0", + "lucide-react": "^0.563.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "recharts": "^2.13.0" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4", + "@types/cors": "^2.8.17", + "@types/express": "^5.0.0", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "@vitejs/plugin-react": "6.0.1", + "concurrently": "^9.0.0", + "tailwindcss": "^4", + "tsx": "^4.19.0", + "typescript": "^5.6.0", + "vite": "8.0.3" + } +} diff --git a/examples/generative-ui-demo/postcss.config.mjs b/examples/generative-ui-demo/postcss.config.mjs new file mode 100644 index 0000000..a7f73a2 --- /dev/null +++ b/examples/generative-ui-demo/postcss.config.mjs @@ -0,0 +1,5 @@ +export default { + plugins: { + '@tailwindcss/postcss': {}, + }, +} diff --git a/examples/generative-ui-demo/server/index.ts b/examples/generative-ui-demo/server/index.ts new file mode 100644 index 0000000..134e8c9 --- /dev/null +++ b/examples/generative-ui-demo/server/index.ts @@ -0,0 +1,109 @@ +import "dotenv/config"; +import express from "express"; +import cors from "cors"; +import { createRuntime } from "@yourgpt/llm-sdk"; +import { createAnthropic } from "@yourgpt/llm-sdk/anthropic"; +import { createOpenAI } from "@yourgpt/llm-sdk/openai"; + +function resolveProvider() { + if (process.env.ANTHROPIC_API_KEY) { + return { + provider: createAnthropic({ apiKey: process.env.ANTHROPIC_API_KEY }), + model: "claude-haiku-4-5", + providerName: "Anthropic", + }; + } + if (process.env.OPENAI_API_KEY) { + return { + provider: createOpenAI({ apiKey: process.env.OPENAI_API_KEY }), + model: "gpt-4o-mini", + providerName: "OpenAI", + }; + } + throw new Error( + "Set ANTHROPIC_API_KEY or OPENAI_API_KEY to run the generative UI demo.", + ); +} + +const { provider, model, providerName } = resolveProvider(); + +const runtime = createRuntime({ + provider, + model, + systemPrompt: `You are a data-rich assistant that always renders visual UI components instead of plain text. + +You have a render_ui tool. Use it proactively based on the request: + +- "table" — any list of items, comparisons, records +- "stat" — numbers, KPIs, metrics with deltas +- "card" — single entity details (person, product, place) +- "chart" — trends and distributions (bar, line, pie, area, scatter) +- "html" — rich, fully custom layouts (see below) + +━━━ HTML TYPE CAPABILITIES ━━━ +The html iframe has TWO libraries pre-loaded: +1. Tailwind CSS (Play CDN) — use any utility class freely +2. Chart.js — create inline charts with + new Chart(...) + +Design in a shadcn/ui style: +- Cards: bg-white rounded-xl border border-gray-200 shadow-sm p-6 +- Headings: text-gray-900 font-semibold text-lg +- Muted: text-gray-500 text-sm +- Badges: bg-blue-50 text-blue-700 px-2.5 py-0.5 rounded-full text-xs font-medium +- Buttons: bg-gray-900 text-white rounded-lg px-4 py-2 text-sm font-medium hover:bg-gray-700 +- Grid: grid grid-cols-3 gap-4 (or 2-col for cards) +- Dividers: border-t border-gray-100 mt-4 pt-4 + +Chart.js usage in html — inline script after canvas: + + + +Use html when asked for dashboards, interactive layouts, shadcn-style components, or anything combining charts + stats + cards in one view. +For html, set the "height" field to fit the content — e.g. "600px" for dashboards, "320px" for a small card. +Always prefer a structured type (table, stat, card) over html when the data fits a single type.`, + maxIterations: 3, +}); + +const app = express(); +app.use(cors()); +app.use(express.json()); + +app.get("/api/chat", (_req, res) => { + res.json({ + status: "ok", + provider: providerName, + model, + demo: "generative-ui", + }); +}); + +app.post("/api/chat", async (req, res) => { + const PORT_NUM = process.env.PORT || 3030; + const url = `http://localhost:${PORT_NUM}/api/chat`; + const webReq = new Request(url, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(req.body), + }); + const response = await runtime.handleRequest(webReq); + res.status(response.status); + response.headers.forEach((val, key) => res.setHeader(key, val)); + const body = await response.text(); + res.send(body); +}); + +const PORT = process.env.PORT ? Number(process.env.PORT) : 3030; +app.listen(PORT, () => + console.log( + `Generative UI server running on http://localhost:${PORT} (${providerName} / ${model})`, + ), +); diff --git a/examples/generative-ui-demo/src/App.tsx b/examples/generative-ui-demo/src/App.tsx new file mode 100644 index 0000000..6e47cdb --- /dev/null +++ b/examples/generative-ui-demo/src/App.tsx @@ -0,0 +1,201 @@ +import { CopilotProvider } from "@yourgpt/copilot-sdk/react"; +import { CopilotChat } from "@yourgpt/copilot-sdk/ui"; +import { useGenerativeUI } from "@yourgpt/copilot-sdk/experimental"; +import { ChartRenderer } from "./ChartRenderer"; +import "@yourgpt/copilot-sdk/ui/styles.css"; + +const PROMPT_SUGGESTIONS = [ + "Build a shadcn-style analytics dashboard with a Chart.js bar chart of monthly revenue and 3 KPI stat cards", + "Show a shadcn card grid of the top 5 programming languages with usage %, trend badge, and color", + "Render a Chart.js doughnut chart of browser market share with a legend", + "Give me a stat dashboard of key web metrics", + "Show me a table of the top 10 S&P 500 companies by market cap", + "Create a card for Elon Musk with key facts", + "Build a shadcn pricing page with 3 tiers, feature lists, and a highlighted popular plan", + "Show a line chart of BTC price over the last 6 months", + "Render a Chart.js radar chart comparing React, Vue, Angular, and Svelte across 5 metrics", +]; + +function GenerativeUIChatInner() { + useGenerativeUI({ + name: "render_ui", + chartRenderer: ChartRenderer, + }); + + return ( + + ); +} + +export default function App() { + return ( + <> + + +
+
+ + {/* Header */} +
+
+ + + + + + + + Generative UI + +
+ + Experimental + +
+ Tables · Stats · Cards · Charts · HTML +
+
+ + {/* Two-panel layout */} +
+ + {/* Left: Prompt suggestions */} +
+

Try asking…

+ {PROMPT_SUGGESTIONS.map((prompt) => ( + + ))} +
+ + {/* Right: Chat */} +
+ +
+
+
+
+ + ); +} diff --git a/examples/generative-ui-demo/src/ChartRenderer.tsx b/examples/generative-ui-demo/src/ChartRenderer.tsx new file mode 100644 index 0000000..1b810be --- /dev/null +++ b/examples/generative-ui-demo/src/ChartRenderer.tsx @@ -0,0 +1,176 @@ +import { + BarChart, + Bar, + LineChart, + Line, + PieChart, + Pie, + Cell, + AreaChart, + Area, + ScatterChart, + Scatter, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + Legend, + ResponsiveContainer, +} from "recharts"; +import type { ChartRendererProps } from "@yourgpt/copilot-sdk/experimental"; + +const COLORS = ["#6366f1", "#10b981", "#f59e0b", "#3b82f6", "#8b5cf6"]; + +// Reshape datasets + labels → recharts data format +function toRechartsData( + labels: string[], + datasets: { label: string; data: number[] }[], +) { + return labels.map((label, i) => { + const point: Record = { name: label }; + for (const ds of datasets) { + point[ds.label] = ds.data[i] ?? 0; + } + return point; + }); +} + +export function ChartRenderer({ payload }: ChartRendererProps) { + const { chartType, labels, datasets, title, xLabel, yLabel } = payload; + const data = toRechartsData(labels, datasets); + const dataKeys = datasets.map((ds) => ds.label); + + return ( +
+ {title && ( +

{title}

+ )} + + {chartType === "pie" ? ( + + ({ + name: d.name, + value: Number(d[dataKeys[0]] ?? 0), + }))} + dataKey="value" + nameKey="name" + cx="50%" + cy="50%" + outerRadius={80} + label={({ name, percent }) => + `${name} ${(percent * 100).toFixed(0)}%` + } + > + {data.map((_, i) => ( + + ))} + + + + + ) : chartType === "line" ? ( + + + + + + {dataKeys.length > 1 && } + {dataKeys.map((key, i) => ( + + ))} + + ) : chartType === "area" ? ( + + + + + + {dataKeys.length > 1 && } + {dataKeys.map((key, i) => ( + + ))} + + ) : chartType === "scatter" ? ( + + + + + + + + ) : ( + // Default: bar + + + + + + {dataKeys.length > 1 && } + {dataKeys.map((key, i) => ( + + ))} + + )} + +
+ ); +} diff --git a/examples/generative-ui-demo/src/index.css b/examples/generative-ui-demo/src/index.css new file mode 100644 index 0000000..f1d8c73 --- /dev/null +++ b/examples/generative-ui-demo/src/index.css @@ -0,0 +1 @@ +@import "tailwindcss"; diff --git a/examples/generative-ui-demo/src/main.tsx b/examples/generative-ui-demo/src/main.tsx new file mode 100644 index 0000000..15753af --- /dev/null +++ b/examples/generative-ui-demo/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import "./index.css"; +import App from "./App"; + +createRoot(document.getElementById("root")!).render( + + + , +); diff --git a/examples/generative-ui-demo/tsconfig.app.json b/examples/generative-ui-demo/tsconfig.app.json new file mode 100644 index 0000000..4d0f4fb --- /dev/null +++ b/examples/generative-ui-demo/tsconfig.app.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + }, + "include": ["src"] +} diff --git a/examples/generative-ui-demo/tsconfig.json b/examples/generative-ui-demo/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/examples/generative-ui-demo/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/examples/generative-ui-demo/tsconfig.node.json b/examples/generative-ui-demo/tsconfig.node.json new file mode 100644 index 0000000..1f27c41 --- /dev/null +++ b/examples/generative-ui-demo/tsconfig.node.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true + }, + "include": ["vite.config.ts", "server"] +} diff --git a/examples/generative-ui-demo/vite.config.ts b/examples/generative-ui-demo/vite.config.ts new file mode 100644 index 0000000..940d76f --- /dev/null +++ b/examples/generative-ui-demo/vite.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import path from "path"; + +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, + }, + server: { + port: 3031, + proxy: { + "/api": "http://localhost:3030", + }, + }, +}); diff --git a/examples/skills-demo/.env.example b/examples/skills-demo/.env.example new file mode 100644 index 0000000..afe6c89 --- /dev/null +++ b/examples/skills-demo/.env.example @@ -0,0 +1,3 @@ +ANTHROPIC_API_KEY=sk-ant-... +# or +OPENAI_API_KEY=sk-... diff --git a/examples/skills-demo/index.html b/examples/skills-demo/index.html new file mode 100644 index 0000000..5208e48 --- /dev/null +++ b/examples/skills-demo/index.html @@ -0,0 +1,13 @@ + + + + + + + Skills Demo + + +
+ + + diff --git a/examples/skills-demo/package.json b/examples/skills-demo/package.json new file mode 100644 index 0000000..da1ec08 --- /dev/null +++ b/examples/skills-demo/package.json @@ -0,0 +1,38 @@ +{ + "name": "skills-demo", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "concurrently \"pnpm dev:server\" \"pnpm dev:client\"", + "dev:client": "vite --port 3033", + "dev:server": "tsx watch server/index.ts", + "build": "tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@yourgpt/copilot-sdk": "workspace:*", + "@yourgpt/llm-sdk": "workspace:*", + "cors": "^2.8.5", + "dotenv": "^16.4.0", + "express": "^4.21.0", + "lucide-react": "^0.563.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4", + "@types/cors": "^2.8.17", + "@types/express": "^5.0.0", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "@vitejs/plugin-react": "6.0.1", + "concurrently": "^9.0.0", + "path": "^0.12.7", + "tailwindcss": "^4", + "tsx": "^4.19.0", + "typescript": "^5.6.0", + "vite": "8.0.3" + } +} diff --git a/examples/skills-demo/postcss.config.mjs b/examples/skills-demo/postcss.config.mjs new file mode 100644 index 0000000..a7f73a2 --- /dev/null +++ b/examples/skills-demo/postcss.config.mjs @@ -0,0 +1,5 @@ +export default { + plugins: { + '@tailwindcss/postcss': {}, + }, +} diff --git a/examples/skills-demo/server/index.ts b/examples/skills-demo/server/index.ts new file mode 100644 index 0000000..0a12863 --- /dev/null +++ b/examples/skills-demo/server/index.ts @@ -0,0 +1,149 @@ +import "dotenv/config"; +import express from "express"; +import cors from "cors"; +import path from "path"; +import { fileURLToPath } from "url"; +import { createRuntime } from "@yourgpt/llm-sdk"; +import { createAnthropic } from "@yourgpt/llm-sdk/anthropic"; +import { createOpenAI } from "@yourgpt/llm-sdk/openai"; +import { loadSkills } from "@yourgpt/copilot-sdk/server"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const app = express(); +app.use(cors()); +app.use(express.json({ limit: "10mb" })); + +// ============================================ +// LOAD SKILLS FROM /skills DIRECTORY +// ============================================ + +const { skills, buildSystemPrompt, tools } = await loadSkills({ + dir: path.join(__dirname, "../skills"), +}); + +console.log(`\nLoaded ${skills.length} skill(s):`); +for (const skill of skills) { + console.log( + ` - ${skill.name} [${skill.strategy ?? "auto"}]: ${skill.description}`, + ); +} + +// ============================================ +// CREATE PROVIDERS +// ============================================ + +const anthropic = createAnthropic({ + apiKey: process.env.ANTHROPIC_API_KEY, +}); + +const openai = createOpenAI({ + apiKey: process.env.OPENAI_API_KEY, +}); + +const provider = process.env.ANTHROPIC_API_KEY ? anthropic : openai; +const model = process.env.ANTHROPIC_API_KEY + ? "claude-haiku-4-5" + : "gpt-4o-mini"; + +console.log( + `\nUsing provider: ${process.env.ANTHROPIC_API_KEY ? "Anthropic" : "OpenAI"}`, +); +console.log(`Using model: ${model}`); + +// ============================================ +// CREATE RUNTIME WITH SKILL TOOL +// ============================================ + +const systemPrompt = buildSystemPrompt( + "You are a helpful assistant. Use available skills to tailor your responses.", +); + +const runtime = createRuntime({ + provider, + model, + systemPrompt, + debug: true, +}); + +// Register the load_skill tool with the runtime +runtime.registerTool({ + name: "load_skill", + description: tools.load_skill.description, + location: "server", + inputSchema: tools.load_skill.parameters, + handler: async (params: { name: string }) => { + return tools.load_skill.execute(params); + }, +}); + +// ============================================ +// ROUTES +// ============================================ + +/** + * POST /api/chat — Main chat endpoint for CopilotProvider + */ +app.post("/api/chat", async (req, res) => { + const url = `http://localhost:${PORT}/api/chat`; + const webReq = new Request(url, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(req.body), + }); + const response = await runtime.handleRequest(webReq); + res.status(response.status); + response.headers.forEach((val, key) => res.setHeader(key, val)); + const body = await response.text(); + res.send(body); +}); + +/** + * GET /api/skills — Returns skill metadata for the UI sidebar + */ +app.get("/api/skills", (_req, res) => { + const skillList = skills.map((skill) => ({ + name: skill.name, + description: skill.description, + strategy: skill.strategy ?? "auto", + version: skill.version, + })); + res.json(skillList); +}); + +/** + * GET /api/health — Health check + */ +app.get("/api/health", (_req, res) => { + res.json({ + status: "ok", + provider: process.env.ANTHROPIC_API_KEY ? "anthropic" : "openai", + model, + skillCount: skills.length, + }); +}); + +// ============================================ +// SERVER START +// ============================================ + +const PORT = parseInt(process.env.PORT ?? "3032", 10); + +app.listen(PORT, () => { + console.log(` +╔══════════════════════════════════════════════════════════════╗ +║ Skills Demo — Express Server ║ +╠══════════════════════════════════════════════════════════════╣ +║ Server: http://localhost:${PORT} ║ +║ Provider: ${(process.env.ANTHROPIC_API_KEY ? "Anthropic" : "OpenAI").padEnd(47)}║ +║ Model: ${model.padEnd(47)}║ +║ Skills: ${String(skills.length).padEnd(47)}║ +╚══════════════════════════════════════════════════════════════╝ + +Endpoints: + POST /api/chat — CopilotProvider chat endpoint + GET /api/skills — Skill metadata for the UI + GET /api/health — Health check +`); +}); diff --git a/examples/skills-demo/src/App.tsx b/examples/skills-demo/src/App.tsx new file mode 100644 index 0000000..68d9b5b --- /dev/null +++ b/examples/skills-demo/src/App.tsx @@ -0,0 +1,295 @@ +import { useState, useEffect } from "react"; +import { GitBranch, Zap, Bot, ChevronRight } from "lucide-react"; +import { CopilotProvider, CopilotChat } from "@yourgpt/copilot-sdk/ui"; +import "@yourgpt/copilot-sdk/ui/styles.css"; + +// ============================================ +// Types +// ============================================ + +interface SkillInfo { + name: string; + description: string; + strategy: "eager" | "auto" | "manual"; + version?: string; +} + +// ============================================ +// Helpers +// ============================================ + +function formatSkillName(name: string): string { + return name + .split("-") + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(" "); +} + +function StrategyBadge({ strategy }: { strategy: SkillInfo["strategy"] }) { + const config: Record< + SkillInfo["strategy"], + { label: string; className: string } + > = { + eager: { + label: "eager", + className: + "bg-emerald-500/15 text-emerald-400 border border-emerald-500/25", + }, + auto: { + label: "auto", + className: "bg-blue-500/15 text-blue-400 border border-blue-500/25", + }, + manual: { + label: "manual", + className: "bg-amber-500/15 text-amber-400 border border-amber-500/25", + }, + }; + + const { label, className } = config[strategy]; + + return ( + + {label} + + ); +} + +// ============================================ +// Toggle Switch +// ============================================ + +function ToggleSwitch({ + checked, + onChange, + id, +}: { + checked: boolean; + onChange: (v: boolean) => void; + id: string; +}) { + return ( + + ); +} + +// ============================================ +// Skill Card +// ============================================ + +function SkillCard({ skill }: { skill: SkillInfo }) { + return ( +
+
+ + {formatSkillName(skill.name)} + + +
+

+ {skill.description} +

+
+ ); +} + +// ============================================ +// Sidebar +// ============================================ + +function Sidebar({ + branchingEnabled, + onBranchingChange, +}: { + branchingEnabled: boolean; + onBranchingChange: (v: boolean) => void; +}) { + const [skills, setSkills] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + fetch("/api/skills") + .then((res) => { + if (!res.ok) throw new Error(`HTTP ${res.status}`); + return res.json() as Promise; + }) + .then((data) => { + setSkills(data); + setLoading(false); + }) + .catch((err: unknown) => { + setError(err instanceof Error ? err.message : "Failed to load skills"); + setLoading(false); + }); + }, []); + + return ( + + ); +} + +// ============================================ +// App +// ============================================ + +export default function App() { + const [branchingEnabled, setBranchingEnabled] = useState(false); + + return ( +
+ + + {/* Chat panel */} +
+ + + +
+
+ ); +} diff --git a/examples/skills-demo/src/index.css b/examples/skills-demo/src/index.css new file mode 100644 index 0000000..f1d8c73 --- /dev/null +++ b/examples/skills-demo/src/index.css @@ -0,0 +1 @@ +@import "tailwindcss"; diff --git a/examples/skills-demo/src/main.tsx b/examples/skills-demo/src/main.tsx new file mode 100644 index 0000000..15753af --- /dev/null +++ b/examples/skills-demo/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import "./index.css"; +import App from "./App"; + +createRoot(document.getElementById("root")!).render( + + + , +); diff --git a/examples/skills-demo/tsconfig.json b/examples/skills-demo/tsconfig.json new file mode 100644 index 0000000..7901e30 --- /dev/null +++ b/examples/skills-demo/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/examples/skills-demo/tsconfig.node.json b/examples/skills-demo/tsconfig.node.json new file mode 100644 index 0000000..1f27c41 --- /dev/null +++ b/examples/skills-demo/tsconfig.node.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true + }, + "include": ["vite.config.ts", "server"] +} diff --git a/examples/skills-demo/vite.config.ts b/examples/skills-demo/vite.config.ts new file mode 100644 index 0000000..f765d45 --- /dev/null +++ b/examples/skills-demo/vite.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import path from "path"; + +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, + }, + server: { + port: 3033, + proxy: { + "/api": "http://localhost:3032", + }, + }, +}); From 27b921bfa7362cc0f2c754648307e5dc2dcf26fb Mon Sep 17 00:00:00 2001 From: Sahil Date: Sat, 28 Mar 2026 21:52:33 +0530 Subject: [PATCH 04/39] feat(skills-demo): redesign as Dash SaaS dashboard with animated skill activation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace generic skills with SaaS-focused: revenue-intelligence, customer-health, incident-runbook - Full dark dashboard UI (Bricolage Grotesque + JetBrains Mono, navy/indigo palette) - Live metrics bar, nav with module links, AI Copilot badge - Skill cards animate on load: scan line sweep → expand → capability reveal with staggered fade-in - Skill state detection via load_skill toolRenderer watching for tool call completion - Branching toggle, demo prompt injectors for demo recording - Updated server system prompt for SaaS context Co-Authored-By: Claude Sonnet 4.6 --- examples/skills-demo/index.html | 5 +- examples/skills-demo/server/index.ts | 11 +- .../skills-demo/skills/customer-health.md | 54 ++ .../skills-demo/skills/incident-runbook.md | 57 ++ .../skills/revenue-intelligence.md | 41 + examples/skills-demo/src/App.tsx | 776 ++++++++++++------ 6 files changed, 691 insertions(+), 253 deletions(-) create mode 100644 examples/skills-demo/skills/customer-health.md create mode 100644 examples/skills-demo/skills/incident-runbook.md create mode 100644 examples/skills-demo/skills/revenue-intelligence.md diff --git a/examples/skills-demo/index.html b/examples/skills-demo/index.html index 5208e48..e272c21 100644 --- a/examples/skills-demo/index.html +++ b/examples/skills-demo/index.html @@ -4,7 +4,10 @@ - Skills Demo + Dash — AI Copilot + + +
diff --git a/examples/skills-demo/server/index.ts b/examples/skills-demo/server/index.ts index 0a12863..c54db60 100644 --- a/examples/skills-demo/server/index.ts +++ b/examples/skills-demo/server/index.ts @@ -57,7 +57,16 @@ console.log(`Using model: ${model}`); // ============================================ const systemPrompt = buildSystemPrompt( - "You are a helpful assistant. Use available skills to tailor your responses.", + `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. + +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 + +Always load the relevant skill before responding to ensure you follow the correct protocol. +Be concise, data-focused, and action-oriented.`, ); const runtime = createRuntime({ diff --git a/examples/skills-demo/skills/customer-health.md b/examples/skills-demo/skills/customer-health.md new file mode 100644 index 0000000..f4dfe59 --- /dev/null +++ b/examples/skills-demo/skills/customer-health.md @@ -0,0 +1,54 @@ +--- +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/incident-runbook.md b/examples/skills-demo/skills/incident-runbook.md new file mode 100644 index 0000000..a381ec4 --- /dev/null +++ b/examples/skills-demo/skills/incident-runbook.md @@ -0,0 +1,57 @@ +--- +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 new file mode 100644 index 0000000..fe259ea --- /dev/null +++ b/examples/skills-demo/skills/revenue-intelligence.md @@ -0,0 +1,41 @@ +--- +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 68d9b5b..f8b2994 100644 --- a/examples/skills-demo/src/App.tsx +++ b/examples/skills-demo/src/App.tsx @@ -1,295 +1,569 @@ -import { useState, useEffect } from "react"; -import { GitBranch, Zap, Bot, ChevronRight } from "lucide-react"; -import { CopilotProvider, CopilotChat } from "@yourgpt/copilot-sdk/ui"; +import { useState, useCallback, useEffect, useRef, useMemo } from "react"; +import { CopilotProvider } from "@yourgpt/copilot-sdk/react"; +import { CopilotChat } from "@yourgpt/copilot-sdk/ui"; import "@yourgpt/copilot-sdk/ui/styles.css"; -// ============================================ -// Types -// ============================================ +// ─── Skill definitions (client-side metadata) ───────────────────────────────── -interface SkillInfo { +interface SkillMeta { + id: string; name: string; - description: string; + icon: string; + shortDesc: string; strategy: "eager" | "auto" | "manual"; - version?: string; -} - -// ============================================ -// Helpers -// ============================================ - -function formatSkillName(name: string): string { - return name - .split("-") - .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) - .join(" "); -} - -function StrategyBadge({ strategy }: { strategy: SkillInfo["strategy"] }) { - const config: Record< - SkillInfo["strategy"], - { label: string; className: string } - > = { - eager: { - label: "eager", - className: - "bg-emerald-500/15 text-emerald-400 border border-emerald-500/25", - }, - auto: { - label: "auto", - className: "bg-blue-500/15 text-blue-400 border border-blue-500/25", - }, - manual: { - label: "manual", - className: "bg-amber-500/15 text-amber-400 border border-amber-500/25", - }, - }; + capabilities: string[]; + color: string; +} - const { label, className } = config[strategy]; +type SkillState = "idle" | "scanning" | "loaded"; - return ( - - {label} - - ); -} +const SKILLS: SkillMeta[] = [ + { + id: "revenue-intelligence", + name: "Revenue Intelligence", + icon: "◈", + shortDesc: "MRR trends, churn analysis & expansion signals", + strategy: "auto", + capabilities: [ + "Monthly recurring revenue breakdown", + "Churn forecasting & root cause", + "Expansion revenue opportunity scoring", + ], + color: "#818cf8", + }, + { + id: "customer-health", + name: "Customer Health", + icon: "◉", + shortDesc: "Account risk scoring & engagement signals", + strategy: "auto", + capabilities: [ + "Health score calculation (0–100)", + "At-risk account early warning", + "Engagement drop-off detection", + ], + color: "#34d399", + }, + { + id: "incident-runbook", + name: "Incident Runbook", + icon: "◬", + shortDesc: "Production incident response protocol", + strategy: "manual", + capabilities: [ + "Severity classification P0–P3", + "Step-by-step response checklist", + "Stakeholder communication templates", + ], + color: "#fb923c", + }, +]; -// ============================================ -// Toggle Switch -// ============================================ +const METRICS = [ + { label: "MRR", value: "$124.8k", change: "+12%", up: true }, + { label: "Churn", value: "2.3%", change: "−0.4%", up: true }, + { label: "DAU", value: "8,429", change: "+5%", up: true }, + { label: "Open P1s", value: "2", change: "+2", up: false }, +]; -function ToggleSwitch({ - checked, - onChange, - id, +const DEMO_PROMPTS = [ + "Analyze our MRR growth and top churn risks this month", + "Which enterprise accounts are most at risk right now?", + "We have a P1 — payment API is returning 503 errors", +]; + +// ─── Skill load notifier (invisible, watches for load_skill tool calls) ─────── + +function SkillLoadNotifier({ + args, + status, + onLoaded, }: { - checked: boolean; - onChange: (v: boolean) => void; - id: string; + args: Record; + status: string; + result?: unknown; + toolCallId: string; + onLoaded: (name: string) => void; }) { - return ( - - ); + const firedRef = useRef(false); + useEffect(() => { + if (status === "success" && args?.name && !firedRef.current) { + firedRef.current = true; + onLoaded(args.name as string); + } + }, [status, args?.name, onLoaded]); + return null; } -// ============================================ -// Skill Card -// ============================================ +// ─── Individual skill card ──────────────────────────────────────────────────── + +function SkillCard({ skill, state }: { skill: SkillMeta; state: SkillState }) { + const isLoaded = state === "loaded"; + const isScanning = state === "scanning"; + const strategyLabel = { + eager: "ALWAYS ON", + auto: "AUTO", + manual: "ON DEMAND", + }; -function SkillCard({ skill }: { skill: SkillInfo }) { return ( -
-
- - {formatSkillName(skill.name)} +
+ {isScanning &&
} + +
+ + {skill.icon} - +
+ {skill.name} + + {strategyLabel[skill.strategy]} + +
+ +
+ +

{skill.shortDesc}

+ +
+
+

✦ Skill active

+
    + {skill.capabilities.map((cap, i) => ( +
  • + + {cap} +
  • + ))} +
-

- {skill.description} -

); } -// ============================================ -// Sidebar -// ============================================ +// ─── Main app ───────────────────────────────────────────────────────────────── -function Sidebar({ - branchingEnabled, - onBranchingChange, -}: { - branchingEnabled: boolean; - onBranchingChange: (v: boolean) => void; -}) { - const [skills, setSkills] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); +export default function App() { + const [skillStates, setSkillStates] = useState>( + () => Object.fromEntries(SKILLS.map((s) => [s.id, "idle"])), + ); + const [branchingEnabled, setBranchingEnabled] = useState(false); - useEffect(() => { - fetch("/api/skills") - .then((res) => { - if (!res.ok) throw new Error(`HTTP ${res.status}`); - return res.json() as Promise; - }) - .then((data) => { - setSkills(data); - setLoading(false); - }) - .catch((err: unknown) => { - setError(err instanceof Error ? err.message : "Failed to load skills"); - setLoading(false); - }); + const handleSkillLoaded = useCallback((skillName: string) => { + if (!SKILLS.find((s) => s.id === skillName)) return; + setSkillStates((prev) => + prev[skillName] === "loaded" + ? prev + : { ...prev, [skillName]: "scanning" }, + ); + setTimeout(() => { + setSkillStates((prev) => ({ ...prev, [skillName]: "loaded" })); + }, 1500); }, []); - return ( - + ); } -// ============================================ -// App -// ============================================ +// ─── All styles ─────────────────────────────────────────────────────────────── -export default function App() { - const [branchingEnabled, setBranchingEnabled] = useState(false); +const CSS = ` +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } - return ( -
- - - {/* Chat panel */} -
- - - -
-
+:root { + --bg: #07090f; + --s1: #0b0e1a; + --s2: #0f1320; + --s3: #141929; + --bd: rgba(255,255,255,0.055); + --bd2: rgba(255,255,255,0.10); + --t1: #e8eaf6; + --t2: #7b82a8; + --t3: #3d4468; + --ok: #34d399; + --err: #f87171; + --acc: #818cf8; + --font: 'Bricolage Grotesque', system-ui, sans-serif; + --mono: 'JetBrains Mono', monospace; +} + +body { + background: var(--bg); + font-family: var(--font); + color: var(--t1); + -webkit-font-smoothing: antialiased; +} + +/* ── Root ── */ +.d-root { display: flex; flex-direction: column; height: 100vh; overflow: hidden; } + +/* ── Nav ── */ +.d-nav { + display: flex; align-items: center; gap: 20px; + padding: 0 20px; height: 50px; flex-shrink: 0; + background: var(--s1); + border-bottom: 1px solid var(--bd); +} +.d-nav__brand { display: flex; align-items: center; gap: 8px; } +.d-nav__logo { font-size: 19px; color: var(--acc); } +.d-nav__name { font-weight: 700; font-size: 15px; letter-spacing: -0.04em; } +.d-nav__platform { font-size: 11px; color: var(--t3); margin-left: 1px; } +.d-nav__links { display: flex; gap: 1px; margin-left: auto; } +.d-nav__link { + padding: 4px 11px; border-radius: 6px; + font-size: 12px; color: var(--t2); cursor: pointer; + transition: all 0.12s; +} +.d-nav__link:hover { background: var(--s2); color: var(--t1); } +.d-nav__copilot { + display: flex; align-items: center; gap: 7px; + padding: 4px 12px; border-radius: 20px; + background: rgba(129,140,248,0.08); + border: 1px solid rgba(129,140,248,0.2); + font-size: 11px; font-weight: 600; color: var(--acc); + letter-spacing: 0.05em; text-transform: uppercase; +} +.d-nav__pulse { + width: 6px; height: 6px; border-radius: 50%; background: var(--acc); + animation: nav-pulse 2.2s ease-in-out infinite; +} +@keyframes nav-pulse { + 0%,100% { opacity:.5; transform:scale(1); } + 50% { opacity:1; transform:scale(1.35); } +} + +/* ── Body ── */ +.d-body { display: flex; flex: 1; min-height: 0; } + +/* ── Sidebar ── */ +.d-sidebar { + width: 296px; flex-shrink: 0; + background: var(--s1); border-right: 1px solid var(--bd); + overflow-y: auto; display: flex; flex-direction: column; + scrollbar-width: thin; scrollbar-color: var(--bd2) transparent; +} + +.d-section { + padding: 14px 14px; + border-bottom: 1px solid var(--bd); +} +.d-section__label { + font-size: 10px; font-weight: 600; letter-spacing: 0.1em; + text-transform: uppercase; color: var(--t3); margin-bottom: 10px; +} +.d-section__header-row { + display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; +} +.d-section__count { + font-family: var(--mono); font-size: 10px; color: var(--t3); + background: var(--s2); padding: 1px 6px; border-radius: 8px; + border: 1px solid var(--bd); +} + +/* ── Metrics ── */ +.d-metrics { display: grid; grid-template-columns: 1fr 1fr; gap: 7px; } +.d-metric { + background: var(--s2); border: 1px solid var(--bd); + border-radius: 8px; padding: 9px 10px 7px; + display: flex; flex-direction: column; gap: 1px; +} +.d-metric__label { font-size: 9.5px; color: var(--t3); font-weight: 500; text-transform: uppercase; letter-spacing: 0.06em; } +.d-metric__val { font-family: var(--mono); font-size: 16px; font-weight: 500; color: var(--t1); line-height: 1.2; } +.d-metric__chg { font-family: var(--mono); font-size: 10px; font-weight: 500; } +.d-metric__chg.up { color: var(--ok); } +.d-metric__chg.dn { color: var(--err); } + +/* ── Skills ── */ +.d-skills { display: flex; flex-direction: column; gap: 7px; } + +.skill-card { + position: relative; overflow: hidden; + border-radius: 10px; padding: 11px; + background: var(--s2); border: 1px solid var(--bd); + transition: border-color 0.45s ease, background 0.45s ease, box-shadow 0.45s ease; +} +.skill-card[data-state="scanning"] { + border-color: rgba(255,255,255,0.13); + background: var(--s3); +} +.skill-card[data-state="loaded"] { + border-color: color-mix(in srgb, var(--sc) 38%, transparent); + background: color-mix(in srgb, var(--sc) 6%, var(--s2)); + box-shadow: 0 0 22px -4px color-mix(in srgb, var(--sc) 22%, transparent); + animation: card-pop 0.55s cubic-bezier(0.22,1,0.36,1); +} +@keyframes card-pop { + 0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--sc) 55%, transparent); } + 45% { box-shadow: 0 0 28px 5px color-mix(in srgb, var(--sc) 32%, transparent); } + 100% { box-shadow: 0 0 22px -4px color-mix(in srgb, var(--sc) 22%, transparent); } +} + +/* Scan line */ +.scan-line { + position: absolute; inset: 0; pointer-events: none; z-index: 10; + background: linear-gradient( + to bottom, + transparent 0%, + rgba(255,255,255,0.03) 40%, + rgba(255,255,255,0.11) 50%, + rgba(255,255,255,0.03) 60%, + transparent 100% ); + animation: scan 1.5s cubic-bezier(0.4,0,0.2,1) forwards; +} +@keyframes scan { + 0% { transform: translateY(-110%); opacity: 1; } + 80% { opacity: 1; } + 100% { transform: translateY(210%); opacity: 0; } +} + +.sc-header { display: flex; align-items: center; gap: 8px; margin-bottom: 5px; } +.sc-icon { + font-size: 17px; line-height: 1; flex-shrink: 0; + color: var(--sc); + transition: transform 0.3s ease; +} +.skill-card[data-state="loaded"] .sc-icon { transform: scale(1.12); } +.sc-icon[data-scanning="true"] { animation: icon-spin 1.5s ease-in-out; } +@keyframes icon-spin { + 0% { transform: rotate(0deg) scale(1); } + 50% { transform: rotate(180deg) scale(1.2); } + 100% { transform: rotate(360deg) scale(1); } +} + +.sc-title-group { display: flex; flex-direction: column; gap: 2px; flex: 1; min-width: 0; } +.sc-name { font-size: 12.5px; font-weight: 600; color: var(--t1); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + +.sc-badge { + font-size: 9px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; + padding: 1px 5px; border-radius: 4px; width: fit-content; +} +.sc-badge--eager { background: rgba(52,211,153,.12); color: #34d399; border: 1px solid rgba(52,211,153,.2); } +.sc-badge--auto { background: rgba(129,140,248,.12); color: #818cf8; border: 1px solid rgba(129,140,248,.2); } +.sc-badge--manual { background: rgba(251,146,60,.12); color: #fb923c; border: 1px solid rgba(251,146,60,.2); } + +.sc-dot { + width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; + background: var(--t3); + transition: background 0.35s, box-shadow 0.35s; +} +.sc-dot[data-active="true"] { + background: var(--sc); + box-shadow: 0 0 7px 1px color-mix(in srgb, var(--sc) 55%, transparent); + animation: dot-breathe 2s ease-in-out infinite; } +@keyframes dot-breathe { + 0%,100% { box-shadow: 0 0 7px 1px color-mix(in srgb, var(--sc) 45%, transparent); } + 50% { box-shadow: 0 0 11px 3px color-mix(in srgb, var(--sc) 65%, transparent); } +} + +.sc-desc { font-size: 11px; color: var(--t2); line-height: 1.4; padding-left: 25px; } + +.sc-expanded { + max-height: 0; overflow: hidden; + transition: max-height 0.55s cubic-bezier(0.16,1,0.3,1); +} +.sc-expanded[data-open="true"] { max-height: 200px; } + +.sc-divider { height: 1px; background: color-mix(in srgb, var(--sc) 25%, transparent); margin: 10px 0 8px; } +.sc-active-label { + font-size: 10px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; + color: var(--sc); margin-bottom: 7px; padding-left: 25px; +} + +.sc-caps { list-style: none; display: flex; flex-direction: column; gap: 5px; } +.sc-cap { + display: flex; align-items: center; gap: 7px; + font-size: 11px; color: var(--t2); padding-left: 25px; + opacity: 0; transform: translateX(-8px); + transition: opacity 0.3s ease, transform 0.3s ease; +} +.sc-cap[data-visible="true"] { opacity: 1; transform: translateX(0); } +.sc-cap-dot { + width: 4px; height: 4px; border-radius: 50%; flex-shrink: 0; + background: var(--sc); +} + +/* ── Branching ── */ +.d-branch { + display: flex; align-items: center; justify-content: space-between; gap: 12px; +} +.d-branch__label { font-size: 12px; font-weight: 600; color: var(--t1); } +.d-branch__desc { font-size: 10px; color: var(--t3); margin-top: 2px; } +.d-toggle { + width: 36px; height: 20px; border-radius: 10px; flex-shrink: 0; cursor: pointer; + background: var(--s3); border: 1px solid var(--bd2); position: relative; + transition: background 0.2s, border-color 0.2s; +} +.d-toggle::after { + content: ''; position: absolute; top: 2px; left: 2px; + width: 14px; height: 14px; border-radius: 50%; background: var(--t3); + transition: transform 0.2s, background 0.2s; +} +.d-toggle--on { background: rgba(129,140,248,.2); border-color: rgba(129,140,248,.4); } +.d-toggle--on::after { transform: translateX(16px); background: var(--acc); } + +/* ── Demo prompts ── */ +.d-prompts { display: flex; flex-direction: column; gap: 6px; } +.d-prompt { + text-align: left; background: var(--s2); border: 1px solid var(--bd); + border-radius: 8px; padding: 8px 10px; + font-size: 11px; font-family: var(--font); color: var(--t2); + cursor: pointer; line-height: 1.4; + transition: background 0.12s, border-color 0.12s, color 0.12s; +} +.d-prompt:hover { background: var(--s3); border-color: var(--bd2); color: var(--t1); } + +/* ── Chat ── */ +.d-chat { flex: 1; min-width: 0; display: flex; flex-direction: column; background: var(--bg); } +.d-copilot { height: 100% !important; } +`; From da36c482349f61755f1201c8a5665e122c204734 Mon Sep 17 00:00:00 2001 From: Sahil Date: Sun, 29 Mar 2026 12:12:24 +0530 Subject: [PATCH 05/39] feat(llm-sdk): add fallback chain & routing strategies Adds @yourgpt/llm-sdk/fallback subpath export: - createFallbackChain() with priority and round-robin routing - Per-model retries with exponential/fixed backoff before fallback - FallbackExhaustedError with per-model failure breakdown - MemoryRoutingStore (default) + pluggable RoutingStore interface - onRetry / onFallback observability callbacks - Two-tier error detection (class-based for complete(), message-regex for stream()) Includes fallback-demo example and docs page. Co-Authored-By: Claude Sonnet 4.6 --- apps/docs/content/docs/providers/fallback.mdx | 314 +++++++++++++ apps/docs/content/docs/providers/index.mdx | 23 + apps/docs/content/docs/providers/meta.json | 10 +- examples/fallback-demo/README.md | 119 +++++ examples/fallback-demo/package.json | 24 + examples/fallback-demo/src/index.ts | 419 +++++++++++++++++ examples/fallback-demo/tsconfig.json | 12 + packages/llm-sdk/package.json | 5 + packages/llm-sdk/src/fallback/chain.ts | 425 ++++++++++++++++++ packages/llm-sdk/src/fallback/errors.ts | 43 ++ packages/llm-sdk/src/fallback/index.ts | 43 ++ .../llm-sdk/src/fallback/routing-store.ts | 36 ++ packages/llm-sdk/src/fallback/types.ts | 236 ++++++++++ packages/llm-sdk/src/index.ts | 14 + packages/llm-sdk/tsup.config.ts | 3 + pnpm-lock.yaml | 75 ++-- 16 files changed, 1759 insertions(+), 42 deletions(-) create mode 100644 apps/docs/content/docs/providers/fallback.mdx create mode 100644 examples/fallback-demo/README.md create mode 100644 examples/fallback-demo/package.json create mode 100644 examples/fallback-demo/src/index.ts create mode 100644 examples/fallback-demo/tsconfig.json create mode 100644 packages/llm-sdk/src/fallback/chain.ts create mode 100644 packages/llm-sdk/src/fallback/errors.ts create mode 100644 packages/llm-sdk/src/fallback/index.ts create mode 100644 packages/llm-sdk/src/fallback/routing-store.ts create mode 100644 packages/llm-sdk/src/fallback/types.ts diff --git a/apps/docs/content/docs/providers/fallback.mdx b/apps/docs/content/docs/providers/fallback.mdx new file mode 100644 index 0000000..25dac37 --- /dev/null +++ b/apps/docs/content/docs/providers/fallback.mdx @@ -0,0 +1,314 @@ +--- +title: Fallback +description: Automatic failover and load distribution across LLM providers +--- + +import { Callout } from 'fumadocs-ui/components/callout'; +import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; + +Automatically retry failed requests with backup models. When your primary provider returns a `5xx` error, rate limit (`429`), or times out — the SDK silently tries the next model in your chain. Your application code doesn't change. + +```ts +import { createFallbackChain } from '@yourgpt/llm-sdk/fallback'; +import { createOpenAI } from '@yourgpt/llm-sdk/openai'; +import { createAnthropic } from '@yourgpt/llm-sdk/anthropic'; + +const chain = createFallbackChain({ + models: [ + createOpenAI({ apiKey: '...' }).languageModel('gpt-5.4'), + createAnthropic({ apiKey: '...' }).languageModel('claude-haiku-4-5'), + ], +}); + +const runtime = createRuntime({ adapter: chain }); +``` + +--- + +## Installation + +```bash +npm install @yourgpt/llm-sdk openai @anthropic-ai/sdk +``` + +--- + +## Basic Usage + +Pass `createFallbackChain()` as the `adapter` in `createRuntime()`. The rest of your server code — streaming, tools, sessions — stays exactly the same. + +```ts title="server.ts" +import { createRuntime } from '@yourgpt/llm-sdk'; +import { createFallbackChain } from '@yourgpt/llm-sdk/fallback'; +import { createOpenAI } from '@yourgpt/llm-sdk/openai'; +import { createAnthropic } from '@yourgpt/llm-sdk/anthropic'; + +const openai = createOpenAI({ apiKey: process.env.OPENAI_API_KEY }); +const anthropic = createAnthropic({ apiKey: process.env.ANTHROPIC_API_KEY }); + +const runtime = createRuntime({ + adapter: createFallbackChain({ + models: [ + openai.languageModel('gpt-5.4'), // tried first + anthropic.languageModel('claude-haiku-4-5'), // tried if OpenAI fails + ], + }), + systemPrompt: 'You are a helpful assistant.', +}); + +// Use exactly as normal — no other changes +app.post('/api/chat', async (req, res) => { + await runtime.stream(req.body).pipeToResponse(res); +}); +``` + +--- + +## What Triggers Fallback + +| Error type | Triggers fallback? | +|---|---| +| `5xx` server errors | ✅ Yes | +| `429` rate limit | ✅ Yes | +| Network timeout / connection refused | ✅ Yes | +| `4xx` client errors (bad key, bad request) | ❌ No — these are your bugs, not provider failures | + + +Once content has started streaming to the client, fallback is not attempted. You cannot restart a stream mid-flight. + + +--- + +## Routing Strategies + +Control which model is tried first on each request. + + + + Always tries models in the order defined. First model handles all traffic until it fails. + + ```ts + createFallbackChain({ + models: [primaryModel, backupModel], + strategy: 'priority', // default — can be omitted + }); + ``` + + + Distributes load evenly. Request 1 starts at model A, request 2 starts at model B, and so on. If the starting model fails, the chain falls through to the next one as usual. + + ```ts + createFallbackChain({ + models: [openaiModel, anthropicModel], + strategy: 'round-robin', + }); + ``` + + + **Multi-instance deployments:** Round-robin state is in-memory by default and resets on restart. For shared state across instances, plug in a custom store (Redis, Upstash, etc.) via the `store` option. + + + + +--- + +## Per-Model Retries + +Retry the same model before moving to the next one. Useful for transient errors like brief rate limits or flaky connections. + +```ts +createFallbackChain({ + models: [openaiModel, anthropicModel], + retries: 2, // retry each model up to 2 times + retryDelay: 500, // base delay: 500ms + retryBackoff: 'exponential', // 500ms → 1000ms → 2000ms (default) + onRetry: ({ model, retryAttempt, maxRetries, delayMs, error }) => { + console.warn(`[retry] ${model} attempt ${retryAttempt}/${maxRetries} — waiting ${delayMs}ms`); + }, +}); +``` + +**Backoff options:** + +| `retryBackoff` | Pattern (retryDelay=500) | +|---|---| +| `exponential` (default) | 500ms → 1000ms → 2000ms | +| `fixed` | 500ms → 500ms → 500ms | + + +With `retries: 2`, each model gets 3 total attempts (1 initial + 2 retries) before the chain moves to the next model. + + +--- + +## Observability Callbacks + +Two callbacks give you visibility into what the chain is doing. + +```ts +createFallbackChain({ + models: [openaiModel, anthropicModel, googleModel], + + // Fires on each per-model retry (before the wait delay) + onRetry: ({ model, provider, error, retryAttempt, maxRetries, delayMs }) => { + console.warn(`[retry] ${provider}/${model} — attempt ${retryAttempt}/${maxRetries}`); + metrics.increment('llm.retry', { provider, model }); + }, + + // Fires when a model is abandoned and the next one is tried + onFallback: ({ attemptedModel, nextModel, error, attempt }) => { + console.warn(`[fallback] ${attemptedModel} → ${nextModel}: ${error.message}`); + metrics.increment('llm.fallback', { from: attemptedModel, to: nextModel }); + }, +}); +``` + +--- + +## Handling Full Failure + +When every model in the chain fails, `FallbackExhaustedError` is thrown. It includes a per-model breakdown of what failed. + +```ts +import { FallbackExhaustedError } from '@yourgpt/llm-sdk/fallback'; + +try { + await runtime.stream(req.body).pipeToResponse(res); +} catch (err) { + if (err instanceof FallbackExhaustedError) { + // Per-model breakdown + for (const f of err.failures) { + console.error( + `${f.provider}/${f.model} failed after ${f.retriesAttempted} retries: ${f.error.message}` + ); + } + res.status(503).json({ error: 'All models unavailable. Try again later.' }); + } +} +``` + +--- + +## Custom Error Filtering + +By default the chain uses sensible rules (5xx, 429, network errors trigger fallback; 4xx does not). Override with `retryableErrors` for custom logic. + +```ts +createFallbackChain({ + models: [openaiModel, anthropicModel], + + // Fall back on any error at all + retryableErrors: () => true, + + // Or — only fall back on rate limits + retryableErrors: (err) => { + return err instanceof Error && /429|rate.?limit/i.test(err.message); + }, +}); +``` + +--- + +## Shared Routing Store (Multi-Instance) + +For round-robin to work correctly across multiple server instances or serverless functions, plug in a shared store. + +```ts +import { createFallbackChain, type RoutingStore } from '@yourgpt/llm-sdk/fallback'; + +// Implement RoutingStore with any backend — Redis, Upstash, Cloudflare KV, etc. +// The SDK ships the interface. You own the implementation. +const redisStore: RoutingStore = { + async get(key) { + const val = await redis.get(key); + return val ? Number(val) : undefined; + }, + async set(key, value) { + await redis.set(key, String(value)); + }, +}; + +createFallbackChain({ + models: [openaiModel, anthropicModel], + strategy: 'round-robin', + store: redisStore, +}); +``` + + +The default `MemoryRoutingStore` is zero-config and works for single-process apps. No store configuration is needed unless you run multiple instances. + + +--- + +## With Tools + +Tools work transparently across fallback providers. Define your tools once — whichever provider handles the request formats them natively. + +```ts +const runtime = createRuntime({ + adapter: createFallbackChain({ + models: [openaiModel, anthropicModel], + }), + tools: [ + { + name: 'get_weather', + description: 'Get current weather for a city', + location: 'server', + inputSchema: { + type: 'object', + properties: { city: { type: 'string' } }, + required: ['city'], + }, + handler: async ({ city }) => fetchWeather(city), + }, + ], +}); +``` + +OpenAI receives tools as function-calling JSON. Anthropic receives them as `tool_use` blocks. **Your handler always runs on your server regardless of which provider responded.** + +--- + +## Full Configuration Reference + +```ts +import { createFallbackChain } from '@yourgpt/llm-sdk/fallback'; + +createFallbackChain({ + // Required: adapters to try in order + models: LLMAdapter[], + + // Routing strategy (default: 'priority') + strategy?: 'priority' | 'round-robin', + + // Pluggable store for round-robin state (default: MemoryRoutingStore) + store?: RoutingStore, + + // Retries per model before moving to next (default: 0) + retries?: number, + + // Base delay between retries in ms (default: 500) + retryDelay?: number, + + // Backoff strategy (default: 'exponential') + retryBackoff?: 'exponential' | 'fixed', + + // Called on each per-model retry attempt + onRetry?: (info: RetryInfo) => void, + + // Called when a model is abandoned and next one is tried + onFallback?: (info: FallbackInfo) => void, + + // Custom predicate to decide which errors trigger fallback/retry + retryableErrors?: (error: unknown) => boolean, +}) +``` + +--- + +## Next Steps + +- [OpenAI](/docs/providers/openai) — Configure your primary provider +- [Anthropic](/docs/providers/anthropic) — Add a Claude fallback +- [Server Storage](/docs/server/storage) — Persist sessions alongside fallback chains diff --git a/apps/docs/content/docs/providers/index.mdx b/apps/docs/content/docs/providers/index.mdx index 7d5b8ac..3d31231 100644 --- a/apps/docs/content/docs/providers/index.mdx +++ b/apps/docs/content/docs/providers/index.mdx @@ -162,3 +162,26 @@ Google and xAI use OpenAI-compatible endpoints. Ollama runs locally and needs no | Google | Fast | Very Good | $ | Multimodal | | xAI | Ultra Fast | Excellent | $ | Speed-critical apps | | Ollama | Varies | Good-Excellent | Free | Privacy, offline, development | + +--- + +## Fallback Chain + +Use multiple providers together for automatic failover and load distribution. + +```ts +import { createFallbackChain } from '@yourgpt/llm-sdk/fallback'; + +const runtime = createRuntime({ + adapter: createFallbackChain({ + models: [ + openai.languageModel('gpt-5.4'), + anthropic.languageModel('claude-haiku-4-5'), + ], + strategy: 'round-robin', + retries: 2, + }), +}); +``` + +[Fallback Chain docs →](/docs/providers/fallback) diff --git a/apps/docs/content/docs/providers/meta.json b/apps/docs/content/docs/providers/meta.json index e0a5f18..31e4c3f 100644 --- a/apps/docs/content/docs/providers/meta.json +++ b/apps/docs/content/docs/providers/meta.json @@ -1,5 +1,13 @@ { "title": "Providers", "icon": "Brain", - "pages": ["openai", "anthropic", "google", "xai", "ollama", "custom-provider"] + "pages": [ + "openai", + "anthropic", + "google", + "xai", + "ollama", + "custom-provider", + "fallback" + ] } diff --git a/examples/fallback-demo/README.md b/examples/fallback-demo/README.md new file mode 100644 index 0000000..4538060 --- /dev/null +++ b/examples/fallback-demo/README.md @@ -0,0 +1,119 @@ +# Fallback Demo + +> Demonstrates `createFallbackChain()` from `@yourgpt/llm-sdk/fallback` — automatic failover, round-robin load distribution, per-model retries, and tools across multiple LLM providers. + +## Features Showcased + +- **Priority fallback** — OpenAI first, Anthropic if it fails +- **Round-robin** — distributes load evenly across providers +- **4xx does NOT trigger fallback** — bad API key throws immediately +- **Forced fallback** — dead primary URL → Anthropic picks up +- **Per-model retries** — retry same model N times before falling back +- **Tools in streaming mode** — tools work transparently across providers +- **Tools in non-streaming mode** — full JSON response with tool results +- **FallbackExhaustedError** — structured error when all models fail + +## Quick Start + +### Prerequisites + +- Node.js 18+ +- **pnpm** (required for workspace setup) +- OpenAI API key +- Anthropic API key + +### Installation + +```bash +# From the monorepo root +pnpm install + +# Set up environment +cp examples/fallback-demo/.env.example examples/fallback-demo/.env +# Edit .env and add your keys + +# Run the demo +cd examples/fallback-demo +pnpm dev +``` + +Server runs on [http://localhost:3000](http://localhost:3000) + +## Environment Variables + +```bash +OPENAI_API_KEY=your-openai-key +ANTHROPIC_API_KEY=your-anthropic-key +``` + +## API Endpoints + +| Endpoint | Description | +| -------------------------------- | ------------------------------------------- | +| `POST /chat/priority` | OpenAI first, Anthropic fallback | +| `POST /chat/round-robin` | Alternates OpenAI / Anthropic per request | +| `POST /chat/bad-key` | 4xx error — fallback NOT triggered | +| `POST /chat/fallback-test` | Dead primary URL → Anthropic picks up | +| `POST /chat/stream/tools` | Streaming with 3 server-side tools | +| `POST /chat/tools` | Non-streaming JSON with tool results | +| `POST /chat/fallback-test/tools` | Forced fallback + tools (Anthropic handles) | +| `POST /chat/retry-test` | 2 retries on dead model before fallback | + +## Test Commands + +### Basic fallback + +```bash +curl -s -X POST http://localhost:3000/chat/priority \ + -H "Content-Type: application/json" \ + -d '{"messages":[{"role":"user","content":"Say hello"}]}' +``` + +### Forced fallback (dead primary → Anthropic) + +```bash +curl -s -X POST http://localhost:3000/chat/fallback-test \ + -H "Content-Type: application/json" \ + -d '{"messages":[{"role":"user","content":"Which model are you?"}]}' +``` + +### Tools via streaming + +```bash +curl -s -X POST http://localhost:3000/chat/stream/tools \ + -H "Content-Type: application/json" \ + -d '{"messages":[{"role":"user","content":"What is the weather in Tokyo and what time is it?"}]}' +``` + +### Non-streaming with tools + +```bash +curl -s -X POST http://localhost:3000/chat/tools \ + -H "Content-Type: application/json" \ + -d '{"messages":[{"role":"user","content":"Calculate 42 * 7 and get the weather in London"}]}' +``` + +### Retries before fallback (watch server logs) + +```bash +curl -s -X POST http://localhost:3000/chat/retry-test \ + -H "Content-Type: application/json" \ + -d '{"messages":[{"role":"user","content":"Hello"}]}' +``` + +## Project Structure + +``` +fallback-demo/ +├── src/ +│ └── index.ts # Express server with all routes +├── package.json +├── tsconfig.json +└── README.md +``` + +## Important Notes + +> **Workspace Dependency**: Uses `workspace:*` dependencies. Run `pnpm install` from the monorepo root — `npm install` will not work. + +> **Tools are provider-agnostic**: Tool definitions are written once. Whichever provider handles the request formats them natively (OpenAI function-calling JSON or Anthropic `tool_use` blocks). Tool handlers always run on your server. diff --git a/examples/fallback-demo/package.json b/examples/fallback-demo/package.json new file mode 100644 index 0000000..1f38b05 --- /dev/null +++ b/examples/fallback-demo/package.json @@ -0,0 +1,24 @@ +{ + "name": "fallback-demo", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "tsx watch src/index.ts", + "start": "tsx src/index.ts" + }, + "dependencies": { + "@anthropic-ai/sdk": "^0.39.0", + "@yourgpt/llm-sdk": "workspace:*", + "cors": "^2.8.5", + "dotenv": "^16.4.0", + "express": "^4.21.0", + "openai": "^4.77.0" + }, + "devDependencies": { + "@types/cors": "^2.8.17", + "@types/express": "^5.0.0", + "tsx": "^4.19.0", + "typescript": "^5.6.0" + } +} diff --git a/examples/fallback-demo/src/index.ts b/examples/fallback-demo/src/index.ts new file mode 100644 index 0000000..dc5a6aa --- /dev/null +++ b/examples/fallback-demo/src/index.ts @@ -0,0 +1,419 @@ +/** + * Fallback Chain Demo + * + * Tests FallbackChain across: + * - Priority fallback (normal) + * - Round-robin load distribution + * - 4xx does NOT trigger fallback (bad key) + * - Forced fallback (dead primary URL → Anthropic picks up) + * - Tools in streaming mode + * - Tools in non-streaming (chat) mode + * + * Run: + * pnpm dev + * + * Test: + * curl -s -X POST http://localhost:3000/chat/stream/tools \ + * -H "Content-Type: application/json" \ + * -d '{"messages":[{"role":"user","content":"What is the weather in Tokyo and what time is it?"}]}' + */ + +import "dotenv/config"; +import express from "express"; +import cors from "cors"; +import { createRuntime, type ToolDefinition } from "@yourgpt/llm-sdk"; +import { createOpenAI } from "@yourgpt/llm-sdk/openai"; +import { createAnthropic } from "@yourgpt/llm-sdk/anthropic"; +import { + createFallbackChain, + FallbackExhaustedError, + MemoryRoutingStore, + type RoutingStore, + type RetryInfo, +} from "@yourgpt/llm-sdk/fallback"; + +const app = express(); +app.use(cors()); +app.use(express.json()); + +// ─── Providers ──────────────────────────────────────────────────────────────── + +const openai = createOpenAI({ apiKey: process.env.OPENAI_API_KEY }); +const anthropic = createAnthropic({ apiKey: process.env.ANTHROPIC_API_KEY }); +const brokenOpenAI = createOpenAI({ apiKey: "sk-INVALID_KEY_FOR_TESTING" }); // gitleaks:allow +const deadOpenAI = createOpenAI({ + apiKey: process.env.OPENAI_API_KEY, + baseUrl: "http://localhost:19999/v1", // nothing here → ECONNREFUSED +}); + +// ─── Tools ──────────────────────────────────────────────────────────────────── +// +// These are server-side tools. The fallback chain wraps the LLM adapter only — +// tools always run on our server regardless of which provider is active. +// The tool definitions are sent to whichever provider ends up handling the request, +// each formatted in that provider's native format by the adapter. + +const WEATHER_DATA: Record = { + tokyo: { temp: "18°C", condition: "Partly cloudy" }, + london: { temp: "12°C", condition: "Rainy" }, + new_york: { temp: "22°C", condition: "Sunny" }, + paris: { temp: "15°C", condition: "Overcast" }, + sydney: { temp: "25°C", condition: "Clear" }, +}; + +const serverTools: ToolDefinition[] = [ + { + name: "get_weather", + description: "Get current weather for a city", + location: "server", + inputSchema: { + type: "object", + properties: { + city: { + type: "string", + description: "City name (e.g. Tokyo, London, New York)", + }, + }, + required: ["city"], + }, + handler: async (params) => { + const { city } = params as { city: string }; + const key = city.toLowerCase().replace(/\s+/g, "_"); + const data = WEATHER_DATA[key] ?? { temp: "20°C", condition: "Unknown" }; + console.log( + `[tool:get_weather] city=${city} → ${data.temp}, ${data.condition}`, + ); + return { city, temperature: data.temp, condition: data.condition }; + }, + }, + { + name: "get_server_time", + description: "Get the current server date and time", + location: "server", + inputSchema: { + type: "object", + properties: {}, + }, + handler: async () => { + const now = new Date().toISOString(); + console.log(`[tool:get_server_time] → ${now}`); + return { time: now }; + }, + }, + { + name: "calculate", + description: "Evaluate a simple math expression and return the result", + location: "server", + inputSchema: { + type: "object", + properties: { + expression: { + type: "string", + description: "Math expression to evaluate, e.g. '12 * 7 + 5'", + }, + }, + required: ["expression"], + }, + handler: async (params) => { + const { expression } = params as { expression: string }; + // Safe eval: only allow numbers and basic operators + if (!/^[\d\s+\-*/().]+$/.test(expression)) { + return { error: "Invalid expression — only basic math allowed" }; + } + // eslint-disable-next-line no-eval + const result = Function(`"use strict"; return (${expression})`)(); + console.log(`[tool:calculate] ${expression} = ${result}`); + return { expression, result }; + }, + }, +]; + +// ─── Helper: fallback chain factory ────────────────────────────────────────── + +function onFallbackLog(label: string) { + return ({ + attemptedModel, + nextModel, + error, + attempt, + }: { + attemptedModel: string; + nextModel: string; + error: Error; + attempt: number; + }) => { + console.warn( + `[fallback:${label}] attempt ${attempt}: "${attemptedModel}" → "${nextModel}" | ${error.message}`, + ); + }; +} + +// ─── Route 1: Priority (no tools) ──────────────────────────────────────────── + +const priorityRuntime = createRuntime({ + adapter: createFallbackChain({ + models: [ + openai.languageModel("gpt-5.4"), + anthropic.languageModel("claude-haiku-4-5"), + ], + strategy: "priority", + onFallback: onFallbackLog("priority"), + }), + systemPrompt: "You are a helpful assistant.", +}); + +app.post("/chat/priority", async (req, res) => { + try { + await priorityRuntime.stream(req.body).pipeToResponse(res); + } catch (err) { + handleError(err, res); + } +}); + +// ─── Route 2: Round-robin (no tools) ───────────────────────────────────────── + +const rrStore: RoutingStore = new MemoryRoutingStore(); + +const roundRobinRuntime = createRuntime({ + adapter: createFallbackChain({ + models: [ + openai.languageModel("gpt-5.4"), + anthropic.languageModel("claude-haiku-4-5"), + ], + strategy: "round-robin", + store: rrStore, + onFallback: onFallbackLog("round-robin"), + }), + systemPrompt: "You are a helpful assistant.", +}); + +app.post("/chat/round-robin", async (req, res) => { + try { + await roundRobinRuntime.stream(req.body).pipeToResponse(res); + } catch (err) { + handleError(err, res); + } +}); + +// ─── Route 3: 4xx does NOT trigger fallback ─────────────────────────────────── + +const badKeyRuntime = createRuntime({ + adapter: createFallbackChain({ + models: [ + brokenOpenAI.languageModel("gpt-5.4"), + anthropic.languageModel("claude-haiku-4-5"), + ], + strategy: "priority", + onFallback: () => { + console.warn( + "[fallback:bad-key] UNEXPECTED — fallback triggered for 4xx!", + ); + }, + }), + systemPrompt: "You are a helpful assistant.", +}); + +app.post("/chat/bad-key", async (req, res) => { + try { + await badKeyRuntime.stream(req.body).pipeToResponse(res); + } catch (err) { + handleError(err, res); + } +}); + +// ─── Route 4: Forced fallback (dead primary URL) ────────────────────────────── + +const forcedFallbackRuntime = createRuntime({ + adapter: createFallbackChain({ + models: [ + deadOpenAI.languageModel("gpt-5.4"), + anthropic.languageModel("claude-haiku-4-5"), + ], + strategy: "priority", + onFallback: onFallbackLog("forced"), + }), + systemPrompt: "You are a helpful assistant.", +}); + +app.post("/chat/fallback-test", async (req, res) => { + try { + await forcedFallbackRuntime.stream(req.body).pipeToResponse(res); + } catch (err) { + handleError(err, res); + } +}); + +// ─── Route 5: Tools + STREAMING ────────────────────────────────────────────── +// +// Primary: OpenAI (gpt-5.4) — formats tools as OpenAI function-calling JSON +// Fallback: Anthropic (claude-haiku-4-5) — formats tools as Anthropic tool_use JSON +// +// The adapter for whichever provider runs transforms the shared ToolDefinition[] +// into that provider's native format. Tools always execute on our server. + +const streamToolsRuntime = createRuntime({ + adapter: createFallbackChain({ + models: [ + openai.languageModel("gpt-5.4"), + anthropic.languageModel("claude-haiku-4-5"), + ], + strategy: "priority", + onFallback: onFallbackLog("tools-stream"), + }), + systemPrompt: + "You are a helpful assistant with access to weather, time, and calculator tools. Use tools when relevant.", + tools: serverTools, +}); + +app.post("/chat/stream/tools", async (req, res) => { + try { + await streamToolsRuntime.stream(req.body).pipeToResponse(res); + } catch (err) { + handleError(err, res); + } +}); + +// ─── Route 6: Tools + NON-STREAMING (chat) ─────────────────────────────────── +// +// Same chain and tools as above but using runtime.chat() which +// collects the full response before returning JSON. + +const chatToolsRuntime = createRuntime({ + adapter: createFallbackChain({ + models: [ + openai.languageModel("gpt-5.4"), + anthropic.languageModel("claude-haiku-4-5"), + ], + strategy: "priority", + onFallback: onFallbackLog("tools-chat"), + }), + systemPrompt: + "You are a helpful assistant with access to weather, time, and calculator tools. Use tools when relevant.", + tools: serverTools, +}); + +app.post("/chat/tools", async (req, res) => { + try { + const result = await chatToolsRuntime.chat(req.body); + res.json(result); + } catch (err) { + handleError(err, res); + } +}); + +// ─── Route 8: Retries before fallback ──────────────────────────────────────── +// +// retries: 2 → tries dead primary 3 times total (initial + 2 retries) before +// giving up and falling back to Anthropic. +// retryDelay: 300ms, retryBackoff: 'exponential' → 300ms, 600ms waits +// onRetry fires on each retry attempt so you can see it in the server log. + +const retriesRuntime = createRuntime({ + adapter: createFallbackChain({ + models: [ + deadOpenAI.languageModel("gpt-5.4"), + anthropic.languageModel("claude-haiku-4-5"), + ], + retries: 2, + retryDelay: 300, + retryBackoff: "exponential", + onRetry: ({ + model, + retryAttempt, + maxRetries, + delayMs, + error, + }: RetryInfo) => { + console.warn( + `[retry] ${model} — attempt ${retryAttempt}/${maxRetries}, waiting ${delayMs}ms | ${error.message}`, + ); + }, + onFallback: ({ attemptedModel, nextModel, attempt }) => { + console.warn( + `[fallback:retries] ${attemptedModel} exhausted all retries (attempt ${attempt}) → ${nextModel}`, + ); + }, + }), + systemPrompt: "You are a helpful assistant.", +}); + +app.post("/chat/retry-test", async (req, res) => { + try { + await retriesRuntime.stream(req.body).pipeToResponse(res); + } catch (err) { + handleError(err, res); + } +}); + +// ─── Route 7: Tools + FORCED FALLBACK (dead primary) ───────────────────────── +// +// Same tools, but primary is a dead URL. +// Anthropic picks up and handles tool calls in its own format. + +const forcedToolsRuntime = createRuntime({ + adapter: createFallbackChain({ + models: [ + deadOpenAI.languageModel("gpt-5.4"), + anthropic.languageModel("claude-haiku-4-5"), + ], + strategy: "priority", + onFallback: onFallbackLog("tools-forced"), + }), + systemPrompt: + "You are a helpful assistant with access to weather, time, and calculator tools. Use tools when relevant.", + tools: serverTools, +}); + +app.post("/chat/fallback-test/tools", async (req, res) => { + try { + await forcedToolsRuntime.stream(req.body).pipeToResponse(res); + } catch (err) { + handleError(err, res); + } +}); + +// ─── Error helper ───────────────────────────────────────────────────────────── + +function handleError(err: unknown, res: express.Response) { + if (err instanceof FallbackExhaustedError) { + res.status(503).json({ + error: "All models in fallback chain failed", + detail: err.failures.map( + (f) => `${f.provider}/${f.model}: ${f.error.message}`, + ), + }); + } else { + res.status(500).json({ error: String(err) }); + } +} + +// ─── Start ──────────────────────────────────────────────────────────────────── + +const PORT = process.env.PORT || 3000; +app.listen(PORT, () => { + console.log(`\nFallback Demo running at http://localhost:${PORT}\n`); + console.log("── Basic routes ──────────────────────────────────────────────"); + console.log( + " POST /chat/priority — OpenAI first, Claude fallback", + ); + console.log(" POST /chat/round-robin — Alternates OpenAI / Claude"); + console.log(" POST /chat/bad-key — 4xx: fallback NOT triggered"); + console.log( + " POST /chat/fallback-test — Dead primary → Claude picks up", + ); + console.log( + "\n── Tool routes ───────────────────────────────────────────────", + ); + console.log( + " POST /chat/stream/tools — Tools via streaming (OpenAI primary)", + ); + console.log( + " POST /chat/tools — Tools via non-streaming JSON", + ); + console.log( + " POST /chat/fallback-test/tools — Tools via streaming (forced fallback → Claude)", + ); + console.log( + " POST /chat/retry-test — Retries dead model 2x before falling back to Claude", + ); +}); diff --git a/examples/fallback-demo/tsconfig.json b/examples/fallback-demo/tsconfig.json new file mode 100644 index 0000000..12a87ac --- /dev/null +++ b/examples/fallback-demo/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "outDir": "dist" + }, + "include": ["src"] +} diff --git a/packages/llm-sdk/package.json b/packages/llm-sdk/package.json index 5a914f6..d9a5b25 100644 --- a/packages/llm-sdk/package.json +++ b/packages/llm-sdk/package.json @@ -55,6 +55,11 @@ "types": "./dist/yourgpt/index.d.ts", "import": "./dist/yourgpt/index.mjs", "require": "./dist/yourgpt/index.js" + }, + "./fallback": { + "types": "./dist/fallback/index.d.ts", + "import": "./dist/fallback/index.mjs", + "require": "./dist/fallback/index.js" } }, "homepage": "https://copilot-sdk.yourgpt.ai", diff --git a/packages/llm-sdk/src/fallback/chain.ts b/packages/llm-sdk/src/fallback/chain.ts new file mode 100644 index 0000000..aada23c --- /dev/null +++ b/packages/llm-sdk/src/fallback/chain.ts @@ -0,0 +1,425 @@ +import type { + LLMAdapter, + ChatCompletionRequest, + CompletionResult, +} from "../adapters/base"; +import type { StreamEvent } from "../core/stream-events"; +import type { FallbackChainConfig, FallbackFailure, RetryInfo } from "./types"; +import { FallbackExhaustedError } from "./errors"; +import { MemoryRoutingStore } from "./routing-store"; + +// Stable key used for round-robin state in the store +const ROUND_ROBIN_KEY = "ygpt_fallback_rr_index"; + +/** + * Determine whether an error should trigger a retry or fallback. + * + * Covers all error shapes across provider SDKs: + * + * OpenAI SDK (@openai/openai-node): + * RateLimitError → status=429 + * InternalServerError → status>=500 + * APIConnectionError → status=undefined, message="Connection error." + * APIConnectionTimeoutError → status=undefined, message="Request timed out." + * APIUserAbortError → status=undefined, message="Aborted" ← NOT retryable + * BadRequestError etc → status=4xx ← NOT retryable + * + * Anthropic SDK (@anthropic-ai/sdk) — identical class/message shapes to OpenAI SDK. + * + * Google SDK & Ollama — fall through to message-based detection. + * + * In streaming mode: adapters swallow thrown errors and yield { type:"error", message }. + * The fallback chain creates `new Error(message)` from those, so only message survives. + * Message-based detection handles that path. + * + * In complete() mode: real SDK class instances are thrown, so constructor.name + status checks fire. + */ +function defaultIsRetryable(error: unknown): boolean { + if (typeof error === "object" && error !== null) { + const ctorName = (error as object).constructor?.name ?? ""; + + // ── Explicit NOT-retryable classes ──────────────────────────────────── + // User aborted the request — never retry, never fall back + if (ctorName === "APIUserAbortError") return false; + + // ── Explicit retryable classes (no status property) ─────────────────── + // Both OpenAI and Anthropic SDKs use these exact class names. + if (ctorName === "APIConnectionError") return true; + if (ctorName === "APIConnectionTimeoutError") return true; + + // ── HTTP status code check (OpenAI, Anthropic, Google, etc.) ────────── + // Both SDKs expose the numeric HTTP status on `.status`. + // `.statusCode` covers some older / third-party adapters. + const status = + (error as { status?: unknown }).status ?? + (error as { statusCode?: unknown }).statusCode; + + if (typeof status === "number") { + if (status === 429) return true; // rate limit + if (status >= 500) return true; // 500, 502, 503, 504, 520-527 (Cloudflare), etc. + if (status >= 400) return false; // 400-428, 430-499 — caller bug, don't retry + } + } + + // ── Message-based detection ─────────────────────────────────────────────── + // Used when errors have been serialised to plain Error objects: + // • stream mode — adapters yield { type:"error", message } instead of throwing + // • Google SDK / Ollama — don't use OpenAI/Anthropic SDK class names + if (error instanceof Error) { + const msg = error.message; + + // Hard-stop: user-initiated abort. + // Node.js AbortController: "The operation was aborted" + // Browser fetch AbortController: "The user aborted a request" + // These must never trigger a retry or fallback. + if (/the operation was aborted/i.test(msg)) return false; + if (/the user aborted a request/i.test(msg)) return false; + + // Hard-stop: any 4xx that is NOT 429. + // e.g. "401 Incorrect API key", "403 Forbidden", "404 Not found" + if (/\b4[0-9]{2}\b/.test(msg) && !/\b429\b/.test(msg)) return false; + + // ── Retryable: rate limit ────────────────────────────────────────────── + // OpenAI stream error event: "429 You exceeded your current quota…" + // Anthropic stream error event: "429 {"error":{"type":"rate_limit_error"…}}" + if (/\b429\b/.test(msg)) return true; + if (/rate[\s_-]?limit/i.test(msg)) return true; + if (/too many requests/i.test(msg)) return true; + if (/quota exceeded/i.test(msg)) return true; // Google + + // ── Retryable: 5xx server errors ────────────────────────────────────── + if (/\b5[0-9]{2}\b/.test(msg)) return true; // any 5xx in message + if (/internal server error/i.test(msg)) return true; + if (/service unavailable/i.test(msg)) return true; + if (/bad gateway/i.test(msg)) return true; + if (/gateway timeout/i.test(msg)) return true; + if (/overloaded/i.test(msg)) return true; // Anthropic "overloaded_error" + + // ── Retryable: connection / timeout ─────────────────────────────────── + // OpenAI SDK APIConnectionError exact message: + if (/^connection error\.?$/i.test(msg)) return true; + // OpenAI SDK APIConnectionTimeoutError exact message: + if (/^request timed out\.?$/i.test(msg)) return true; + // General timeout patterns (Google SDK, Ollama, custom adapters) + if (/timed?\s*out/i.test(msg)) return true; + if (/timeout/i.test(msg)) return true; + // fetch() failure (browser + Node.js undici) + if (/fetch failed/i.test(msg)) return true; + + // ── Retryable: Node.js network error codes ──────────────────────────── + // These appear in the message when a raw network error bubbles up. + if (/ECONNREFUSED/.test(msg)) return true; // connection refused + if (/ECONNRESET/.test(msg)) return true; // connection reset by peer + if (/ETIMEDOUT/.test(msg)) return true; // TCP timeout + if (/ENOTFOUND/.test(msg)) return true; // DNS lookup failure + if (/ENETUNREACH/.test(msg)) return true; // no route to host + if (/EHOSTUNREACH/.test(msg)) return true; // host unreachable + } + + return false; +} + +/** Calculate delay for a given retry attempt */ +function calcDelay( + base: number, + attempt: number, + backoff: "exponential" | "fixed", +): number { + if (backoff === "fixed") return base; + return base * Math.pow(2, attempt - 1); // exponential: base, base*2, base*4... +} + +/** Sleep helper */ +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +// ─── Internal resolved config type ─────────────────────────────────────────── + +type ResolvedConfig = Required< + Omit +> & + Pick; + +// ─── FallbackChain ──────────────────────────────────────────────────────────── + +class FallbackChain implements LLMAdapter { + private readonly _config: ResolvedConfig; + + constructor(config: FallbackChainConfig) { + if (config.models.length === 0) { + throw new Error("FallbackChain requires at least one model."); + } + + this._config = { + models: config.models, + strategy: config.strategy ?? "priority", + store: config.store ?? new MemoryRoutingStore(), + retries: config.retries ?? 0, + retryDelay: config.retryDelay ?? 500, + retryBackoff: config.retryBackoff ?? "exponential", + onFallback: config.onFallback, + onRetry: config.onRetry, + retryableErrors: config.retryableErrors, + }; + } + + get provider(): string { + return "fallback-chain"; + } + + get model(): string { + return this._config.models.map((m) => `${m.provider}/${m.model}`).join(","); + } + + private async _startIndex(): Promise { + if (this._config.strategy !== "round-robin") return 0; + const stored = await this._config.store.get(ROUND_ROBIN_KEY); + return typeof stored === "number" ? stored % this._config.models.length : 0; + } + + private async _advanceIndex(successfulIndex: number): Promise { + if (this._config.strategy !== "round-robin") return; + const next = (successfulIndex + 1) % this._config.models.length; + await this._config.store.set(ROUND_ROBIN_KEY, next); + } + + private _isRetryable(error: unknown): boolean { + return this._config.retryableErrors + ? this._config.retryableErrors(error) + : defaultIsRetryable(error); + } + + /** + * Try streaming from a single adapter, with per-model retries. + * + * Returns an async generator that either: + * - yields all chunks on success, then returns + * - throws the final error if all retries exhausted or error is non-retryable + * + * The `retriesAttempted` out-param is filled via the returned object so callers + * can record it in FallbackFailure. + */ + private async *_streamWithRetries( + adapter: LLMAdapter, + request: ChatCompletionRequest, + out: { retriesAttempted: number }, + ): AsyncGenerator { + const { retries, retryDelay, retryBackoff, onRetry } = this._config; + const maxAttempts = retries + 1; // initial attempt + retries + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + let contentStarted = false; + let failureError: Error | null = null; + + try { + for await (const chunk of adapter.stream(request)) { + if (chunk.type === "error") { + if (!contentStarted) { + const msg = + (chunk as { type: "error"; message?: string }).message ?? + "Unknown error"; + failureError = new Error(msg); + break; + } + yield chunk; + return; + } + + if (chunk.type === "message:start") continue; + + contentStarted = true; + yield chunk; + } + } catch (error) { + if (contentStarted) throw error; + if (!this._isRetryable(error)) throw error; + failureError = + error instanceof Error ? error : new Error(String(error)); + } + + if (failureError === null) return; // success + + // Non-retryable (4xx) — checked in catch above, but also check error-event path + if (!this._isRetryable(failureError)) throw failureError; + + out.retriesAttempted = attempt - 1; // attempts so far beyond initial + + // If we have more retries left, wait then retry the same model + if (attempt < maxAttempts) { + const delayMs = calcDelay(retryDelay, attempt, retryBackoff); + const retryInfo: RetryInfo = { + model: adapter.model, + provider: adapter.provider, + error: failureError, + retryAttempt: attempt, + maxRetries: retries, + delayMs, + }; + onRetry?.(retryInfo); + await sleep(delayMs); + continue; + } + + // All retries for this model exhausted — throw so the outer loop can try next model + out.retriesAttempted = retries; + throw failureError; + } + } + + async *stream(request: ChatCompletionRequest): AsyncGenerator { + const { models, onFallback } = this._config; + const startIndex = await this._startIndex(); + const failures: FallbackFailure[] = []; + + for (let i = 0; i < models.length; i++) { + const index = (startIndex + i) % models.length; + const adapter = models[index]; + const out = { retriesAttempted: 0 }; + + try { + yield* this._streamWithRetries(adapter, request, out); + // Success + await this._advanceIndex(index); + return; + } catch (error) { + // Non-retryable (4xx) — rethrow immediately, don't try next model + if (!this._isRetryable(error)) throw error; + + const failure: FallbackFailure = { + model: adapter.model, + provider: adapter.provider, + error: error instanceof Error ? error : new Error(String(error)), + attempt: i + 1, + retriesAttempted: out.retriesAttempted, + }; + failures.push(failure); + + const nextOffset = i + 1; + if (nextOffset < models.length && onFallback) { + const nextIndex = (startIndex + nextOffset) % models.length; + onFallback({ + attemptedModel: adapter.model, + nextModel: models[nextIndex].model, + error: failure.error, + attempt: failure.attempt, + }); + } + } + } + + throw new FallbackExhaustedError(failures); + } + + async complete(request: ChatCompletionRequest): Promise { + const { models, onFallback, retries, retryDelay, retryBackoff, onRetry } = + this._config; + const startIndex = await this._startIndex(); + const failures: FallbackFailure[] = []; + + for (let i = 0; i < models.length; i++) { + const index = (startIndex + i) % models.length; + const adapter = models[index]; + + if (!adapter.complete) { + failures.push({ + model: adapter.model, + provider: adapter.provider, + error: new Error( + `Adapter ${adapter.provider}/${adapter.model} does not implement complete()`, + ), + attempt: i + 1, + retriesAttempted: 0, + }); + continue; + } + + const maxAttempts = retries + 1; + let lastError: Error | null = null; + let retriesAttempted = 0; + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + const result = await adapter.complete(request); + await this._advanceIndex(index); + return result; + } catch (error) { + if (!this._isRetryable(error)) throw error; + + lastError = error instanceof Error ? error : new Error(String(error)); + retriesAttempted = attempt - 1; + + if (attempt < maxAttempts) { + const delayMs = calcDelay(retryDelay, attempt, retryBackoff); + onRetry?.({ + model: adapter.model, + provider: adapter.provider, + error: lastError, + retryAttempt: attempt, + maxRetries: retries, + delayMs, + }); + await sleep(delayMs); + } + } + } + + const failure: FallbackFailure = { + model: adapter.model, + provider: adapter.provider, + error: lastError!, + attempt: i + 1, + retriesAttempted, + }; + failures.push(failure); + + const nextOffset = i + 1; + if (nextOffset < models.length && onFallback) { + const nextIndex = (startIndex + nextOffset) % models.length; + onFallback({ + attemptedModel: adapter.model, + nextModel: models[nextIndex].model, + error: failure.error, + attempt: failure.attempt, + }); + } + } + + throw new FallbackExhaustedError(failures); + } +} + +/** + * Create a fallback chain that tries each model in order, with optional + * per-model retries before moving to the next model. + * + * @example + * ```typescript + * import { createFallbackChain } from '@yourgpt/llm-sdk/fallback'; + * import { createRuntime } from '@yourgpt/llm-sdk'; + * import { createOpenAI } from '@yourgpt/llm-sdk/openai'; + * import { createAnthropic } from '@yourgpt/llm-sdk/anthropic'; + * + * const chain = createFallbackChain({ + * models: [ + * createOpenAI({ apiKey: '...' }).languageModel('gpt-5.4'), + * createAnthropic({ apiKey: '...' }).languageModel('claude-haiku-4-5'), + * ], + * retries: 2, // retry each model up to 2 times before moving on + * retryDelay: 500, // 500ms → 1000ms (exponential) + * retryBackoff: 'exponential', + * strategy: 'round-robin', + * onRetry: ({ model, retryAttempt, maxRetries, delayMs, error }) => { + * console.warn(`[retry] ${model} attempt ${retryAttempt}/${maxRetries} — waiting ${delayMs}ms`); + * }, + * onFallback: ({ attemptedModel, nextModel, attempt }) => { + * console.warn(`[fallback] ${attemptedModel} gave up after retries → ${nextModel}`); + * }, + * }); + * + * const runtime = createRuntime({ adapter: chain }); + * ``` + */ +export function createFallbackChain(config: FallbackChainConfig): LLMAdapter { + return new FallbackChain(config); +} diff --git a/packages/llm-sdk/src/fallback/errors.ts b/packages/llm-sdk/src/fallback/errors.ts new file mode 100644 index 0000000..b6a00eb --- /dev/null +++ b/packages/llm-sdk/src/fallback/errors.ts @@ -0,0 +1,43 @@ +import type { FallbackFailure } from "./types"; + +/** + * Thrown when every model in the fallback chain has failed. + * + * The `failures` array provides a per-model breakdown of what failed + * and why, so you can log or surface the full picture. + * + * @example + * ```typescript + * import { FallbackExhaustedError } from '@yourgpt/llm-sdk/fallback'; + * + * try { + * const result = await runtime.chat(request); + * } catch (err) { + * if (err instanceof FallbackExhaustedError) { + * for (const f of err.failures) { + * console.error(`${f.provider}/${f.model} (attempt ${f.attempt}): ${f.error.message}`); + * } + * } + * } + * ``` + */ +export class FallbackExhaustedError extends Error { + /** Per-model breakdown of every failed attempt */ + readonly failures: FallbackFailure[]; + + constructor(failures: FallbackFailure[]) { + const summary = failures + .map((f) => `${f.provider}/${f.model}: ${f.error.message}`) + .join("; "); + + super( + `All ${failures.length} model(s) in the fallback chain failed. ${summary}`, + ); + + this.name = "FallbackExhaustedError"; + this.failures = failures; + + // Preserve prototype chain in transpiled environments + Object.setPrototypeOf(this, new.target.prototype); + } +} diff --git a/packages/llm-sdk/src/fallback/index.ts b/packages/llm-sdk/src/fallback/index.ts new file mode 100644 index 0000000..e1bce5d --- /dev/null +++ b/packages/llm-sdk/src/fallback/index.ts @@ -0,0 +1,43 @@ +/** + * @yourgpt/llm-sdk/fallback + * + * Fallback Chain & Routing Strategies + * + * Automatically retries failed LLM requests with backup models. + * Supports priority (default) and round-robin routing strategies. + * + * @example + * ```typescript + * import { createFallbackChain, MemoryRoutingStore } from '@yourgpt/llm-sdk/fallback'; + * import { createRuntime } from '@yourgpt/llm-sdk'; + * import { createOpenAI } from '@yourgpt/llm-sdk/openai'; + * import { createAnthropic } from '@yourgpt/llm-sdk/anthropic'; + * + * const chain = createFallbackChain({ + * models: [ + * createOpenAI({ apiKey: '...' }).languageModel('gpt-4o'), + * createAnthropic({ apiKey: '...' }).languageModel('claude-3-5-sonnet-20241022'), + * ], + * strategy: 'round-robin', + * onFallback: ({ attemptedModel, nextModel, error, attempt }) => { + * console.warn(`Attempt ${attempt}: ${attemptedModel} failed, trying ${nextModel}`); + * }, + * }); + * + * const runtime = createRuntime({ adapter: chain }); + * ``` + */ + +export { createFallbackChain } from "./chain"; +export { FallbackExhaustedError } from "./errors"; +export { MemoryRoutingStore } from "./routing-store"; + +export type { + RoutingStore, + RoutingStrategy, + RetryBackoff, + FallbackChainConfig, + FallbackFailure, + FallbackInfo, + RetryInfo, +} from "./types"; diff --git a/packages/llm-sdk/src/fallback/routing-store.ts b/packages/llm-sdk/src/fallback/routing-store.ts new file mode 100644 index 0000000..95b9ea8 --- /dev/null +++ b/packages/llm-sdk/src/fallback/routing-store.ts @@ -0,0 +1,36 @@ +import type { RoutingStore } from "./types"; + +/** + * Built-in in-memory routing store. + * + * Works out of the box for single-process applications. + * State resets on restart and is NOT shared across instances. + * + * For production multi-instance or serverless deployments, + * implement your own RoutingStore backed by Redis, Upstash, + * Cloudflare KV, DynamoDB, or any other persistent store. + * + * @example + * ```typescript + * // Default — created automatically by createFallbackChain + * const chain = createFallbackChain({ models: [...] }); + * + * // Explicit — pass your own store + * const chain = createFallbackChain({ + * models: [...], + * strategy: 'round-robin', + * store: new MemoryRoutingStore(), + * }); + * ``` + */ +export class MemoryRoutingStore implements RoutingStore { + private readonly _map = new Map(); + + async get(key: string): Promise { + return this._map.get(key); + } + + async set(key: string, value: number): Promise { + this._map.set(key, value); + } +} diff --git a/packages/llm-sdk/src/fallback/types.ts b/packages/llm-sdk/src/fallback/types.ts new file mode 100644 index 0000000..59f3fc8 --- /dev/null +++ b/packages/llm-sdk/src/fallback/types.ts @@ -0,0 +1,236 @@ +/** + * Fallback Chain & Routing Strategy Types + */ + +import type { LLMAdapter } from "../adapters/base"; + +// ============================================ +// Routing Store Interface +// ============================================ + +/** + * Pluggable state store for routing strategies. + * + * Round-robin and other stateful strategies use this to persist + * which model was last used. The default implementation is in-memory. + * + * For multi-instance or serverless deployments, plug in your own: + * Redis, Upstash, Cloudflare KV, DynamoDB, etc. + * + * @example + * ```typescript + * // Redis-backed store (example — bring your own client) + * const redisStore: RoutingStore = { + * async get(key) { + * const val = await redis.get(key); + * return val ? Number(val) : undefined; + * }, + * async set(key, value) { + * await redis.set(key, value); + * }, + * }; + * ``` + */ +export interface RoutingStore { + /** Get the stored value for a key */ + get(key: string): Promise; + /** Set the stored value for a key */ + set(key: string, value: number): Promise; +} + +// ============================================ +// Failure & Callback Types +// ============================================ + +/** + * A single failed model in the fallback chain (after all retries exhausted) + */ +export interface FallbackFailure { + /** Model ID that failed */ + model: string; + /** Provider name */ + provider: string; + /** The last error from this model */ + error: Error; + /** Which model in the chain this was (1-based) */ + attempt: number; + /** How many times this model was retried before giving up */ + retriesAttempted: number; +} + +/** + * Passed to the onFallback callback when a model is abandoned and the next one is tried + */ +export interface FallbackInfo { + /** Model that just failed (after all its retries) */ + attemptedModel: string; + /** Model that will be tried next */ + nextModel: string; + /** The last error from the failed model */ + error: Error; + /** Which model in the chain this was (1-based) */ + attempt: number; +} + +/** + * Passed to the onRetry callback on each per-model retry attempt + */ +export interface RetryInfo { + /** Model being retried */ + model: string; + /** Provider name */ + provider: string; + /** The error that triggered this retry */ + error: Error; + /** Which retry attempt this is (1-based: 1 = first retry after initial failure) */ + retryAttempt: number; + /** Total retries configured for this chain */ + maxRetries: number; + /** How long (ms) we will wait before retrying */ + delayMs: number; +} + +// ============================================ +// Strategy & Config +// ============================================ + +/** + * How the chain decides which model to try first. + * + * - `priority` — always try models in defined order (default) + * - `round-robin` — rotate starting model evenly across calls + */ +export type RoutingStrategy = "priority" | "round-robin"; + +/** + * Backoff strategy between per-model retries. + * + * - `exponential` — delay doubles on each retry: 500ms → 1000ms → 2000ms (default) + * - `fixed` — same delay every retry: 500ms → 500ms → 500ms + */ +export type RetryBackoff = "exponential" | "fixed"; + +/** + * Configuration for createFallbackChain() + */ +export interface FallbackChainConfig { + /** + * Ordered list of adapters to try. + * On failure, the chain moves to the next adapter in this list. + * + * @example + * ```typescript + * import { createOpenAI } from '@yourgpt/llm-sdk/openai'; + * import { createAnthropic } from '@yourgpt/llm-sdk/anthropic'; + * + * const openai = createOpenAI({ apiKey: '...' }); + * const anthropic = createAnthropic({ apiKey: '...' }); + * + * const chain = createFallbackChain({ + * models: [ + * openai.languageModel('gpt-4o'), + * anthropic.languageModel('claude-3-5-sonnet-20241022'), + * ], + * }); + * ``` + */ + models: LLMAdapter[]; + + /** + * Routing strategy controlling which model is tried first. + * @default 'priority' + */ + strategy?: RoutingStrategy; + + /** + * State store for strategies that require persistence (e.g., round-robin). + * Defaults to an in-memory store (MemoryRoutingStore). + * + * Replace with a shared store (Redis, Upstash, etc.) for multi-instance + * or serverless deployments where round-robin state must be shared. + */ + store?: RoutingStore; + + /** + * Number of times to retry the same model before moving to the next one. + * + * LiteLLM equivalent: `num_retries` + * + * @default 0 (no retries — fail immediately and move to next model) + * + * @example + * ```typescript + * // Try each model up to 3 times before falling back + * createFallbackChain({ models: [...], retries: 3 }) + * ``` + */ + retries?: number; + + /** + * Base delay in milliseconds between per-model retries. + * + * With `retryBackoff: 'exponential'` (default): + * retry 1 → retryDelay ms + * retry 2 → retryDelay * 2 ms + * retry 3 → retryDelay * 4 ms + * + * With `retryBackoff: 'fixed'`: + * every retry → retryDelay ms + * + * @default 500 + */ + retryDelay?: number; + + /** + * Backoff strategy between per-model retries. + * @default 'exponential' + */ + retryBackoff?: RetryBackoff; + + /** + * Called on each per-model retry attempt (before the delay). + * Use for logging, metrics, or alerting per retry. + * + * @example + * ```typescript + * onRetry: ({ model, retryAttempt, maxRetries, delayMs, error }) => { + * console.warn(`[retry] ${model} attempt ${retryAttempt}/${maxRetries} — waiting ${delayMs}ms | ${error.message}`); + * } + * ``` + */ + onRetry?: (info: RetryInfo) => void; + + /** + * Called each time a model is abandoned and the next one is tried. + * Use for logging, metrics, or alerting. + * + * @example + * ```typescript + * onFallback: ({ attemptedModel, nextModel, error, attempt }) => { + * console.warn(`[fallback] attempt ${attempt}: ${attemptedModel} failed → ${nextModel}`, error.message); + * } + * ``` + */ + onFallback?: (info: FallbackInfo) => void; + + /** + * Custom predicate to decide whether an error should trigger a fallback. + * + * By default, the following trigger fallback: + * - HTTP 5xx server errors + * - HTTP 429 rate limit errors + * - Network timeouts and connection failures + * + * The following do NOT trigger fallback by default: + * - HTTP 4xx client errors (bad request, invalid API key, etc.) + * + * Override this to extend or restrict fallback behavior. + * + * @example + * ```typescript + * // Also fall back on any error + * retryableErrors: () => true, + * ``` + */ + retryableErrors?: (error: unknown) => boolean; +} diff --git a/packages/llm-sdk/src/index.ts b/packages/llm-sdk/src/index.ts index a5af166..4218f5b 100644 --- a/packages/llm-sdk/src/index.ts +++ b/packages/llm-sdk/src/index.ts @@ -218,3 +218,17 @@ export { generateToolCallId, generateThreadId, } from "./core/utils"; + +// ============================================ +// Fallback Chain & Routing (types only from root) +// ============================================ +// Full implementation: import from '@yourgpt/llm-sdk/fallback' +export type { + RoutingStore, + RoutingStrategy, + RetryBackoff, + FallbackChainConfig, + FallbackFailure, + FallbackInfo, + RetryInfo, +} from "./fallback/types"; diff --git a/packages/llm-sdk/tsup.config.ts b/packages/llm-sdk/tsup.config.ts index 418863c..78f76db 100644 --- a/packages/llm-sdk/tsup.config.ts +++ b/packages/llm-sdk/tsup.config.ts @@ -19,6 +19,9 @@ export default defineConfig({ // YourGPT storage adapter "yourgpt/index": "src/yourgpt/index.ts", + + // Fallback chain & routing + "fallback/index": "src/fallback/index.ts", }, format: ["cjs", "esm"], dts: true, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2eb1776..b44c66f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -418,6 +418,40 @@ importers: specifier: ^5.6.0 version: 5.9.3 + examples/fallback-demo: + dependencies: + '@anthropic-ai/sdk': + specifier: ^0.39.0 + version: 0.39.0 + '@yourgpt/llm-sdk': + specifier: workspace:* + version: link:../../packages/llm-sdk + cors: + specifier: ^2.8.5 + version: 2.8.6 + dotenv: + specifier: ^16.4.0 + version: 16.6.1 + express: + specifier: ^4.21.0 + version: 4.22.1 + openai: + specifier: ^4.77.0 + version: 4.104.0(ws@8.18.0)(zod@3.25.76) + devDependencies: + '@types/cors': + specifier: ^2.8.17 + version: 2.8.19 + '@types/express': + specifier: ^5.0.0 + version: 5.0.6 + tsx: + specifier: ^4.19.0 + version: 4.21.0 + typescript: + specifier: ^5.6.0 + version: 5.9.3 + examples/headless-slack-demo: dependencies: '@yourgpt/copilot-sdk': @@ -1142,40 +1176,6 @@ importers: specifier: ^5 version: 5.9.3 - examples/yourgpt-server-demo: - dependencies: - '@yourgpt/llm-sdk': - specifier: workspace:* - version: link:../../packages/llm-sdk - cors: - specifier: ^2.8.5 - version: 2.8.6 - dotenv: - specifier: ^16.4.0 - version: 16.6.1 - express: - specifier: ^4.21.0 - version: 4.22.1 - ws: - specifier: ^8.18.0 - version: 8.18.0 - devDependencies: - '@types/cors': - specifier: ^2.8.17 - version: 2.8.19 - '@types/express': - specifier: ^5.0.0 - version: 5.0.6 - '@types/ws': - specifier: ^8.5.13 - version: 8.18.1 - tsx: - specifier: ^4.19.0 - version: 4.21.0 - typescript: - specifier: ^5.6.0 - version: 5.9.3 - packages/copilot-sdk: dependencies: '@base-ui/react': @@ -4097,9 +4097,6 @@ packages: '@types/validate-npm-package-name@4.0.2': resolution: {integrity: sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==} - '@types/ws@8.18.1': - resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} - '@typescript-eslint/eslint-plugin@8.50.0': resolution: {integrity: sha512-O7QnmOXYKVtPrfYzMolrCTfkezCJS9+ljLdKW/+DCvRsc3UAz+sbH6Xcsv7p30+0OwUbeWfUDAQE0vpabZ3QLg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -11325,10 +11322,6 @@ snapshots: '@types/validate-npm-package-name@4.0.2': {} - '@types/ws@8.18.1': - dependencies: - '@types/node': 20.19.27 - '@typescript-eslint/eslint-plugin@8.50.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 From 391b195cd4ca77583110afebcb5bd9e3e7bc5d61 Mon Sep 17 00:00:00 2001 From: Sahil Date: Sun, 29 Mar 2026 12:24:06 +0530 Subject: [PATCH 06/39] fix: sync pnpm lockfile with generative-ui-demo and skills-demo packages Co-Authored-By: Claude Sonnet 4.6 --- pnpm-lock.yaml | 625 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 598 insertions(+), 27 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 134fb50..bd7fad4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -44,7 +44,7 @@ importers: version: 16.2.5(@types/react@18.3.27)(lucide-react@0.562.0(react@19.2.1))(next@16.0.10(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(sass@1.97.0))(react-dom@19.2.1(react@19.2.1))(react-router@7.13.1(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1)(zod@4.3.6) fumadocs-mdx: specifier: ^14.1.1 - version: 14.1.1(fumadocs-core@16.2.5(@types/react@18.3.27)(lucide-react@0.562.0(react@19.2.1))(next@16.0.10(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(sass@1.97.0))(react-dom@19.2.1(react@19.2.1))(react-router@7.13.1(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1)(zod@4.3.6))(next@16.0.10(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(sass@1.97.0))(react@19.2.1)(vite@6.4.1(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2)) + version: 14.1.1(fumadocs-core@16.2.5(@types/react@18.3.27)(lucide-react@0.562.0(react@19.2.1))(next@16.0.10(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(sass@1.97.0))(react-dom@19.2.1(react@19.2.1))(react-router@7.13.1(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1)(zod@4.3.6))(next@16.0.10(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(sass@1.97.0))(react@19.2.1)(vite@6.4.1(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2)) fumadocs-ui: specifier: ^16.2.5 version: 16.2.5(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(lucide-react@0.562.0(react@19.2.1))(next@16.0.10(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(sass@1.97.0))(react-dom@19.2.1(react@19.2.1))(react-router@7.13.1(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1)(tailwindcss@4.1.18)(zod@4.3.6) @@ -452,6 +452,73 @@ importers: specifier: ^5.6.0 version: 5.9.3 + examples/generative-ui-demo: + dependencies: + '@yourgpt/copilot-sdk': + specifier: workspace:* + version: link:../../packages/copilot-sdk + '@yourgpt/llm-sdk': + specifier: workspace:* + version: link:../../packages/llm-sdk + cors: + specifier: ^2.8.5 + version: 2.8.6 + dotenv: + specifier: ^16.4.0 + version: 16.6.1 + express: + specifier: ^4.21.0 + version: 4.22.1 + lucide-react: + specifier: ^0.563.0 + version: 0.563.0(react@19.2.3) + react: + specifier: ^19.0.0 + version: 19.2.3 + react-dom: + specifier: ^19.0.0 + version: 19.2.3(react@19.2.3) + recharts: + specifier: ^2.13.0 + version: 2.15.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + devDependencies: + '@tailwindcss/postcss': + specifier: ^4 + version: 4.1.18 + '@types/cors': + specifier: ^2.8.17 + version: 2.8.19 + '@types/express': + specifier: ^5.0.0 + version: 5.0.6 + '@types/node': + specifier: ^20 + version: 20.19.27 + '@types/react': + specifier: ^18.2.0 + version: 18.3.27 + '@types/react-dom': + specifier: ^18.2.0 + version: 18.3.7(@types/react@18.3.27) + '@vitejs/plugin-react': + specifier: 6.0.1 + version: 6.0.1(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)) + concurrently: + specifier: ^9.0.0 + version: 9.2.1 + tailwindcss: + specifier: ^4 + version: 4.2.1 + tsx: + specifier: ^4.19.0 + version: 4.21.0 + typescript: + specifier: ^5.6.0 + version: 5.9.3 + vite: + specifier: 8.0.3 + version: 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) + examples/headless-slack-demo: dependencies: '@yourgpt/copilot-sdk': @@ -502,7 +569,7 @@ importers: version: 18.3.7(@types/react@18.3.27) '@vitejs/plugin-react': specifier: ^4.3.4 - version: 4.7.0(vite@6.4.1(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.7.0(vite@6.4.1(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2)) concurrently: specifier: ^9.0.0 version: 9.2.1 @@ -517,7 +584,7 @@ importers: version: 5.9.3 vite: specifier: ^6.0.0 - version: 6.4.1(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2) + version: 6.4.1(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2) examples/mcp-demo: dependencies: @@ -603,7 +670,7 @@ importers: devDependencies: '@tailwindcss/vite': specifier: ^4.0.0 - version: 4.2.1(vite@6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.2.1(vite@6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2)) '@types/react': specifier: ^18.2.0 version: 18.3.27 @@ -612,7 +679,7 @@ importers: version: 18.3.7(@types/react@18.3.27) '@vitejs/plugin-react': specifier: ^4.3.0 - version: 4.7.0(vite@6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.7.0(vite@6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2)) tailwindcss: specifier: ^4.0.0 version: 4.1.18 @@ -621,7 +688,7 @@ importers: version: 5.9.3 vite: specifier: ^6.0.0 - version: 6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2) + version: 6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2) examples/ollama-demo/server: dependencies: @@ -1060,6 +1127,73 @@ importers: specifier: ^5 version: 5.9.3 + examples/skills-demo: + dependencies: + '@yourgpt/copilot-sdk': + specifier: workspace:* + version: link:../../packages/copilot-sdk + '@yourgpt/llm-sdk': + specifier: workspace:* + version: link:../../packages/llm-sdk + cors: + specifier: ^2.8.5 + version: 2.8.6 + dotenv: + specifier: ^16.4.0 + version: 16.6.1 + express: + specifier: ^4.21.0 + version: 4.22.1 + lucide-react: + specifier: ^0.563.0 + version: 0.563.0(react@19.2.3) + react: + specifier: ^19.0.0 + version: 19.2.3 + react-dom: + specifier: ^19.0.0 + version: 19.2.3(react@19.2.3) + devDependencies: + '@tailwindcss/postcss': + specifier: ^4 + version: 4.1.18 + '@types/cors': + specifier: ^2.8.17 + version: 2.8.19 + '@types/express': + specifier: ^5.0.0 + version: 5.0.6 + '@types/node': + specifier: ^20 + version: 20.19.27 + '@types/react': + specifier: ^18.2.0 + version: 18.3.27 + '@types/react-dom': + specifier: ^18.2.0 + version: 18.3.7(@types/react@18.3.27) + '@vitejs/plugin-react': + specifier: 6.0.1 + version: 6.0.1(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)) + concurrently: + specifier: ^9.0.0 + version: 9.2.1 + path: + specifier: ^0.12.7 + version: 0.12.7 + tailwindcss: + specifier: ^4 + version: 4.2.1 + tsx: + specifier: ^4.19.0 + version: 4.21.0 + typescript: + specifier: ^5.6.0 + version: 5.9.3 + vite: + specifier: 8.0.3 + version: 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) + examples/support-tickets-demo: dependencies: '@radix-ui/react-slot': @@ -1244,7 +1378,7 @@ importers: version: 18.3.1(react@18.3.1) tsup: specifier: ^8.0.2 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + version: 8.5.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) typescript: specifier: ^5.4.0 version: 5.9.3 @@ -1269,7 +1403,7 @@ importers: version: 22.19.3 tsup: specifier: ^8.3.5 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + version: 8.5.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) typescript: specifier: ^5.7.2 version: 5.9.3 @@ -1278,7 +1412,7 @@ importers: devDependencies: tsup: specifier: ^8.0.2 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + version: 8.5.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) typescript: specifier: ^5.4.0 version: 5.9.3 @@ -1306,7 +1440,7 @@ importers: version: 4.104.0(ws@8.18.0)(zod@3.25.76) tsup: specifier: ^8.0.2 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + version: 8.5.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) typescript: specifier: ^5.4.0 version: 5.9.3 @@ -2555,6 +2689,12 @@ packages: '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} + '@napi-rs/wasm-runtime@1.1.2': + resolution: {integrity: sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + '@next/env@16.0.10': resolution: {integrity: sha512-8tuaQkyDVgeONQ1MeT9Mkk8pQmZapMKFh5B+OrFUlG3rVmYTXcXlBetBgTurKXGaIZvkoqRT9JL5K3phXcgang==} @@ -2758,6 +2898,9 @@ packages: resolution: {integrity: sha512-APwpZ+FTGMryo4QEeD6ti+Ei8suBkvxe8PeWdUcQHVfJDpjpt4c1dKojjNswcBmdeWSiiTYcnkKKH+yuo6727g==} engines: {node: '>= 20.0.0'} + '@oxc-project/types@0.122.0': + resolution: {integrity: sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==} + '@parcel/watcher-android-arm64@2.5.1': resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} engines: {node: '>= 10.0.0'} @@ -3600,9 +3743,104 @@ packages: '@radix-ui/rect@1.1.1': resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + '@rolldown/binding-android-arm64@1.0.0-rc.12': + resolution: {integrity: sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.0-rc.12': + resolution: {integrity: sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.0-rc.12': + resolution: {integrity: sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.0-rc.12': + resolution: {integrity: sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12': + resolution: {integrity: sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12': + resolution: {integrity: sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.12': + resolution: {integrity: sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.12': + resolution: {integrity: sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.12': + resolution: {integrity: sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12': + resolution: {integrity: sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12': + resolution: {integrity: sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + '@rolldown/pluginutils@1.0.0-beta.27': resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + '@rolldown/pluginutils@1.0.0-rc.12': + resolution: {integrity: sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==} + + '@rolldown/pluginutils@1.0.0-rc.7': + resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==} + '@rollup/rollup-android-arm-eabi@4.53.3': resolution: {integrity: sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==} cpu: [arm] @@ -4260,6 +4498,19 @@ packages: peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + '@vitejs/plugin-react@6.0.1': + resolution: {integrity: sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 + babel-plugin-react-compiler: ^1.0.0 + vite: ^8.0.0 + peerDependenciesMeta: + '@rolldown/plugin-babel': + optional: true + babel-plugin-react-compiler: + optional: true + '@yourgpt/copilot-sdk@2.1.5-alpha.8': resolution: {integrity: sha512-5dtH/F8rmlv+V78xTnMoEBAMQYAr1YGCxNWyX+2V004xTHzgqTsdgZCNaYlFkNbv0PMNCwTfH9hfabhBEviVkQ==} engines: {node: '>=18'} @@ -5723,6 +5974,9 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + inherits@2.0.3: + resolution: {integrity: sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==} + inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -6050,6 +6304,12 @@ packages: cpu: [arm64] os: [android] + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + lightningcss-darwin-arm64@1.30.2: resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==} engines: {node: '>= 12.0.0'} @@ -6062,6 +6322,12 @@ packages: cpu: [arm64] os: [darwin] + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + lightningcss-darwin-x64@1.30.2: resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==} engines: {node: '>= 12.0.0'} @@ -6074,6 +6340,12 @@ packages: cpu: [x64] os: [darwin] + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + lightningcss-freebsd-x64@1.30.2: resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==} engines: {node: '>= 12.0.0'} @@ -6086,6 +6358,12 @@ packages: cpu: [x64] os: [freebsd] + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + lightningcss-linux-arm-gnueabihf@1.30.2: resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==} engines: {node: '>= 12.0.0'} @@ -6098,6 +6376,12 @@ packages: cpu: [arm] os: [linux] + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + lightningcss-linux-arm64-gnu@1.30.2: resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==} engines: {node: '>= 12.0.0'} @@ -6110,6 +6394,12 @@ packages: cpu: [arm64] os: [linux] + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + lightningcss-linux-arm64-musl@1.30.2: resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} engines: {node: '>= 12.0.0'} @@ -6122,6 +6412,12 @@ packages: cpu: [arm64] os: [linux] + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + lightningcss-linux-x64-gnu@1.30.2: resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} engines: {node: '>= 12.0.0'} @@ -6134,6 +6430,12 @@ packages: cpu: [x64] os: [linux] + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + lightningcss-linux-x64-musl@1.30.2: resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} engines: {node: '>= 12.0.0'} @@ -6146,6 +6448,12 @@ packages: cpu: [x64] os: [linux] + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + lightningcss-win32-arm64-msvc@1.30.2: resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} engines: {node: '>= 12.0.0'} @@ -6158,6 +6466,12 @@ packages: cpu: [arm64] os: [win32] + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + lightningcss-win32-x64-msvc@1.30.2: resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==} engines: {node: '>= 12.0.0'} @@ -6170,6 +6484,12 @@ packages: cpu: [x64] os: [win32] + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + lightningcss@1.30.2: resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} engines: {node: '>= 12.0.0'} @@ -6178,6 +6498,10 @@ packages: resolution: {integrity: sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==} engines: {node: '>= 12.0.0'} + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + lilconfig@3.1.3: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} @@ -6863,6 +7187,9 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} + path@0.12.7: + resolution: {integrity: sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -6877,6 +7204,10 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + pidtree@0.6.0: resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==} engines: {node: '>=0.10'} @@ -6935,6 +7266,10 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} + postcss@8.5.8: + resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} + engines: {node: ^10 || ^12 || >=14} + powershell-utils@0.1.0: resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==} engines: {node: '>=20'} @@ -6960,6 +7295,10 @@ packages: printable-characters@1.0.42: resolution: {integrity: sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==} + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + prompts@2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} @@ -7256,6 +7595,11 @@ packages: rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + rolldown@1.0.0-rc.12: + resolution: {integrity: sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + rollup-plugin-inject@3.0.2: resolution: {integrity: sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w==} deprecated: This package has been deprecated and is no longer maintained. Please use @rollup/plugin-inject. @@ -7912,6 +8256,9 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + util@0.10.4: + resolution: {integrity: sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==} + utils-merge@1.0.1: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} @@ -7979,6 +8326,49 @@ packages: yaml: optional: true + vite@8.0.3: + resolution: {integrity: sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.0 + esbuild: ^0.27.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + web-namespaces@2.0.1: resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} @@ -9359,6 +9749,13 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true + '@napi-rs/wasm-runtime@1.1.2(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)': + dependencies: + '@emnapi/core': 1.7.1 + '@emnapi/runtime': 1.7.1 + '@tybys/wasm-util': 0.10.1 + optional: true + '@next/env@16.0.10': {} '@next/env@16.1.1': {} @@ -9482,6 +9879,8 @@ snapshots: '@orama/orama@3.1.17': {} + '@oxc-project/types@0.122.0': {} + '@parcel/watcher-android-arm64@2.5.1': optional: true @@ -10924,8 +11323,62 @@ snapshots: '@radix-ui/rect@1.1.1': {} + '@rolldown/binding-android-arm64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.12': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.12(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)': + dependencies: + '@napi-rs/wasm-runtime': 1.1.2(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12': + optional: true + '@rolldown/pluginutils@1.0.0-beta.27': {} + '@rolldown/pluginutils@1.0.0-rc.12': {} + + '@rolldown/pluginutils@1.0.0-rc.7': {} + '@rollup/rollup-android-arm-eabi@4.53.3': optional: true @@ -11198,12 +11651,12 @@ snapshots: postcss-selector-parser: 6.0.10 tailwindcss: 4.1.18 - '@tailwindcss/vite@4.2.1(vite@6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2))': + '@tailwindcss/vite@4.2.1(vite@6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@tailwindcss/node': 4.2.1 '@tailwindcss/oxide': 4.2.1 tailwindcss: 4.2.1 - vite: 6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2) + vite: 6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2) '@ts-morph/common@0.27.0': dependencies: @@ -11531,7 +11984,7 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true - '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2))': + '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@babel/core': 7.28.5 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.5) @@ -11539,11 +11992,11 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.27 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 6.4.1(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2) + vite: 6.4.1(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - supports-color - '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2))': + '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@babel/core': 7.28.5 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.5) @@ -11551,10 +12004,15 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.27 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2) + vite: 6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - supports-color + '@vitejs/plugin-react@6.0.1(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))': + dependencies: + '@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) @@ -13072,7 +13530,7 @@ snapshots: transitivePeerDependencies: - supports-color - fumadocs-mdx@14.1.1(fumadocs-core@16.2.5(@types/react@18.3.27)(lucide-react@0.562.0(react@19.2.1))(next@16.0.10(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(sass@1.97.0))(react-dom@19.2.1(react@19.2.1))(react-router@7.13.1(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1)(zod@4.3.6))(next@16.0.10(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(sass@1.97.0))(react@19.2.1)(vite@6.4.1(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2)): + fumadocs-mdx@14.1.1(fumadocs-core@16.2.5(@types/react@18.3.27)(lucide-react@0.562.0(react@19.2.1))(next@16.0.10(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(sass@1.97.0))(react-dom@19.2.1(react@19.2.1))(react-router@7.13.1(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1)(zod@4.3.6))(next@16.0.10(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(sass@1.97.0))(react@19.2.1)(vite@6.4.1(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2)): dependencies: '@mdx-js/mdx': 3.1.1 '@standard-schema/spec': 1.1.0 @@ -13095,7 +13553,7 @@ snapshots: optionalDependencies: next: 16.0.10(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(sass@1.97.0) react: 19.2.1 - vite: 6.4.1(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2) + vite: 6.4.1(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - supports-color @@ -13454,6 +13912,8 @@ snapshots: imurmurhash@0.1.4: {} + inherits@2.0.3: {} + inherits@2.0.4: {} inline-style-parser@0.2.7: {} @@ -13739,66 +14199,99 @@ snapshots: lightningcss-android-arm64@1.31.1: optional: true + lightningcss-android-arm64@1.32.0: + optional: true + lightningcss-darwin-arm64@1.30.2: optional: true lightningcss-darwin-arm64@1.31.1: optional: true + lightningcss-darwin-arm64@1.32.0: + optional: true + lightningcss-darwin-x64@1.30.2: optional: true lightningcss-darwin-x64@1.31.1: optional: true + lightningcss-darwin-x64@1.32.0: + optional: true + lightningcss-freebsd-x64@1.30.2: optional: true lightningcss-freebsd-x64@1.31.1: optional: true + lightningcss-freebsd-x64@1.32.0: + optional: true + lightningcss-linux-arm-gnueabihf@1.30.2: optional: true lightningcss-linux-arm-gnueabihf@1.31.1: optional: true + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + lightningcss-linux-arm64-gnu@1.30.2: optional: true lightningcss-linux-arm64-gnu@1.31.1: optional: true + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + lightningcss-linux-arm64-musl@1.30.2: optional: true lightningcss-linux-arm64-musl@1.31.1: optional: true + lightningcss-linux-arm64-musl@1.32.0: + optional: true + lightningcss-linux-x64-gnu@1.30.2: optional: true lightningcss-linux-x64-gnu@1.31.1: optional: true + lightningcss-linux-x64-gnu@1.32.0: + optional: true + lightningcss-linux-x64-musl@1.30.2: optional: true lightningcss-linux-x64-musl@1.31.1: optional: true + lightningcss-linux-x64-musl@1.32.0: + optional: true + lightningcss-win32-arm64-msvc@1.30.2: optional: true lightningcss-win32-arm64-msvc@1.31.1: optional: true + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + lightningcss-win32-x64-msvc@1.30.2: optional: true lightningcss-win32-x64-msvc@1.31.1: optional: true + lightningcss-win32-x64-msvc@1.32.0: + optional: true + lightningcss@1.30.2: dependencies: detect-libc: 2.1.2 @@ -13831,6 +14324,22 @@ snapshots: lightningcss-win32-arm64-msvc: 1.31.1 lightningcss-win32-x64-msvc: 1.31.1 + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + lilconfig@3.1.3: {} lines-and-columns@1.2.4: {} @@ -14810,6 +15319,11 @@ snapshots: path-type@4.0.0: {} + path@0.12.7: + dependencies: + process: 0.11.10 + util: 0.10.4 + pathe@2.0.3: {} picocolors@1.1.1: {} @@ -14818,6 +15332,8 @@ snapshots: picomatch@4.0.3: {} + picomatch@4.0.4: {} + pidtree@0.6.0: {} pify@4.0.1: {} @@ -14834,12 +15350,12 @@ snapshots: possible-typed-array-names@1.1.0: {} - postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(yaml@2.8.2): + postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(yaml@2.8.2): dependencies: lilconfig: 3.1.3 optionalDependencies: jiti: 2.6.1 - postcss: 8.5.6 + postcss: 8.5.8 tsx: 4.21.0 yaml: 2.8.2 @@ -14865,6 +15381,12 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postcss@8.5.8: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + powershell-utils@0.1.0: {} prelude-ls@1.2.1: {} @@ -14879,6 +15401,8 @@ snapshots: printable-characters@1.0.42: {} + process@0.11.10: {} + prompts@2.4.2: dependencies: kleur: 3.0.3 @@ -15324,6 +15848,30 @@ snapshots: rfdc@1.4.1: {} + rolldown@1.0.0-rc.12(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1): + dependencies: + '@oxc-project/types': 0.122.0 + '@rolldown/pluginutils': 1.0.0-rc.12 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-rc.12 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.12 + '@rolldown/binding-darwin-x64': 1.0.0-rc.12 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.12 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.12 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.12 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.12 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.12 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.12(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1) + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.12 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.12 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + rollup-plugin-inject@3.0.2: dependencies: estree-walker: 0.6.1 @@ -15987,7 +16535,7 @@ snapshots: tslib@2.8.1: {} - tsup@8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2): + tsup@8.5.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2): dependencies: bundle-require: 5.1.0(esbuild@0.27.1) cac: 6.7.14 @@ -15998,7 +16546,7 @@ snapshots: fix-dts-default-cjs-exports: 1.0.1 joycon: 3.1.1 picocolors: 1.1.1 - postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(yaml@2.8.2) + postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(yaml@2.8.2) resolve-from: 5.0.0 rollup: 4.53.3 source-map: 0.7.6 @@ -16007,7 +16555,7 @@ snapshots: tinyglobby: 0.2.15 tree-kill: 1.2.2 optionalDependencies: - postcss: 8.5.6 + postcss: 8.5.8 typescript: 5.9.3 transitivePeerDependencies: - jiti @@ -16275,6 +16823,10 @@ snapshots: util-deprecate@1.0.2: {} + util@0.10.4: + dependencies: + inherits: 2.0.3 + utils-merge@1.0.1: {} utrie@1.0.2: @@ -16317,7 +16869,7 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 - vite@6.4.1(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2): + vite@6.4.1(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.3) @@ -16329,12 +16881,12 @@ snapshots: '@types/node': 20.19.27 fsevents: 2.3.3 jiti: 2.6.1 - lightningcss: 1.31.1 + lightningcss: 1.32.0 sass: 1.97.0 tsx: 4.21.0 yaml: 2.8.2 - vite@6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2): + vite@6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.3) @@ -16346,11 +16898,30 @@ snapshots: '@types/node': 22.19.3 fsevents: 2.3.3 jiti: 2.6.1 - lightningcss: 1.31.1 + lightningcss: 1.32.0 sass: 1.97.0 tsx: 4.21.0 yaml: 2.8.2 + 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): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.8 + rolldown: 1.0.0-rc.12(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1) + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 20.19.27 + esbuild: 0.27.1 + fsevents: 2.3.3 + jiti: 2.6.1 + sass: 1.97.0 + tsx: 4.21.0 + yaml: 2.8.2 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + web-namespaces@2.0.1: {} web-streams-polyfill@3.3.3: {} From bae84197cbc95691aeab1c076e8f3a1a1eb4bdcb Mon Sep 17 00:00:00 2001 From: Sahil Date: Sun, 29 Mar 2026 12:27:36 +0530 Subject: [PATCH 07/39] fix(examples): add composite:true to tsconfig.node.json in skills-demo and generative-ui-demo Co-Authored-By: Claude Sonnet 4.6 --- examples/generative-ui-demo/tsconfig.node.json | 2 +- examples/skills-demo/tsconfig.node.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/generative-ui-demo/tsconfig.node.json b/examples/generative-ui-demo/tsconfig.node.json index 1f27c41..f8aea3b 100644 --- a/examples/generative-ui-demo/tsconfig.node.json +++ b/examples/generative-ui-demo/tsconfig.node.json @@ -9,7 +9,7 @@ "allowImportingTsExtensions": true, "isolatedModules": true, "moduleDetection": "force", - "noEmit": true + "composite": true }, "include": ["vite.config.ts", "server"] } diff --git a/examples/skills-demo/tsconfig.node.json b/examples/skills-demo/tsconfig.node.json index 1f27c41..f8aea3b 100644 --- a/examples/skills-demo/tsconfig.node.json +++ b/examples/skills-demo/tsconfig.node.json @@ -9,7 +9,7 @@ "allowImportingTsExtensions": true, "isolatedModules": true, "moduleDetection": "force", - "noEmit": true + "composite": true }, "include": ["vite.config.ts", "server"] } From 421ebf51444bff05d38f978bf01e1d80ecafcc39 Mon Sep 17 00:00:00 2001 From: Sahil Date: Sun, 29 Mar 2026 12:31:20 +0530 Subject: [PATCH 08/39] fix(examples): add emitDeclarationOnly to satisfy allowImportingTsExtensions with composite Co-Authored-By: Claude Sonnet 4.6 --- examples/generative-ui-demo/tsconfig.node.json | 3 ++- examples/skills-demo/tsconfig.node.json | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/generative-ui-demo/tsconfig.node.json b/examples/generative-ui-demo/tsconfig.node.json index f8aea3b..211e1d8 100644 --- a/examples/generative-ui-demo/tsconfig.node.json +++ b/examples/generative-ui-demo/tsconfig.node.json @@ -9,7 +9,8 @@ "allowImportingTsExtensions": true, "isolatedModules": true, "moduleDetection": "force", - "composite": true + "composite": true, + "emitDeclarationOnly": true }, "include": ["vite.config.ts", "server"] } diff --git a/examples/skills-demo/tsconfig.node.json b/examples/skills-demo/tsconfig.node.json index f8aea3b..211e1d8 100644 --- a/examples/skills-demo/tsconfig.node.json +++ b/examples/skills-demo/tsconfig.node.json @@ -9,7 +9,8 @@ "allowImportingTsExtensions": true, "isolatedModules": true, "moduleDetection": "force", - "composite": true + "composite": true, + "emitDeclarationOnly": true }, "include": ["vite.config.ts", "server"] } From 2874d1604798772f5db87c5f14954243d925d4a1 Mon Sep 17 00:00:00 2001 From: Sahil Date: Sun, 29 Mar 2026 19:19:18 +0530 Subject: [PATCH 09/39] feat(skills-demo): enhance skills system with dynamic skill registration and UI improvements - Added support for dynamic skills registration via API, allowing skills to be registered at runtime. - Updated server to handle dynamic skills, including new endpoints for skill management. - Introduced a new skill for frontend design with detailed guidelines. - Improved UI with new font styles (DM Sans and DM Mono) and animations for skill activation. - Enhanced the skills panel to display both static and dynamic skills, improving user experience. Co-Authored-By: Claude Sonnet 4.6 --- examples/skills-demo/index.html | 2 +- examples/skills-demo/package.json | 4 +- examples/skills-demo/server/index.ts | 147 +- .../skills-demo/skills/frontend-design.md | 57 + examples/skills-demo/src/App.tsx | 1237 ++++++++++------- examples/skills-demo/src/index.css | 752 ++++++++++ examples/skills-demo/vite.config.ts | 3 + pnpm-lock.yaml | 687 ++++++++- 8 files changed, 2316 insertions(+), 573 deletions(-) create mode 100644 examples/skills-demo/skills/frontend-design.md diff --git a/examples/skills-demo/index.html b/examples/skills-demo/index.html index e272c21..fbeeca4 100644 --- a/examples/skills-demo/index.html +++ b/examples/skills-demo/index.html @@ -7,7 +7,7 @@ Dash — AI Copilot - +
diff --git a/examples/skills-demo/package.json b/examples/skills-demo/package.json index da1ec08..42b15b6 100644 --- a/examples/skills-demo/package.json +++ b/examples/skills-demo/package.json @@ -18,7 +18,8 @@ "express": "^4.21.0", "lucide-react": "^0.563.0", "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "vaul": "^1.1.2" }, "devDependencies": { "@tailwindcss/postcss": "^4", @@ -32,6 +33,7 @@ "path": "^0.12.7", "tailwindcss": "^4", "tsx": "^4.19.0", + "tw-animate-css": "^1.4.0", "typescript": "^5.6.0", "vite": "8.0.3" } diff --git a/examples/skills-demo/server/index.ts b/examples/skills-demo/server/index.ts index c54db60..565c225 100644 --- a/examples/skills-demo/server/index.ts +++ b/examples/skills-demo/server/index.ts @@ -2,6 +2,7 @@ import "dotenv/config"; import express from "express"; import cors from "cors"; import path from "path"; +import { Readable } from "stream"; import { fileURLToPath } from "url"; import { createRuntime } from "@yourgpt/llm-sdk"; import { createAnthropic } from "@yourgpt/llm-sdk/anthropic"; @@ -30,6 +31,19 @@ for (const skill of skills) { ); } +// ============================================ +// DYNAMIC SKILLS (registered at runtime) +// ============================================ + +interface DynamicSkill { + name: string; + description: string; + content: string; + strategy: string; +} + +const dynamicSkills: DynamicSkill[] = []; + // ============================================ // CREATE PROVIDERS // ============================================ @@ -65,8 +79,7 @@ When a user asks about: - 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 -Always load the relevant skill before responding to ensure you follow the correct protocol. -Be concise, data-focused, and action-oriented.`, +Always load the relevant skill before responding.`, ); const runtime = createRuntime({ @@ -83,6 +96,19 @@ runtime.registerTool({ location: "server", 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)); + // Check dynamicSkills first before falling back to file-based skills + const dynamic = dynamicSkills.find((s) => s.name === params.name); + if (dynamic) { + return { + name: dynamic.name, + description: dynamic.description, + content: dynamic.content, + strategy: dynamic.strategy, + source: "dynamic", + }; + } return tools.load_skill.execute(params); }, }); @@ -93,32 +119,128 @@ runtime.registerTool({ /** * POST /api/chat — Main chat endpoint for CopilotProvider + * Rebuilds system prompt per-request to include any dynamically registered skills. */ app.post("/api/chat", async (req, res) => { const url = `http://localhost:${PORT}/api/chat`; + + // Inject dynamic skills into the system prompt so the AI knows they exist + const body = { ...req.body }; + if (dynamicSkills.length > 0) { + const dynamicSection = dynamicSkills + .map( + (s) => + `- "${s.name}" [${s.strategy}]: ${s.description} → load with load_skill("${s.name}")`, + ) + .join("\n"); + body.systemPrompt = + systemPrompt + + `\n\n## Additional Skills (dropped at runtime)\n${dynamicSection}`; + } + const webReq = new Request(url, { method: "POST", headers: { "content-type": "application/json" }, - body: JSON.stringify(req.body), + body: JSON.stringify(body), }); const response = await runtime.handleRequest(webReq); res.status(response.status); response.headers.forEach((val, key) => res.setHeader(key, val)); - const body = await response.text(); - res.send(body); + if (response.body) { + Readable.fromWeb( + response.body as Parameters[0], + ).pipe(res); + } else { + res.send(await response.text()); + } }); /** - * GET /api/skills — Returns skill metadata for the UI sidebar + * GET /api/skills — Returns skill metadata for the UI sidebar (static + dynamic) */ app.get("/api/skills", (_req, res) => { - const skillList = skills.map((skill) => ({ + const staticList = skills.map((skill) => ({ name: skill.name, description: skill.description, strategy: skill.strategy ?? "auto", version: skill.version, + source: "file", })); - res.json(skillList); + + const dynamicList = dynamicSkills.map((skill) => ({ + name: skill.name, + description: skill.description, + strategy: skill.strategy, + source: "dropped", + })); + + res.json([...staticList, ...dynamicList]); +}); + +/** + * GET /api/skills/:name — Returns full content of a skill (reuses load_skill handler) + */ +app.get("/api/skills/:name", async (req, res) => { + const { name } = req.params; + try { + // Reuse the same handler registered with the runtime — covers both file and dynamic skills + const dynamic = dynamicSkills.find((s) => s.name === name); + const result = dynamic + ? { + name: dynamic.name, + description: dynamic.description, + content: dynamic.content, + strategy: dynamic.strategy, + source: "dynamic", + } + : await tools.load_skill.execute({ name }); + res.json(result); + } catch { + res.status(404).json({ error: "Skill not found" }); + } +}); + +/** + * POST /api/skills/register — Register a new dynamic skill from a dropped file + */ +app.post("/api/skills/register", (req, res) => { + const { name, description, content, strategy } = req.body as { + name?: string; + description?: string; + content?: string; + strategy?: string; + }; + + if (!name || !content) { + res.status(400).json({ error: "name and content are required" }); + return; + } + + const skill: DynamicSkill = { + name, + description: description ?? "", + content, + strategy: strategy ?? "auto", + }; + + // Deduplicate by name — replace existing if present + const existingIdx = dynamicSkills.findIndex((s) => s.name === name); + if (existingIdx !== -1) { + dynamicSkills[existingIdx] = skill; + } else { + dynamicSkills.push(skill); + } + + console.log(` + Dynamic skill registered: ${name} [${skill.strategy}]`); + res.json({ ok: true, name, strategy: skill.strategy }); +}); + +/** + * DELETE /api/skills/dynamic — Clears all dropped skills (called on client load) + */ +app.delete("/api/skills/dynamic", (_req, res) => { + dynamicSkills.length = 0; + res.json({ ok: true }); }); /** @@ -129,7 +251,7 @@ app.get("/api/health", (_req, res) => { status: "ok", provider: process.env.ANTHROPIC_API_KEY ? "anthropic" : "openai", model, - skillCount: skills.length, + skillCount: skills.length + dynamicSkills.length, }); }); @@ -151,8 +273,9 @@ app.listen(PORT, () => { ╚══════════════════════════════════════════════════════════════╝ Endpoints: - POST /api/chat — CopilotProvider chat endpoint - GET /api/skills — Skill metadata for the UI - GET /api/health — Health check + POST /api/chat — CopilotProvider chat endpoint + GET /api/skills — Skill metadata for the UI + POST /api/skills/register — Register a dynamic skill from dropped file + GET /api/health — Health check `); }); diff --git a/examples/skills-demo/skills/frontend-design.md b/examples/skills-demo/skills/frontend-design.md new file mode 100644 index 0000000..ac479b5 --- /dev/null +++ b/examples/skills-demo/skills/frontend-design.md @@ -0,0 +1,57 @@ +--- +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/src/App.tsx b/examples/skills-demo/src/App.tsx index f8b2994..6282381 100644 --- a/examples/skills-demo/src/App.tsx +++ b/examples/skills-demo/src/App.tsx @@ -1,569 +1,788 @@ -import { useState, useCallback, useEffect, useRef, useMemo } from "react"; -import { CopilotProvider } from "@yourgpt/copilot-sdk/react"; -import { CopilotChat } from "@yourgpt/copilot-sdk/ui"; +import { + useState, + useEffect, + useContext, + createContext, + useCallback, +} from "react"; +import { Drawer } from "vaul"; +import { CopilotProvider, useCopilot } from "@yourgpt/copilot-sdk/react"; +import { + CopilotChat, + PromptInput, + PromptInputTextarea, + PromptInputActions, + PromptInputAction, + SendIcon, + StopIcon, +} from "@yourgpt/copilot-sdk/ui"; +import type { ToolRendererProps } from "@yourgpt/copilot-sdk/ui"; import "@yourgpt/copilot-sdk/ui/styles.css"; -// ─── Skill definitions (client-side metadata) ───────────────────────────────── +// ─── Skill Activity Context ─────────────────────────────────────────────────── -interface SkillMeta { - id: string; - name: string; - icon: string; - shortDesc: string; - strategy: "eager" | "auto" | "manual"; - capabilities: string[]; - color: string; +interface SkillActivity { + executingSkill: string | null; + loadedSkills: Set; + setExecutingSkill: (name: string | null) => void; + addLoadedSkill: (name: string) => void; } -type SkillState = "idle" | "scanning" | "loaded"; +const SkillActivityContext = createContext({ + executingSkill: null, + loadedSkills: new Set(), + setExecutingSkill: () => {}, + addLoadedSkill: () => {}, +}); -const SKILLS: SkillMeta[] = [ - { - id: "revenue-intelligence", - name: "Revenue Intelligence", - icon: "◈", - shortDesc: "MRR trends, churn analysis & expansion signals", - strategy: "auto", - capabilities: [ - "Monthly recurring revenue breakdown", - "Churn forecasting & root cause", - "Expansion revenue opportunity scoring", - ], - color: "#818cf8", +// ─── Skill Domain Icons ─────────────────────────────────────────────────────── + +function RevenueIcon() { + return ( + + + + + + ); +} + +function HealthIcon() { + return ( + + + + + ); +} + +function IncidentIcon() { + return ( + + + + + + ); +} + +function DefaultSkillIcon() { + return ( + + + + + ); +} + +// ─── Skill Config Map ───────────────────────────────────────────────────────── + +interface SkillConfig { + color: string; + bg: string; + Icon: () => JSX.Element; +} + +const SKILL_CONFIGS: Record = { + "revenue-intelligence": { + color: "#0d9488", + bg: "rgba(13, 148, 136, 0.08)", + Icon: RevenueIcon, }, - { - id: "customer-health", - name: "Customer Health", - icon: "◉", - shortDesc: "Account risk scoring & engagement signals", - strategy: "auto", - capabilities: [ - "Health score calculation (0–100)", - "At-risk account early warning", - "Engagement drop-off detection", - ], - color: "#34d399", + "customer-health": { + color: "#f59e0b", + bg: "rgba(245, 158, 11, 0.08)", + Icon: HealthIcon, }, - { - id: "incident-runbook", - name: "Incident Runbook", - icon: "◬", - shortDesc: "Production incident response protocol", - strategy: "manual", - capabilities: [ - "Severity classification P0–P3", - "Step-by-step response checklist", - "Stakeholder communication templates", - ], - color: "#fb923c", + "incident-runbook": { + color: "#ef4444", + bg: "rgba(239, 68, 68, 0.08)", + Icon: IncidentIcon, }, -]; +}; -const METRICS = [ - { label: "MRR", value: "$124.8k", change: "+12%", up: true }, - { label: "Churn", value: "2.3%", change: "−0.4%", up: true }, - { label: "DAU", value: "8,429", change: "+5%", up: true }, - { label: "Open P1s", value: "2", change: "+2", up: false }, -]; +// ─── General Icons ──────────────────────────────────────────────────────────── -const DEMO_PROMPTS = [ - "Analyze our MRR growth and top churn risks this month", - "Which enterprise accounts are most at risk right now?", - "We have a P1 — payment API is returning 503 errors", -]; +function AiIdeaIcon({ className = "" }: { className?: string }) { + return ( + + + + ); +} + +function CheckCircleIcon({ className = "" }: { className?: string }) { + return ( + + + + ); +} -// ─── Skill load notifier (invisible, watches for load_skill tool calls) ─────── +// ─── TextShimmer ────────────────────────────────────────────────────────────── -function SkillLoadNotifier({ - args, - status, - onLoaded, +function TextShimmer({ + children, + duration = 1.8, }: { - args: Record; - status: string; - result?: unknown; - toolCallId: string; - onLoaded: (name: string) => void; + children: string; + duration?: number; }) { - const firedRef = useRef(false); - useEffect(() => { - if (status === "success" && args?.name && !firedRef.current) { - firedRef.current = true; - onLoaded(args.name as string); - } - }, [status, args?.name, onLoaded]); - return null; + return ( + + {children} + + ); } -// ─── Individual skill card ──────────────────────────────────────────────────── +// ─── Tool Renderers ─────────────────────────────────────────────────────────── -function SkillCard({ skill, state }: { skill: SkillMeta; state: SkillState }) { - const isLoaded = state === "loaded"; - const isScanning = state === "scanning"; - const strategyLabel = { - eager: "ALWAYS ON", - auto: "AUTO", - manual: "ON DEMAND", - }; +function SkillLoadedCard({ execution }: ToolRendererProps) { + const { setExecutingSkill, addLoadedSkill } = + useContext(SkillActivityContext); - return ( -
- {isScanning &&
} + const skillName = (execution.args?.name ?? + execution.args?.skill_name ?? + execution.args?.skill ?? + "skill") as string; -
- - {skill.icon} - -
- {skill.name} - - {strategyLabel[skill.strategy]} - -
- + // Sync state to skills panel — must be before any early return (Rules of Hooks) + useEffect(() => { + if (execution.status === "pending" || execution.status === "executing") { + setExecutingSkill(skillName); + } else if (execution.status === "completed" && execution.result) { + addLoadedSkill(skillName); + const t = setTimeout(() => setExecutingSkill(null), 600); + return () => clearTimeout(t); + } + }, [ + execution.status, + execution.result, + skillName, + setExecutingSkill, + addLoadedSkill, + ]); + + // Guard phantom completed-without-result double-fire from SDK + if (execution.status === "completed" && !execution.result) return null; + + if (execution.status === "pending" || execution.status === "executing") { + return ( +
+ + Reading from: + {skillName}
+ ); + } -

{skill.shortDesc}

- -
-
-

✦ Skill active

-
    - {skill.capabilities.map((cap, i) => ( -
  • - - {cap} -
  • - ))} -
+ if (execution.status === "error" || execution.status === "failed") { + return ( +
+ + Failed to load skill: {skillName} +
+ ); + } + + // Completed with result — show a distinct "read" confirmation + return ( +
+ +

+ Reading from:{" "} + {skillName} +

); } -// ─── Main app ───────────────────────────────────────────────────────────────── +function FallbackToolCard({ execution }: ToolRendererProps) { + const label = execution.name + .replace(/_/g, " ") + .replace(/\b\w/g, (c) => c.toUpperCase()); -export default function App() { - const [skillStates, setSkillStates] = useState>( - () => Object.fromEntries(SKILLS.map((s) => [s.id, "idle"])), - ); - const [branchingEnabled, setBranchingEnabled] = useState(false); - - const handleSkillLoaded = useCallback((skillName: string) => { - if (!SKILLS.find((s) => s.id === skillName)) return; - setSkillStates((prev) => - prev[skillName] === "loaded" - ? prev - : { ...prev, [skillName]: "scanning" }, + if (execution.status === "pending" || execution.status === "executing") { + return ( +
+ {`Running ${label}…`} +
); - setTimeout(() => { - setSkillStates((prev) => ({ ...prev, [skillName]: "loaded" })); - }, 1500); - }, []); + } - const toolRenderers = useMemo( - () => ({ - load_skill: (props: { - args: Record; - status: string; - result?: unknown; - toolCallId: string; - }) => , - }), - [handleSkillLoaded], + const isError = execution.status === "error" || execution.status === "failed"; + return ( +
+ {isError ? ( + + ) : ( + + )} + {label} +
); +} - const injectPrompt = (text: string) => { - const ta = document.querySelector( - "textarea[placeholder]", - ); - if (!ta) return; - const setter = Object.getOwnPropertyDescriptor( - window.HTMLTextAreaElement.prototype, - "value", - )?.set; - setter?.call(ta, text); - ta.dispatchEvent(new Event("input", { bubbles: true })); - ta.focus(); +const toolRenderers = { load_skill: SkillLoadedCard }; + +// ─── Custom Fixed Input ─────────────────────────────────────────────────────── +// Uses useCopilot() (CopilotProvider-level) instead of useCopilotChatContext() +// so it can live as a sibling to CopilotChat without being inside renderInput. +// This lets the SDK's use-stick-to-bottom auto-scroll work correctly. + +function CustomInput() { + const { sendMessage, stop, isLoading } = useCopilot(); + const [value, setValue] = useState(""); + + const submit = () => { + if (!value.trim() || isLoading) return; + sendMessage(value); + setValue(""); }; return ( - <> - -
- {/* Top nav */} -
-
- - Dash - Operations Platform -
- -
- - AI Copilot -
-
- -
- {/* Sidebar */} - - - {/* Chat */} -
- - - -
-
-
- +
+ + + + + + + + +
); } -// ─── All styles ─────────────────────────────────────────────────────────────── - -const CSS = ` -*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } - -:root { - --bg: #07090f; - --s1: #0b0e1a; - --s2: #0f1320; - --s3: #141929; - --bd: rgba(255,255,255,0.055); - --bd2: rgba(255,255,255,0.10); - --t1: #e8eaf6; - --t2: #7b82a8; - --t3: #3d4468; - --ok: #34d399; - --err: #f87171; - --acc: #818cf8; - --font: 'Bricolage Grotesque', system-ui, sans-serif; - --mono: 'JetBrains Mono', monospace; -} +// ─── Welcome message (bypasses home screen; shown on load) ─────────────────── -body { - background: var(--bg); - font-family: var(--font); - color: var(--t1); - -webkit-font-smoothing: antialiased; -} +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.", + createdAt: new Date(), + }, +]; -/* ── Root ── */ -.d-root { display: flex; flex-direction: column; height: 100vh; overflow: hidden; } +// ─── Logo avatar ───────────────────────────────────────────────────────────── -/* ── Nav ── */ -.d-nav { - display: flex; align-items: center; gap: 20px; - padding: 0 20px; height: 50px; flex-shrink: 0; - background: var(--s1); - border-bottom: 1px solid var(--bd); -} -.d-nav__brand { display: flex; align-items: center; gap: 8px; } -.d-nav__logo { font-size: 19px; color: var(--acc); } -.d-nav__name { font-weight: 700; font-size: 15px; letter-spacing: -0.04em; } -.d-nav__platform { font-size: 11px; color: var(--t3); margin-left: 1px; } -.d-nav__links { display: flex; gap: 1px; margin-left: auto; } -.d-nav__link { - padding: 4px 11px; border-radius: 6px; - font-size: 12px; color: var(--t2); cursor: pointer; - transition: all 0.12s; -} -.d-nav__link:hover { background: var(--s2); color: var(--t1); } -.d-nav__copilot { - display: flex; align-items: center; gap: 7px; - padding: 4px 12px; border-radius: 20px; - background: rgba(129,140,248,0.08); - border: 1px solid rgba(129,140,248,0.2); - font-size: 11px; font-weight: 600; color: var(--acc); - letter-spacing: 0.05em; text-transform: uppercase; -} -.d-nav__pulse { - width: 6px; height: 6px; border-radius: 50%; background: var(--acc); - animation: nav-pulse 2.2s ease-in-out infinite; -} -@keyframes nav-pulse { - 0%,100% { opacity:.5; transform:scale(1); } - 50% { opacity:1; transform:scale(1.35); } +function LogoAvatar() { + return ( + + + + + ); } -/* ── Body ── */ -.d-body { display: flex; flex: 1; min-height: 0; } +// ─── Chat Inner ─────────────────────────────────────────────────────────────── -/* ── Sidebar ── */ -.d-sidebar { - width: 296px; flex-shrink: 0; - background: var(--s1); border-right: 1px solid var(--bd); - overflow-y: auto; display: flex; flex-direction: column; - scrollbar-width: thin; scrollbar-color: var(--bd2) transparent; +function ChatInner() { + return ( +
+ , + className: + "!bg-white !rounded-full shadow-sm flex items-center justify-center", + }} + /> +
+ ); } -.d-section { - padding: 14px 14px; - border-bottom: 1px solid var(--bd); -} -.d-section__label { - font-size: 10px; font-weight: 600; letter-spacing: 0.1em; - text-transform: uppercase; color: var(--t3); margin-bottom: 10px; -} -.d-section__header-row { - display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; -} -.d-section__count { - font-family: var(--mono); font-size: 10px; color: var(--t3); - background: var(--s2); padding: 1px 6px; border-radius: 8px; - border: 1px solid var(--bd); -} +// ─── Skills Panel ───────────────────────────────────────────────────────────── -/* ── Metrics ── */ -.d-metrics { display: grid; grid-template-columns: 1fr 1fr; gap: 7px; } -.d-metric { - background: var(--s2); border: 1px solid var(--bd); - border-radius: 8px; padding: 9px 10px 7px; - display: flex; flex-direction: column; gap: 1px; -} -.d-metric__label { font-size: 9.5px; color: var(--t3); font-weight: 500; text-transform: uppercase; letter-spacing: 0.06em; } -.d-metric__val { font-family: var(--mono); font-size: 16px; font-weight: 500; color: var(--t1); line-height: 1.2; } -.d-metric__chg { font-family: var(--mono); font-size: 10px; font-weight: 500; } -.d-metric__chg.up { color: var(--ok); } -.d-metric__chg.dn { color: var(--err); } - -/* ── Skills ── */ -.d-skills { display: flex; flex-direction: column; gap: 7px; } - -.skill-card { - position: relative; overflow: hidden; - border-radius: 10px; padding: 11px; - background: var(--s2); border: 1px solid var(--bd); - transition: border-color 0.45s ease, background 0.45s ease, box-shadow 0.45s ease; -} -.skill-card[data-state="scanning"] { - border-color: rgba(255,255,255,0.13); - background: var(--s3); -} -.skill-card[data-state="loaded"] { - border-color: color-mix(in srgb, var(--sc) 38%, transparent); - background: color-mix(in srgb, var(--sc) 6%, var(--s2)); - box-shadow: 0 0 22px -4px color-mix(in srgb, var(--sc) 22%, transparent); - animation: card-pop 0.55s cubic-bezier(0.22,1,0.36,1); -} -@keyframes card-pop { - 0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--sc) 55%, transparent); } - 45% { box-shadow: 0 0 28px 5px color-mix(in srgb, var(--sc) 32%, transparent); } - 100% { box-shadow: 0 0 22px -4px color-mix(in srgb, var(--sc) 22%, transparent); } -} +interface Skill { + name: string; + description: string; + strategy: string; + version?: string; + source?: string; +} + +function slugToTitle(slug: string): string { + return slug.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()); +} + +function parseFrontmatter(content: string): Record { + const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/); + if (!match) return {}; + const result: Record = {}; + for (const line of match[1].split(/\r?\n/)) { + const colonIdx = line.indexOf(":"); + if (colonIdx === -1) continue; + const key = line.slice(0, colonIdx).trim(); + const val = line + .slice(colonIdx + 1) + .trim() + .replace(/^["']|["']$/g, ""); + if (key) result[key] = val; + } + return result; +} + +function SkillCard({ skill }: { skill: Skill }) { + const { executingSkill, loadedSkills } = useContext(SkillActivityContext); + const isExecuting = executingSkill === skill.name; + const isLoaded = loadedSkills.has(skill.name); + const isActive = isExecuting || isLoaded; + + const config = SKILL_CONFIGS[skill.name]; + const Icon = config?.Icon ?? DefaultSkillIcon; + const color = config?.color ?? "#6b7280"; + const bg = config?.bg ?? "rgba(107, 114, 128, 0.08)"; + const displayName = slugToTitle(skill.name); + + const [open, setOpen] = useState(false); + const [content, setContent] = useState(null); + + const handleOpen = (val: boolean) => { + setOpen(val); + if (val && content === null) { + fetch(`/api/skills/${skill.name}`) + .then((r) => r.json()) + .then((d) => { + // Strip frontmatter block + const raw: string = d.content ?? ""; + const stripped = raw.replace(/^---[\s\S]*?---\r?\n/, "").trim(); + setContent(stripped); + }) + .catch(() => setContent("Could not load skill content.")); + } + }; -/* Scan line */ -.scan-line { - position: absolute; inset: 0; pointer-events: none; z-index: 10; - background: linear-gradient( - to bottom, - transparent 0%, - rgba(255,255,255,0.03) 40%, - rgba(255,255,255,0.11) 50%, - rgba(255,255,255,0.03) 60%, - transparent 100% + const cardEl = ( +
+
+
+ +
+
+
+ + {displayName} + +
+ {isExecuting && !isLoaded && ( + + Loading… + + )} + {isLoaded && !isExecuting && ( + + ✓ Loaded + + )} + {!isActive && ( + + {skill.source === "dropped" ? "Dropped" : "Skill"} + + )} +
+
+

{skill.description}

+
+
+ {isLoaded && ( +
+ + + + + Loaded by Copilot +
+ )} +
); - animation: scan 1.5s cubic-bezier(0.4,0,0.2,1) forwards; -} -@keyframes scan { - 0% { transform: translateY(-110%); opacity: 1; } - 80% { opacity: 1; } - 100% { transform: translateY(210%); opacity: 0; } -} -.sc-header { display: flex; align-items: center; gap: 8px; margin-bottom: 5px; } -.sc-icon { - font-size: 17px; line-height: 1; flex-shrink: 0; - color: var(--sc); - transition: transform 0.3s ease; -} -.skill-card[data-state="loaded"] .sc-icon { transform: scale(1.12); } -.sc-icon[data-scanning="true"] { animation: icon-spin 1.5s ease-in-out; } -@keyframes icon-spin { - 0% { transform: rotate(0deg) scale(1); } - 50% { transform: rotate(180deg) scale(1.2); } - 100% { transform: rotate(360deg) scale(1); } + return ( + + {cardEl} + + + + {/* Drag handle */} +
+ + {/* Header */} +
+
+ +
+
+ + {displayName} + +

{skill.description}

+
+
+ + {skill.strategy} + + {skill.version && ( + v{skill.version} + )} +
+
+ + {/* Content */} +
+ {content === null ? ( +

Loading…

+ ) : ( +
{content}
+ )} +
+ + + + ); } -.sc-title-group { display: flex; flex-direction: column; gap: 2px; flex: 1; min-width: 0; } -.sc-name { font-size: 12.5px; font-weight: 600; color: var(--t1); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +function SkillsPanel() { + const [skills, setSkills] = useState([]); + const [isDragging, setIsDragging] = useState(false); -.sc-badge { - font-size: 9px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; - padding: 1px 5px; border-radius: 4px; width: fit-content; -} -.sc-badge--eager { background: rgba(52,211,153,.12); color: #34d399; border: 1px solid rgba(52,211,153,.2); } -.sc-badge--auto { background: rgba(129,140,248,.12); color: #818cf8; border: 1px solid rgba(129,140,248,.2); } -.sc-badge--manual { background: rgba(251,146,60,.12); color: #fb923c; border: 1px solid rgba(251,146,60,.2); } - -.sc-dot { - width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; - background: var(--t3); - transition: background 0.35s, box-shadow 0.35s; -} -.sc-dot[data-active="true"] { - background: var(--sc); - box-shadow: 0 0 7px 1px color-mix(in srgb, var(--sc) 55%, transparent); - animation: dot-breathe 2s ease-in-out infinite; -} -@keyframes dot-breathe { - 0%,100% { box-shadow: 0 0 7px 1px color-mix(in srgb, var(--sc) 45%, transparent); } - 50% { box-shadow: 0 0 11px 3px color-mix(in srgb, var(--sc) 65%, transparent); } -} + useEffect(() => { + // Clear any dropped skills from previous session on page load + fetch("/api/skills/dynamic", { method: "DELETE" }).catch(() => {}); + fetch("/api/skills") + .then((r) => r.json()) + .then((data: Skill[]) => setSkills(data)) + .catch(() => {}); + }, []); -.sc-desc { font-size: 11px; color: var(--t2); line-height: 1.4; padding-left: 25px; } + // Window-level drag — whole page is the drop target + useEffect(() => { + let depth = 0; + + const onDragEnter = (e: DragEvent) => { + if (!e.dataTransfer?.types.includes("Files")) return; + depth++; + if (depth === 1) setIsDragging(true); + }; + + const onDragLeave = () => { + depth = Math.max(0, depth - 1); + if (depth === 0) setIsDragging(false); + }; + + const onDragOver = (e: DragEvent) => { + e.preventDefault(); + }; + + const onDrop = async (e: DragEvent) => { + e.preventDefault(); + depth = 0; + setIsDragging(false); + + const files = Array.from(e.dataTransfer?.files ?? []).filter( + (f) => f.name.endsWith(".md") || f.name.endsWith(".txt"), + ); + if (!files.length) return; + + for (const file of files) { + const content = await file.text(); + const frontmatter = parseFrontmatter(content); + const baseName = file.name.replace(/\.(md|txt)$/, ""); + const name = frontmatter.name || baseName; + const description = + frontmatter.description || `Skill loaded from ${file.name}`; + const strategy = frontmatter.strategy || "auto"; + + try { + await fetch("/api/skills/register", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ name, description, content, strategy }), + }); + setSkills((prev) => { + const filtered = prev.filter((s) => s.name !== name); + return [ + ...filtered, + { name, description, strategy, source: "dropped" }, + ]; + }); + } catch { + // silently ignore + } + } + }; + + window.addEventListener("dragenter", onDragEnter); + window.addEventListener("dragleave", onDragLeave); + window.addEventListener("dragover", onDragOver); + window.addEventListener("drop", onDrop); + + return () => { + window.removeEventListener("dragenter", onDragEnter); + window.removeEventListener("dragleave", onDragLeave); + window.removeEventListener("dragover", onDragOver); + window.removeEventListener("drop", onDrop); + }; + }, []); -.sc-expanded { - max-height: 0; overflow: hidden; - transition: max-height 0.55s cubic-bezier(0.16,1,0.3,1); -} -.sc-expanded[data-open="true"] { max-height: 200px; } + return ( + + ); } -.d-prompt:hover { background: var(--s3); border-color: var(--bd2); color: var(--t1); } -/* ── Chat ── */ -.d-chat { flex: 1; min-width: 0; display: flex; flex-direction: column; background: var(--bg); } -.d-copilot { height: 100% !important; } -`; +// ─── App ────────────────────────────────────────────────────────────────────── + +export default function App() { + const [executingSkill, setExecutingSkill] = useState(null); + const [loadedSkills, setLoadedSkills] = useState>(new Set()); + + const addLoadedSkill = useCallback((name: string) => { + setLoadedSkills((prev) => { + const next = new Set(prev); + next.add(name); + return next; + }); + }, []); + + return ( + +
+
+ +
+ + + +
+
+
+
+ ); +} diff --git a/examples/skills-demo/src/index.css b/examples/skills-demo/src/index.css index f1d8c73..0835172 100644 --- a/examples/skills-demo/src/index.css +++ b/examples/skills-demo/src/index.css @@ -1 +1,753 @@ @import "tailwindcss"; +@import "tw-animate-css"; + +/* Include SDK package for Tailwind class detection */ +@source "../node_modules/@yourgpt/copilot-sdk/dist/**/*.{js,ts,jsx,tsx}"; + +@custom-variant dark (&:is(.dark *)); + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --radius-2xl: calc(var(--radius) + 8px); +} + +/* ── Light theme — shadcn CSS variables ── */ +:root { + --radius: 0.75rem; + --background: #ffffff; + --foreground: #0d1117; + --card: #ffffff; + --card-foreground: #0d1117; + --popover: #ffffff; + --popover-foreground: #0d1117; + --primary: #0d9488; + --primary-foreground: #ffffff; + --secondary: #f0faf9; + --secondary-foreground: #0d1117; + --muted: #f4faf9; + --muted-foreground: #6b7280; + --accent: #e6faf7; + --accent-foreground: #0d1117; + --destructive: oklch(0.577 0.245 27.325); + --border: rgba(0, 0, 0, 0.08); + --input: rgba(0, 0, 0, 0.06); + --ring: #0d9488; + + /* ── App design tokens ── */ + --app-font: "DM Sans", system-ui, sans-serif; + --app-mono: "DM Mono", monospace; + --app-teal: #0d9488; + --app-teal-light: #99f6e4; + --app-bg-top: #ffffff; + --app-bg-bottom: #dcfffe; +} + +@layer base { + *, + *::before, + *::after { + box-sizing: border-box; + } + * { + @apply border-border outline-ring/50; + } + html, + body, + #root { + height: 100%; + } + body { + font-family: var(--app-font); + color: var(--foreground); + background: linear-gradient(160deg, var(--app-bg-top) 0%, var(--app-bg-bottom) 100%); + background-attachment: fixed; + min-height: 100vh; + -webkit-font-smoothing: antialiased; + } +} + +/* ── TextShimmer ── */ +@keyframes shimmer-text { + from { + background-position: 200% center; + } + to { + background-position: -200% center; + } +} +.text-shimmer { + background: linear-gradient(to right, rgba(13, 148, 136, 0.5) 0%, rgba(13, 148, 136, 1) 30%, rgba(5, 200, 180, 1) 50%, rgba(13, 148, 136, 1) 70%, rgba(13, 148, 136, 0.5) 100%); + background-size: 200% auto; + background-clip: text; + -webkit-background-clip: text; + color: transparent; + animation: shimmer-text linear infinite; + font-weight: 500; + font-size: 0.75rem; +} + +/* ── Layout — 50/50 split ── */ + +.root { + background: linear-gradient(160deg, var(--app-bg-top) 0%, var(--app-bg-bottom) 100%); +} + +.app { + display: grid; + grid-template-columns: 1fr 1fr; + height: 100vh; + overflow: hidden; + /* Required for Vaul shouldScaleBackground — needs a background to show the scale transform */ +} + +.chat-panel { + height: 100%; + overflow: hidden; + display: flex; + flex-direction: column; + position: relative; +} + +/* ── SDK overrides: transparent backgrounds ── */ +.chat-panel { + --background: transparent; + --card: transparent; + --muted: rgba(255, 255, 255, 0.72); + --border: rgba(0, 0, 0, 0.07); +} + +/* CopilotChat must fill the chat panel so its internal scroll container is bounded */ +.chat-panel .copilot-fill { + flex: 1; + min-height: 0; + overflow: hidden; +} + +/* Header: transparent, no bottom border */ +.copilot-fill > div:first-child { + background: transparent !important; + border-bottom: none !important; +} + +/* Messages area: pad bottom so content clears the fixed input */ +.copilot-fill > div:nth-child(2) { + padding-bottom: 110px !important; +} + +/* Fade-out gradient above fixed input — only covers the right chat column */ + +/* ── AI message bubble: subtle card style ── */ +.csdk-assistant-message .csdk-message-content { + background: rgba(255, 255, 255, 0.65); + border-radius: 0.875rem; + box-shadow: + 0 1px 6px rgba(0, 0, 0, 0.06), + 0 0 0 1px rgba(0, 0, 0, 0.04); + padding: 0.625rem 0.875rem !important; +} + +/* ── Custom input (via renderInput prop) ── */ +.custom-input-fixed { + position: fixed; + bottom: 20px; + left: 75vw; + transform: translateX(-50%); + width: min(600px, calc(50vw - 48px)); + z-index: 50; +} + +.custom-prompt-input { + background: #ffffff; + border-radius: 1.25rem; + border: 1px solid rgba(0, 0, 0, 0.08); + box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08); + padding: 0.5rem 0.5rem 0.5rem 1rem; + display: flex; + align-items: flex-end; + gap: 0.5rem; + cursor: text; +} + +.custom-prompt-textarea { + flex: 1; + border: none !important; + outline: none !important; + box-shadow: none !important; + background: transparent !important; + resize: none; + font-family: var(--app-font); + font-size: 0.9rem; + color: var(--foreground); + line-height: 1.5; + padding: 0.35rem 0 !important; + max-height: 160px; +} + +.custom-prompt-textarea::placeholder { + color: #9ca3af; +} + +.custom-prompt-actions { + display: flex; + align-items: center; + gap: 0.25rem; + flex-shrink: 0; + padding-bottom: 0.1rem; +} + +.custom-send-btn { + display: flex; + align-items: center; + justify-content: center; + width: 34px; + height: 34px; + border-radius: 50%; + background: var(--app-teal); + color: #ffffff; + border: none; + cursor: pointer; + transition: background 0.15s ease; +} + +.custom-send-btn:hover { + background: color-mix(in srgb, var(--app-teal) 80%, #000); +} + +/* ── Skills Panel — transparent, gradient shows through ── */ +.skills-panel { + position: sticky; + top: 0; + height: 100vh; + overflow-y: auto; + padding: 56px 52px; + scrollbar-width: thin; + scrollbar-color: rgba(0, 0, 0, 0.08) transparent; + /* No background — gradient bleeds through */ +} + +/* Webkit scrollbar for skills panel */ +.skills-panel::-webkit-scrollbar { + width: 4px; +} +.skills-panel::-webkit-scrollbar-track { + background: transparent; +} +.skills-panel::-webkit-scrollbar-thumb { + background: rgba(0, 0, 0, 0.1); + border-radius: 4px; +} + +.skills-panel-heading { + margin-bottom: 28px; +} + +.skills-panel-eyebrow { + display: flex; + align-items: center; + gap: 6px; + font-size: 11px; + font-weight: 600; + letter-spacing: 0.06em; + color: var(--app-teal); + text-transform: uppercase; + margin-bottom: 20px; +} + +@keyframes livePulse { + 0%, + 100% { + opacity: 1; + box-shadow: 0 0 0 0 rgba(13, 148, 136, 0.4); + } + 50% { + opacity: 0.7; + box-shadow: 0 0 0 4px rgba(13, 148, 136, 0); + } +} +.skills-panel-live-dot { + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--app-teal); + animation: livePulse 2s ease-in-out infinite; + flex-shrink: 0; +} + +.skills-panel-title { + font-size: clamp(1.75rem, 2.8vw, 2.6rem); + font-weight: 700; + color: var(--foreground); + line-height: 1.15; + letter-spacing: -0.03em; + margin: 0 0 12px; + text-wrap: balance; +} + +.skills-panel-subtitle { + font-size: 0.875rem; + color: #9ca3af; + margin: 0 0 0; + font-weight: 400; + line-height: 1.5; +} + +.skills-panel-hint { + font-size: 0.72rem; + color: #aab5c0; + margin-top: 14px; + margin-bottom: 20px; + line-height: 1.5; + display: flex; + align-items: center; + gap: 5px; + flex-wrap: wrap; + max-width: 380px; +} + +.skills-panel-hint::before { + content: ""; + display: inline-block; + width: 14px; + height: 14px; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none'%3E%3Cpath d='M8 2v7M5.5 6 8 3l2.5 3M3 12.5h10' stroke='%2300b5a8' stroke-width='1.4' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-size: contain; + flex-shrink: 0; + opacity: 0.7; +} + +.skills-panel-hint code { + font-family: var(--app-mono); + font-size: 0.7rem; + background: rgba(13, 148, 136, 0.07); + border-radius: 3px; + padding: 1px 4px; + color: var(--app-teal); +} + +.skills-list { + display: flex; + flex-direction: column; + gap: 10px; + max-width: 380px; +} + +.skills-empty { + font-size: 0.8125rem; + color: #9ca3af; + text-align: center; + padding: 24px 0; +} + +/* ── Skill card ── */ +.skill-card { + background: rgba(255, 255, 255, 0.45); + border-radius: 12px; + border: 1px solid rgba(0, 0, 0, 0.05); + box-shadow: none; + padding: 13px 14px; + overflow: hidden; + transition: + box-shadow 0.18s ease, + border-color 0.2s ease, + background 0.2s ease, + transform 0.18s cubic-bezier(0.34, 1.56, 0.64, 1); + cursor: pointer; + will-change: transform; +} + +/* Hover: same feel as active — opaque bg + lift */ +.skill-card:hover { + background: rgba(255, 255, 255, 0.88); + border-color: rgba(0, 0, 0, 0.07); + box-shadow: + 0 2px 8px rgba(0, 0, 0, 0.05), + 0 4px 16px rgba(0, 0, 0, 0.04); + transform: translateY(-1px); +} + +/* Press: scale down — physics-active-state */ +.skill-card:active { + transform: scale(0.97); + box-shadow: none; + transition: transform 0.1s ease, box-shadow 0.1s ease; +} + +/* Active/loaded state — white bg + same shadow as hover, colored border accent */ +.skill-card--active { + background: rgba(255, 255, 255, 0.88); + border-color: color-mix(in srgb, var(--skill-color) 22%, transparent); + box-shadow: + 0 2px 8px rgba(0, 0, 0, 0.05), + 0 4px 16px rgba(0, 0, 0, 0.04); +} + +/* Executing: colored glow pulse */ +@keyframes skillPulse { + 0%, 100% { box-shadow: 0 0 0 1px color-mix(in srgb, var(--skill-color) 14%, transparent), 0 2px 8px rgba(0,0,0,0.05); } + 50% { box-shadow: 0 0 0 1px color-mix(in srgb, var(--skill-color) 28%, transparent), 0 4px 20px color-mix(in srgb, var(--skill-color) 16%, transparent); } +} +.skill-card--executing { + animation: skillPulse 1.6s ease-in-out infinite; +} + +/* Card layout: icon on left, content on right */ +.skill-card-body { + display: flex; + align-items: flex-start; + gap: 11px; +} + +.skill-icon-wrap { + display: flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + border-radius: 9px; + flex-shrink: 0; + transition: + background 0.22s ease, + color 0.22s ease; +} + +.skill-card-content { + flex: 1; + min-width: 0; +} + +.skill-card-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 6px; + margin-bottom: 5px; +} + +.skill-card-title { + font-size: 13.5px; + font-weight: 600; + color: var(--foreground); + line-height: 1.3; + min-width: 0; + flex: 1; + transition: color 0.22s ease; +} + +.skill-card-right { + display: flex; + align-items: center; + gap: 5px; + flex-shrink: 0; +} + +/* Pulsing dot while executing */ +@keyframes dotPulse { + 0%, + 100% { + opacity: 1; + transform: scale(1); + } + 50% { + opacity: 0.45; + transform: scale(0.75); + } +} +.skill-executing-dot { + width: 7px; + height: 7px; + border-radius: 50%; + animation: dotPulse 1s ease-in-out infinite; + flex-shrink: 0; +} + +/* "✓ Active" badge shown when loaded */ +.skill-loaded-badge { + font-size: 10px; + font-weight: 600; + letter-spacing: 0.03em; + border-radius: 5px; + padding: 2px 6px; + border: 1px solid transparent; + white-space: nowrap; +} + +.skill-badge { + font-size: 10px; + font-weight: 500; + letter-spacing: 0.02em; + border-radius: 5px; + padding: 2px 6px; + white-space: nowrap; +} + +.skill-badge--file { + background: rgba(0, 0, 0, 0.05); + border: 1px solid rgba(0, 0, 0, 0.07); + color: #9ca3af; +} + +.skill-badge--dropped { + background: rgba(13, 148, 136, 0.08); + border: 1px solid rgba(13, 148, 136, 0.2); + color: var(--app-teal); +} + +.skill-card-desc { + font-size: 11.5px; + color: #6b7280; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + line-height: 1.45; +} + +/* "Loaded by Copilot" strip — slides in */ +@keyframes slideIn { + from { + opacity: 0; + transform: translateY(-4px); + max-height: 0; + } + to { + opacity: 1; + transform: translateY(0); + max-height: 40px; + } +} +.skill-loaded-section { + display: flex; + align-items: center; + gap: 5px; + margin-top: 10px; + padding-top: 9px; + border-top: 1px solid rgba(0, 0, 0, 0.06); + font-size: 10.5px; + font-weight: 500; + animation: slideIn 0.3s ease forwards; + overflow: hidden; +} + +/* ── Drop overlay — frosted glass, no dashed border ── */ +@keyframes overlayFadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +/* Spring-like entrance: cubic-bezier overshoot — physics-spring-for-overshoot */ +@keyframes dropPillEnter { + from { + opacity: 0; + transform: translateY(14px) scale(0.93); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +.skills-drop-overlay { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: rgba(220, 253, 252, 0.28); + backdrop-filter: blur(5px) saturate(1.1); + -webkit-backdrop-filter: blur(5px) saturate(1.1); + display: flex; + align-items: center; + justify-content: center; + z-index: 100; + pointer-events: none; + /* easing-entrance-ease-out */ + animation: overlayFadeIn 0.18s ease-out forwards; +} + +.skills-drop-overlay-inner { + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; + color: var(--app-teal); + font-size: 13.5px; + font-weight: 600; + /* White pill with layered shadow — visual-layered-shadows */ + background: rgba(255, 255, 255, 0.9); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + border-radius: 18px; + border: 1px solid rgba(255, 255, 255, 0.95); + padding: 22px 36px; + box-shadow: + 0 8px 32px rgba(13, 148, 136, 0.14), + 0 2px 8px rgba(0, 0, 0, 0.06), + 0 0 0 0.5px rgba(13, 148, 136, 0.08); + /* Spring overshoot entrance — timing-under-300ms: 260ms */ + animation: dropPillEnter 0.26s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; +} + +.skills-drop-overlay-inner svg { + opacity: 0.85; +} + +.csdk-input { + background: white; +} + +/* ── Skill Drawer (Vaul) ── */ + +/* staging-dim-background: 40% overlay */ +.skill-drawer-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.32); + z-index: 200; +} + +/* Bottom sheet — spring physics handled by Vaul internally */ +.skill-drawer-content { + position: fixed; + bottom: 0; + left: 0; + right: 0; + z-index: 201; + background: #ffffff; + border-radius: 20px 20px 0 0; + /* visual-layered-shadows */ + box-shadow: + 0 -1px 0 rgba(0, 0, 0, 0.06), + 0 -8px 32px rgba(0, 0, 0, 0.1), + 0 -2px 8px rgba(0, 0, 0, 0.05); + max-height: 82vh; + display: flex; + flex-direction: column; + outline: none; +} + +/* Drag handle — visual-concentric-radius: inside 20px rounded corner */ +.skill-drawer-handle { + width: 40px; + height: 4px; + border-radius: 2px; + background: rgba(0, 0, 0, 0.12); + margin: 14px auto 0; + flex-shrink: 0; +} + +/* Header row */ +.skill-drawer-header { + display: flex; + align-items: flex-start; + gap: 14px; + padding: 20px 24px 18px; + border-bottom: 1px solid; + flex-shrink: 0; +} + +.skill-drawer-icon { + width: 42px; + height: 42px; + border-radius: 11px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.skill-drawer-header-text { + flex: 1; + min-width: 0; +} + +.skill-drawer-title { + font-size: 15px; + font-weight: 700; + line-height: 1.3; + letter-spacing: -0.02em; + margin: 0 0 4px; +} + +.skill-drawer-subtitle { + font-size: 12px; + color: #6b7280; + line-height: 1.45; + margin: 0; +} + +.skill-drawer-meta { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 5px; + flex-shrink: 0; +} + +.skill-drawer-badge { + font-size: 10px; + font-weight: 600; + letter-spacing: 0.04em; + text-transform: uppercase; + border-radius: 5px; + padding: 3px 7px; + border: 1px solid; + white-space: nowrap; +} + +.skill-drawer-version { + font-size: 10px; + color: #9ca3af; + font-family: var(--app-mono); +} + +/* Scrollable body */ +.skill-drawer-body { + flex: 1; + overflow-y: auto; + padding: 20px 24px 36px; + scrollbar-width: thin; + scrollbar-color: rgba(0, 0, 0, 0.08) transparent; +} + +.skill-drawer-loading { + font-size: 12px; + color: #9ca3af; +} + +/* Plain text — type-text-wrap-pretty for body text */ +.skill-drawer-text { + font-family: var(--app-font); + font-size: 12px; + color: #374151; + line-height: 1.65; + white-space: pre-wrap; + word-break: break-word; + margin: 0; + text-wrap: pretty; +} diff --git a/examples/skills-demo/vite.config.ts b/examples/skills-demo/vite.config.ts index f765d45..a4b9481 100644 --- a/examples/skills-demo/vite.config.ts +++ b/examples/skills-demo/vite.config.ts @@ -7,7 +7,10 @@ export default defineConfig({ resolve: { alias: { "@": path.resolve(__dirname, "./src"), + react: path.resolve(__dirname, "./node_modules/react"), + "react-dom": path.resolve(__dirname, "./node_modules/react-dom"), }, + dedupe: ["react", "react-dom"], }, server: { port: 3033, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2eb1776..5d18830 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -44,7 +44,7 @@ importers: version: 16.2.5(@types/react@18.3.27)(lucide-react@0.562.0(react@19.2.1))(next@16.0.10(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(sass@1.97.0))(react-dom@19.2.1(react@19.2.1))(react-router@7.13.1(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1)(zod@4.3.6) fumadocs-mdx: specifier: ^14.1.1 - version: 14.1.1(fumadocs-core@16.2.5(@types/react@18.3.27)(lucide-react@0.562.0(react@19.2.1))(next@16.0.10(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(sass@1.97.0))(react-dom@19.2.1(react@19.2.1))(react-router@7.13.1(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1)(zod@4.3.6))(next@16.0.10(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(sass@1.97.0))(react@19.2.1)(vite@6.4.1(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2)) + version: 14.1.1(fumadocs-core@16.2.5(@types/react@18.3.27)(lucide-react@0.562.0(react@19.2.1))(next@16.0.10(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(sass@1.97.0))(react-dom@19.2.1(react@19.2.1))(react-router@7.13.1(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1)(zod@4.3.6))(next@16.0.10(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(sass@1.97.0))(react@19.2.1)(vite@6.4.1(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2)) fumadocs-ui: specifier: ^16.2.5 version: 16.2.5(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(lucide-react@0.562.0(react@19.2.1))(next@16.0.10(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(sass@1.97.0))(react-dom@19.2.1(react@19.2.1))(react-router@7.13.1(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1)(tailwindcss@4.1.18)(zod@4.3.6) @@ -418,6 +418,73 @@ importers: specifier: ^5.6.0 version: 5.9.3 + examples/generative-ui-demo: + dependencies: + '@yourgpt/copilot-sdk': + specifier: workspace:* + version: link:../../packages/copilot-sdk + '@yourgpt/llm-sdk': + specifier: workspace:* + version: link:../../packages/llm-sdk + cors: + specifier: ^2.8.5 + version: 2.8.6 + dotenv: + specifier: ^16.4.0 + version: 16.6.1 + express: + specifier: ^4.21.0 + version: 4.22.1 + lucide-react: + specifier: ^0.563.0 + version: 0.563.0(react@19.2.3) + react: + specifier: ^19.0.0 + version: 19.2.3 + react-dom: + specifier: ^19.0.0 + version: 19.2.3(react@19.2.3) + recharts: + specifier: ^2.13.0 + version: 2.15.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + devDependencies: + '@tailwindcss/postcss': + specifier: ^4 + version: 4.1.18 + '@types/cors': + specifier: ^2.8.17 + version: 2.8.19 + '@types/express': + specifier: ^5.0.0 + version: 5.0.6 + '@types/node': + specifier: ^20 + version: 20.19.27 + '@types/react': + specifier: ^18.2.0 + version: 18.3.27 + '@types/react-dom': + specifier: ^18.2.0 + version: 18.3.7(@types/react@18.3.27) + '@vitejs/plugin-react': + specifier: 6.0.1 + version: 6.0.1(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)) + concurrently: + specifier: ^9.0.0 + version: 9.2.1 + tailwindcss: + specifier: ^4 + version: 4.2.1 + tsx: + specifier: ^4.19.0 + version: 4.21.0 + typescript: + specifier: ^5.6.0 + version: 5.9.3 + vite: + specifier: 8.0.3 + version: 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) + examples/headless-slack-demo: dependencies: '@yourgpt/copilot-sdk': @@ -468,7 +535,7 @@ importers: version: 18.3.7(@types/react@18.3.27) '@vitejs/plugin-react': specifier: ^4.3.4 - version: 4.7.0(vite@6.4.1(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.7.0(vite@6.4.1(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2)) concurrently: specifier: ^9.0.0 version: 9.2.1 @@ -483,7 +550,7 @@ importers: version: 5.9.3 vite: specifier: ^6.0.0 - version: 6.4.1(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2) + version: 6.4.1(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2) examples/mcp-demo: dependencies: @@ -569,7 +636,7 @@ importers: devDependencies: '@tailwindcss/vite': specifier: ^4.0.0 - version: 4.2.1(vite@6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.2.1(vite@6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2)) '@types/react': specifier: ^18.2.0 version: 18.3.27 @@ -578,7 +645,7 @@ importers: version: 18.3.7(@types/react@18.3.27) '@vitejs/plugin-react': specifier: ^4.3.0 - version: 4.7.0(vite@6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.7.0(vite@6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2)) tailwindcss: specifier: ^4.0.0 version: 4.1.18 @@ -587,7 +654,7 @@ importers: version: 5.9.3 vite: specifier: ^6.0.0 - version: 6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2) + version: 6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2) examples/ollama-demo/server: dependencies: @@ -1026,6 +1093,79 @@ importers: specifier: ^5 version: 5.9.3 + examples/skills-demo: + dependencies: + '@yourgpt/copilot-sdk': + specifier: workspace:* + version: link:../../packages/copilot-sdk + '@yourgpt/llm-sdk': + specifier: workspace:* + version: link:../../packages/llm-sdk + cors: + specifier: ^2.8.5 + version: 2.8.6 + dotenv: + specifier: ^16.4.0 + version: 16.6.1 + express: + specifier: ^4.21.0 + version: 4.22.1 + lucide-react: + specifier: ^0.563.0 + version: 0.563.0(react@19.2.3) + react: + specifier: ^19.0.0 + version: 19.2.3 + react-dom: + specifier: ^19.0.0 + version: 19.2.3(react@19.2.3) + vaul: + specifier: ^1.1.2 + version: 1.1.2(@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) + devDependencies: + '@tailwindcss/postcss': + specifier: ^4 + version: 4.1.18 + '@types/cors': + specifier: ^2.8.17 + version: 2.8.19 + '@types/express': + specifier: ^5.0.0 + version: 5.0.6 + '@types/node': + specifier: ^20 + version: 20.19.27 + '@types/react': + specifier: ^18.2.0 + version: 18.3.27 + '@types/react-dom': + specifier: ^18.2.0 + version: 18.3.7(@types/react@18.3.27) + '@vitejs/plugin-react': + specifier: 6.0.1 + version: 6.0.1(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)) + concurrently: + specifier: ^9.0.0 + version: 9.2.1 + path: + specifier: ^0.12.7 + version: 0.12.7 + tailwindcss: + specifier: ^4 + version: 4.2.1 + tsx: + specifier: ^4.19.0 + version: 4.21.0 + tw-animate-css: + specifier: ^1.4.0 + version: 1.4.0 + typescript: + specifier: ^5.6.0 + version: 5.9.3 + vite: + specifier: 8.0.3 + version: 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) + examples/support-tickets-demo: dependencies: '@radix-ui/react-slot': @@ -1244,7 +1384,7 @@ importers: version: 18.3.1(react@18.3.1) tsup: specifier: ^8.0.2 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + version: 8.5.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) typescript: specifier: ^5.4.0 version: 5.9.3 @@ -1269,7 +1409,7 @@ importers: version: 22.19.3 tsup: specifier: ^8.3.5 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + version: 8.5.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) typescript: specifier: ^5.7.2 version: 5.9.3 @@ -1278,7 +1418,7 @@ importers: devDependencies: tsup: specifier: ^8.0.2 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + version: 8.5.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) typescript: specifier: ^5.4.0 version: 5.9.3 @@ -1306,7 +1446,7 @@ importers: version: 4.104.0(ws@8.18.0)(zod@3.25.76) tsup: specifier: ^8.0.2 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + version: 8.5.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) typescript: specifier: ^5.4.0 version: 5.9.3 @@ -2555,6 +2695,12 @@ packages: '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} + '@napi-rs/wasm-runtime@1.1.2': + resolution: {integrity: sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + '@next/env@16.0.10': resolution: {integrity: sha512-8tuaQkyDVgeONQ1MeT9Mkk8pQmZapMKFh5B+OrFUlG3rVmYTXcXlBetBgTurKXGaIZvkoqRT9JL5K3phXcgang==} @@ -2758,6 +2904,9 @@ packages: resolution: {integrity: sha512-APwpZ+FTGMryo4QEeD6ti+Ei8suBkvxe8PeWdUcQHVfJDpjpt4c1dKojjNswcBmdeWSiiTYcnkKKH+yuo6727g==} engines: {node: '>= 20.0.0'} + '@oxc-project/types@0.122.0': + resolution: {integrity: sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==} + '@parcel/watcher-android-arm64@2.5.1': resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} engines: {node: '>= 10.0.0'} @@ -3600,9 +3749,104 @@ packages: '@radix-ui/rect@1.1.1': resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + '@rolldown/binding-android-arm64@1.0.0-rc.12': + resolution: {integrity: sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.0-rc.12': + resolution: {integrity: sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.0-rc.12': + resolution: {integrity: sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.0-rc.12': + resolution: {integrity: sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12': + resolution: {integrity: sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12': + resolution: {integrity: sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.12': + resolution: {integrity: sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.12': + resolution: {integrity: sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.12': + resolution: {integrity: sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12': + resolution: {integrity: sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12': + resolution: {integrity: sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + '@rolldown/pluginutils@1.0.0-beta.27': resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + '@rolldown/pluginutils@1.0.0-rc.12': + resolution: {integrity: sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==} + + '@rolldown/pluginutils@1.0.0-rc.7': + resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==} + '@rollup/rollup-android-arm-eabi@4.53.3': resolution: {integrity: sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==} cpu: [arm] @@ -4263,6 +4507,19 @@ packages: peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + '@vitejs/plugin-react@6.0.1': + resolution: {integrity: sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 + babel-plugin-react-compiler: ^1.0.0 + vite: ^8.0.0 + peerDependenciesMeta: + '@rolldown/plugin-babel': + optional: true + babel-plugin-react-compiler: + optional: true + abort-controller@3.0.0: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} @@ -4926,10 +5183,6 @@ packages: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} - enhanced-resolve@5.18.4: - resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==} - engines: {node: '>=10.13.0'} - enhanced-resolve@5.19.0: resolution: {integrity: sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==} engines: {node: '>=10.13.0'} @@ -5699,6 +5952,9 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + inherits@2.0.3: + resolution: {integrity: sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==} + inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -6026,6 +6282,12 @@ packages: cpu: [arm64] os: [android] + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + lightningcss-darwin-arm64@1.30.2: resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==} engines: {node: '>= 12.0.0'} @@ -6038,6 +6300,12 @@ packages: cpu: [arm64] os: [darwin] + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + lightningcss-darwin-x64@1.30.2: resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==} engines: {node: '>= 12.0.0'} @@ -6050,6 +6318,12 @@ packages: cpu: [x64] os: [darwin] + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + lightningcss-freebsd-x64@1.30.2: resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==} engines: {node: '>= 12.0.0'} @@ -6062,6 +6336,12 @@ packages: cpu: [x64] os: [freebsd] + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + lightningcss-linux-arm-gnueabihf@1.30.2: resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==} engines: {node: '>= 12.0.0'} @@ -6074,6 +6354,12 @@ packages: cpu: [arm] os: [linux] + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + lightningcss-linux-arm64-gnu@1.30.2: resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==} engines: {node: '>= 12.0.0'} @@ -6086,6 +6372,12 @@ packages: cpu: [arm64] os: [linux] + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + lightningcss-linux-arm64-musl@1.30.2: resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} engines: {node: '>= 12.0.0'} @@ -6098,6 +6390,12 @@ packages: cpu: [arm64] os: [linux] + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + lightningcss-linux-x64-gnu@1.30.2: resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} engines: {node: '>= 12.0.0'} @@ -6110,6 +6408,12 @@ packages: cpu: [x64] os: [linux] + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + lightningcss-linux-x64-musl@1.30.2: resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} engines: {node: '>= 12.0.0'} @@ -6122,6 +6426,12 @@ packages: cpu: [x64] os: [linux] + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + lightningcss-win32-arm64-msvc@1.30.2: resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} engines: {node: '>= 12.0.0'} @@ -6134,6 +6444,12 @@ packages: cpu: [arm64] os: [win32] + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + lightningcss-win32-x64-msvc@1.30.2: resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==} engines: {node: '>= 12.0.0'} @@ -6146,6 +6462,12 @@ packages: cpu: [x64] os: [win32] + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + lightningcss@1.30.2: resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} engines: {node: '>= 12.0.0'} @@ -6154,6 +6476,10 @@ packages: resolution: {integrity: sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==} engines: {node: '>= 12.0.0'} + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + lilconfig@3.1.3: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} @@ -6839,6 +7165,9 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} + path@0.12.7: + resolution: {integrity: sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -6853,6 +7182,10 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + pidtree@0.6.0: resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==} engines: {node: '>=0.10'} @@ -6911,6 +7244,10 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} + postcss@8.5.8: + resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} + engines: {node: ^10 || ^12 || >=14} + powershell-utils@0.1.0: resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==} engines: {node: '>=20'} @@ -6936,6 +7273,10 @@ packages: printable-characters@1.0.42: resolution: {integrity: sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==} + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + prompts@2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} @@ -7232,6 +7573,11 @@ packages: rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + rolldown@1.0.0-rc.12: + resolution: {integrity: sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + rollup-plugin-inject@3.0.2: resolution: {integrity: sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w==} deprecated: This package has been deprecated and is no longer maintained. Please use @rollup/plugin-inject. @@ -7888,6 +8234,9 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + util@0.10.4: + resolution: {integrity: sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==} + utils-merge@1.0.1: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} @@ -7903,6 +8252,12 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + vaul@1.1.2: + resolution: {integrity: sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + vfile-location@5.0.3: resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} @@ -7955,6 +8310,49 @@ packages: yaml: optional: true + vite@8.0.3: + resolution: {integrity: sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.0 + esbuild: ^0.27.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + web-namespaces@2.0.1: resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} @@ -9310,6 +9708,13 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true + '@napi-rs/wasm-runtime@1.1.2(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)': + dependencies: + '@emnapi/core': 1.7.1 + '@emnapi/runtime': 1.7.1 + '@tybys/wasm-util': 0.10.1 + optional: true + '@next/env@16.0.10': {} '@next/env@16.1.1': {} @@ -9433,6 +9838,8 @@ snapshots: '@orama/orama@3.1.17': {} + '@oxc-project/types@0.122.0': {} + '@parcel/watcher-android-arm64@2.5.1': optional: true @@ -10875,8 +11282,62 @@ snapshots: '@radix-ui/rect@1.1.1': {} + '@rolldown/binding-android-arm64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.12': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.12(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)': + dependencies: + '@napi-rs/wasm-runtime': 1.1.2(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12': + optional: true + '@rolldown/pluginutils@1.0.0-beta.27': {} + '@rolldown/pluginutils@1.0.0-rc.12': {} + + '@rolldown/pluginutils@1.0.0-rc.7': {} + '@rollup/rollup-android-arm-eabi@4.53.3': optional: true @@ -11012,7 +11473,7 @@ snapshots: '@tailwindcss/node@4.1.18': dependencies: '@jridgewell/remapping': 2.3.5 - enhanced-resolve: 5.18.4 + enhanced-resolve: 5.19.0 jiti: 2.6.1 lightningcss: 1.30.2 magic-string: 0.30.21 @@ -11144,12 +11605,12 @@ snapshots: postcss-selector-parser: 6.0.10 tailwindcss: 4.1.18 - '@tailwindcss/vite@4.2.1(vite@6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2))': + '@tailwindcss/vite@4.2.1(vite@6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@tailwindcss/node': 4.2.1 '@tailwindcss/oxide': 4.2.1 tailwindcss: 4.2.1 - vite: 6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2) + vite: 6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2) '@ts-morph/common@0.27.0': dependencies: @@ -11481,7 +11942,7 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true - '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2))': + '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@babel/core': 7.28.5 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.5) @@ -11489,11 +11950,11 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.27 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 6.4.1(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2) + vite: 6.4.1(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - supports-color - '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2))': + '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@babel/core': 7.28.5 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.5) @@ -11501,10 +11962,15 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.27 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2) + vite: 6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - supports-color + '@vitejs/plugin-react@6.0.1(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))': + dependencies: + '@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) + abort-controller@3.0.0: dependencies: event-target-shim: 5.0.1 @@ -12121,11 +12587,6 @@ snapshots: encodeurl@2.0.0: {} - enhanced-resolve@5.18.4: - dependencies: - graceful-fs: 4.2.11 - tapable: 2.3.0 - enhanced-resolve@5.19.0: dependencies: graceful-fs: 4.2.11 @@ -12357,7 +12818,7 @@ snapshots: '@next/eslint-plugin-next': 16.0.10 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-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.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)) @@ -12378,7 +12839,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-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-import: 2.32.0(eslint-import-resolver-typescript@3.10.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)) @@ -12398,7 +12859,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-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-import: 2.32.0(eslint-import-resolver-typescript@3.10.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)) @@ -12420,7 +12881,7 @@ snapshots: transitivePeerDependencies: - supports-color - 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-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 @@ -12431,11 +12892,11 @@ 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-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-import: 2.32.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)): + 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)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 @@ -12446,18 +12907,18 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(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@9.39.2(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(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: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) 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-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -12482,7 +12943,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.2(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -12500,7 +12961,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - 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-import@2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -12844,6 +13305,10 @@ snapshots: optionalDependencies: picomatch: 4.0.3 + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + fetch-blob@3.2.0: dependencies: node-domexception: 1.0.0 @@ -12988,7 +13453,7 @@ snapshots: transitivePeerDependencies: - supports-color - fumadocs-mdx@14.1.1(fumadocs-core@16.2.5(@types/react@18.3.27)(lucide-react@0.562.0(react@19.2.1))(next@16.0.10(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(sass@1.97.0))(react-dom@19.2.1(react@19.2.1))(react-router@7.13.1(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1)(zod@4.3.6))(next@16.0.10(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(sass@1.97.0))(react@19.2.1)(vite@6.4.1(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2)): + fumadocs-mdx@14.1.1(fumadocs-core@16.2.5(@types/react@18.3.27)(lucide-react@0.562.0(react@19.2.1))(next@16.0.10(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(sass@1.97.0))(react-dom@19.2.1(react@19.2.1))(react-router@7.13.1(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1)(zod@4.3.6))(next@16.0.10(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(sass@1.97.0))(react@19.2.1)(vite@6.4.1(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2)): dependencies: '@mdx-js/mdx': 3.1.1 '@standard-schema/spec': 1.1.0 @@ -13011,7 +13476,7 @@ snapshots: optionalDependencies: next: 16.0.10(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(sass@1.97.0) react: 19.2.1 - vite: 6.4.1(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2) + vite: 6.4.1(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - supports-color @@ -13370,6 +13835,8 @@ snapshots: imurmurhash@0.1.4: {} + inherits@2.0.3: {} + inherits@2.0.4: {} inline-style-parser@0.2.7: {} @@ -13655,66 +14122,99 @@ snapshots: lightningcss-android-arm64@1.31.1: optional: true + lightningcss-android-arm64@1.32.0: + optional: true + lightningcss-darwin-arm64@1.30.2: optional: true lightningcss-darwin-arm64@1.31.1: optional: true + lightningcss-darwin-arm64@1.32.0: + optional: true + lightningcss-darwin-x64@1.30.2: optional: true lightningcss-darwin-x64@1.31.1: optional: true + lightningcss-darwin-x64@1.32.0: + optional: true + lightningcss-freebsd-x64@1.30.2: optional: true lightningcss-freebsd-x64@1.31.1: optional: true + lightningcss-freebsd-x64@1.32.0: + optional: true + lightningcss-linux-arm-gnueabihf@1.30.2: optional: true lightningcss-linux-arm-gnueabihf@1.31.1: optional: true + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + lightningcss-linux-arm64-gnu@1.30.2: optional: true lightningcss-linux-arm64-gnu@1.31.1: optional: true + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + lightningcss-linux-arm64-musl@1.30.2: optional: true lightningcss-linux-arm64-musl@1.31.1: optional: true + lightningcss-linux-arm64-musl@1.32.0: + optional: true + lightningcss-linux-x64-gnu@1.30.2: optional: true lightningcss-linux-x64-gnu@1.31.1: optional: true + lightningcss-linux-x64-gnu@1.32.0: + optional: true + lightningcss-linux-x64-musl@1.30.2: optional: true lightningcss-linux-x64-musl@1.31.1: optional: true + lightningcss-linux-x64-musl@1.32.0: + optional: true + lightningcss-win32-arm64-msvc@1.30.2: optional: true lightningcss-win32-arm64-msvc@1.31.1: optional: true + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + lightningcss-win32-x64-msvc@1.30.2: optional: true lightningcss-win32-x64-msvc@1.31.1: optional: true + lightningcss-win32-x64-msvc@1.32.0: + optional: true + lightningcss@1.30.2: dependencies: detect-libc: 2.1.2 @@ -13747,6 +14247,22 @@ snapshots: lightningcss-win32-arm64-msvc: 1.31.1 lightningcss-win32-x64-msvc: 1.31.1 + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + lilconfig@3.1.3: {} lines-and-columns@1.2.4: {} @@ -14722,6 +15238,11 @@ snapshots: path-type@4.0.0: {} + path@0.12.7: + dependencies: + process: 0.11.10 + util: 0.10.4 + pathe@2.0.3: {} picocolors@1.1.1: {} @@ -14730,6 +15251,8 @@ snapshots: picomatch@4.0.3: {} + picomatch@4.0.4: {} + pidtree@0.6.0: {} pify@4.0.1: {} @@ -14746,12 +15269,12 @@ snapshots: possible-typed-array-names@1.1.0: {} - postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(yaml@2.8.2): + postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(yaml@2.8.2): dependencies: lilconfig: 3.1.3 optionalDependencies: jiti: 2.6.1 - postcss: 8.5.6 + postcss: 8.5.8 tsx: 4.21.0 yaml: 2.8.2 @@ -14777,6 +15300,12 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postcss@8.5.8: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + powershell-utils@0.1.0: {} prelude-ls@1.2.1: {} @@ -14791,6 +15320,8 @@ snapshots: printable-characters@1.0.42: {} + process@0.11.10: {} + prompts@2.4.2: dependencies: kleur: 3.0.3 @@ -15236,6 +15767,30 @@ snapshots: rfdc@1.4.1: {} + rolldown@1.0.0-rc.12(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1): + dependencies: + '@oxc-project/types': 0.122.0 + '@rolldown/pluginutils': 1.0.0-rc.12 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-rc.12 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.12 + '@rolldown/binding-darwin-x64': 1.0.0-rc.12 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.12 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.12 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.12 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.12 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.12 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.12(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1) + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.12 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.12 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + rollup-plugin-inject@3.0.2: dependencies: estree-walker: 0.6.1 @@ -15824,8 +16379,8 @@ snapshots: tinyglobby@0.2.15: dependencies: - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 tldts-core@7.0.19: {} @@ -15879,7 +16434,7 @@ snapshots: tslib@2.8.1: {} - tsup@8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2): + tsup@8.5.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2): dependencies: bundle-require: 5.1.0(esbuild@0.27.1) cac: 6.7.14 @@ -15890,7 +16445,7 @@ snapshots: fix-dts-default-cjs-exports: 1.0.1 joycon: 3.1.1 picocolors: 1.1.1 - postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(yaml@2.8.2) + postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(yaml@2.8.2) resolve-from: 5.0.0 rollup: 4.53.3 source-map: 0.7.6 @@ -15899,7 +16454,7 @@ snapshots: tinyglobby: 0.2.15 tree-kill: 1.2.2 optionalDependencies: - postcss: 8.5.6 + postcss: 8.5.8 typescript: 5.9.3 transitivePeerDependencies: - jiti @@ -16163,6 +16718,10 @@ snapshots: util-deprecate@1.0.2: {} + util@0.10.4: + dependencies: + inherits: 2.0.3 + utils-merge@1.0.1: {} utrie@1.0.2: @@ -16173,6 +16732,15 @@ snapshots: vary@1.1.2: {} + vaul@1.1.2(@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: + '@radix-ui/react-dialog': 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) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + vfile-location@5.0.3: dependencies: '@types/unist': 3.0.3 @@ -16205,7 +16773,7 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 - vite@6.4.1(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2): + vite@6.4.1(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.3) @@ -16217,12 +16785,12 @@ snapshots: '@types/node': 20.19.27 fsevents: 2.3.3 jiti: 2.6.1 - lightningcss: 1.31.1 + lightningcss: 1.32.0 sass: 1.97.0 tsx: 4.21.0 yaml: 2.8.2 - vite@6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2): + vite@6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.97.0)(tsx@4.21.0)(yaml@2.8.2): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.3) @@ -16234,10 +16802,29 @@ snapshots: '@types/node': 22.19.3 fsevents: 2.3.3 jiti: 2.6.1 - lightningcss: 1.31.1 + lightningcss: 1.32.0 + sass: 1.97.0 + tsx: 4.21.0 + yaml: 2.8.2 + + 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): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.8 + rolldown: 1.0.0-rc.12(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1) + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 20.19.27 + esbuild: 0.27.1 + fsevents: 2.3.3 + jiti: 2.6.1 sass: 1.97.0 tsx: 4.21.0 yaml: 2.8.2 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' web-namespaces@2.0.1: {} From b324ba52cf43e7e92694d300a152d88d74250931 Mon Sep 17 00:00:00 2001 From: Sahil Date: Sun, 29 Mar 2026 19:21:19 +0530 Subject: [PATCH 10/39] chore: update pnpm-lock.yaml with new dependencies and versions - Added 'vaul' version 1.1.2 and 'tw-animate-css' version 1.4.0 to dependencies. - Updated 'yourgpt-server-demo' with new dependencies including 'cors', 'dotenv', 'express', and 'ws'. - Updated devDependencies for 'yourgpt-server-demo' with '@types/cors', '@types/express', '@types/ws', 'tsx', and 'typescript'. - Updated integrity checks for '@types/ws' and 'vaul' in the lockfile. --- pnpm-lock.yaml | 70 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 66 insertions(+), 4 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bd7fad4..f5c0b7c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1153,6 +1153,9 @@ importers: react-dom: specifier: ^19.0.0 version: 19.2.3(react@19.2.3) + vaul: + specifier: ^1.1.2 + version: 1.1.2(@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) devDependencies: '@tailwindcss/postcss': specifier: ^4 @@ -1187,6 +1190,9 @@ importers: tsx: specifier: ^4.19.0 version: 4.21.0 + tw-animate-css: + specifier: ^1.4.0 + version: 1.4.0 typescript: specifier: ^5.6.0 version: 5.9.3 @@ -1310,6 +1316,40 @@ importers: specifier: ^5 version: 5.9.3 + examples/yourgpt-server-demo: + dependencies: + '@yourgpt/llm-sdk': + specifier: workspace:* + version: link:../../packages/llm-sdk + cors: + specifier: ^2.8.5 + version: 2.8.6 + dotenv: + specifier: ^16.4.0 + version: 16.6.1 + express: + specifier: ^4.21.0 + version: 4.22.1 + ws: + specifier: ^8.18.0 + version: 8.18.0 + devDependencies: + '@types/cors': + specifier: ^2.8.17 + version: 2.8.19 + '@types/express': + specifier: ^5.0.0 + version: 5.0.6 + '@types/ws': + specifier: ^8.5.13 + version: 8.18.1 + tsx: + specifier: ^4.19.0 + version: 4.21.0 + typescript: + specifier: ^5.6.0 + version: 5.9.3 + packages/copilot-sdk: dependencies: '@base-ui/react': @@ -4335,6 +4375,9 @@ packages: '@types/validate-npm-package-name@4.0.2': resolution: {integrity: sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==} + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + '@typescript-eslint/eslint-plugin@8.50.0': resolution: {integrity: sha512-O7QnmOXYKVtPrfYzMolrCTfkezCJS9+ljLdKW/+DCvRsc3UAz+sbH6Xcsv7p30+0OwUbeWfUDAQE0vpabZ3QLg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -8274,6 +8317,12 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + vaul@1.1.2: + resolution: {integrity: sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + vfile-location@5.0.3: resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} @@ -11832,6 +11881,10 @@ snapshots: '@types/validate-npm-package-name@4.0.2': {} + '@types/ws@8.18.1': + dependencies: + '@types/node': 20.19.27 + '@typescript-eslint/eslint-plugin@8.50.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -12920,7 +12973,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-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-import: 2.32.0(eslint-import-resolver-typescript@3.10.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)) @@ -12940,7 +12993,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-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-import: 2.32.0(eslint-import-resolver-typescript@3.10.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)) @@ -12973,7 +13026,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-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-import: 2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -13042,7 +13095,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - 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-import@2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -16837,6 +16890,15 @@ snapshots: vary@1.1.2: {} + vaul@1.1.2(@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: + '@radix-ui/react-dialog': 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) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + vfile-location@5.0.3: dependencies: '@types/unist': 3.0.3 From a2f5667b15482a681c3a317be3272ca10a5dbd2f Mon Sep 17 00:00:00 2001 From: Sahil Date: Sun, 29 Mar 2026 19:58:06 +0530 Subject: [PATCH 11/39] feat(copilot-sdk): add streamMode prop for agent response bubble behavior MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 'multi-step' (default) — one bubble per server agent iteration (OpenAI/LiteLLM style), default unchanged - 'single-turn' — all iterations collapsed into one bubble per user turn (Vercel AI SDK / Claude.ai style) Usage: Co-Authored-By: Claude Sonnet 4.6 --- .../copilot-sdk/src/chat/ChatWithTools.ts | 7 + .../src/chat/classes/AbstractChat.ts | 131 ++++++++++++------ packages/copilot-sdk/src/chat/types/chat.ts | 9 ++ .../src/react/provider/CopilotProvider.tsx | 11 ++ 4 files changed, 114 insertions(+), 44 deletions(-) diff --git a/packages/copilot-sdk/src/chat/ChatWithTools.ts b/packages/copilot-sdk/src/chat/ChatWithTools.ts index 66a7126..0b428ca 100644 --- a/packages/copilot-sdk/src/chat/ChatWithTools.ts +++ b/packages/copilot-sdk/src/chat/ChatWithTools.ts @@ -57,6 +57,12 @@ export interface ChatWithToolsConfig { yourgptConfig?: YourGPTConfig; /** Enable debug logging */ debug?: boolean; + /** + * Controls how multi-turn agent responses appear in the UI. + * - `'multi-step'` (default) — one bubble per server agent iteration. + * - `'single-turn'` — all iterations collapsed into one bubble per user turn. + */ + streamMode?: "multi-step" | "single-turn"; /** Initial messages */ initialMessages?: UIMessage[]; /** Initial tools to register */ @@ -152,6 +158,7 @@ export class ChatWithTools { onCreateSession: config.onCreateSession, yourgptConfig: config.yourgptConfig, debug: config.debug, + streamMode: config.streamMode, initialMessages: config.initialMessages, state: config.state, transport: config.transport, diff --git a/packages/copilot-sdk/src/chat/classes/AbstractChat.ts b/packages/copilot-sdk/src/chat/classes/AbstractChat.ts index 3fa471c..f2b0359 100644 --- a/packages/copilot-sdk/src/chat/classes/AbstractChat.ts +++ b/packages/copilot-sdk/src/chat/classes/AbstractChat.ts @@ -1107,58 +1107,93 @@ export class AbstractChat { return; } - // 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) { - this.debug("message:end mid-stream", { - messageId: this.streamState.messageId, - contentLength: this.streamState.content.length, - toolCallsInState: this.streamState.toolCalls?.length ?? 0, - chunkCount, - }); + // Handle message:end mid-stream (server-side agent loop turn completed). + // Behaviour depends on streamMode: + // 'multi-step' (default) — finalize a new UIMessage per iteration. + // 'single-turn' — skip entirely; keep accumulating into the + // same streamState so all iterations collapse + // into one bubble (Vercel AI SDK / Claude.ai style). + if (chunk.type === "message:end" && this.streamState) { + if (this.config.streamMode === "single-turn") { + this.debug( + "message:end mid-stream (single-turn: keeping streamState alive)", + { + messageId: this.streamState.messageId, + contentLength: this.streamState.content.length, + chunkCount, + }, + ); + continue; + } - // Finalize current message with its content and tool calls - const turnMessage = streamStateToMessage(this.streamState) as T; + // multi-step (default): finalize current turn as its own UIMessage + if (!this.streamState.content) { + // Nothing streamed yet for this turn — skip finalization + } else { + this.debug("message:end mid-stream", { + messageId: this.streamState.messageId, + contentLength: this.streamState.content.length, + toolCallsInState: this.streamState.toolCalls?.length ?? 0, + chunkCount, + }); - // Add toolCallsHidden metadata if applicable - const toolCallsHidden: Record = {}; - for (const [id, result] of this.streamState.toolResults) { - if (result.hidden !== undefined) { - toolCallsHidden[id] = result.hidden; + // Finalize current message with its content and tool calls + const turnMessage = streamStateToMessage(this.streamState) as T; + + // Add toolCallsHidden metadata if applicable + const toolCallsHidden: Record = {}; + for (const [id, result] of this.streamState.toolResults) { + if (result.hidden !== undefined) { + toolCallsHidden[id] = result.hidden; + } } - } - if ( - turnMessage.toolCalls?.length && - Object.keys(toolCallsHidden).length > 0 - ) { - (turnMessage as T & { metadata?: Record }).metadata = - { + if ( + turnMessage.toolCalls?.length && + Object.keys(toolCallsHidden).length > 0 + ) { + ( + turnMessage as T & { metadata?: Record } + ).metadata = { ...(turnMessage as T & { metadata?: Record }) .metadata, toolCallsHidden, }; - } + } - this.state.updateMessageById( - this.streamState.messageId, - (existing) => ({ - ...turnMessage, - ...(existing.parentId !== undefined - ? { parentId: existing.parentId } - : {}), - ...(existing.childrenIds !== undefined - ? { childrenIds: existing.childrenIds } - : {}), - }), - ); - this.callbacks.onMessageFinish?.(turnMessage); + this.state.updateMessageById( + this.streamState.messageId, + (existing) => ({ + ...turnMessage, + ...(existing.parentId !== undefined + ? { parentId: existing.parentId } + : {}), + ...(existing.childrenIds !== undefined + ? { childrenIds: existing.childrenIds } + : {}), + }), + ); + this.callbacks.onMessageFinish?.(turnMessage); - // Reset stream state for next turn - will be initialized on next message:start - this.streamState = null; - continue; + // Reset stream state — next message:start will create a new message + this.streamState = null; + continue; + } } - // Handle message:start after a mid-stream finalization + // Handle message:start mid-stream: + // single-turn — streamState is still alive, skip to keep accumulating. + // multi-step — streamState was reset to null above; fall through to + // the message:start === null handler below. + if (chunk.type === "message:start" && this.streamState !== null) { + if (this.config.streamMode === "single-turn") { + this.debug( + "message:start mid-stream (single-turn: streamState already active, skipping)", + ); + continue; + } + } + + // Handle message:start after a mid-stream finalization (multi-step mode) if (chunk.type === "message:start" && this.streamState === null) { this.debug("message:start after mid-stream end - creating new message"); // Capture the current leaf BEFORE pushing the new message so the @@ -1428,9 +1463,17 @@ export class AbstractChat { ), }); - const currentStreamToolCallIds = new Set( - this.streamState?.toolCalls?.map((toolCall) => toolCall.id) ?? [], - ); + // In single-turn mode all server-tool IDs land in streamState.toolResults + // (via action:start/args/end chunks). Include them so done.messages doesn't + // re-insert those tools as duplicates. + const currentStreamToolCallIds = new Set([ + ...(this.streamState?.toolCalls?.map((toolCall) => toolCall.id) ?? + []), + ...(this.config.streamMode === "single-turn" && + this.streamState?.toolResults + ? Array.from(this.streamState.toolResults.keys()) + : []), + ]); const messagesToInsert: T[] = []; // Build hidden map from stream state's toolResults diff --git a/packages/copilot-sdk/src/chat/types/chat.ts b/packages/copilot-sdk/src/chat/types/chat.ts index 898c40a..e282dfa 100644 --- a/packages/copilot-sdk/src/chat/types/chat.ts +++ b/packages/copilot-sdk/src/chat/types/chat.ts @@ -99,6 +99,15 @@ export interface ChatConfig { yourgptConfig?: YourGPTConfig; /** Enable debug logging */ debug?: boolean; + /** + * Controls how multi-turn agent responses appear in the UI. + * + * - `'multi-step'` (default) — each server agent iteration gets its own + * assistant bubble. Mirrors OpenAI / LiteLLM multi-turn structure. + * - `'single-turn'` — all iterations are accumulated into one bubble, + * finalized when the server sends `done`. Same as Vercel AI SDK / Claude.ai. + */ + streamMode?: "multi-step" | "single-turn"; /** Available tools (passed to LLM) */ tools?: ToolDefinition[]; /** Optional prompt/tool optimization controls */ diff --git a/packages/copilot-sdk/src/react/provider/CopilotProvider.tsx b/packages/copilot-sdk/src/react/provider/CopilotProvider.tsx index 2bc0dd3..a0e3144 100644 --- a/packages/copilot-sdk/src/react/provider/CopilotProvider.tsx +++ b/packages/copilot-sdk/src/react/provider/CopilotProvider.tsx @@ -320,6 +320,15 @@ export interface CopilotProviderProps { parseError?: (status: number, body: unknown) => string | null | undefined; /** Enable/disable streaming (default: true) */ streaming?: boolean; + /** + * Controls how multi-turn agent responses appear in the UI. + * + * - `'multi-step'` (default) — each server agent iteration gets its own + * assistant bubble. Mirrors OpenAI / LiteLLM multi-turn structure. + * - `'single-turn'` — all iterations are accumulated into one bubble, + * finalized when the server sends `done`. Same as Vercel AI SDK / Claude.ai. + */ + streamMode?: "multi-step" | "single-turn"; /** * Custom headers to send with each request * Can be static object or getter function for dynamic resolution. @@ -560,6 +569,7 @@ export function CopilotProvider(props: CopilotProviderProps) { onError, parseError, streaming, + streamMode, headers, body, debug = false, @@ -658,6 +668,7 @@ export function CopilotProvider(props: CopilotProviderProps) { yourgptConfig, initialMessages: uiInitialMessages, streaming, + streamMode, headers, body, parseError, From fea0899d703391cffdf1f497dbb3c17e4e0c8047 Mon Sep 17 00:00:00 2001 From: Sahil Date: Mon, 30 Mar 2026 15:06:29 +0530 Subject: [PATCH 12/39] chore: remove tool-search-implementation.md file - Deleted the comprehensive documentation for the tool management branch, which included details on features, configurations, and known issues related to the tool management stack across copilot-sdk and llm-sdk. --- tool-search-implementation.md | 253 ---------------------------------- 1 file changed, 253 deletions(-) delete mode 100644 tool-search-implementation.md diff --git a/tool-search-implementation.md b/tool-search-implementation.md deleted file mode 100644 index 2dd6941..0000000 --- a/tool-search-implementation.md +++ /dev/null @@ -1,253 +0,0 @@ -# Tool Management Branch Summary - -Branch: `codex/tool-management-core` - -## Scope - -This branch adds the first full tool-management stack across `copilot-sdk`, `llm-sdk`, and the experimental demos. - -It covers: - -- tool profiles and selective loading -- deferred tool loading -- manual tool search fallback -- native provider tool search hooks for Anthropic and OpenAI -- prompt-side tool result truncation and context compaction groundwork -- mixed client/server tool catalog support -- provider payload debug logging -- experimental scale-testing demo with 100 tools - -## Main Features Added - -### 1. Framework-agnostic prompt/tool optimization (`copilot-sdk`) - -Added shared optimization support in the chat/core layer: - -- tool profile selection -- dynamic tool narrowing -- tool result truncation controls -- context budget reporting -- history compaction with continuity summaries - -Public APIs added: - -- `setOptimizationConfig(...)` -- `setToolProfile(...)` -- `getContextUsage()` - -Main files: - -- `packages/copilot-sdk/src/chat/optimizations.ts` -- `packages/copilot-sdk/src/chat/ChatWithTools.ts` -- `packages/copilot-sdk/src/chat/classes/AbstractChat.ts` -- `packages/copilot-sdk/src/core/types/tools.ts` - -### 2. Tool metadata and selection pipeline (`llm-sdk`) - -Added richer tool metadata and request-time selection: - -- `category` -- `group` -- `profiles` -- `searchKeywords` -- `deferLoading` - -Selection features: - -- profile-based filtering -- include/exclude selectors -- dynamic ranking by recent query/context -- strict deferred loading mode -- request-level `toolProfile` - -Main files: - -- `packages/llm-sdk/src/core/stream-events.ts` -- `packages/llm-sdk/src/server/tool-selection.ts` -- `packages/llm-sdk/src/server/runtime.ts` -- `packages/llm-sdk/src/server/agent-loop.ts` - -### 3. Manual deferred tool search fallback - -Added SDK-managed `search_tools` fallback for providers/models without native search support. - -Behavior: - -- full tool catalog stays on the server -- deferred tools stay out of the initial model-facing tool list -- model can call `search_tools` -- runtime loads matching deferred tools into the next loop iteration - -Supports: - -- mixed server tools + client tools -- profile-aware search -- BM25-style ranking - -Main files: - -- `packages/llm-sdk/src/server/tool-selection.ts` -- `packages/llm-sdk/src/server/runtime.ts` - -### 4. Native provider tool search support - -Added provider-aware search mode selection: - -- `search.mode = "auto" | "native" | "manual"` - -Current behavior: - -- Anthropic Sonnet 4 / Opus 4 supported models -> native Anthropic search path -- OpenAI `gpt-5.4+` supported models -> internal OpenAI Responses-based native path -- all other providers/models -> manual `search_tools` fallback - -Anthropic native path: - -- adds `tool_search_tool_bm25_20251119` or regex variant -- passes deferred tools with `defer_loading: true` - -OpenAI native path: - -- uses internal Responses-based adapter branch -- keeps public SDK/frontend usage unchanged - -Main files: - -- `packages/llm-sdk/src/adapters/anthropic.ts` -- `packages/llm-sdk/src/adapters/openai.ts` -- `packages/llm-sdk/src/server/tool-selection.ts` - -### 5. Mixed client/server catalog support - -Added `toolCatalog` transport support so the runtime can search/select from the full catalog: - -- server tools from runtime config -- client tools registered in the browser - -This allows deferred client tools to be discovered by search even when they are not initially exposed to the model. - -Main files: - -- `packages/copilot-sdk/src/chat/interfaces/ChatTransport.ts` -- `packages/copilot-sdk/src/chat/adapters/HttpTransport.ts` -- `packages/copilot-sdk/src/chat/classes/AbstractChat.ts` -- `packages/llm-sdk/src/server/types.ts` -- `packages/llm-sdk/src/server/runtime.ts` - -### 6. Provider payload logging - -Added adapter-level debug payload logging for request/response inspection. - -Supported across: - -- OpenAI -- Anthropic -- Azure -- Google -- xAI -- Ollama - -Current behavior: - -- logs request payloads -- logs final provider responses -- suppresses per-event stream spam - -Main file: - -- `packages/llm-sdk/src/adapters/base.ts` - -### 7. Experimental Tool Scale Lab - -Added a dedicated experimental demo for scale testing: - -- 100 tools total -- 30 server tools -- 70 client tools -- profile switching -- deferred loading -- manual/native search path testing -- provider behavior testing - -Main files: - -- `examples/experimental/app/tool-scale/page.tsx` -- `examples/experimental/app/api/chat/tool-scale/route.ts` -- `examples/experimental/lib/tool-scale/catalog.ts` -- `examples/experimental/lib/tool-scale/server-tools.ts` -- `examples/experimental/lib/tool-scale/client-tools.ts` - -## Config Examples - -### Runtime tool selection - -```ts -agentLoop: { - enabled: true, - toolSelection: { - enabled: true, - defaultProfile: "support", - includeUnprofiled: false, - dynamicSelection: { - enabled: true, - maxTools: 6, - }, - search: { - enabled: true, - mode: "auto", - strictDeferredLoading: true, - maxResults: 6, - metaToolName: "search_tools", - anthropicVariant: "bm25", - }, - }, -} -``` - -### Client-side optimization - -```ts -optimization: { - toolProfiles: { - enabled: true, - defaultProfile: "support", - }, - toolResultConfig: { - truncation: { - enabled: true, - strategy: "smart", - hardMaxChars: 12000, - }, - }, - contextManagement: { - enabled: true, - history: { - maxMessages: 20, - pruneStrategy: "summarize", - }, - }, - contextBudget: { - enabled: true, - budget: { - contextWindowTokens: 128000, - toolResultsShare: 0.3, - }, - }, -} -``` - -## Current Known Caveats - -These are not fully closed out yet: - -- mixed same-turn server + client tool calls still need more hardening in the runtime loop -- OpenAI manual fallback + continuation path needs more validation -- OpenAI native Responses path currently preserves the SDK contract, but is not full event-by-event Responses streaming yet -- no dedicated automated tests were added in this branch yet - -## Suggested Next Steps - -- add tests for tool selection, deferred loading, and continuation ordering -- tighten manual search scoring so profile-only matches do not leak through -- harden mixed same-turn server/client tool execution ordering -- improve OpenAI Responses-native streaming parity From 8790f9af8f9dbbca1767488e7a9cd4a30480755b Mon Sep 17 00:00:00 2001 From: ankushchhabradelta4infotech-ai Date: Mon, 30 Mar 2026 18:41:52 +0530 Subject: [PATCH 13/39] single stream --- .../copilot-sdk/src/chat/classes/AbstractChat.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/copilot-sdk/src/chat/classes/AbstractChat.ts b/packages/copilot-sdk/src/chat/classes/AbstractChat.ts index f2b0359..2283574 100644 --- a/packages/copilot-sdk/src/chat/classes/AbstractChat.ts +++ b/packages/copilot-sdk/src/chat/classes/AbstractChat.ts @@ -118,6 +118,7 @@ export class AbstractChat { debug: init.debug, optimization: init.optimization, yourgptConfig: init.yourgptConfig, + streamMode: init.streamMode, }; // Use provided state or create default @@ -1495,6 +1496,18 @@ export class AbstractChat { continue; } + // single-turn: ALL assistant content (including intermediate tool-calling + // messages from earlier server iterations) is already accumulated into the + // one streaming message via message:delta. Inserting them from done.messages + // creates duplicate bubbles after streaming ends. Skip ALL assistant messages + // in single-turn mode — tool execution display is driven by streamState.toolResults. + if ( + this.config.streamMode === "single-turn" && + msg.role === "assistant" + ) { + continue; + } + // The current streamed turn already becomes an assistant message from // streamState/tool_calls handling. Skip the duplicate copy from the // done payload, but keep assistant tool_call messages from earlier From a906808e575fb876989859e441591a60055d99da Mon Sep 17 00:00:00 2001 From: Sahil Date: Tue, 31 Mar 2026 19:21:53 +0530 Subject: [PATCH 14/39] 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 15/39] 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 16/39] 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 17/39] 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 18/39] 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 19/39] 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 20/39] 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 21/39] =?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 `