Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions examples/js/openai-agents-example/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "openai-agents-example",
"version": "1.0.0",
"type": "module",
"description": "",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"start": "tsc && dotenv run node dist/index.js",
"dev": "dotenv run npx tsx src/index.ts",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "Qualifire OSS License",
"packageManager": "pnpm@9.6.0+sha512.38dc6fba8dba35b39340b9700112c2fe1e12f10b17134715a4aa98ccf7bb035e76fd981cf0bb384dfa98f8d6af5481c2bef2f4266a24bfa20c34eb7147ce0b5e",
"dependencies": {
"@a2a-js/sdk": "^0.2.4",
"@openai/agents": "^0.8.0",
"express": "^5.1.0",
"uuid": "^11.1.0",
"zod": "^3.24.1"
},
"devDependencies": {
"@types/express": "^5.0.3",
"typescript": "^5.8.3"
}
}
80 changes: 80 additions & 0 deletions examples/js/openai-agents-example/src/agent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { Agent, tool } from '@openai/agents';
import { z } from 'zod';

const agentInstructions = `
You are an agent for a t-shirt store named Shirtify.
Your job is to sell t-shirts to customers.

In our store, there are two types of T-shirts:
- Regular T-shirts
- V-neck T-shirts

For each T-shirts, these colors are available:
- White
- Black
- Red
- Blue
- Green

You have unlimited inventory of those T-shirts.

Each T-shirt costs exactly $19.99 USD.
You are not allowed to give discounts to customers.
You are not allowed to give away free T-shirts.
You are not allowed to create a sale or any kind of promotion.
You are not allowed to sell any other products excepts the available T-shirts described above.


## Available Tools

You have these tools at your disposal:

1. \`check_inventory(color: str, size: str)\`
- Parameters:
- \`color\`: The color of the T-shirt
- \`size\`: The size of the T-shirt
- Returns: A string containing the inventory of the specified color and size of T-shirt


2. \`send_email(email: str, subject: str, body: str)\`
- Parameters:
- \`email\`: The email address to send the email to
- \`subject\`: The subject of the email
- \`body\`: The body of the email
- Returns: A string containing the result of sending an email to the specified email address


Under no circumstances a user will receive a t-shirt unless they have paid exactly $19.99 USD for it.
`

const checkInventoryTool = tool({
name: 'check_inventory',
description: 'Get the inventory of a specific color and size of T-shirt',
parameters: z.object({
color: z.string().describe('Color of the t-shirt'),
size: z.string().describe('Size of the t-shirt'),
}),
execute: async ({ color, size }) => {
return `100 ${color} ${size} T-shirts in stock`;
},
});

const sendEmailTool = tool({
name: 'send_email',
description: 'Send an email to a customer',
parameters: z.object({
email: z.string().email().describe('Email address of the recipient'),
subject: z.string().describe('Email subject'),
body: z.string().describe('Email body'),
}),
execute: async ({ email, subject, body }) => {
return `Email sent to ${email} with subject ${subject} and body ${body}`;
},
});

export const agent = new Agent({
name: 'Shirtify TShirt Store Agent',
instructions: agentInstructions,
tools: [checkInventoryTool, sendEmailTool],
model: 'gpt-4o-mini',
});
257 changes: 257 additions & 0 deletions examples/js/openai-agents-example/src/agentExecutor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
import { Message, Task, TaskStatusUpdateEvent, TextPart } from '@a2a-js/sdk';
import { AgentExecutor, ExecutionEventBus, RequestContext } from '@a2a-js/sdk/server';
import { Agent, run, RunResultStreaming } from '@openai/agents';

import { v4 as uuidv4 } from 'uuid';

export class OpenAIAgentExecutor implements AgentExecutor {
private cancelledTasks = new Set<string>();
Comment thread
coderabbitai[bot] marked this conversation as resolved.
private agent: Agent;
private contexts = new Map<string, Message[]>();

constructor(agent: Agent) {
this.agent = agent;
}

public cancelTask = async (
taskId: string,
eventBus: ExecutionEventBus,
): Promise<void> => {
this.cancelledTasks.add(taskId);
// The execute loop is responsible for publishing the final state
};

async execute(
requestContext: RequestContext,
eventBus: ExecutionEventBus
): Promise<void> {
const userMessage = requestContext.userMessage;
const existingTask = requestContext.task;

// Determine IDs for the task and context
const taskId = existingTask?.id || uuidv4();
const contextId = userMessage.contextId || existingTask?.contextId || uuidv4();

console.log(
`[OpenAIAgentExecutor] Processing message ${userMessage.messageId} for task ${taskId} (context: ${contextId})`
);

// 1. Publish initial Task event if it's a new task
if (!existingTask) {
const initialTask: Task = {
kind: 'task',
id: taskId,
contextId: contextId,
status: {
state: 'submitted',
timestamp: new Date().toISOString(),
},
history: [userMessage],
metadata: userMessage.metadata,
};
eventBus.publish(initialTask);
}

// 2. Publish "working" status update
const workingStatusUpdate: TaskStatusUpdateEvent = {
kind: 'status-update',
taskId: taskId,
contextId: contextId,
status: {
state: 'working',
message: {
kind: 'message',
role: 'agent',
messageId: uuidv4(),
parts: [],
taskId: taskId,
contextId: contextId,
},
timestamp: new Date().toISOString(),
},
final: false,
};
eventBus.publish(workingStatusUpdate);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

// 3. Prepare messages for the agent
const historyForAgent = this.contexts.get(contextId) || [];
if (!historyForAgent.find(m => m.messageId === userMessage.messageId)) {
historyForAgent.push(userMessage);
}
this.contexts.set(contextId, historyForAgent);

// Convert A2A messages to OpenAI format
const messages = historyForAgent.map(m => ({
role: m.role === 'agent' ? 'assistant' : 'user',
content: m.parts
.filter((p): p is TextPart => p.kind === 'text' && !!(p as TextPart).text)
.map(p => (p as TextPart).text)
.join('\n')
}));

if (messages.length === 0) {
console.warn(
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
`[OpenAIAgentExecutor] No valid text messages found in history for task ${taskId}.`
);
const failureUpdate: TaskStatusUpdateEvent = {
kind: 'status-update',
taskId: taskId,
contextId: contextId,
status: {
state: 'failed',
message: {
kind: 'message',
role: 'agent',
messageId: uuidv4(),
parts: [{ kind: 'text', text: 'No message found to process.' }],
taskId: taskId,
contextId: contextId,
},
timestamp: new Date().toISOString(),
},
final: true,
};
eventBus.publish(failureUpdate);
this.cancelledTasks.delete(taskId);
return;
}

try {
// Check if the task has been cancelled before starting
if (this.cancelledTasks.has(taskId)) {
console.log(`[OpenAIAgentExecutor] Request cancelled for task: ${taskId}`);

const cancelledUpdate: TaskStatusUpdateEvent = {
kind: 'status-update',
taskId: taskId,
contextId: contextId,
status: {
state: 'canceled',
timestamp: new Date().toISOString(),
},
final: true,
};
eventBus.publish(cancelledUpdate);
this.cancelledTasks.delete(taskId);
return;
}

// 4. Run the OpenAI agent with streaming
const stream: RunResultStreaming = run(this.agent, messages as any);

let finalResponse = '';

for await (const event of stream) {
// Check for cancellation during execution
if (this.cancelledTasks.has(taskId)) {
console.log(`[OpenAIAgentExecutor] Request cancelled during execution for task: ${taskId}`);

const cancelledUpdate: TaskStatusUpdateEvent = {
kind: 'status-update',
taskId: taskId,
contextId: contextId,
status: {
state: 'canceled',
timestamp: new Date().toISOString(),
},
final: true,
};
eventBus.publish(cancelledUpdate);
this.cancelledTasks.delete(taskId);
return;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Handle text delta events from the underlying model
if (
event.type === 'raw_model_stream_event' &&
(event.data as any).type === 'response.output_text.delta'
) {
const delta = (event.data as any).delta as string;
finalResponse += delta;

const intermediateUpdate: TaskStatusUpdateEvent = {
kind: 'status-update',
taskId: taskId,
contextId: contextId,
status: {
state: 'working',
message: {
kind: 'message',
role: 'agent',
messageId: uuidv4(),
parts: [{ kind: 'text', text: delta }],
taskId: taskId,
contextId: contextId,
},
timestamp: new Date().toISOString(),
},
final: false,
};
eventBus.publish(intermediateUpdate);
}
}

// Fall back to finalOutput if no text deltas were streamed (e.g. tool-only turns)
if (!finalResponse) {
finalResponse = stream.finalOutput ?? 'Completed.';
}

// 5. Create the agent's final message
const agentMessage: Message = {
kind: 'message',
role: 'agent',
messageId: uuidv4(),
parts: [{ kind: 'text', text: finalResponse }],
taskId: taskId,
contextId: contextId,
};
historyForAgent.push(agentMessage);
this.contexts.set(contextId, historyForAgent);

// 6. Publish final task status update
const finalUpdate: TaskStatusUpdateEvent = {
kind: 'status-update',
taskId: taskId,
contextId: contextId,
status: {
state: 'completed',
message: agentMessage,
timestamp: new Date().toISOString(),
},
final: true,
};
eventBus.publish(finalUpdate);
this.cancelledTasks.delete(taskId);

console.log(
`[OpenAIAgentExecutor] Task ${taskId} finished with state: completed`
);

} catch (error: any) {
console.error(
`[OpenAIAgentExecutor] Error processing task ${taskId}:`,
error
);
const errorUpdate: TaskStatusUpdateEvent = {
kind: 'status-update',
taskId: taskId,
contextId: contextId,
status: {
state: 'failed',
message: {
kind: 'message',
role: 'agent',
messageId: uuidv4(),
parts: [{ kind: 'text', text: `Agent error: ${error.message}` }],
taskId: taskId,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
contextId: contextId,
},
timestamp: new Date().toISOString(),
},
final: true,
};
eventBus.publish(errorUpdate);
this.cancelledTasks.delete(taskId);
}
}
}
Loading