diff --git a/.changeset/wide-results-invite.md b/.changeset/wide-results-invite.md new file mode 100644 index 0000000..efba40e --- /dev/null +++ b/.changeset/wide-results-invite.md @@ -0,0 +1,7 @@ +--- +'@openai/agents-realtime': patch +'@openai/agents-core': patch +--- + +- Add safeToRunInBrowser property to FunctionTool inteface +- Add init option validation to detect local agents as tools in web browser to RealtimeAgent diff --git a/packages/agents-core/src/agent.ts b/packages/agents-core/src/agent.ts index 01b05f5..96d24d1 100644 --- a/packages/agents-core/src/agent.ts +++ b/packages/agents-core/src/agent.ts @@ -217,6 +217,11 @@ export interface AgentConfiguration< * to `true`. This ensures that the agent doesn't enter an infinite loop of tool usage. */ resetToolChoice: boolean; + + /** + * Whether the agent is safe to run in a browser environment. Defaults to false. + */ + safeToRunInBrowser?: boolean; } export type AgentOptions< @@ -314,6 +319,7 @@ export class Agent< outputType: TOutput = 'text' as TOutput; toolUseBehavior: ToolUseBehavior; resetToolChoice: boolean; + safeToRunInBrowser: boolean; constructor(config: AgentOptions) { super(); @@ -335,6 +341,7 @@ export class Agent< } this.toolUseBehavior = config.toolUseBehavior ?? 'run_llm_again'; this.resetToolChoice = config.resetToolChoice ?? true; + this.safeToRunInBrowser = config.safeToRunInBrowser ?? false; // --- Runtime warning for handoff output type compatibility --- if ( @@ -437,6 +444,7 @@ export class Agent< additionalProperties: false, }, strict: true, + safeToRunInBrowser: this.safeToRunInBrowser, execute: async (data, context) => { if (!isAgentToolInput(data)) { throw new ModelBehaviorError('Agent tool called with invalid input'); diff --git a/packages/agents-core/src/tool.ts b/packages/agents-core/src/tool.ts index 8e7f001..b378db0 100644 --- a/packages/agents-core/src/tool.ts +++ b/packages/agents-core/src/tool.ts @@ -73,6 +73,11 @@ export type FunctionTool< * program has to resolve by approving or rejecting the tool call. */ needsApproval: ToolApprovalFunction; + + /** + * Whether the tool is safe to run in a browser. If true, the tool can be called from a browser. Default: false + */ + safeToRunInBrowser?: boolean; }; /** @@ -326,6 +331,11 @@ type StrictToolOptions< * program has to resolve by approving or rejecting the tool call. */ needsApproval?: boolean | ToolApprovalFunction; + + /** + * Whether the tool is safe to run in a browser. If true, the tool can be called from a browser. Default: false + */ + safeToRunInBrowser?: boolean; }; /** @@ -373,6 +383,11 @@ type NonStrictToolOptions< * program has to resolve by approving or rejecting the tool call. */ needsApproval?: boolean | ToolApprovalFunction; + + /** + * Whether the tool is safe to run in a browser. If true, the tool can be called from a browser. Default: false + */ + safeToRunInBrowser?: boolean; }; /** @@ -497,5 +512,6 @@ export function tool< strict: strictMode, invoke, needsApproval, + safeToRunInBrowser: options.safeToRunInBrowser, }; } diff --git a/packages/agents-realtime/src/realtimeAgent.ts b/packages/agents-realtime/src/realtimeAgent.ts index dc6476f..e35ca62 100644 --- a/packages/agents-realtime/src/realtimeAgent.ts +++ b/packages/agents-realtime/src/realtimeAgent.ts @@ -4,8 +4,10 @@ import { Handoff, TextOutput, UnknownContext, + UserError, } from '@openai/agents-core'; import { RealtimeContextData } from './realtimeSession'; +import { isBrowserEnvironment } from '@openai/agents-core/_shims'; export type RealtimeAgentConfiguration = Partial< Omit< @@ -72,6 +74,15 @@ export class RealtimeAgent extends Agent< readonly voice: string; constructor(config: RealtimeAgentConfiguration) { + if (isBrowserEnvironment() && config.tools) { + for (const tool of config.tools) { + if (tool.type === 'function' && tool.safeToRunInBrowser !== true) { + throw new UserError( + `Local agent as a tool detected: ${tool.name}. Please use a tool that makes requests to your server-side agent logic, rather than converting a locally running client-side agent into a tool. If the agent is safe to run with sufficient security measures (e.g., custom proxy / authentication for model requests), set the agent's safeToRunInBrowser option to true.`, + ); + } + } + } super( config as AgentConfiguration, TextOutput>, ); diff --git a/packages/agents-realtime/test/realtimeAgent.test.ts b/packages/agents-realtime/test/realtimeAgent.test.ts new file mode 100644 index 0000000..f582a62 --- /dev/null +++ b/packages/agents-realtime/test/realtimeAgent.test.ts @@ -0,0 +1,69 @@ +import { describe, it, expect, vi } from 'vitest'; + +import { RealtimeAgent } from '../src/realtimeAgent'; +import { Agent } from '@openai/agents-core'; + +const mockResponses = [true, true, false]; +vi.mock('@openai/agents-core/_shims', async (importOriginal) => { + return { + ...(await importOriginal()), + isBrowserEnvironment: vi.fn(() => mockResponses.shift() ?? true), + }; +}); + +describe('RealtimeAgent', () => { + it('detects local agents as tools (browser)', async () => { + const localToolAgent = new Agent({ + name: 'local_agent', + instructions: 'You are a local agent', + }); + expect(() => { + new RealtimeAgent({ + name: 'A', + tools: [ + localToolAgent.asTool({ + toolName: 'local_agent tool', + toolDescription: 'You are a local agent', + }), + ], + }); + }).toThrowError( + "Local agent as a tool detected: local_agent_tool. Please use a tool that makes requests to your server-side agent logic, rather than converting a locally running client-side agent into a tool. If the agent is safe to run with sufficient security measures (e.g., custom proxy / authentication for model requests), set the agent's safeToRunInBrowser option to true.", + ); + }); + + it('accepts safeToRunInBrowser agents as tools (browser)', async () => { + const localToolAgent = new Agent({ + name: 'local_agent', + instructions: 'You are a local agent', + safeToRunInBrowser: true, + }); + const realtimeAgent = new RealtimeAgent({ + name: 'A', + tools: [ + localToolAgent.asTool({ + toolName: 'local_agent tool', + toolDescription: 'You are a local agent', + }), + ], + }); + expect(realtimeAgent).toBeDefined(); + }); + + it('does not detect local agents as tools (server)', () => { + const localToolAgent = new Agent({ + name: 'local_agent', + instructions: 'You are a local agent', + }); + const realtimeAgent = new RealtimeAgent({ + name: 'A', + tools: [ + localToolAgent.asTool({ + toolName: 'local_agent tool', + toolDescription: 'You are a local agent', + }), + ], + }); + expect(realtimeAgent).toBeDefined(); + }); +});