From 8490e3eaa073e5c94a5076ad6e5e68eb0102c839 Mon Sep 17 00:00:00 2001 From: Dominik Kundel Date: Fri, 13 Jun 2025 11:49:05 -0700 Subject: [PATCH 1/7] wip --- examples/nextjs/.gitignore | 41 +++++++ examples/nextjs/README.md | 19 ++++ examples/nextjs/next.config.ts | 7 ++ examples/nextjs/package.json | 32 ++++++ examples/nextjs/postcss.config.mjs | 5 + examples/nextjs/public/file.svg | 1 + examples/nextjs/public/globe.svg | 1 + examples/nextjs/public/next.svg | 1 + examples/nextjs/public/vercel.svg | 1 + examples/nextjs/public/window.svg | 1 + examples/nextjs/src/agents.ts | 19 ++++ examples/nextjs/src/app/api/basic/route.ts | 23 ++++ examples/nextjs/src/app/favicon.ico | Bin 0 -> 25931 bytes examples/nextjs/src/app/globals.css | 26 +++++ examples/nextjs/src/app/layout.tsx | 19 ++++ examples/nextjs/src/app/page.tsx | 36 +++++++ examples/nextjs/src/components/App.tsx | 73 +++++++++++++ examples/nextjs/src/components/History.tsx | 66 ++++++++++++ .../src/components/icons/ArrowUpIcon.tsx | 20 ++++ .../nextjs/src/components/icons/ClockIcon.tsx | 20 ++++ .../src/components/icons/FunctionsIcon.tsx | 18 ++++ .../src/components/messages/FunctionCall.tsx | 56 ++++++++++ .../src/components/messages/TextMessage.tsx | 26 +++++ examples/nextjs/src/components/ui/Button.tsx | 55 ++++++++++ examples/nextjs/src/components/ui/utils.ts | 6 ++ examples/nextjs/src/db.ts | 29 +++++ examples/nextjs/tsconfig.json | 27 +++++ examples/nextjs/vercel.json | 4 + pnpm-lock.yaml | 100 +++++++++++++++--- 29 files changed, 718 insertions(+), 14 deletions(-) create mode 100644 examples/nextjs/.gitignore create mode 100644 examples/nextjs/README.md create mode 100644 examples/nextjs/next.config.ts create mode 100644 examples/nextjs/package.json create mode 100644 examples/nextjs/postcss.config.mjs create mode 100644 examples/nextjs/public/file.svg create mode 100644 examples/nextjs/public/globe.svg create mode 100644 examples/nextjs/public/next.svg create mode 100644 examples/nextjs/public/vercel.svg create mode 100644 examples/nextjs/public/window.svg create mode 100644 examples/nextjs/src/agents.ts create mode 100644 examples/nextjs/src/app/api/basic/route.ts create mode 100644 examples/nextjs/src/app/favicon.ico create mode 100644 examples/nextjs/src/app/globals.css create mode 100644 examples/nextjs/src/app/layout.tsx create mode 100644 examples/nextjs/src/app/page.tsx create mode 100644 examples/nextjs/src/components/App.tsx create mode 100644 examples/nextjs/src/components/History.tsx create mode 100644 examples/nextjs/src/components/icons/ArrowUpIcon.tsx create mode 100644 examples/nextjs/src/components/icons/ClockIcon.tsx create mode 100644 examples/nextjs/src/components/icons/FunctionsIcon.tsx create mode 100644 examples/nextjs/src/components/messages/FunctionCall.tsx create mode 100644 examples/nextjs/src/components/messages/TextMessage.tsx create mode 100644 examples/nextjs/src/components/ui/Button.tsx create mode 100644 examples/nextjs/src/components/ui/utils.ts create mode 100644 examples/nextjs/src/db.ts create mode 100644 examples/nextjs/tsconfig.json create mode 100644 examples/nextjs/vercel.json diff --git a/examples/nextjs/.gitignore b/examples/nextjs/.gitignore new file mode 100644 index 00000000..5ef6a520 --- /dev/null +++ b/examples/nextjs/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/examples/nextjs/README.md b/examples/nextjs/README.md new file mode 100644 index 00000000..9e063f79 --- /dev/null +++ b/examples/nextjs/README.md @@ -0,0 +1,19 @@ +# Realtime Next.js Demo + +This example shows how to combine Next.js with the OpenAI Agents SDK to create a realtime voice agent. + +## Run the example + +Set the `OPENAI_API_KEY` environment variable and run: + +```bash +pnpm examples:realtime-next +``` + +Open [http://localhost:3000](http://localhost:3000) in your browser and start talking. + +## Endpoints + +- **`/`** – WebRTC voice demo using the `RealtimeSession` class. Code in `src/app/page.tsx`. +- **`/websocket`** – Same agent over WebSockets. Code in `src/app/websocket/page.tsx`. +- **`/raw-client`** – Low-level WebRTC example using `OpenAIRealtimeWebRTC`. Code in `src/app/raw-client/page.tsx`. diff --git a/examples/nextjs/next.config.ts b/examples/nextjs/next.config.ts new file mode 100644 index 00000000..5e891cf0 --- /dev/null +++ b/examples/nextjs/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from 'next'; + +const nextConfig: NextConfig = { + /* config options here */ +}; + +export default nextConfig; diff --git a/examples/nextjs/package.json b/examples/nextjs/package.json new file mode 100644 index 00000000..6af729c1 --- /dev/null +++ b/examples/nextjs/package.json @@ -0,0 +1,32 @@ +{ + "name": "nextjs", + "private": true, + "scripts": { + "dev": "next dev --turbopack", + "build": "next build", + "start": "next start", + "lint": "next lint", + "build-check": "tsc --noEmit" + }, + "dependencies": { + "@openai/agents": "workspace:*", + "@radix-ui/react-slot": "^1.2.3", + "@tanstack/react-query": "^5.80.7", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "next": "15.3.2", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "tailwind-merge": "^3.3.0", + "wavtools": "^0.1.5", + "zod": "~3.25.40" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "tailwindcss": "^4", + "typescript": "^5" + } +} diff --git a/examples/nextjs/postcss.config.mjs b/examples/nextjs/postcss.config.mjs new file mode 100644 index 00000000..ba720fe5 --- /dev/null +++ b/examples/nextjs/postcss.config.mjs @@ -0,0 +1,5 @@ +const config = { + plugins: ['@tailwindcss/postcss'], +}; + +export default config; diff --git a/examples/nextjs/public/file.svg b/examples/nextjs/public/file.svg new file mode 100644 index 00000000..004145cd --- /dev/null +++ b/examples/nextjs/public/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/nextjs/public/globe.svg b/examples/nextjs/public/globe.svg new file mode 100644 index 00000000..567f17b0 --- /dev/null +++ b/examples/nextjs/public/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/nextjs/public/next.svg b/examples/nextjs/public/next.svg new file mode 100644 index 00000000..5174b28c --- /dev/null +++ b/examples/nextjs/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/nextjs/public/vercel.svg b/examples/nextjs/public/vercel.svg new file mode 100644 index 00000000..77053960 --- /dev/null +++ b/examples/nextjs/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/nextjs/public/window.svg b/examples/nextjs/public/window.svg new file mode 100644 index 00000000..b2b2a44f --- /dev/null +++ b/examples/nextjs/public/window.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/nextjs/src/agents.ts b/examples/nextjs/src/agents.ts new file mode 100644 index 00000000..84a648d1 --- /dev/null +++ b/examples/nextjs/src/agents.ts @@ -0,0 +1,19 @@ +import { Agent, tool } from '@openai/agents'; +import z from 'zod'; + +const getWeather = tool({ + name: 'getWeather', + description: 'Get the weather for a given city', + parameters: z.object({ + city: z.string(), + }), + execute: async ({ city }) => { + return `The weather in ${city} is sunny.`; + }, +}); + +export const agent = new Agent({ + name: 'Basic Agent', + instructions: 'You are a basic agent.', + tools: [getWeather], +}); diff --git a/examples/nextjs/src/app/api/basic/route.ts b/examples/nextjs/src/app/api/basic/route.ts new file mode 100644 index 00000000..cf22b480 --- /dev/null +++ b/examples/nextjs/src/app/api/basic/route.ts @@ -0,0 +1,23 @@ +import { NextRequest, NextResponse } from 'next/server'; + +import { agent } from '@/agents'; +import { run } from '@openai/agents'; + +export async function POST(req: NextRequest) { + try { + const { messages } = await req.json(); + const input = messages ?? []; + const result = await run(agent, input); + // Example: echo back the received data + return NextResponse.json({ + response: result.finalOutput, + history: result.history, + }); + } catch (error) { + console.error(error); + return NextResponse.json( + { error: 'Invalid request body' }, + { status: 400 }, + ); + } +} diff --git a/examples/nextjs/src/app/favicon.ico b/examples/nextjs/src/app/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..718d6fea4835ec2d246af9800eddb7ffb276240c GIT binary patch literal 25931 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 LrJugUA?W`A8`#=m literal 0 HcmV?d00001 diff --git a/examples/nextjs/src/app/globals.css b/examples/nextjs/src/app/globals.css new file mode 100644 index 00000000..37d72f8a --- /dev/null +++ b/examples/nextjs/src/app/globals.css @@ -0,0 +1,26 @@ +@import 'tailwindcss'; + +:root { + --background: #ffffff; + --foreground: #171717; +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --font-sans: var(--font-geist-sans); + --font-mono: var(--font-geist-mono); +} + +@media (prefers-color-scheme: dark) { + :root { + --background: #0a0a0a; + --foreground: #ededed; + } +} + +body { + background: var(--background); + color: var(--foreground); + font-family: Arial, Helvetica, sans-serif; +} diff --git a/examples/nextjs/src/app/layout.tsx b/examples/nextjs/src/app/layout.tsx new file mode 100644 index 00000000..156a45bd --- /dev/null +++ b/examples/nextjs/src/app/layout.tsx @@ -0,0 +1,19 @@ +import type { Metadata } from 'next'; +import './globals.css'; + +export const metadata: Metadata = { + title: 'Agent SDK Next.js Demo', + description: 'A demo of the Agent SDK in Next.js', +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + {children} + + ); +} diff --git a/examples/nextjs/src/app/page.tsx b/examples/nextjs/src/app/page.tsx new file mode 100644 index 00000000..c8d50dbf --- /dev/null +++ b/examples/nextjs/src/app/page.tsx @@ -0,0 +1,36 @@ +'use client'; + +import type { AgentInputItem } from '@openai/agents'; +import { useState } from 'react'; +import { App } from '@/components/App'; + +export default function Home() { + const [history, setHistory] = useState([]); + + const handleSend = async (message: string) => { + setHistory([ + ...history, + { + type: 'message', + role: 'assistant', + content: [], + status: 'in_progress', + }, + ]); + + const response = await fetch('/api/basic', { + method: 'POST', + body: JSON.stringify({ + messages: [ + ...history, + { type: 'message', role: 'user', content: message }, + ], + }), + }); + const data = await response.json(); + console.log(data); + setHistory(data.history); + }; + + return ; +} diff --git a/examples/nextjs/src/components/App.tsx b/examples/nextjs/src/components/App.tsx new file mode 100644 index 00000000..90a56f2d --- /dev/null +++ b/examples/nextjs/src/components/App.tsx @@ -0,0 +1,73 @@ +import type { AgentInputItem } from '@openai/agents'; +import { History } from '@/components/History'; +import { Button } from '@/components/ui/Button'; +import { useState } from 'react'; +import ArrowUpIcon from './icons/ArrowUpIcon'; + +export type AppProps = { + title?: string; + history?: AgentInputItem[]; + onSend: (message: string) => void; +}; + +export function App({ title = 'Agent Demo', history, onSend }: AppProps) { + const [message, setMessage] = useState(''); + const [isLoading, setIsLoading] = useState(false); + + const handleSend = async () => { + if (!message.trim()) return; + setIsLoading(true); + const msg = message; + setMessage(''); + await onSend(msg); + setIsLoading(false); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!message.trim()) return; + await handleSend(); + }; + + return ( +
+
+
+

{title}

+
+
+
+ {history && history.length > 0 ? ( + + ) : ( +
+ No history available +
+ )} +
+
+ setMessage(e.target.value)} + disabled={isLoading} + /> + +
+
+
+
+ ); +} diff --git a/examples/nextjs/src/components/History.tsx b/examples/nextjs/src/components/History.tsx new file mode 100644 index 00000000..af8df300 --- /dev/null +++ b/examples/nextjs/src/components/History.tsx @@ -0,0 +1,66 @@ +import type { AgentInputItem } from '@openai/agents'; +import { TextMessage } from './messages/TextMessage'; +import { FunctionCallMessage } from './messages/FunctionCall'; + +export type HistoryProps = { + history: AgentInputItem[]; +}; + +export function History({ history }: HistoryProps) { + return ( +
+ {history.map((item, idx) => { + if (item.type === 'function_call') { + return ; + } + + if (item.type === 'message') { + if (typeof item.content === 'string') { + return ( + + ); + } + + return ( + 0 + ? item.content + .map((content) => { + if ( + content.type === 'input_text' || + content.type === 'output_text' + ) { + return content.text; + } + if (content.type === 'audio') { + return content.transcript ?? '⚫︎⚫︎⚫︎'; + } + + if (content.type === 'refusal') { + return content.refusal; + } + + return ''; + }) + .join('\n') + : '⚫︎⚫︎⚫︎' + } + isUser={item.role === 'user'} + key={item?.id ?? JSON.stringify(item.content) + idx} + /> + ); + } + + return null; + })} +
+ ); +} diff --git a/examples/nextjs/src/components/icons/ArrowUpIcon.tsx b/examples/nextjs/src/components/icons/ArrowUpIcon.tsx new file mode 100644 index 00000000..282200e9 --- /dev/null +++ b/examples/nextjs/src/components/icons/ArrowUpIcon.tsx @@ -0,0 +1,20 @@ +import * as React from 'react'; + +const ArrowUpIcon = (props: React.SVGProps) => ( + + + +); + +export default ArrowUpIcon; diff --git a/examples/nextjs/src/components/icons/ClockIcon.tsx b/examples/nextjs/src/components/icons/ClockIcon.tsx new file mode 100644 index 00000000..5fc79955 --- /dev/null +++ b/examples/nextjs/src/components/icons/ClockIcon.tsx @@ -0,0 +1,20 @@ +import * as React from 'react'; + +const ClockIcon = (props: React.SVGProps) => ( + + + +); + +export default ClockIcon; diff --git a/examples/nextjs/src/components/icons/FunctionsIcon.tsx b/examples/nextjs/src/components/icons/FunctionsIcon.tsx new file mode 100644 index 00000000..7fa7031d --- /dev/null +++ b/examples/nextjs/src/components/icons/FunctionsIcon.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; + +const FunctionsIcon = (props: React.SVGProps) => ( + + + +); + +export default FunctionsIcon; diff --git a/examples/nextjs/src/components/messages/FunctionCall.tsx b/examples/nextjs/src/components/messages/FunctionCall.tsx new file mode 100644 index 00000000..8415fa31 --- /dev/null +++ b/examples/nextjs/src/components/messages/FunctionCall.tsx @@ -0,0 +1,56 @@ +import React from 'react'; + +import ClockIcon from '@/components/icons/ClockIcon'; +import type { FunctionCallItem } from '@openai/agents'; +import FunctionsIcon from '@/components/icons/FunctionsIcon'; + +type FunctionCallMessageProps = { + message: FunctionCallItem; +}; + +export function FunctionCallMessage({ message }: FunctionCallMessageProps) { + // let output = message?.output; + // try { + // if (message.output) { + // output = JSON.stringify(JSON.parse(message.output), null, 2); + // } + // } catch { + // output = message.output; + // } + const output = ''; + return ( +
+
+
+
+
+ +
+ {message.status === 'completed' + ? `Called ${message.name}` + : `Calling ${message.name}...`} +
+
+
+ +
+
+
+                {JSON.stringify(JSON.parse(message.arguments), null, 2)}
+              
+
+
+ {output ? ( +
{output}
+ ) : ( +
+ Waiting for result... +
+ )} +
+
+
+
+
+ ); +} diff --git a/examples/nextjs/src/components/messages/TextMessage.tsx b/examples/nextjs/src/components/messages/TextMessage.tsx new file mode 100644 index 00000000..13ea59c2 --- /dev/null +++ b/examples/nextjs/src/components/messages/TextMessage.tsx @@ -0,0 +1,26 @@ +import clsx from 'clsx'; +import React from 'react'; + +type TextMessageProps = { + text: string; + isUser: boolean; +}; + +export function TextMessage({ text, isUser }: TextMessageProps) { + return ( +
+
+ {text} +
+
+ ); +} diff --git a/examples/nextjs/src/components/ui/Button.tsx b/examples/nextjs/src/components/ui/Button.tsx new file mode 100644 index 00000000..c1def60d --- /dev/null +++ b/examples/nextjs/src/components/ui/Button.tsx @@ -0,0 +1,55 @@ +import { Slot } from '@radix-ui/react-slot'; +import { cva, type VariantProps } from 'class-variance-authority'; +import * as React from 'react'; + +import { cn } from '@/components/ui/utils'; + +const buttonVariants = cva( + 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors cursor-pointer focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0', + { + variants: { + variant: { + ghost: + 'text-gray-800 disabled:text-gray-300 hover:bg-gray-100 hover:text-black', + primary: 'bg-black text-white hover:bg-gray-800 disabled:bg-gray-300', + outline: + 'border border-2 border-gray-100 text-gray-800 hover:bg-gray-100 hover:text-black', + stop: 'bg-red-500 text-white hover:bg-red-600 disabled:bg-red-300', + }, + size: { + default: 'h-9 px-4 py-2', + sm: 'h-8 rounded-md px-3 text-xs', + lg: 'h-10 rounded-md px-8', + icon: 'h-10 w-10 rounded-full [&_svg]:size-6', + iconSmall: 'h-8 w-8 rounded-full [&_svg]:size-6', + iconTiny: 'h-6 w-6 rounded-full', + }, + }, + defaultVariants: { + variant: 'ghost', + size: 'default', + }, + }, +); + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean; +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : 'button'; + return ( + + ); + }, +); +Button.displayName = 'Button'; + +export { Button, buttonVariants }; diff --git a/examples/nextjs/src/components/ui/utils.ts b/examples/nextjs/src/components/ui/utils.ts new file mode 100644 index 00000000..2819a830 --- /dev/null +++ b/examples/nextjs/src/components/ui/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from 'clsx'; +import { twMerge } from 'tailwind-merge'; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/examples/nextjs/src/db.ts b/examples/nextjs/src/db.ts new file mode 100644 index 00000000..f7206b6b --- /dev/null +++ b/examples/nextjs/src/db.ts @@ -0,0 +1,29 @@ +/** + * This is just a super simple in-memory database for the demo. + * In a real application, you would use a proper database that persists the data. + */ + +export class Database { + #database: Map; + + constructor() { + this.#database = new Map(); + } + + async get(key: string) { + return this.#database.get(key); + } + + async set(key: string, value: any) { + this.#database.set(key, value); + } +} + +let database: Database | undefined; + +export function db() { + if (!database) { + database = new Database(); + } + return database; +} diff --git a/examples/nextjs/tsconfig.json b/examples/nextjs/tsconfig.json new file mode 100644 index 00000000..c1334095 --- /dev/null +++ b/examples/nextjs/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/examples/nextjs/vercel.json b/examples/nextjs/vercel.json new file mode 100644 index 00000000..a667db8c --- /dev/null +++ b/examples/nextjs/vercel.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://openapi.vercel.sh/vercel.json", + "framework": "nextjs" +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dfdbc640..d218497d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -236,6 +236,61 @@ importers: specifier: ~3.25.40 version: 3.25.62 + examples/nextjs: + dependencies: + '@openai/agents': + specifier: workspace:* + version: link:../../packages/agents + '@radix-ui/react-slot': + specifier: ^1.2.3 + version: 1.2.3(@types/react@19.1.8)(react@19.1.0) + '@tanstack/react-query': + specifier: ^5.80.7 + version: 5.80.7(react@19.1.0) + class-variance-authority: + specifier: ^0.7.1 + version: 0.7.1 + clsx: + specifier: ^2.1.1 + version: 2.1.1 + next: + specifier: 15.3.2 + version: 15.3.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: + specifier: ^19.0.0 + version: 19.1.0 + react-dom: + specifier: ^19.0.0 + version: 19.1.0(react@19.1.0) + tailwind-merge: + specifier: ^3.3.0 + version: 3.3.1 + wavtools: + specifier: ^0.1.5 + version: 0.1.5 + zod: + specifier: ~3.25.40 + version: 3.25.62 + devDependencies: + '@tailwindcss/postcss': + specifier: ^4 + version: 4.1.10 + '@types/node': + specifier: ^20 + version: 20.19.0 + '@types/react': + specifier: ^19 + version: 19.1.8 + '@types/react-dom': + specifier: ^19 + version: 19.1.6(@types/react@19.1.8) + tailwindcss: + specifier: ^4 + version: 4.1.10 + typescript: + specifier: ^5 + version: 5.8.3 + examples/realtime-demo: dependencies: '@tailwindcss/vite': @@ -1703,6 +1758,14 @@ packages: peerDependencies: vite: ^5.2.0 || ^6 + '@tanstack/query-core@5.80.7': + resolution: {integrity: sha512-s09l5zeUKC8q7DCCCIkVSns8zZrK4ZDT6ryEjxNBFi68G4z2EBobBS7rdOY3r6W1WbUDpc1fe5oY+YO/+2UVUg==} + + '@tanstack/react-query@5.80.7': + resolution: {integrity: sha512-u2F0VK6+anItoEvB3+rfvTO9GEh2vb00Je05OwlUe/A0lkJBgW1HckiY3f9YZa+jx6IOe4dHPh10dyp9aY3iRQ==} + peerDependencies: + react: ^18 || ^19 + '@types/braces@3.0.5': resolution: {integrity: sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w==} @@ -2271,9 +2334,6 @@ packages: resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==} engines: {node: '>=16'} - caniuse-lite@1.0.30001707: - resolution: {integrity: sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw==} - caniuse-lite@1.0.30001722: resolution: {integrity: sha512-DCQHBBZtiK6JVkAGw7drvAMK0Q0POD/xZvEmDp6baiMMP6QXXk9HpD6mNYBZWhOPG6LvIDb82ITqtWjhDckHCA==} @@ -6175,8 +6235,8 @@ snapshots: hast-util-to-html: 9.0.5 hast-util-to-text: 4.0.2 hastscript: 9.0.1 - postcss: 8.5.3 - postcss-nested: 6.2.0(postcss@8.5.3) + postcss: 8.5.5 + postcss-nested: 6.2.0(postcss@8.5.5) unist-util-visit: 5.0.0 unist-util-visit-parents: 6.0.1 @@ -6807,7 +6867,7 @@ snapshots: '@alloc/quick-lru': 5.2.0 '@tailwindcss/node': 4.1.10 '@tailwindcss/oxide': 4.1.10 - postcss: 8.5.3 + postcss: 8.5.5 tailwindcss: 4.1.10 '@tailwindcss/vite@4.1.10(vite@6.3.5(@types/node@24.0.1)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.1)(yaml@2.7.1))': @@ -6817,6 +6877,13 @@ snapshots: tailwindcss: 4.1.10 vite: 6.3.5(@types/node@24.0.1)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.1)(yaml@2.7.1) + '@tanstack/query-core@5.80.7': {} + + '@tanstack/react-query@5.80.7(react@19.1.0)': + dependencies: + '@tanstack/query-core': 5.80.7 + react: 19.1.0 + '@types/braces@3.0.5': {} '@types/chai@5.2.2': @@ -6839,7 +6906,7 @@ snapshots: '@types/fontkit@2.0.8': dependencies: - '@types/node': 24.0.1 + '@types/node': 22.15.31 '@types/hast@3.0.4': dependencies: @@ -6867,7 +6934,7 @@ snapshots: '@types/node-fetch@2.6.12': dependencies: - '@types/node': 18.19.111 + '@types/node': 22.15.31 form-data: 4.0.2 '@types/node@12.20.55': {} @@ -6889,6 +6956,7 @@ snapshots: '@types/node@24.0.1': dependencies: undici-types: 7.8.0 + optional: true '@types/react-dom@19.1.6(@types/react@19.1.8)': dependencies: @@ -6900,7 +6968,7 @@ snapshots: '@types/sax@1.2.7': dependencies: - '@types/node': 17.0.45 + '@types/node': 22.15.31 '@types/unist@2.0.11': {} @@ -6908,7 +6976,7 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 24.0.1 + '@types/node': 22.15.31 '@typescript-eslint/eslint-plugin@8.34.0(@typescript-eslint/parser@8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)': dependencies: @@ -7615,8 +7683,6 @@ snapshots: camelcase@8.0.0: {} - caniuse-lite@1.0.30001707: {} - caniuse-lite@1.0.30001722: {} caseless@0.12.0: {} @@ -9790,7 +9856,7 @@ snapshots: '@swc/counter': 0.1.3 '@swc/helpers': 0.5.15 busboy: 1.6.0 - caniuse-lite: 1.0.30001707 + caniuse-lite: 1.0.30001722 postcss: 8.4.31 react: 19.1.0 react-dom: 19.1.0(react@19.1.0) @@ -10142,6 +10208,11 @@ snapshots: postcss: 8.5.3 postcss-selector-parser: 6.1.2 + postcss-nested@6.2.0(postcss@8.5.5): + dependencies: + postcss: 8.5.5 + postcss-selector-parser: 6.1.2 + postcss-selector-parser@6.1.2: dependencies: cssesc: 3.0.0 @@ -11198,7 +11269,8 @@ snapshots: undici-types@6.21.0: {} - undici-types@7.8.0: {} + undici-types@7.8.0: + optional: true unicode-properties@1.4.1: dependencies: From a824176feaea8f8a5316959d4524d99ddf49f020 Mon Sep 17 00:00:00 2001 From: Dominik Kundel Date: Fri, 13 Jun 2025 16:16:18 -0700 Subject: [PATCH 2/7] fix(interruptions): avoid infinite loop. add interruptions helper --- .changeset/weak-views-care.md | 5 +++++ packages/agents-core/src/run.ts | 10 ++++++++++ packages/agents-core/src/runState.ts | 10 ++++++++++ 3 files changed, 25 insertions(+) create mode 100644 .changeset/weak-views-care.md diff --git a/.changeset/weak-views-care.md b/.changeset/weak-views-care.md new file mode 100644 index 00000000..cb1dcd9f --- /dev/null +++ b/.changeset/weak-views-care.md @@ -0,0 +1,5 @@ +--- +'@openai/agents-core': patch +--- + +fix(interruptions): avoid accidental infinite loop if all interruptions were not cleared. expose interruptions helper on state diff --git a/packages/agents-core/src/run.ts b/packages/agents-core/src/run.ts index 8271033b..c451bda5 100644 --- a/packages/agents-core/src/run.ts +++ b/packages/agents-core/src/run.ts @@ -290,6 +290,12 @@ export class Runner extends RunHooks> { state._originalInput = turnResult.originalInput; state._generatedItems = turnResult.generatedItems; state._currentStep = turnResult.nextStep; + + if (turnResult.nextStep.type === 'next_step_interruption') { + // we are still in an interruption, so we need to avoid an infinite loop + return new RunResult(state); + } + continue; } @@ -641,6 +647,10 @@ export class Runner extends RunHooks> { result.state._originalInput = turnResult.originalInput; result.state._generatedItems = turnResult.generatedItems; result.state._currentStep = turnResult.nextStep; + if (turnResult.nextStep.type === 'next_step_interruption') { + // we are still in an interruption, so we need to avoid an infinite loop + return; + } continue; } diff --git a/packages/agents-core/src/runState.ts b/packages/agents-core/src/runState.ts index 980601e3..ca485ed2 100644 --- a/packages/agents-core/src/runState.ts +++ b/packages/agents-core/src/runState.ts @@ -324,6 +324,16 @@ export class RunState> { this._trace = getCurrentTrace(); } + /** + * Returns all interruptions if the current step is an interruption otherwise returns an empty array. + */ + getInterruptions() { + if (this._currentStep?.type !== 'next_step_interruption') { + return []; + } + return this._currentStep.data.interruptions; + } + /** * Approves a tool call requested by the agent through an interruption and approval item request. * From f6528acdf01a720dd979d758a0a6642d1cdd63a3 Mon Sep 17 00:00:00 2001 From: Dominik Kundel Date: Fri, 13 Jun 2025 16:17:40 -0700 Subject: [PATCH 3/7] mostly working experience --- eslint.config.mjs | 1 + examples/nextjs/components.json | 21 + examples/nextjs/package.json | 5 +- examples/nextjs/src/agents.ts | 2 + examples/nextjs/src/app/api/basic/route.ts | 95 ++++- examples/nextjs/src/app/globals.css | 122 +++++- examples/nextjs/src/app/page.tsx | 60 ++- examples/nextjs/src/components/Approvals.tsx | 145 +++++++ examples/nextjs/src/components/ui/Button.tsx | 3 + examples/nextjs/src/components/ui/dialog.tsx | 143 +++++++ examples/nextjs/src/db.ts | 10 +- examples/nextjs/src/lib/utils.ts | 6 + pnpm-lock.yaml | 419 ++++++++++++++++++- 13 files changed, 995 insertions(+), 37 deletions(-) create mode 100644 examples/nextjs/components.json create mode 100644 examples/nextjs/src/components/Approvals.tsx create mode 100644 examples/nextjs/src/components/ui/dialog.tsx create mode 100644 examples/nextjs/src/lib/utils.ts diff --git a/eslint.config.mjs b/eslint.config.mjs index f7938ad7..151a3817 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -22,6 +22,7 @@ export default tseslint.config( '**/docs/.astro/**', 'examples/realtime-next/**', 'examples/realtime-demo/**', + 'examples/nextjs/**', 'integration-tests//**', ]), eslint.configs.recommended, diff --git a/examples/nextjs/components.json b/examples/nextjs/components.json new file mode 100644 index 00000000..3289f237 --- /dev/null +++ b/examples/nextjs/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} diff --git a/examples/nextjs/package.json b/examples/nextjs/package.json index 6af729c1..f9fed44d 100644 --- a/examples/nextjs/package.json +++ b/examples/nextjs/package.json @@ -10,14 +10,16 @@ }, "dependencies": { "@openai/agents": "workspace:*", + "@radix-ui/react-dialog": "^1.1.14", "@radix-ui/react-slot": "^1.2.3", "@tanstack/react-query": "^5.80.7", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "lucide-react": "^0.515.0", "next": "15.3.2", "react": "^19.0.0", "react-dom": "^19.0.0", - "tailwind-merge": "^3.3.0", + "tailwind-merge": "^3.3.1", "wavtools": "^0.1.5", "zod": "~3.25.40" }, @@ -27,6 +29,7 @@ "@types/react": "^19", "@types/react-dom": "^19", "tailwindcss": "^4", + "tw-animate-css": "^1.3.4", "typescript": "^5" } } diff --git a/examples/nextjs/src/agents.ts b/examples/nextjs/src/agents.ts index 84a648d1..0bb4950c 100644 --- a/examples/nextjs/src/agents.ts +++ b/examples/nextjs/src/agents.ts @@ -10,6 +10,8 @@ const getWeather = tool({ execute: async ({ city }) => { return `The weather in ${city} is sunny.`; }, + + needsApproval: true, }); export const agent = new Agent({ diff --git a/examples/nextjs/src/app/api/basic/route.ts b/examples/nextjs/src/app/api/basic/route.ts index cf22b480..e5346564 100644 --- a/examples/nextjs/src/app/api/basic/route.ts +++ b/examples/nextjs/src/app/api/basic/route.ts @@ -1,23 +1,104 @@ import { NextRequest, NextResponse } from 'next/server'; +import { randomUUID } from 'node:crypto'; import { agent } from '@/agents'; -import { run } from '@openai/agents'; +import { + AgentInputItem, + Runner, + RunState, + RunToolApprovalItem, +} from '@openai/agents'; +import { db } from '@/db'; + +function generateConversationId() { + return `conv_${randomUUID().replace(/-/g, '').slice(0, 24)}`; +} export async function POST(req: NextRequest) { try { - const { messages } = await req.json(); - const input = messages ?? []; - const result = await run(agent, input); - // Example: echo back the received data + const data = await req.json(); + let { messages, conversationId, decisions } = data; + + if (!messages) { + messages = []; + } + + if (!conversationId) { + conversationId = generateConversationId(); + } + + if (!decisions) { + decisions = null; + } + + const runner = new Runner({ + groupId: conversationId, + }); + + let input: AgentInputItem[] | RunState; + if ( + Object.keys(decisions).length > 0 && + data.conversationId /* original conversationId */ + ) { + const stateString = await db().get(data.conversationId); + + if (!stateString) { + return NextResponse.json( + { error: 'Conversation not found' }, + { status: 404 }, + ); + } + + const state = await RunState.fromString(agent, stateString); + + const interruptions = state.getInterruptions(); + + console.log('interruptions', interruptions); + console.log('decisions', decisions); + interruptions.forEach((item: RunToolApprovalItem) => { + if (item.type === 'tool_approval_item' && 'callId' in item.rawItem) { + const callId = item.rawItem.callId; + + if (decisions[callId] === 'approved') { + state.approve(item); + } else if (decisions[callId] === 'rejected') { + state.reject(item); + } + } + }); + + input = state; + } else { + input = messages; + } + + const result = await runner.run(agent, input); + + if (result.interruptions.length > 0) { + // We need to handle the interruptions here. + + // store the state in the database + await db().set(conversationId, JSON.stringify(result.state)); + + return NextResponse.json({ + conversationId, + approvals: result.interruptions + .filter((item) => item.type === 'tool_approval_item') + .map((item) => item.toJSON()), + history: result.history, + }); + } + return NextResponse.json({ response: result.finalOutput, history: result.history, + conversationId, }); } catch (error) { console.error(error); return NextResponse.json( - { error: 'Invalid request body' }, - { status: 400 }, + { error: 'Internal server error' }, + { status: 500 }, ); } } diff --git a/examples/nextjs/src/app/globals.css b/examples/nextjs/src/app/globals.css index 37d72f8a..31ec2d75 100644 --- a/examples/nextjs/src/app/globals.css +++ b/examples/nextjs/src/app/globals.css @@ -1,26 +1,122 @@ @import 'tailwindcss'; +@import 'tw-animate-css'; -:root { - --background: #ffffff; - --foreground: #171717; -} +@custom-variant dark (&:is(.dark *)); @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); --font-sans: var(--font-geist-sans); --font-mono: var(--font-geist-mono); + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar); + --color-chart-5: var(--chart-5); + --color-chart-4: var(--chart-4); + --color-chart-3: var(--chart-3); + --color-chart-2: var(--chart-2); + --color-chart-1: var(--chart-1); + --color-ring: var(--ring); + --color-input: var(--input); + --color-border: var(--border); + --color-destructive: var(--destructive); + --color-accent-foreground: var(--accent-foreground); + --color-accent: var(--accent); + --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-primary-foreground: var(--primary-foreground); + --color-primary: var(--primary); + --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); } -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; - } +:root { + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); } -body { - background: var(--background); - color: var(--foreground); - font-family: Arial, Helvetica, sans-serif; +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } } diff --git a/examples/nextjs/src/app/page.tsx b/examples/nextjs/src/app/page.tsx index c8d50dbf..74d8ab00 100644 --- a/examples/nextjs/src/app/page.tsx +++ b/examples/nextjs/src/app/page.tsx @@ -1,15 +1,32 @@ 'use client'; -import type { AgentInputItem } from '@openai/agents'; +import type { AgentInputItem, RunToolApprovalItem } from '@openai/agents'; import { useState } from 'react'; import { App } from '@/components/App'; +import { Approvals } from '@/components/Approvals'; export default function Home() { const [history, setHistory] = useState([]); + const [conversationId, setConversationId] = useState(null); + const [approvals, setApprovals] = useState< + ReturnType[] + >([]); + + async function makeRequest({ + message, + decisions, + }: { + message?: string; + decisions?: Map; + }) { + const messages = [...history]; + + if (message) { + messages.push({ type: 'message', role: 'user', content: message }); + } - const handleSend = async (message: string) => { setHistory([ - ...history, + ...messages, { type: 'message', role: 'assistant', @@ -21,16 +38,39 @@ export default function Home() { const response = await fetch('/api/basic', { method: 'POST', body: JSON.stringify({ - messages: [ - ...history, - { type: 'message', role: 'user', content: message }, - ], + messages, + conversationId, + decisions: Object.fromEntries(decisions ?? []), }), }); + const data = await response.json(); - console.log(data); - setHistory(data.history); + + if (data.conversationId) { + setConversationId(data.conversationId); + } + + if (data.history) { + setHistory(data.history); + } + + if (data.approvals) { + setApprovals(data.approvals); + } + } + + const handleSend = async (message: string) => { + await makeRequest({ message }); }; - return ; + async function handleDone(decisions: Map) { + await makeRequest({ decisions }); + } + + return ( + <> + + + + ); } diff --git a/examples/nextjs/src/components/Approvals.tsx b/examples/nextjs/src/components/Approvals.tsx new file mode 100644 index 00000000..b64ed2da --- /dev/null +++ b/examples/nextjs/src/components/Approvals.tsx @@ -0,0 +1,145 @@ +import type { RunToolApprovalItem } from '@openai/agents'; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { Button } from './ui/Button'; +import { useEffect, useState } from 'react'; + +type Item = ReturnType; + +function ToolApprovalEntry({ + approval, + onApprove, + onReject, + decision, +}: { + approval: Item; + onApprove: () => void; + onReject: () => void; + decision: 'approved' | 'rejected' | undefined; +}) { + return ( +
+

+ Tool {approval.rawItem?.name} +

+
+        {approval.rawItem?.arguments}
+      
+ {decision === undefined && ( +
+ + +
+ )} + {decision === 'approved' && ( +

✔︎ Approved

+ )} + {decision === 'rejected' && ( +

✖︎ Rejected

+ )} +
+ ); +} + +export function Approvals({ + approvals, + onDone, +}: { + approvals: ReturnType[]; + onDone: (decisions: Map) => void; +}) { + const [decisions, setDecisions] = useState< + Map + >(new Map()); + const [isOpen, setIsOpen] = useState(approvals.length > 0); + + useEffect(() => { + setDecisions(new Map()); + if (approvals.length > 0) { + setIsOpen(true); + } + }, [approvals]); + + function handleApprove(approval: Item) { + if (approval.rawItem?.id) { + setDecisions((prev) => { + const newDecisions = new Map(prev); + newDecisions.set(approval.rawItem?.callId ?? '', 'approved'); + return newDecisions; + }); + } + } + + function handleReject(approval: Item) { + if (approval.rawItem?.id) { + setDecisions((prev) => { + const newDecisions = new Map(prev); + newDecisions.set(approval.rawItem?.callId ?? '', 'rejected'); + return newDecisions; + }); + } + } + + function handleDone() { + onDone(decisions); + setIsOpen(false); + } + + if (approvals.length === 0) { + return null; + } + + const agentName = approvals[0].agent.name; + + return ( + + + + Approval required + + The agent {agentName} is requesting approval for the following + action{approvals.length > 1 ? 's' : ''}: + + +
+ {approvals + .filter( + (item) => + item.type === 'tool_approval_item' && + item.rawItem?.type === 'function_call', + ) + .map((approval) => ( + handleApprove(approval)} + onReject={() => handleReject(approval)} + /> + ))} +
+ + + +
+
+ ); +} diff --git a/examples/nextjs/src/components/ui/Button.tsx b/examples/nextjs/src/components/ui/Button.tsx index c1def60d..b0e648fb 100644 --- a/examples/nextjs/src/components/ui/Button.tsx +++ b/examples/nextjs/src/components/ui/Button.tsx @@ -12,6 +12,8 @@ const buttonVariants = cva( ghost: 'text-gray-800 disabled:text-gray-300 hover:bg-gray-100 hover:text-black', primary: 'bg-black text-white hover:bg-gray-800 disabled:bg-gray-300', + secondary: + 'bg-gray-100 text-gray-800 hover:bg-gray-200 disabled:bg-gray-300', outline: 'border border-2 border-gray-100 text-gray-800 hover:bg-gray-100 hover:text-black', stop: 'bg-red-500 text-white hover:bg-red-600 disabled:bg-red-300', @@ -19,6 +21,7 @@ const buttonVariants = cva( size: { default: 'h-9 px-4 py-2', sm: 'h-8 rounded-md px-3 text-xs', + smRounded: 'h-8 rounded-full px-3 text-xs', lg: 'h-10 rounded-md px-8', icon: 'h-10 w-10 rounded-full [&_svg]:size-6', iconSmall: 'h-8 w-8 rounded-full [&_svg]:size-6', diff --git a/examples/nextjs/src/components/ui/dialog.tsx b/examples/nextjs/src/components/ui/dialog.tsx new file mode 100644 index 00000000..f6c4e9fa --- /dev/null +++ b/examples/nextjs/src/components/ui/dialog.tsx @@ -0,0 +1,143 @@ +'use client'; + +import * as React from 'react'; +import * as DialogPrimitive from '@radix-ui/react-dialog'; +import { XIcon } from 'lucide-react'; + +import { cn } from '@/lib/utils'; + +function Dialog({ + ...props +}: React.ComponentProps) { + return ; +} + +function DialogTrigger({ + ...props +}: React.ComponentProps) { + return ; +} + +function DialogPortal({ + ...props +}: React.ComponentProps) { + return ; +} + +function DialogClose({ + ...props +}: React.ComponentProps) { + return ; +} + +function DialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DialogContent({ + className, + children, + showCloseButton = true, + ...props +}: React.ComponentProps & { + showCloseButton?: boolean; +}) { + return ( + + + + {children} + {showCloseButton && ( + + + Close + + )} + + + ); +} + +function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +function DialogFooter({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +function DialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +}; diff --git a/examples/nextjs/src/db.ts b/examples/nextjs/src/db.ts index f7206b6b..8d504284 100644 --- a/examples/nextjs/src/db.ts +++ b/examples/nextjs/src/db.ts @@ -3,8 +3,8 @@ * In a real application, you would use a proper database that persists the data. */ -export class Database { - #database: Map; +export class Database { + #database: Map; constructor() { this.#database = new Map(); @@ -14,16 +14,16 @@ export class Database { return this.#database.get(key); } - async set(key: string, value: any) { + async set(key: string, value: Value) { this.#database.set(key, value); } } -let database: Database | undefined; +let database: Database | undefined; export function db() { if (!database) { - database = new Database(); + database = new Database(); } return database; } diff --git a/examples/nextjs/src/lib/utils.ts b/examples/nextjs/src/lib/utils.ts new file mode 100644 index 00000000..2819a830 --- /dev/null +++ b/examples/nextjs/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from 'clsx'; +import { twMerge } from 'tailwind-merge'; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d218497d..ab4ee2da 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -241,6 +241,9 @@ importers: '@openai/agents': specifier: workspace:* version: link:../../packages/agents + '@radix-ui/react-dialog': + specifier: ^1.1.14 + version: 1.1.14(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@radix-ui/react-slot': specifier: ^1.2.3 version: 1.2.3(@types/react@19.1.8)(react@19.1.0) @@ -253,6 +256,9 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 + lucide-react: + specifier: ^0.515.0 + version: 0.515.0(react@19.1.0) next: specifier: 15.3.2 version: 15.3.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -263,7 +269,7 @@ importers: specifier: ^19.0.0 version: 19.1.0(react@19.1.0) tailwind-merge: - specifier: ^3.3.0 + specifier: ^3.3.1 version: 3.3.1 wavtools: specifier: ^0.1.5 @@ -287,6 +293,9 @@ importers: tailwindcss: specifier: ^4 version: 4.1.10 + tw-animate-css: + specifier: ^1.3.4 + version: 1.3.4 typescript: specifier: ^5 version: 5.8.3 @@ -1471,6 +1480,9 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@radix-ui/primitive@1.1.2': + resolution: {integrity: sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==} + '@radix-ui/react-compose-refs@1.1.2': resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} peerDependencies: @@ -1480,6 +1492,111 @@ packages: '@types/react': optional: true + '@radix-ui/react-context@1.1.2': + resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dialog@1.1.14': + resolution: {integrity: sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-dismissable-layer@1.1.10': + resolution: {integrity: sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-focus-guards@1.1.2': + resolution: {integrity: sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-focus-scope@1.1.7': + resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-id@1.1.1': + resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-portal@1.1.9': + resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-presence@1.1.4': + resolution: {integrity: sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.1.3': + resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-slot@1.2.3': resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} peerDependencies: @@ -1489,6 +1606,51 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-callback-ref@1.1.1': + resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.2.2': + resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-effect-event@0.0.2': + resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-escape-keydown@1.1.1': + resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.1': + resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@rollup/pluginutils@5.1.4': resolution: {integrity: sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==} engines: {node: '>=14.0.0'} @@ -2133,6 +2295,10 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} + engines: {node: '>=10'} + aria-query@5.3.2: resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} engines: {node: '>= 0.4'} @@ -2629,6 +2795,9 @@ packages: resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} engines: {node: '>=8'} + detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + deterministic-object-hash@2.0.2: resolution: {integrity: sha512-KxektNH63SrbfUyDiwXqRb1rLwKt33AmMv+5Nhsw1kqZ13SJBRTgZHtGbE+hH3a1mVW1cz+4pqSWVPAtLVXTzQ==} engines: {node: '>=18'} @@ -3113,6 +3282,10 @@ packages: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} + get-nonce@1.0.1: + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} + get-proto@1.0.1: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} @@ -3706,6 +3879,11 @@ packages: resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} engines: {node: '>=12'} + lucide-react@0.515.0: + resolution: {integrity: sha512-Sy7bY0MeicRm2pzrnoHm2h6C1iVoeHyBU2fjdQDsXGP51fhkhau1/ZV/dzrcxEmAKsxYb6bGaIsMnGHuQ5s0dw==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + lunr@2.3.9: resolution: {integrity: sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==} @@ -4540,6 +4718,36 @@ packages: peerDependencies: react: ^19.1.0 + react-remove-scroll-bar@2.3.8: + resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-remove-scroll@2.7.1: + resolution: {integrity: sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react-style-singleton@2.2.3: + resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + react@19.1.0: resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==} engines: {node: '>=0.10.0'} @@ -5189,6 +5397,9 @@ packages: tunnel-agent@0.6.0: resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + tw-animate-css@1.3.4: + resolution: {integrity: sha512-dd1Ht6/YQHcNbq0znIT6dG8uhO7Ce+VIIhZUhjsryXsMPJQz3bZg7Q2eNzLwipb25bRZslGb2myio5mScd1TFg==} + tweetnacl@0.14.5: resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==} @@ -5395,6 +5606,26 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + use-callback-ref@1.3.3: + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sidecar@1.1.3: + resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -6621,12 +6852,108 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true + '@radix-ui/primitive@1.1.2': {} + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.1.8)(react@19.1.0)': dependencies: react: 19.1.0 optionalDependencies: '@types/react': 19.1.8 + '@radix-ui/react-context@1.1.2(@types/react@19.1.8)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.8 + + '@radix-ui/react-dialog@1.1.14(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.8)(react@19.1.0) + '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-focus-guards': 1.1.2(@types/react@19.1.8)(react@19.1.0) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.8)(react@19.1.0) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-slot': 1.2.3(@types/react@19.1.8)(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.8)(react@19.1.0) + aria-hidden: 1.2.6 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react-remove-scroll: 2.7.1(@types/react@19.1.8)(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.8 + '@types/react-dom': 19.1.6(@types/react@19.1.8) + + '@radix-ui/react-dismissable-layer@1.1.10(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.8)(react@19.1.0) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.1.8)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.8 + '@types/react-dom': 19.1.6(@types/react@19.1.8) + + '@radix-ui/react-focus-guards@1.1.2(@types/react@19.1.8)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.8 + + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.8)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.8 + '@types/react-dom': 19.1.6(@types/react@19.1.8) + + '@radix-ui/react-id@1.1.1(@types/react@19.1.8)(react@19.1.0)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.8)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.8 + + '@radix-ui/react-portal@1.1.9(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.8)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.8 + '@types/react-dom': 19.1.6(@types/react@19.1.8) + + '@radix-ui/react-presence@1.1.4(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.8)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.8 + '@types/react-dom': 19.1.6(@types/react@19.1.8) + + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-slot': 1.2.3(@types/react@19.1.8)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.8 + '@types/react-dom': 19.1.6(@types/react@19.1.8) + '@radix-ui/react-slot@1.2.3(@types/react@19.1.8)(react@19.1.0)': dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.0) @@ -6634,6 +6961,40 @@ snapshots: optionalDependencies: '@types/react': 19.1.8 + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.1.8)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.8 + + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.1.8)(react@19.1.0)': + dependencies: + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.1.8)(react@19.1.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.8)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.8 + + '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.1.8)(react@19.1.0)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.8)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.8 + + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.1.8)(react@19.1.0)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.8)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.8 + + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.1.8)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.8 + '@rollup/pluginutils@5.1.4(rollup@4.38.0)': dependencies: '@types/estree': 1.0.8 @@ -7379,6 +7740,10 @@ snapshots: argparse@2.0.1: {} + aria-hidden@1.2.6: + dependencies: + tslib: 2.8.1 + aria-query@5.3.2: {} array-flatten@1.1.1: {} @@ -7926,6 +8291,8 @@ snapshots: detect-libc@2.0.4: {} + detect-node-es@1.1.0: {} + deterministic-object-hash@2.0.2: dependencies: base-64: 1.0.0 @@ -8574,6 +8941,8 @@ snapshots: hasown: 2.0.2 math-intrinsics: 1.1.0 + get-nonce@1.0.1: {} + get-proto@1.0.1: dependencies: dunder-proto: 1.0.1 @@ -9265,6 +9634,10 @@ snapshots: lru-cache@7.18.3: {} + lucide-react@0.515.0(react@19.1.0): + dependencies: + react: 19.1.0 + lunr@2.3.9: {} magic-string@0.30.17: @@ -10326,6 +10699,33 @@ snapshots: react: 19.1.0 scheduler: 0.26.0 + react-remove-scroll-bar@2.3.8(@types/react@19.1.8)(react@19.1.0): + dependencies: + react: 19.1.0 + react-style-singleton: 2.2.3(@types/react@19.1.8)(react@19.1.0) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.1.8 + + react-remove-scroll@2.7.1(@types/react@19.1.8)(react@19.1.0): + dependencies: + react: 19.1.0 + react-remove-scroll-bar: 2.3.8(@types/react@19.1.8)(react@19.1.0) + react-style-singleton: 2.2.3(@types/react@19.1.8)(react@19.1.0) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@19.1.8)(react@19.1.0) + use-sidecar: 1.1.3(@types/react@19.1.8)(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.8 + + react-style-singleton@2.2.3(@types/react@19.1.8)(react@19.1.0): + dependencies: + get-nonce: 1.0.1 + react: 19.1.0 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.1.8 + react@19.1.0: {} read-cache@1.0.0: @@ -11204,6 +11604,8 @@ snapshots: dependencies: safe-buffer: 5.2.1 + tw-animate-css@1.3.4: {} + tweetnacl@0.14.5: {} typanion@3.14.0: {} @@ -11378,6 +11780,21 @@ snapshots: dependencies: punycode: 2.3.1 + use-callback-ref@1.3.3(@types/react@19.1.8)(react@19.1.0): + dependencies: + react: 19.1.0 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.1.8 + + use-sidecar@1.1.3(@types/react@19.1.8)(react@19.1.0): + dependencies: + detect-node-es: 1.1.0 + react: 19.1.0 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.1.8 + util-deprecate@1.0.2: {} utils-merge@1.0.1: {} From 413232733f1ad4d30d6126be85bf50bce44d9b91 Mon Sep 17 00:00:00 2001 From: Dominik Kundel Date: Fri, 13 Jun 2025 16:21:57 -0700 Subject: [PATCH 4/7] fix(interruptions): avoid double counting tool calls --- .changeset/dark-melons-go.md | 5 +++++ packages/agents-core/src/run.ts | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 .changeset/dark-melons-go.md diff --git a/.changeset/dark-melons-go.md b/.changeset/dark-melons-go.md new file mode 100644 index 00000000..194ee765 --- /dev/null +++ b/.changeset/dark-melons-go.md @@ -0,0 +1,5 @@ +--- +'@openai/agents-core': patch +--- + +fix(interruptions): avoid double outputting function calls for approval requests diff --git a/packages/agents-core/src/run.ts b/packages/agents-core/src/run.ts index c451bda5..49fde567 100644 --- a/packages/agents-core/src/run.ts +++ b/packages/agents-core/src/run.ts @@ -179,7 +179,9 @@ export function getTurnInput( originalInput: string | AgentInputItem[], generatedItems: RunItem[], ): AgentInputItem[] { - const rawItems = generatedItems.map((item) => item.rawItem); + const rawItems = generatedItems + .filter((item) => item.type !== 'tool_approval_item') // don't include approval items to avoid double function calls + .map((item) => item.rawItem); if (typeof originalInput === 'string') { originalInput = [{ type: 'message', role: 'user', content: originalInput }]; From 50f442829be9938fb30cb19b7a87ff5edaa1ce8e Mon Sep 17 00:00:00 2001 From: Dominik Kundel Date: Fri, 13 Jun 2025 16:44:23 -0700 Subject: [PATCH 5/7] add some comments --- examples/nextjs/src/app/api/basic/route.ts | 16 ++- examples/nextjs/src/app/page.tsx | 13 ++ examples/nextjs/src/components/App.tsx | 10 +- examples/nextjs/src/components/Approvals.tsx | 6 + examples/nextjs/src/components/History.tsx | 120 ++++++++++++------ .../src/components/messages/FunctionCall.tsx | 30 +++-- 6 files changed, 143 insertions(+), 52 deletions(-) diff --git a/examples/nextjs/src/app/api/basic/route.ts b/examples/nextjs/src/app/api/basic/route.ts index e5346564..9af50520 100644 --- a/examples/nextjs/src/app/api/basic/route.ts +++ b/examples/nextjs/src/app/api/basic/route.ts @@ -24,6 +24,8 @@ export async function POST(req: NextRequest) { } if (!conversationId) { + // we will generate a conversation ID so we can keep track of the state in case of conversations + // this is just a key that we can use to store information in the database conversationId = generateConversationId(); } @@ -40,6 +42,7 @@ export async function POST(req: NextRequest) { Object.keys(decisions).length > 0 && data.conversationId /* original conversationId */ ) { + // If we receive a new request with decisions, we will look up the current state in the database const stateString = await db().get(data.conversationId); if (!stateString) { @@ -49,13 +52,13 @@ export async function POST(req: NextRequest) { ); } + // We then deserialize the state so we can manipulate it and continue the run const state = await RunState.fromString(agent, stateString); const interruptions = state.getInterruptions(); - console.log('interruptions', interruptions); - console.log('decisions', decisions); interruptions.forEach((item: RunToolApprovalItem) => { + // For each interruption, we will then check if the decision is to approve or reject the tool call if (item.type === 'tool_approval_item' && 'callId' in item.rawItem) { const callId = item.rawItem.callId; @@ -67,19 +70,26 @@ export async function POST(req: NextRequest) { } }); + // We will use the new updated state to continue the run input = state; } else { + // If we don't have any decisions, we will just assume this is a regular chat and use the messages + // as input for the next run input = messages; } const result = await runner.run(agent, input); if (result.interruptions.length > 0) { - // We need to handle the interruptions here. + // If the run resulted in one or more interruptions, we will store the current state in the database // store the state in the database await db().set(conversationId, JSON.stringify(result.state)); + // We will return all the interruptions as approval requests to the UI/client so it can generate + // the UI for approvals + // We will also still return the history that contains the tool calls and potentially any interim + // text response the agent might have generated (like announcing that it's calling a function) return NextResponse.json({ conversationId, approvals: result.interruptions diff --git a/examples/nextjs/src/app/page.tsx b/examples/nextjs/src/app/page.tsx index 74d8ab00..7c0e754a 100644 --- a/examples/nextjs/src/app/page.tsx +++ b/examples/nextjs/src/app/page.tsx @@ -27,6 +27,7 @@ export default function Home() { setHistory([ ...messages, + // This is just a placeholder to show on the UI to show the agent is working { type: 'message', role: 'assistant', @@ -35,6 +36,8 @@ export default function Home() { }, ]); + // We will send the messages to the API route along with the conversation ID if we have one + // and the decisions if we had any approvals in this turn const response = await fetch('/api/basic', { method: 'POST', body: JSON.stringify({ @@ -56,6 +59,8 @@ export default function Home() { if (data.approvals) { setApprovals(data.approvals); + } else { + setApprovals([]); } } @@ -70,6 +75,14 @@ export default function Home() { return ( <> + {/** + * If we have any approvals, we will show the approvals component to allow the user to + * approve or reject the tool calls. If we don't have any approvals, we will just show the + * history. Once all the approvals are done, we will call the handleDone function to continue + * the run. What kind of UI you render to show approval requests is up to you. You could also + * render them as part of the chat history. We are rendering them separately here to show + * that it can be an entirely different UI. + */} ); diff --git a/examples/nextjs/src/components/App.tsx b/examples/nextjs/src/components/App.tsx index 90a56f2d..9d84b3dd 100644 --- a/examples/nextjs/src/components/App.tsx +++ b/examples/nextjs/src/components/App.tsx @@ -1,7 +1,7 @@ import type { AgentInputItem } from '@openai/agents'; import { History } from '@/components/History'; import { Button } from '@/components/ui/Button'; -import { useState } from 'react'; +import { useState, useRef, useEffect } from 'react'; import ArrowUpIcon from './icons/ArrowUpIcon'; export type AppProps = { @@ -13,6 +13,13 @@ export type AppProps = { export function App({ title = 'Agent Demo', history, onSend }: AppProps) { const [message, setMessage] = useState(''); const [isLoading, setIsLoading] = useState(false); + const inputRef = useRef(null); + + useEffect(() => { + if (!isLoading) { + inputRef.current?.focus(); + } + }, [isLoading]); const handleSend = async () => { if (!message.trim()) return; @@ -56,6 +63,7 @@ export function App({ title = 'Agent Demo', history, onSend }: AppProps) { placeholder="Ask me anything..." onChange={(e) => setMessage(e.target.value)} disabled={isLoading} + ref={inputRef} />