@openai/agents-core
Version:
The OpenAI Agents SDK is a lightweight yet powerful framework for building multi-agent workflows.
910 lines • 64.5 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Runner = void 0;
exports.run = run;
exports.getTurnInput = getTurnInput;
exports.selectModel = selectModel;
exports.getTracing = getTracing;
const agent_1 = require("./agent.js");
const guardrail_1 = require("./guardrail.js");
const providers_1 = require("./providers.js");
const runContext_1 = require("./runContext.js");
const result_1 = require("./result.js");
const lifecycle_1 = require("./lifecycle.js");
const logger_1 = __importDefault(require("./logger.js"));
const serialize_1 = require("./utils/serialize.js");
const errors_1 = require("./errors.js");
const runImplementation_1 = require("./runImplementation.js");
const context_1 = require("./tracing/context.js");
const tracing_1 = require("./tracing/index.js");
const usage_1 = require("./usage.js");
const events_1 = require("./events.js");
const runState_1 = require("./runState.js");
const protocol_1 = require("./types/protocol.js");
const tools_1 = require("./utils/tools.js");
const defaultModel_1 = require("./defaultModel.js");
const base64_1 = require("./utils/base64.js");
const smartString_1 = require("./utils/smartString.js");
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);
}
}
/**
* Orchestrates agent execution, including guardrails, tool calls, session persistence, and
* tracing. Reuse a `Runner` instance when you want consistent configuration across multiple runs.
*/
class Runner extends lifecycle_1.RunHooks {
config;
/**
* Creates a runner with optional defaults that apply to every subsequent run invocation.
*
* @param config - Overrides for models, guardrails, tracing, or session behavior.
*/
constructor(config = {}) {
super();
this.config = {
modelProvider: config.modelProvider ?? (0, providers_1.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,
sessionInputCallback: config.sessionInputCallback,
callModelInputFilter: config.callModelInputFilter,
};
this.inputGuardrailDefs = (config.inputGuardrails ?? []).map(guardrail_1.defineInputGuardrail);
this.outputGuardrailDefs = (config.outputGuardrails ?? []).map(guardrail_1.defineOutputGuardrail);
}
async run(agent, input, options = {
stream: false,
context: undefined,
}) {
const resolvedOptions = options ?? { stream: false, context: undefined };
// Per-run options take precedence over runner defaults for session memory behavior.
const sessionInputCallback = resolvedOptions.sessionInputCallback ?? this.config.sessionInputCallback;
// Likewise allow callers to override callModelInputFilter on individual runs.
const callModelInputFilter = resolvedOptions.callModelInputFilter ?? this.config.callModelInputFilter;
const hasCallModelInputFilter = Boolean(callModelInputFilter);
const effectiveOptions = {
...resolvedOptions,
sessionInputCallback,
callModelInputFilter,
};
const serverManagesConversation = Boolean(effectiveOptions.conversationId) ||
Boolean(effectiveOptions.previousResponseId);
// When the server tracks conversation history we defer to it for previous turns so local session
// persistence can focus solely on the new delta being generated in this process.
const session = effectiveOptions.session;
const resumingFromState = input instanceof runState_1.RunState;
let sessionInputOriginalSnapshot = session && resumingFromState ? [] : undefined;
let sessionInputFilteredSnapshot = undefined;
// Tracks remaining persistence slots per AgentInputItem key so resumed sessions only write each original occurrence once.
let sessionInputPendingWriteCounts = session && resumingFromState ? new Map() : undefined;
// Keeps track of which inputs should be written back to session memory. `sourceItems` reflects
// the original objects (so we can respect resume counts) while `filteredItems`, when present,
// contains the filtered/redacted clones that must be persisted for history.
// The helper reconciles the filtered copies produced by callModelInputFilter with their original
// counterparts so resume-from-state bookkeeping stays consistent and duplicate references only
// consume a single persistence slot.
const recordSessionItemsForPersistence = (sourceItems, filteredItems) => {
const pendingWriteCounts = sessionInputPendingWriteCounts;
if (filteredItems !== undefined) {
if (!pendingWriteCounts) {
sessionInputFilteredSnapshot = filteredItems.map((item) => structuredClone(item));
return;
}
const persistableItems = [];
const sourceOccurrenceCounts = new WeakMap();
// Track how many times each original object appears so duplicate references only consume one persistence slot.
for (const source of sourceItems) {
if (!source || typeof source !== 'object') {
continue;
}
const nextCount = (sourceOccurrenceCounts.get(source) ?? 0) + 1;
sourceOccurrenceCounts.set(source, nextCount);
}
// Let filtered items without a one-to-one source match claim any remaining persistence count.
const consumeAnyPendingWriteSlot = () => {
for (const [key, remaining] of pendingWriteCounts) {
if (remaining > 0) {
pendingWriteCounts.set(key, remaining - 1);
return true;
}
}
return false;
};
for (let i = 0; i < filteredItems.length; i++) {
const filteredItem = filteredItems[i];
if (!filteredItem) {
continue;
}
let allocated = false;
const source = sourceItems[i];
if (source && typeof source === 'object') {
const pendingOccurrences = (sourceOccurrenceCounts.get(source) ?? 0) - 1;
sourceOccurrenceCounts.set(source, pendingOccurrences);
if (pendingOccurrences > 0) {
continue;
}
const sourceKey = getAgentInputItemKey(source);
const remaining = pendingWriteCounts.get(sourceKey) ?? 0;
if (remaining > 0) {
pendingWriteCounts.set(sourceKey, remaining - 1);
persistableItems.push(structuredClone(filteredItem));
allocated = true;
continue;
}
}
const filteredKey = getAgentInputItemKey(filteredItem);
const filteredRemaining = pendingWriteCounts.get(filteredKey) ?? 0;
if (filteredRemaining > 0) {
pendingWriteCounts.set(filteredKey, filteredRemaining - 1);
persistableItems.push(structuredClone(filteredItem));
allocated = true;
continue;
}
if (!source && consumeAnyPendingWriteSlot()) {
persistableItems.push(structuredClone(filteredItem));
allocated = true;
}
if (!allocated &&
!source &&
sessionInputFilteredSnapshot === undefined) {
// Preserve at least one copy so later persistence resolves even when no counters remain.
persistableItems.push(structuredClone(filteredItem));
}
}
if (persistableItems.length > 0 ||
sessionInputFilteredSnapshot === undefined) {
sessionInputFilteredSnapshot = persistableItems;
}
return;
}
const filtered = [];
if (!pendingWriteCounts) {
for (const item of sourceItems) {
if (!item) {
continue;
}
filtered.push(structuredClone(item));
}
}
else {
for (const item of sourceItems) {
if (!item) {
continue;
}
const key = getAgentInputItemKey(item);
const remaining = pendingWriteCounts.get(key) ?? 0;
if (remaining <= 0) {
continue;
}
pendingWriteCounts.set(key, remaining - 1);
filtered.push(structuredClone(item));
}
}
if (filtered.length > 0) {
sessionInputFilteredSnapshot = filtered;
}
else if (sessionInputFilteredSnapshot === undefined) {
sessionInputFilteredSnapshot = [];
}
};
// Determine which items should be committed to session memory for this turn.
// Filters take precedence because they reflect the exact payload delivered to the model.
const resolveSessionItemsForPersistence = () => {
if (sessionInputFilteredSnapshot !== undefined) {
return sessionInputFilteredSnapshot;
}
if (hasCallModelInputFilter) {
return undefined;
}
return sessionInputOriginalSnapshot;
};
let preparedInput = input;
if (!(preparedInput instanceof runState_1.RunState)) {
if (session && Array.isArray(preparedInput) && !sessionInputCallback) {
throw new errors_1.UserError('RunConfig.sessionInputCallback must be provided when using session history with list inputs.');
}
const prepared = await (0, runImplementation_1.prepareInputItemsWithSession)(preparedInput, session, sessionInputCallback, {
// When the server tracks conversation state we only send the new turn inputs;
// previous messages are recovered via conversationId/previousResponseId.
includeHistoryInPreparedInput: !serverManagesConversation,
preserveDroppedNewItems: serverManagesConversation,
});
if (serverManagesConversation && session) {
// When the server manages memory we only persist the new turn inputs locally so the
// conversation service stays the single source of truth for prior exchanges.
const sessionItems = prepared.sessionItems;
if (sessionItems && sessionItems.length > 0) {
preparedInput = sessionItems;
}
else {
preparedInput = prepared.preparedInput;
}
}
else {
preparedInput = prepared.preparedInput;
}
if (session) {
const items = prepared.sessionItems ?? [];
// Clone the items that will be persisted so later mutations (filters, hooks) cannot desync history.
sessionInputOriginalSnapshot = items.map((item) => structuredClone(item));
// Reset pending counts so each prepared item reserves exactly one write slot until filters resolve matches.
sessionInputPendingWriteCounts = new Map();
for (const item of items) {
const key = getAgentInputItemKey(item);
sessionInputPendingWriteCounts.set(key, (sessionInputPendingWriteCounts.get(key) ?? 0) + 1);
}
}
}
// Streaming runs persist the input asynchronously, so track a one-shot helper
// that can be awaited from multiple branches without double-writing.
let ensureStreamInputPersisted;
// Sessions remain usable alongside server-managed conversations (e.g., OpenAIConversationsSession)
// so callers can reuse callbacks, resume-from-state logic, and other helpers without duplicating
// remote history, so persistence is gated on serverManagesConversation.
if (session && !serverManagesConversation) {
let persisted = false;
ensureStreamInputPersisted = async () => {
if (persisted) {
return;
}
const itemsToPersist = resolveSessionItemsForPersistence();
if (!itemsToPersist || itemsToPersist.length === 0) {
return;
}
persisted = true;
await (0, runImplementation_1.saveStreamInputToSession)(session, itemsToPersist);
};
}
const executeRun = async () => {
if (effectiveOptions.stream) {
const streamResult = await this.#runIndividualStream(agent, preparedInput, effectiveOptions, ensureStreamInputPersisted, recordSessionItemsForPersistence);
return streamResult;
}
const runResult = await this.#runIndividualNonStream(agent, preparedInput, effectiveOptions, recordSessionItemsForPersistence);
// See note above: allow sessions to run for callbacks/state but skip writes when the server
// is the source of truth for transcript history.
if (session && !serverManagesConversation) {
await (0, runImplementation_1.saveToSession)(session, resolveSessionItemsForPersistence(), runResult);
}
return runResult;
};
if (preparedInput instanceof runState_1.RunState && preparedInput._trace) {
return (0, context_1.withTrace)(preparedInput._trace, async () => {
if (preparedInput._currentAgentSpan) {
(0, context_1.setCurrentSpan)(preparedInput._currentAgentSpan);
}
return executeRun();
});
}
return (0, context_1.getOrCreateTrace)(async () => executeRun(), {
traceId: this.config.traceId,
name: this.config.workflowName,
groupId: this.config.groupId,
metadata: this.config.traceMetadata,
});
}
// --------------------------------------------------------------
// Internals
// --------------------------------------------------------------
inputGuardrailDefs;
outputGuardrailDefs;
/**
* @internal
* Resolves the effective model once so both run loops obey the same precedence rules.
*/
async #resolveModelForAgent(agent) {
const explictlyModelSet = (agent.model !== undefined &&
agent.model !== agent_1.Agent.DEFAULT_MODEL_PLACEHOLDER) ||
(this.config.model !== undefined &&
this.config.model !== agent_1.Agent.DEFAULT_MODEL_PLACEHOLDER);
let resolvedModel = selectModel(agent.model, this.config.model);
if (typeof resolvedModel === 'string') {
resolvedModel = await this.config.modelProvider.getModel(resolvedModel);
}
return { model: resolvedModel, explictlyModelSet };
}
/**
* @internal
*/
async #runIndividualNonStream(startingAgent, input, options,
// sessionInputUpdate lets the caller adjust queued session items after filters run so we
// persist exactly what we send to the model (e.g., after redactions or truncation).
sessionInputUpdate) {
return (0, context_1.withNewSpanContext)(async () => {
// if we have a saved state we use that one, otherwise we create a new one
const isResumedState = input instanceof runState_1.RunState;
const state = isResumedState
? input
: new runState_1.RunState(options.context instanceof runContext_1.RunContext
? options.context
: new runContext_1.RunContext(options.context), input, startingAgent, options.maxTurns ?? DEFAULT_MAX_TURNS);
const serverConversationTracker = options.conversationId || options.previousResponseId
? new ServerConversationTracker({
conversationId: options.conversationId,
previousResponseId: options.previousResponseId,
})
: undefined;
if (serverConversationTracker && isResumedState) {
serverConversationTracker.primeFromState({
originalInput: state._originalInput,
generatedItems: state._generatedItems,
modelResponses: state._modelResponses,
});
}
try {
while (true) {
// 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_1.default.debug('Continuing from interruption');
if (!state._lastTurnResponse || !state._lastProcessedResponse) {
throw new errors_1.UserError('No model response found in previous state', state);
}
const turnResult = await (0, runImplementation_1.resolveInterruptedTurn)(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;
if (turnResult.nextStep.type === 'next_step_run_again') {
state._currentTurnPersistedItemCount = 0;
}
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 result_1.RunResult(state);
}
continue;
}
if (state._currentStep.type === 'next_step_run_again') {
const artifacts = await prepareAgentArtifacts(state);
state._currentTurn++;
state._currentTurnPersistedItemCount = 0;
if (state._currentTurn > state._maxTurns) {
state._currentAgentSpan?.setError({
message: 'Max turns exceeded',
data: { max_turns: state._maxTurns },
});
throw new errors_1.MaxTurnsExceededError(`Max turns (${state._maxTurns}) exceeded`, state);
}
logger_1.default.debug(`Running agent ${state._currentAgent.name} (turn ${state._currentTurn})`);
if (state._currentTurn === 1) {
await this.#runInputGuardrails(state);
}
const turnInput = serverConversationTracker
? serverConversationTracker.prepareInput(state._originalInput, state._generatedItems)
: 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);
}
const preparedCall = await this.#prepareModelCall(state, options, artifacts, turnInput, serverConversationTracker, sessionInputUpdate);
state._lastTurnResponse = await preparedCall.model.getResponse({
systemInstructions: preparedCall.modelInput.instructions,
prompt: preparedCall.prompt,
// Explicit agent/run config models should take precedence over prompt defaults.
...(preparedCall.explictlyModelSet
? { overridePromptModel: true }
: {}),
input: preparedCall.modelInput.input,
previousResponseId: preparedCall.previousResponseId,
conversationId: preparedCall.conversationId,
modelSettings: preparedCall.modelSettings,
tools: preparedCall.serializedTools,
outputType: (0, tools_1.convertAgentOutputTypeToSerializable)(state._currentAgent.outputType),
handoffs: preparedCall.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;
// After each turn record the items echoed by the server so future requests only
// include the incremental inputs that have not yet been acknowledged.
serverConversationTracker?.trackServerItems(state._lastTurnResponse);
const processedResponse = (0, runImplementation_1.processModelResponse)(state._lastTurnResponse, state._currentAgent, preparedCall.tools, preparedCall.handoffs);
state._lastProcessedResponse = processedResponse;
const turnResult = await (0, runImplementation_1.resolveTurnAfterModelResponse)(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;
if (turnResult.nextStep.type === 'next_step_run_again') {
state._currentTurnPersistedItemCount = 0;
}
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 result_1.RunResult(state);
}
else if (state._currentStep &&
state._currentStep.type === 'next_step_handoff') {
state._currentAgent = state._currentStep.newAgent;
if (state._currentAgentSpan) {
state._currentAgentSpan.end();
(0, context_1.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 result_1.RunResult(state);
}
else {
logger_1.default.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();
}
(0, context_1.resetCurrentSpan)();
}
}
});
}
/**
* @internal
*/
async #runStreamLoop(result, options, isResumedState, ensureStreamInputPersisted, sessionInputUpdate) {
const serverManagesConversation = Boolean(options.conversationId) || Boolean(options.previousResponseId);
const serverConversationTracker = serverManagesConversation
? new ServerConversationTracker({
conversationId: options.conversationId,
previousResponseId: options.previousResponseId,
})
: undefined;
let handedInputToModel = false;
let streamInputPersisted = false;
const persistStreamInputIfNeeded = async () => {
if (streamInputPersisted || !ensureStreamInputPersisted) {
return;
}
// Both success and error paths call this helper, so guard against multiple writes.
await ensureStreamInputPersisted();
streamInputPersisted = true;
};
if (serverConversationTracker && isResumedState) {
serverConversationTracker.primeFromState({
originalInput: result.state._originalInput,
generatedItems: result.state._generatedItems,
modelResponses: result.state._modelResponses,
});
}
try {
while (true) {
const currentAgent = result.state._currentAgent;
result.state._currentStep = result.state._currentStep ?? {
type: 'next_step_run_again',
};
if (result.state._currentStep.type === 'next_step_interruption') {
logger_1.default.debug('Continuing from interruption');
if (!result.state._lastTurnResponse ||
!result.state._lastProcessedResponse) {
throw new errors_1.UserError('No model response found in previous state', result.state);
}
const turnResult = await (0, runImplementation_1.resolveInterruptedTurn)(result.state._currentAgent, result.state._originalInput, result.state._generatedItems, result.state._lastTurnResponse, result.state._lastProcessedResponse, this, result.state);
(0, runImplementation_1.addStepToRunResult)(result, turnResult);
result.state._toolUseTracker.addToolUse(result.state._currentAgent, result.state._lastProcessedResponse.toolsUsed);
result.state._originalInput = turnResult.originalInput;
result.state._generatedItems = turnResult.generatedItems;
if (turnResult.nextStep.type === 'next_step_run_again') {
result.state._currentTurnPersistedItemCount = 0;
}
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') {
const artifacts = await prepareAgentArtifacts(result.state);
result.state._currentTurn++;
result.state._currentTurnPersistedItemCount = 0;
if (result.state._currentTurn > result.state._maxTurns) {
result.state._currentAgentSpan?.setError({
message: 'Max turns exceeded',
data: { max_turns: result.state._maxTurns },
});
throw new errors_1.MaxTurnsExceededError(`Max turns (${result.state._maxTurns}) exceeded`, result.state);
}
logger_1.default.debug(`Running agent ${currentAgent.name} (turn ${result.state._currentTurn})`);
if (result.state._currentTurn === 1) {
await this.#runInputGuardrails(result.state);
}
const turnInput = serverConversationTracker
? serverConversationTracker.prepareInput(result.input, result.newItems)
: 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;
const preparedCall = await this.#prepareModelCall(result.state, options, artifacts, turnInput, serverConversationTracker, sessionInputUpdate);
handedInputToModel = true;
await persistStreamInputIfNeeded();
for await (const event of preparedCall.model.getStreamedResponse({
systemInstructions: preparedCall.modelInput.instructions,
prompt: preparedCall.prompt,
// Streaming requests should also honor explicitly chosen models.
...(preparedCall.explictlyModelSet
? { overridePromptModel: true }
: {}),
input: preparedCall.modelInput.input,
previousResponseId: preparedCall.previousResponseId,
conversationId: preparedCall.conversationId,
modelSettings: preparedCall.modelSettings,
tools: preparedCall.serializedTools,
handoffs: preparedCall.serializedHandoffs,
outputType: (0, tools_1.convertAgentOutputTypeToSerializable)(currentAgent.outputType),
tracing: getTracing(this.config.tracingDisabled, this.config.traceIncludeSensitiveData),
signal: options.signal,
})) {
if (event.type === 'response_done') {
const parsed = protocol_1.StreamEventResponseCompleted.parse(event);
finalResponse = {
usage: new usage_1.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 events_1.RunRawModelStreamEvent(event));
}
result.state._noActiveAgentRun = false;
if (!finalResponse) {
throw new errors_1.ModelBehaviorError('Model did not produce a final response!', result.state);
}
result.state._lastTurnResponse = finalResponse;
// Keep the tracker in sync with the streamed response so reconnections remain accurate.
serverConversationTracker?.trackServerItems(finalResponse);
result.state._modelResponses.push(result.state._lastTurnResponse);
const processedResponse = (0, runImplementation_1.processModelResponse)(result.state._lastTurnResponse, currentAgent, preparedCall.tools, preparedCall.handoffs);
result.state._lastProcessedResponse = processedResponse;
// Record the items emitted directly from the model response so we do not
// stream them again after tools and other side effects finish.
const preToolItems = new Set(processedResponse.newItems);
if (preToolItems.size > 0) {
(0, runImplementation_1.streamStepItemsToRunResult)(result, processedResponse.newItems);
}
const turnResult = await (0, runImplementation_1.resolveTurnAfterModelResponse)(currentAgent, result.state._originalInput, result.state._generatedItems, result.state._lastTurnResponse, result.state._lastProcessedResponse, this, result.state);
(0, runImplementation_1.addStepToRunResult)(result, turnResult, {
skipItems: preToolItems,
});
result.state._toolUseTracker.addToolUse(currentAgent, processedResponse.toolsUsed);
result.state._originalInput = turnResult.originalInput;
result.state._generatedItems = turnResult.generatedItems;
if (turnResult.nextStep.type === 'next_step_run_again') {
result.state._currentTurnPersistedItemCount = 0;
}
result.state._currentStep = turnResult.nextStep;
}
if (result.state._currentStep.type === 'next_step_final_output') {
await this.#runOutputGuardrails(result.state, result.state._currentStep.output);
await persistStreamInputIfNeeded();
// Guardrails must succeed before persisting session memory to avoid storing blocked outputs.
if (!serverManagesConversation) {
await (0, runImplementation_1.saveStreamResultToSession)(options.session, result);
}
this.emit('agent_end', result.state._context, currentAgent, result.state._currentStep.output);
currentAgent.emit('agent_end', result.state._context, 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
await persistStreamInputIfNeeded();
if (!serverManagesConversation) {
await (0, runImplementation_1.saveStreamResultToSession)(options.session, result);
}
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();
(0, context_1.resetCurrentSpan)();
}
result.state._currentAgentSpan = undefined;
result._addItem(new events_1.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_1.default.debug('Running next loop');
}
}
}
catch (error) {
if (handedInputToModel && !streamInputPersisted) {
await persistStreamInputIfNeeded();
}
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();
}
(0, context_1.resetCurrentSpan)();
}
}
}
/**
* @internal
*/
async #runIndividualStream(agent, input, options, ensureStreamInputPersisted, sessionInputUpdate) {
options = options ?? {};
return (0, context_1.withNewSpanContext)(async () => {
// Initialize or reuse existing state
const isResumedState = input instanceof runState_1.RunState;
const state = isResumedState
? input
: new runState_1.RunState(options.context instanceof runContext_1.RunContext
? options.context
: new runContext_1.RunContext(options.context), input, agent, options.maxTurns ?? DEFAULT_MAX_TURNS);
// Initialize the streamed result with existing state
const result = new result_1.StreamedRunResult({
signal: options.signal,
state,
});
// Setup defaults
result.maxTurns = options.maxTurns ?? state._maxTurns;
// Continue the stream loop without blocking
const streamLoopPromise = this.#runStreamLoop(result, options, isResumedState, ensureStreamInputPersisted, sessionInputUpdate).then(() => {
result._done();
}, (err) => {
result._raiseError(err);
});
// Attach the stream loop promise so trace end waits for the loop to complete
result._setStreamLoopPromise(streamLoopPromise);
return result;
});
}
async #runInputGuardrails(state) {
const guardrails = this.inputGuardrailDefs.concat(state._currentAgent.inputGuardrails.map(guardrail_1.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 (0, tracing_1.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 errors_1.InputGuardrailTripwireTriggered(`Input guardrail triggered: ${JSON.stringify(result.output.outputInfo)}`, result, state);
}
}
}
catch (e) {
if (e instanceof errors_1.InputGuardrailTripwireTriggered) {
throw e;
}
// roll back the current turn to enable reruns
state._currentTurn--;
throw new errors_1.GuardrailExecutionError(`Input guardrail failed to complete: ${e}`, e, state);
}
}
}
async #runOutputGuardrails(state, output) {
const guardrails = this.outputGuardrailDefs.concat(state._currentAgent.outputGuardrails.map(guardrail_1.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 (0, tracing_1.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 errors_1.OutputGuardrailTripwireTriggered(`Output guardrail triggered: ${JSON.stringify(result.output.outputInfo)}`, result, state);
}
}
}
catch (e) {
if (e instanceof errors_1.OutputGuardrailTripwireTriggered) {
throw e;
}
throw new errors_1.GuardrailExecutionError(`Output guardrail failed to complete: ${e}`, e, state);
}
}
}
/**
* @internal
* Applies call-level filters and merges session updates so the model request mirrors exactly
* what we persisted for history.
*/
async #prepareModelCall(state, options, artifacts, turnInput, serverConversationTracker, sessionInputUpdate) {
const { model, explictlyModelSet } = await this.#resolveModelForAgent(state._currentAgent);
let modelSettings = {
...this.config.modelSettings,
...state._currentAgent.modelSettings,
};
modelSettings = adjustModelSettingsForNonGPT5RunnerModel(explictlyModelSet, state._currentAgent.modelSettings, model, modelSettings);
modelSettings = (0, runImplementation_1.maybeResetToolChoice)(state._currentAgent, state._toolUseTracker, modelSettings);
const systemInstructions = await state._currentAgent.getSystemPrompt(state._context);
const prompt = await state._currentAgent.getPrompt(state._context);
const { modelInput, sourceItems, persistedItems, filterApplied } = await applyCallModelInputFilter(state._currentAgent, options.callModelInputFilter, state._context, turnInput, systemInstructions);
// Inform the tracker which exact original objects made it to the provider so future turns
// only send the delta that has not yet been acknowledged by the server.
serverConversationTracker?.markInputAsSent(sourceItems);
// Provide filtered clones whenever filters run so session history mirrors the model payload.
// Returning an empty array is intentional: it tells the session layer to persist "nothing"
// instead of falling back to the unfiltered originals when the filter redacts everything.
sessionInputUpdate?.(sourceItems, filterApplied ? persistedItems : undefined);
const previousResponseId = serverConversationTracker?.previousResponseId ??
options.previousResponseId;
const conversationId = serverConversationTracker?.conversationId ?? options.conversationId;
return {
...artifacts,
model,
explictlyModelSet,
modelSettings,
modelInput,
prompt,
previousResponseId,
conversationId,
};
}
}
exports.Runner = Runner;
/**
* Constructs the model input array for the current turn by combining the original turn input with
* any new run items (excluding tool approval placeholders). This helps ensure that repeated calls
* to the Responses API only send newly generated content.
*
* See: https://platform.openai.com/docs/guides/conversation-state?api-mode=responses.
*/
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);
return [...toAgentInputList(originalInput), ...rawItems];
}
// --------------------------------------------------------------
// Internal helpers
// --------------------------------------------------------------
const DEFAULT_MAX_TURNS = 10;
let _defaultRunner = undefined;
function getDefaultRunner() {
if (_defaultRunner) {
return _defaultRunner;
}
_defaultRunner = new Runner();
return _defaultRunner;
}
/**
* Resolves the effective model for the next turn by giving precedence to the agent-specific
* configuration when present, otherwise falling back to the runner-level default.
*/
function selectModel(agentModel, runConfigModel) {
// When initializing an agent without model name, the model property is set to an empty string. So,
// * agentModel === Agent.DEFAULT_MODEL_PLACEHOLDER & runConfigModel exists, runConfigModel will be used
// * agentModel is set, the agentModel will be used over runConfigModel
if ((typeof agentModel === 'string' &&
agentModel !== agent_1.Agent.DEFAULT_MODEL_PLACEHOLDER) ||
agentModel // any truthy value
) {
return agentModel;
}
return runConfigModel ?? agentModel ?? agent_1.Agent.DEFAULT_MODEL_PLACEHOLDER;
}
/**
* Normalizes tracing configuration into the format expected by model providers.
* Returns `false` to disable tracing, `true` to include full payload data, or
* `'enabled_without_data'` to omit sensitive content while still emitting spans.
*/
function getTracing(tracingDisabled, traceIncludeSensitiveData) {
if (tracingDisabled) {
return false;
}
if (traceIncludeSensitiveData) {
return true;
}
return 'enabled_without_data';
}
/**
* @internal
*/
async function applyCallModelInputFilter(agent, callModelInputFilter, context, inputItems, systemInstructions) {
const cloneInputItems = (items, map) => items.map((item) => {
const cloned = structuredClone(item);
if (map && cloned && typeof cloned === 'object') {
map.set(cloned, item);
}
return cloned;
});
// Record the relationship between the cloned array passed to filters and the original inputs.
const cloneMap = new WeakMap();
const originalPool = buildAgentInputPool(inputItems);
const fallbackOriginals = [];
// Track any original object inputs so filtered replacements can still mark them as delivered.
for (const item of inputItems) {
if (item && typeof item === 'object') {
fallbackOriginals.push(item);
}
}
const removeFromFallback = (candidate) => {
if (!candidate || typeof candidate !== 'object') {
return;
}
const index = fallbackOriginals.findIndex((original) => original === candidate);
if (index !== -