UNPKG

@openai/agents-core

Version:

The OpenAI Agents SDK is a lightweight yet powerful framework for building multi-agent workflows.

623 lines 24.3 kB
import { z } from 'zod'; import { Agent } from "./agent.mjs"; import { RunMessageOutputItem, RunToolApprovalItem, RunToolCallItem, RunToolCallOutputItem, RunReasoningItem, RunHandoffCallItem, RunHandoffOutputItem, } from "./items.mjs"; import { RunContext } from "./runContext.mjs"; import { getTurnInput } from "./run.mjs"; import { AgentToolUseTracker, nextStepSchema, } from "./runImplementation.mjs"; import { SystemError, UserError } from "./errors.mjs"; import { getGlobalTraceProvider } from "./tracing/provider.mjs"; import { Usage } from "./usage.mjs"; import { getCurrentTrace } from "./tracing/index.mjs"; import logger from "./logger.mjs"; import { handoff } from "./handoff.mjs"; import * as protocol from "./types/protocol.mjs"; import { safeExecute } from "./utils/safeExecute.mjs"; /** * The schema version of the serialized run state. This is used to ensure that the serialized * run state is compatible with the current version of the SDK. * If anything in this schema changes, the version will have to be incremented. */ export const CURRENT_SCHEMA_VERSION = '1.0'; const $schemaVersion = z.literal(CURRENT_SCHEMA_VERSION); const serializedAgentSchema = z.object({ name: z.string(), }); const serializedSpanBase = z.object({ object: z.literal('trace.span'), id: z.string(), trace_id: z.string(), parent_id: z.string().nullable(), started_at: z.string().nullable(), ended_at: z.string().nullable(), error: z .object({ message: z.string(), data: z.record(z.string(), z.any()).optional(), }) .nullable(), span_data: z.record(z.string(), z.any()), }); const SerializedSpan = serializedSpanBase.extend({ previous_span: z.lazy(() => SerializedSpan).optional(), }); const usageSchema = z.object({ requests: z.number(), inputTokens: z.number(), outputTokens: z.number(), totalTokens: z.number(), }); const modelResponseSchema = z.object({ usage: usageSchema, output: z.array(protocol.OutputModelItem), responseId: z.string().optional(), providerData: z.record(z.string(), z.any()).optional(), }); const itemSchema = z.discriminatedUnion('type', [ z.object({ type: z.literal('message_output_item'), rawItem: protocol.AssistantMessageItem, agent: serializedAgentSchema, }), z.object({ type: z.literal('tool_call_item'), rawItem: protocol.ToolCallItem.or(protocol.HostedToolCallItem), agent: serializedAgentSchema, }), z.object({ type: z.literal('tool_call_output_item'), rawItem: protocol.FunctionCallResultItem, agent: serializedAgentSchema, output: z.string(), }), z.object({ type: z.literal('reasoning_item'), rawItem: protocol.ReasoningItem, agent: serializedAgentSchema, }), z.object({ type: z.literal('handoff_call_item'), rawItem: protocol.FunctionCallItem, agent: serializedAgentSchema, }), z.object({ type: z.literal('handoff_output_item'), rawItem: protocol.FunctionCallResultItem, sourceAgent: serializedAgentSchema, targetAgent: serializedAgentSchema, }), z.object({ type: z.literal('tool_approval_item'), rawItem: protocol.FunctionCallItem.or(protocol.HostedToolCallItem), agent: serializedAgentSchema, }), ]); const serializedTraceSchema = z.object({ object: z.literal('trace'), id: z.string(), workflow_name: z.string(), group_id: z.string().nullable(), metadata: z.record(z.string(), z.any()), }); const serializedProcessedResponseSchema = z.object({ newItems: z.array(itemSchema), toolsUsed: z.array(z.string()), handoffs: z.array(z.object({ toolCall: z.any(), handoff: z.any(), })), functions: z.array(z.object({ toolCall: z.any(), tool: z.any(), })), computerActions: z.array(z.object({ toolCall: z.any(), computer: z.any(), })), mcpApprovalRequests: z .array(z.object({ requestItem: z.object({ // protocol.HostedToolCallItem rawItem: z.object({ type: z.literal('hosted_tool_call'), name: z.string(), arguments: z.string().optional(), status: z.string().optional(), output: z.string().optional(), // this always exists but marked as optional for early version compatibility; when releasing 1.0, we can remove the nullable and optional providerData: z.record(z.string(), z.any()).nullable().optional(), }), }), // HostedMCPTool mcpTool: z.object({ type: z.literal('hosted_tool'), name: z.literal('hosted_mcp'), providerData: z.record(z.string(), z.any()), }), })) .optional(), }); const guardrailFunctionOutputSchema = z.object({ tripwireTriggered: z.boolean(), outputInfo: z.any(), }); const inputGuardrailResultSchema = z.object({ guardrail: z.object({ type: z.literal('input'), name: z.string(), }), output: guardrailFunctionOutputSchema, }); const outputGuardrailResultSchema = z.object({ guardrail: z.object({ type: z.literal('output'), name: z.string(), }), agentOutput: z.any(), agent: serializedAgentSchema, output: guardrailFunctionOutputSchema, }); export const SerializedRunState = z.object({ $schemaVersion, currentTurn: z.number(), currentAgent: serializedAgentSchema, originalInput: z.string().or(z.array(protocol.ModelItem)), modelResponses: z.array(modelResponseSchema), context: z.object({ usage: usageSchema, approvals: z.record(z.string(), z.object({ approved: z.array(z.string()).or(z.boolean()), rejected: z.array(z.string()).or(z.boolean()), })), context: z.record(z.string(), z.any()), }), toolUseTracker: z.record(z.string(), z.array(z.string())), maxTurns: z.number(), currentAgentSpan: SerializedSpan.nullable().optional(), noActiveAgentRun: z.boolean(), inputGuardrailResults: z.array(inputGuardrailResultSchema), outputGuardrailResults: z.array(outputGuardrailResultSchema), currentStep: nextStepSchema.optional(), lastModelResponse: modelResponseSchema.optional(), generatedItems: z.array(itemSchema), lastProcessedResponse: serializedProcessedResponseSchema.optional(), currentTurnPersistedItemCount: z.number().int().min(0).optional(), trace: serializedTraceSchema.nullable(), }); /** * Serializable snapshot of an agent's run, including context, usage and trace. * While this class has publicly writable properties (prefixed with `_`), they are not meant to be * used directly. To read these properties, use the `RunResult` instead. * * Manipulation of the state directly can lead to unexpected behavior and should be avoided. * Instead, use the `approve` and `reject` methods to interact with the state. */ export class RunState { /** * Current turn number in the conversation. */ _currentTurn = 0; /** * The agent currently handling the conversation. */ _currentAgent; /** * Original user input prior to any processing. */ _originalInput; /** * Responses from the model so far. */ _modelResponses; /** * Active tracing span for the current agent if tracing is enabled. */ _currentAgentSpan; /** * Run context tracking approvals, usage, and other metadata. */ _context; /** * Tracks what tools each agent has used. */ _toolUseTracker; /** * Items generated by the agent during the run. */ _generatedItems; /** * Number of `_generatedItems` already flushed to session storage for the current turn. * * Persisting the entire turn on every save would duplicate responses and tool outputs. * Instead, `saveToSession` appends only the delta since the previous write. This counter * tracks how many generated run items from *this turn* were already written so the next * save can slice off only the new entries. When a turn is interrupted (e.g., awaiting tool * approval) and later resumed, we rewind the counter before continuing so the pending tool * output still gets stored. */ _currentTurnPersistedItemCount; /** * Maximum allowed turns before forcing termination. */ _maxTurns; /** * Whether the run has an active agent step in progress. */ _noActiveAgentRun = true; /** * Last model response for the previous turn. */ _lastTurnResponse; /** * Results from input guardrails applied to the run. */ _inputGuardrailResults; /** * Results from output guardrails applied to the run. */ _outputGuardrailResults; /** * Next step computed for the agent to take. */ _currentStep = undefined; /** * Parsed model response after applying guardrails and tools. */ _lastProcessedResponse = undefined; /** * Trace associated with this run if tracing is enabled. */ _trace = null; constructor(context, originalInput, startingAgent, maxTurns) { this._context = context; this._originalInput = structuredClone(originalInput); this._modelResponses = []; this._currentAgentSpan = undefined; this._currentAgent = startingAgent; this._toolUseTracker = new AgentToolUseTracker(); this._generatedItems = []; this._currentTurnPersistedItemCount = 0; this._maxTurns = maxTurns; this._inputGuardrailResults = []; this._outputGuardrailResults = []; this._trace = getCurrentTrace(); } /** * The history of the agent run. This includes the input items and the new items generated during the run. * * This can be used as inputs for the next agent run. */ get history() { return getTurnInput(this._originalInput, this._generatedItems); } /** * 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. * * To approve the request use this method and then run the agent again with the same state object * to continue the execution. * * By default it will only approve the current tool call. To allow the tool to be used multiple * times throughout the run, set the `alwaysApprove` option to `true`. * * @param approvalItem - The tool call approval item to approve. * @param options - Options for the approval. */ approve(approvalItem, options = { alwaysApprove: false }) { this._context.approveTool(approvalItem, options); } /** * Rejects a tool call requested by the agent through an interruption and approval item request. * * To reject the request use this method and then run the agent again with the same state object * to continue the execution. * * By default it will only reject the current tool call. To allow the tool to be used multiple * times throughout the run, set the `alwaysReject` option to `true`. * * @param approvalItem - The tool call approval item to reject. * @param options - Options for the rejection. */ reject(approvalItem, options = { alwaysReject: false }) { this._context.rejectTool(approvalItem, options); } /** * Serializes the run state to a JSON object. * * This method is used to serialize the run state to a JSON object that can be used to * resume the run later. * * @returns The serialized run state. */ toJSON() { const output = { $schemaVersion: CURRENT_SCHEMA_VERSION, currentTurn: this._currentTurn, currentAgent: { name: this._currentAgent.name, }, originalInput: this._originalInput, modelResponses: this._modelResponses.map((response) => { return { usage: { requests: response.usage.requests, inputTokens: response.usage.inputTokens, outputTokens: response.usage.outputTokens, totalTokens: response.usage.totalTokens, }, output: response.output, responseId: response.responseId, providerData: response.providerData, }; }), context: this._context.toJSON(), toolUseTracker: this._toolUseTracker.toJSON(), maxTurns: this._maxTurns, currentAgentSpan: this._currentAgentSpan?.toJSON(), noActiveAgentRun: this._noActiveAgentRun, inputGuardrailResults: this._inputGuardrailResults, outputGuardrailResults: this._outputGuardrailResults.map((r) => ({ ...r, agent: r.agent.toJSON(), })), currentStep: this._currentStep, lastModelResponse: this._lastTurnResponse, generatedItems: this._generatedItems.map((item) => item.toJSON()), currentTurnPersistedItemCount: this._currentTurnPersistedItemCount, lastProcessedResponse: this._lastProcessedResponse, trace: this._trace ? this._trace.toJSON() : null, }; // parsing the schema to ensure the output is valid for reparsing const parsed = SerializedRunState.safeParse(output); if (!parsed.success) { throw new SystemError(`Failed to serialize run state. ${parsed.error.message}`); } return parsed.data; } /** * Serializes the run state to a string. * * This method is used to serialize the run state to a string that can be used to * resume the run later. * * @returns The serialized run state. */ toString() { return JSON.stringify(this.toJSON()); } /** * Deserializes a run state from a string. * * This method is used to deserialize a run state from a string that was serialized using the * `toString` method. */ static async fromString(initialAgent, str) { const [parsingError, jsonResult] = await safeExecute(() => JSON.parse(str)); if (parsingError) { throw new UserError(`Failed to parse run state. ${parsingError instanceof Error ? parsingError.message : String(parsingError)}`); } const currentSchemaVersion = jsonResult.$schemaVersion; if (!currentSchemaVersion) { throw new UserError('Run state is missing schema version'); } if (currentSchemaVersion !== CURRENT_SCHEMA_VERSION) { throw new UserError(`Run state schema version ${currentSchemaVersion} is not supported. Please use version ${CURRENT_SCHEMA_VERSION}`); } const stateJson = SerializedRunState.parse(JSON.parse(str)); const agentMap = buildAgentMap(initialAgent); // // Rebuild the context // const context = new RunContext(stateJson.context.context); context._rebuildApprovals(stateJson.context.approvals); // // Find the current agent from the initial agent // const currentAgent = agentMap.get(stateJson.currentAgent.name); if (!currentAgent) { throw new UserError(`Agent ${stateJson.currentAgent.name} not found`); } const state = new RunState(context, '', currentAgent, stateJson.maxTurns); state._currentTurn = stateJson.currentTurn; // rebuild tool use tracker state._toolUseTracker = new AgentToolUseTracker(); for (const [agentName, toolNames] of Object.entries(stateJson.toolUseTracker)) { state._toolUseTracker.addToolUse(agentMap.get(agentName), toolNames); } // rebuild current agent span if (stateJson.currentAgentSpan) { if (!stateJson.trace) { logger.warn('Trace is not set, skipping tracing setup'); } const trace = getGlobalTraceProvider().createTrace({ traceId: stateJson.trace?.id, name: stateJson.trace?.workflow_name, groupId: stateJson.trace?.group_id ?? undefined, metadata: stateJson.trace?.metadata, }); state._currentAgentSpan = deserializeSpan(trace, stateJson.currentAgentSpan); state._trace = trace; } state._noActiveAgentRun = stateJson.noActiveAgentRun; state._inputGuardrailResults = stateJson.inputGuardrailResults; state._outputGuardrailResults = stateJson.outputGuardrailResults.map((r) => ({ ...r, agent: agentMap.get(r.agent.name), })); state._currentStep = stateJson.currentStep; state._originalInput = stateJson.originalInput; state._modelResponses = stateJson.modelResponses.map(deserializeModelResponse); state._lastTurnResponse = stateJson.lastModelResponse ? deserializeModelResponse(stateJson.lastModelResponse) : undefined; state._generatedItems = stateJson.generatedItems.map((item) => deserializeItem(item, agentMap)); state._currentTurnPersistedItemCount = stateJson.currentTurnPersistedItemCount ?? 0; state._lastProcessedResponse = stateJson.lastProcessedResponse ? await deserializeProcessedResponse(agentMap, state._currentAgent, state._context, stateJson.lastProcessedResponse) : undefined; if (stateJson.currentStep?.type === 'next_step_handoff') { state._currentStep = { type: 'next_step_handoff', newAgent: agentMap.get(stateJson.currentStep.newAgent.name), }; } return state; } } /** * @internal */ export function buildAgentMap(initialAgent) { const map = new Map(); const queue = [initialAgent]; while (queue.length > 0) { const currentAgent = queue.shift(); if (map.has(currentAgent.name)) { continue; } map.set(currentAgent.name, currentAgent); for (const handoff of currentAgent.handoffs) { if (handoff instanceof Agent) { if (!map.has(handoff.name)) { queue.push(handoff); } } else if (handoff.agent) { if (!map.has(handoff.agent.name)) { queue.push(handoff.agent); } } } } return map; } /** * @internal */ export function deserializeSpan(trace, serializedSpan) { const spanData = serializedSpan.span_data; const previousSpan = serializedSpan.previous_span ? deserializeSpan(trace, serializedSpan.previous_span) : undefined; const span = getGlobalTraceProvider().createSpan({ spanId: serializedSpan.id, traceId: serializedSpan.trace_id, parentId: serializedSpan.parent_id ?? undefined, startedAt: serializedSpan.started_at ?? undefined, endedAt: serializedSpan.ended_at ?? undefined, data: spanData, }, trace); span.previousSpan = previousSpan; return span; } /** * @internal */ export function deserializeModelResponse(serializedModelResponse) { const usage = new Usage(); usage.requests = serializedModelResponse.usage.requests; usage.inputTokens = serializedModelResponse.usage.inputTokens; usage.outputTokens = serializedModelResponse.usage.outputTokens; usage.totalTokens = serializedModelResponse.usage.totalTokens; return { usage, output: serializedModelResponse.output.map((item) => protocol.OutputModelItem.parse(item)), responseId: serializedModelResponse.responseId, providerData: serializedModelResponse.providerData, }; } /** * @internal */ export function deserializeItem(serializedItem, agentMap) { switch (serializedItem.type) { case 'message_output_item': return new RunMessageOutputItem(serializedItem.rawItem, agentMap.get(serializedItem.agent.name)); case 'tool_call_item': return new RunToolCallItem(serializedItem.rawItem, agentMap.get(serializedItem.agent.name)); case 'tool_call_output_item': return new RunToolCallOutputItem(serializedItem.rawItem, agentMap.get(serializedItem.agent.name), serializedItem.output); case 'reasoning_item': return new RunReasoningItem(serializedItem.rawItem, agentMap.get(serializedItem.agent.name)); case 'handoff_call_item': return new RunHandoffCallItem(serializedItem.rawItem, agentMap.get(serializedItem.agent.name)); case 'handoff_output_item': return new RunHandoffOutputItem(serializedItem.rawItem, agentMap.get(serializedItem.sourceAgent.name), agentMap.get(serializedItem.targetAgent.name)); case 'tool_approval_item': return new RunToolApprovalItem(serializedItem.rawItem, agentMap.get(serializedItem.agent.name)); } } /** * @internal */ async function deserializeProcessedResponse(agentMap, currentAgent, context, serializedProcessedResponse) { const allTools = await currentAgent.getAllTools(context); const tools = new Map(allTools .filter((tool) => tool.type === 'function') .map((tool) => [tool.name, tool])); const computerTools = new Map(allTools .filter((tool) => tool.type === 'computer') .map((tool) => [tool.name, tool])); const handoffs = new Map(currentAgent.handoffs.map((entry) => { if (entry instanceof Agent) { return [entry.name, handoff(entry)]; } return [entry.toolName, entry]; })); const result = { newItems: serializedProcessedResponse.newItems.map((item) => deserializeItem(item, agentMap)), toolsUsed: serializedProcessedResponse.toolsUsed, handoffs: serializedProcessedResponse.handoffs.map((handoff) => { if (!handoffs.has(handoff.handoff.toolName)) { throw new UserError(`Handoff ${handoff.handoff.toolName} not found`); } return { toolCall: handoff.toolCall, handoff: handoffs.get(handoff.handoff.toolName), }; }), functions: await Promise.all(serializedProcessedResponse.functions.map(async (functionCall) => { if (!tools.has(functionCall.tool.name)) { throw new UserError(`Tool ${functionCall.tool.name} not found`); } return { toolCall: functionCall.toolCall, tool: tools.get(functionCall.tool.name), }; })), computerActions: serializedProcessedResponse.computerActions.map((computerAction) => { const toolName = computerAction.computer.name; if (!computerTools.has(toolName)) { throw new UserError(`Computer tool ${toolName} not found`); } return { toolCall: computerAction.toolCall, computer: computerTools.get(toolName), }; }), mcpApprovalRequests: (serializedProcessedResponse.mcpApprovalRequests ?? []).map((approvalRequest) => ({ requestItem: new RunToolApprovalItem(approvalRequest.requestItem .rawItem, currentAgent), mcpTool: approvalRequest.mcpTool, })), }; return { ...result, hasToolsOrApprovalsToRun() { return (result.handoffs.length > 0 || result.functions.length > 0 || result.mcpApprovalRequests.length > 0 || result.computerActions.length > 0); }, }; } //# sourceMappingURL=runState.mjs.map