@openai/agents-core
Version:
The OpenAI Agents SDK is a lightweight yet powerful framework for building multi-agent workflows.
549 lines • 29.1 kB
JavaScript
import { Agent } from "./agent.js";
import { defineInputGuardrail, defineOutputGuardrail, } from "./guardrail.js";
import { getHandoff } from "./handoff.js";
import { getDefaultModelProvider } from "./providers.js";
import { RunContext } from "./runContext.js";
import { RunResult, StreamedRunResult } from "./result.js";
import { RunHooks } from "./lifecycle.js";
import logger from "./logger.js";
import { serializeTool, serializeHandoff } from "./utils/serialize.js";
import { GuardrailExecutionError, InputGuardrailTripwireTriggered, MaxTurnsExceededError, ModelBehaviorError, OutputGuardrailTripwireTriggered, UserError, } from "./errors.js";
import { addStepToRunResult, executeInterruptedToolsAndSideEffects, executeToolsAndSideEffects, maybeResetToolChoice, processModelResponse, } from "./runImplementation.js";
import { getOrCreateTrace, resetCurrentSpan, setCurrentSpan, withNewSpanContext, withTrace, } from "./tracing/context.js";
import { createAgentSpan, withGuardrailSpan } from "./tracing/index.js";
import { Usage } from "./usage.js";
import { RunAgentUpdatedStreamEvent, RunRawModelStreamEvent } from "./events.js";
import { RunState } from "./runState.js";
import { StreamEventResponseCompleted } from "./types/protocol.js";
import { convertAgentOutputTypeToSerializable } from "./utils/tools.js";
const DEFAULT_MAX_TURNS = 10;
/**
* @internal
*/
export function getTracing(tracingDisabled, traceIncludeSensitiveData) {
if (tracingDisabled) {
return false;
}
if (traceIncludeSensitiveData) {
return true;
}
return 'enabled_without_data';
}
export function getTurnInput(originalInput, generatedItems) {
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 }];
}
return [...originalInput, ...rawItems];
}
/**
* A Runner is responsible for running an agent workflow.
*/
export class Runner extends RunHooks {
config;
inputGuardrailDefs;
outputGuardrailDefs;
constructor(config = {}) {
super();
this.config = {
modelProvider: config.modelProvider ?? getDefaultModelProvider(),
model: config.model,
modelSettings: config.modelSettings,
handoffInputFilter: config.handoffInputFilter,
inputGuardrails: config.inputGuardrails,
outputGuardrails: config.outputGuardrails,
tracingDisabled: config.tracingDisabled ?? false,
traceIncludeSensitiveData: config.traceIncludeSensitiveData ?? true,
workflowName: config.workflowName ?? 'Agent workflow',
traceId: config.traceId,
groupId: config.groupId,
traceMetadata: config.traceMetadata,
};
this.inputGuardrailDefs = (config.inputGuardrails ?? []).map(defineInputGuardrail);
this.outputGuardrailDefs = (config.outputGuardrails ?? []).map(defineOutputGuardrail);
}
/**
* @internal
*/
async #runIndividualNonStream(startingAgent, input, options) {
return withNewSpanContext(async () => {
// if we have a saved state we use that one, otherwise we create a new one
const state = input instanceof RunState
? input
: new RunState(options.context instanceof RunContext
? options.context
: new RunContext(options.context), input, startingAgent, options.maxTurns ?? DEFAULT_MAX_TURNS);
try {
while (true) {
let model = selectModel(state._currentAgent.model, this.config.model);
if (typeof model === 'string') {
model = await this.config.modelProvider.getModel(model);
}
// if we don't have a current step, we treat this as a new run
state._currentStep = state._currentStep ?? {
type: 'next_step_run_again',
};
if (state._currentStep.type === 'next_step_interruption') {
logger.debug('Continuing from interruption');
if (!state._lastTurnResponse || !state._lastProcessedResponse) {
throw new UserError('No model response found in previous state', state);
}
const turnResult = await executeInterruptedToolsAndSideEffects(state._currentAgent, state._originalInput, state._generatedItems, state._lastTurnResponse, state._lastProcessedResponse, this, state);
state._toolUseTracker.addToolUse(state._currentAgent, state._lastProcessedResponse.toolsUsed);
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;
}
if (state._currentStep.type === 'next_step_run_again') {
const handoffs = [];
if (state._currentAgent.handoffs) {
// While this array usually must not be undefined,
// we've added this check to prevent unexpected runtime errors like https://github.com/openai/openai-agents-js/issues/138
handoffs.push(...state._currentAgent.handoffs.map(getHandoff));
}
if (!state._currentAgentSpan) {
const handoffNames = handoffs.map((h) => h.agentName);
state._currentAgentSpan = createAgentSpan({
data: {
name: state._currentAgent.name,
handoffs: handoffNames,
output_type: state._currentAgent.outputSchemaName,
},
});
state._currentAgentSpan.start();
setCurrentSpan(state._currentAgentSpan);
}
const tools = await state._currentAgent.getAllTools(state._context);
const serializedTools = tools.map((t) => serializeTool(t));
const serializedHandoffs = handoffs.map((h) => serializeHandoff(h));
if (state._currentAgentSpan) {
state._currentAgentSpan.spanData.tools = tools.map((t) => t.name);
}
state._currentTurn++;
if (state._currentTurn > state._maxTurns) {
state._currentAgentSpan?.setError({
message: 'Max turns exceeded',
data: { max_turns: state._maxTurns },
});
throw new MaxTurnsExceededError(`Max turns (${state._maxTurns}) exceeded`, state);
}
logger.debug(`Running agent ${state._currentAgent.name} (turn ${state._currentTurn})`);
if (state._currentTurn === 1) {
await this.#runInputGuardrails(state);
}
const turnInput = getTurnInput(state._originalInput, state._generatedItems);
if (state._noActiveAgentRun) {
state._currentAgent.emit('agent_start', state._context, state._currentAgent);
this.emit('agent_start', state._context, state._currentAgent);
}
let modelSettings = {
...this.config.modelSettings,
...state._currentAgent.modelSettings,
};
modelSettings = maybeResetToolChoice(state._currentAgent, state._toolUseTracker, modelSettings);
state._lastTurnResponse = await model.getResponse({
systemInstructions: await state._currentAgent.getSystemPrompt(state._context),
prompt: await state._currentAgent.getPrompt(state._context),
input: turnInput,
previousResponseId: options.previousResponseId,
modelSettings,
tools: serializedTools,
outputType: convertAgentOutputTypeToSerializable(state._currentAgent.outputType),
handoffs: serializedHandoffs,
tracing: getTracing(this.config.tracingDisabled, this.config.traceIncludeSensitiveData),
signal: options.signal,
});
state._modelResponses.push(state._lastTurnResponse);
state._context.usage.add(state._lastTurnResponse.usage);
state._noActiveAgentRun = false;
const processedResponse = processModelResponse(state._lastTurnResponse, state._currentAgent, tools, handoffs);
state._lastProcessedResponse = processedResponse;
const turnResult = await executeToolsAndSideEffects(state._currentAgent, state._originalInput, state._generatedItems, state._lastTurnResponse, state._lastProcessedResponse, this, state);
state._toolUseTracker.addToolUse(state._currentAgent, state._lastProcessedResponse.toolsUsed);
state._originalInput = turnResult.originalInput;
state._generatedItems = turnResult.generatedItems;
state._currentStep = turnResult.nextStep;
}
if (state._currentStep &&
state._currentStep.type === 'next_step_final_output') {
await this.#runOutputGuardrails(state, state._currentStep.output);
this.emit('agent_end', state._context, state._currentAgent, state._currentStep.output);
state._currentAgent.emit('agent_end', state._context, state._currentStep.output);
return new RunResult(state);
}
else if (state._currentStep &&
state._currentStep.type === 'next_step_handoff') {
state._currentAgent = state._currentStep.newAgent;
if (state._currentAgentSpan) {
state._currentAgentSpan.end();
resetCurrentSpan();
state._currentAgentSpan = undefined;
}
state._noActiveAgentRun = true;
// we've processed the handoff, so we need to run the loop again
state._currentStep = { type: 'next_step_run_again' };
}
else if (state._currentStep &&
state._currentStep.type === 'next_step_interruption') {
// interrupted. Don't run any guardrails
return new RunResult(state);
}
else {
logger.debug('Running next loop');
}
}
}
catch (err) {
if (state._currentAgentSpan) {
state._currentAgentSpan.setError({
message: 'Error in agent run',
data: { error: String(err) },
});
}
throw err;
}
finally {
if (state._currentAgentSpan) {
if (state._currentStep?.type !== 'next_step_interruption') {
// don't end the span if the run was interrupted
state._currentAgentSpan.end();
}
resetCurrentSpan();
}
}
});
}
async #runInputGuardrails(state) {
const guardrails = this.inputGuardrailDefs.concat(state._currentAgent.inputGuardrails.map(defineInputGuardrail));
if (guardrails.length > 0) {
const guardrailArgs = {
agent: state._currentAgent,
input: state._originalInput,
context: state._context,
};
try {
const results = await Promise.all(guardrails.map(async (guardrail) => {
return withGuardrailSpan(async (span) => {
const result = await guardrail.run(guardrailArgs);
span.spanData.triggered = result.output.tripwireTriggered;
return result;
}, { data: { name: guardrail.name } }, state._currentAgentSpan);
}));
for (const result of results) {
if (result.output.tripwireTriggered) {
if (state._currentAgentSpan) {
state._currentAgentSpan.setError({
message: 'Guardrail tripwire triggered',
data: { guardrail: result.guardrail.name },
});
}
throw new InputGuardrailTripwireTriggered(`Input guardrail triggered: ${JSON.stringify(result.output.outputInfo)}`, result, state);
}
}
}
catch (e) {
if (e instanceof InputGuardrailTripwireTriggered) {
throw e;
}
// roll back the current turn to enable reruns
state._currentTurn--;
throw new GuardrailExecutionError(`Input guardrail failed to complete: ${e}`, e, state);
}
}
}
async #runOutputGuardrails(state, output) {
const guardrails = this.outputGuardrailDefs.concat(state._currentAgent.outputGuardrails.map(defineOutputGuardrail));
if (guardrails.length > 0) {
const agentOutput = state._currentAgent.processFinalOutput(output);
const guardrailArgs = {
agent: state._currentAgent,
agentOutput,
context: state._context,
details: { modelResponse: state._lastTurnResponse },
};
try {
const results = await Promise.all(guardrails.map(async (guardrail) => {
return withGuardrailSpan(async (span) => {
const result = await guardrail.run(guardrailArgs);
span.spanData.triggered = result.output.tripwireTriggered;
return result;
}, { data: { name: guardrail.name } }, state._currentAgentSpan);
}));
for (const result of results) {
if (result.output.tripwireTriggered) {
if (state._currentAgentSpan) {
state._currentAgentSpan.setError({
message: 'Guardrail tripwire triggered',
data: { guardrail: result.guardrail.name },
});
}
throw new OutputGuardrailTripwireTriggered(`Output guardrail triggered: ${JSON.stringify(result.output.outputInfo)}`, result, state);
}
}
}
catch (e) {
if (e instanceof OutputGuardrailTripwireTriggered) {
throw e;
}
throw new GuardrailExecutionError(`Output guardrail failed to complete: ${e}`, e, state);
}
}
}
/**
* @internal
*/
async #runStreamLoop(result, options) {
try {
while (true) {
const currentAgent = result.state._currentAgent;
const handoffs = currentAgent.handoffs.map(getHandoff);
const tools = await currentAgent.getAllTools(result.state._context);
const serializedTools = tools.map((t) => serializeTool(t));
const serializedHandoffs = handoffs.map((h) => serializeHandoff(h));
result.state._currentStep = result.state._currentStep ?? {
type: 'next_step_run_again',
};
if (result.state._currentStep.type === 'next_step_interruption') {
logger.debug('Continuing from interruption');
if (!result.state._lastTurnResponse ||
!result.state._lastProcessedResponse) {
throw new UserError('No model response found in previous state', result.state);
}
const turnResult = await executeInterruptedToolsAndSideEffects(result.state._currentAgent, result.state._originalInput, result.state._generatedItems, result.state._lastTurnResponse, result.state._lastProcessedResponse, this, result.state);
addStepToRunResult(result, turnResult);
result.state._toolUseTracker.addToolUse(result.state._currentAgent, result.state._lastProcessedResponse.toolsUsed);
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;
}
if (result.state._currentStep.type === 'next_step_run_again') {
if (!result.state._currentAgentSpan) {
const handoffNames = handoffs.map((h) => h.agentName);
result.state._currentAgentSpan = createAgentSpan({
data: {
name: currentAgent.name,
handoffs: handoffNames,
tools: tools.map((t) => t.name),
output_type: currentAgent.outputSchemaName,
},
});
result.state._currentAgentSpan.start();
setCurrentSpan(result.state._currentAgentSpan);
}
result.state._currentTurn++;
if (result.state._currentTurn > result.state._maxTurns) {
result.state._currentAgentSpan?.setError({
message: 'Max turns exceeded',
data: { max_turns: result.state._maxTurns },
});
throw new MaxTurnsExceededError(`Max turns (${result.state._maxTurns}) exceeded`, result.state);
}
logger.debug(`Running agent ${currentAgent.name} (turn ${result.state._currentTurn})`);
let model = selectModel(currentAgent.model, this.config.model);
if (typeof model === 'string') {
model = await this.config.modelProvider.getModel(model);
}
if (result.state._currentTurn === 1) {
await this.#runInputGuardrails(result.state);
}
let modelSettings = {
...this.config.modelSettings,
...currentAgent.modelSettings,
};
modelSettings = maybeResetToolChoice(currentAgent, result.state._toolUseTracker, modelSettings);
const turnInput = getTurnInput(result.input, result.newItems);
if (result.state._noActiveAgentRun) {
currentAgent.emit('agent_start', result.state._context, currentAgent);
this.emit('agent_start', result.state._context, currentAgent);
}
let finalResponse = undefined;
for await (const event of model.getStreamedResponse({
systemInstructions: await currentAgent.getSystemPrompt(result.state._context),
prompt: await currentAgent.getPrompt(result.state._context),
input: turnInput,
previousResponseId: options.previousResponseId,
modelSettings,
tools: serializedTools,
handoffs: serializedHandoffs,
outputType: convertAgentOutputTypeToSerializable(currentAgent.outputType),
tracing: getTracing(this.config.tracingDisabled, this.config.traceIncludeSensitiveData),
signal: options.signal,
})) {
if (event.type === 'response_done') {
const parsed = StreamEventResponseCompleted.parse(event);
finalResponse = {
usage: new Usage(parsed.response.usage),
output: parsed.response.output,
responseId: parsed.response.id,
};
}
if (result.cancelled) {
// When the user's code exits a loop to consume the stream, we need to break
// this loop to prevent internal false errors and unnecessary processing
return;
}
result._addItem(new RunRawModelStreamEvent(event));
}
result.state._noActiveAgentRun = false;
if (!finalResponse) {
throw new ModelBehaviorError('Model did not produce a final response!', result.state);
}
result.state._lastTurnResponse = finalResponse;
result.state._modelResponses.push(result.state._lastTurnResponse);
const processedResponse = processModelResponse(result.state._lastTurnResponse, currentAgent, tools, handoffs);
result.state._lastProcessedResponse = processedResponse;
const turnResult = await executeToolsAndSideEffects(currentAgent, result.state._originalInput, result.state._generatedItems, result.state._lastTurnResponse, result.state._lastProcessedResponse, this, result.state);
addStepToRunResult(result, turnResult);
result.state._toolUseTracker.addToolUse(currentAgent, processedResponse.toolsUsed);
result.state._originalInput = turnResult.originalInput;
result.state._generatedItems = turnResult.generatedItems;
result.state._currentStep = turnResult.nextStep;
}
if (result.state._currentStep.type === 'next_step_final_output') {
await this.#runOutputGuardrails(result.state, result.state._currentStep.output);
return;
}
else if (result.state._currentStep.type === 'next_step_interruption') {
// we are done for now. Don't run any output guardrails
return;
}
else if (result.state._currentStep.type === 'next_step_handoff') {
result.state._currentAgent = result.state._currentStep
?.newAgent;
if (result.state._currentAgentSpan) {
result.state._currentAgentSpan.end();
resetCurrentSpan();
}
result.state._currentAgentSpan = undefined;
result._addItem(new RunAgentUpdatedStreamEvent(result.state._currentAgent));
result.state._noActiveAgentRun = true;
// we've processed the handoff, so we need to run the loop again
result.state._currentStep = {
type: 'next_step_run_again',
};
}
else {
logger.debug('Running next loop');
}
}
}
catch (error) {
if (result.state._currentAgentSpan) {
result.state._currentAgentSpan.setError({
message: 'Error in agent run',
data: { error: String(error) },
});
}
throw error;
}
finally {
if (result.state._currentAgentSpan) {
if (result.state._currentStep?.type !== 'next_step_interruption') {
result.state._currentAgentSpan.end();
}
resetCurrentSpan();
}
}
}
/**
* @internal
*/
async #runIndividualStream(agent, input, options) {
options = options ?? {};
return withNewSpanContext(async () => {
// Initialize or reuse existing state
const state = input instanceof RunState
? input
: new RunState(options.context instanceof RunContext
? options.context
: new RunContext(options.context), input, agent, options.maxTurns ?? DEFAULT_MAX_TURNS);
// Initialize the streamed result with existing state
const result = new StreamedRunResult({
signal: options.signal,
state,
});
// Setup defaults
result.maxTurns = options.maxTurns ?? state._maxTurns;
// Continue the stream loop without blocking
this.#runStreamLoop(result, options).then(() => {
result._done();
}, (err) => {
result._raiseError(err);
});
return result;
});
}
run(agent, input, options = {
stream: false,
context: undefined,
}) {
if (input instanceof RunState && input._trace) {
return withTrace(input._trace, async () => {
if (input._currentAgentSpan) {
setCurrentSpan(input._currentAgentSpan);
}
if (options?.stream) {
return this.#runIndividualStream(agent, input, options);
}
else {
return this.#runIndividualNonStream(agent, input, options);
}
});
}
return getOrCreateTrace(async () => {
if (options?.stream) {
return this.#runIndividualStream(agent, input, options);
}
else {
return this.#runIndividualNonStream(agent, input, options);
}
}, {
traceId: this.config.traceId,
name: this.config.workflowName,
groupId: this.config.groupId,
metadata: this.config.traceMetadata,
});
}
}
let _defaultRunner = undefined;
function getDefaultRunner() {
if (_defaultRunner) {
return _defaultRunner;
}
_defaultRunner = new Runner();
return _defaultRunner;
}
export function selectModel(agentModel, runConfigModel) {
// When initializing an agent without model name, the model property is set to an empty string. So,
// * agentModel === '' & runConfigModel exists, runConfigModel will be used
// * agentModel is set, the agentModel will be used over runConfigModel
if ((typeof agentModel === 'string' &&
agentModel !== Agent.DEFAULT_MODEL_PLACEHOLDER) ||
agentModel // any truthy value
) {
return agentModel;
}
return runConfigModel ?? agentModel ?? Agent.DEFAULT_MODEL_PLACEHOLDER;
}
export async function run(agent, input, options) {
const runner = getDefaultRunner();
if (options?.stream) {
return await runner.run(agent, input, options);
}
else {
return await runner.run(agent, input, options);
}
}
//# sourceMappingURL=run.js.map