UNPKG

@openai/agents-core

Version:

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

870 lines (869 loc) 50.9 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Runner = exports.getTurnInput = exports.selectModel = exports.getTracing = void 0; exports.run = run; const agent_1 = require("./agent.js"); const events_1 = require("./events.js"); const errors_1 = require("./errors.js"); const guardrail_1 = require("./guardrail.js"); const lifecycle_1 = require("./lifecycle.js"); const logger_1 = __importDefault(require("./logger.js")); const providers_1 = require("./providers.js"); const runContext_1 = require("./runContext.js"); const result_1 = require("./result.js"); const runState_1 = require("./runState.js"); const tool_1 = require("./tool.js"); const context_1 = require("./tracing/context.js"); const usage_1 = require("./usage.js"); const tools_1 = require("./utils/tools.js"); const constants_1 = require("./runner/constants.js"); const protocol_1 = require("./types/protocol.js"); const conversation_1 = require("./runner/conversation.js"); const guardrails_1 = require("./runner/guardrails.js"); const modelSettings_1 = require("./runner/modelSettings.js"); const modelOutputs_1 = require("./runner/modelOutputs.js"); const streaming_1 = require("./runner/streaming.js"); const sessionPersistence_1 = require("./runner/sessionPersistence.js"); const turnResolution_1 = require("./runner/turnResolution.js"); const turnPreparation_1 = require("./runner/turnPreparation.js"); const runLoop_1 = require("./runner/runLoop.js"); const tracing_1 = require("./runner/tracing.js"); const errorHandlers_1 = require("./runner/errorHandlers.js"); var tracing_2 = require("./runner/tracing.js"); Object.defineProperty(exports, "getTracing", { enumerable: true, get: function () { return tracing_2.getTracing; } }); var modelSettings_2 = require("./runner/modelSettings.js"); Object.defineProperty(exports, "selectModel", { enumerable: true, get: function () { return modelSettings_2.selectModel; } }); var items_1 = require("./runner/items.js"); Object.defineProperty(exports, "getTurnInput", { enumerable: true, get: function () { return items_1.getTurnInput; } }); 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; traceOverrides; /** * 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, tracing: config.tracing, sessionInputCallback: config.sessionInputCallback, callModelInputFilter: config.callModelInputFilter, toolErrorFormatter: config.toolErrorFormatter, reasoningItemIdPolicy: config.reasoningItemIdPolicy, }; this.traceOverrides = { ...(config.traceId !== undefined ? { traceId: config.traceId } : {}), ...(config.workflowName !== undefined ? { workflowName: config.workflowName } : {}), ...(config.groupId !== undefined ? { groupId: config.groupId } : {}), ...(config.traceMetadata !== undefined ? { traceMetadata: config.traceMetadata } : {}), ...(config.tracing?.apiKey !== undefined ? { tracingApiKey: config.tracing.apiKey } : {}), }; 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; // Per-run callback can override runner-level tool error formatting defaults. const toolErrorFormatter = resolvedOptions.toolErrorFormatter ?? this.config.toolErrorFormatter; const reasoningItemIdPolicy = resolvedOptions.reasoningItemIdPolicy; const hasCallModelInputFilter = Boolean(callModelInputFilter); const tracingConfig = resolvedOptions.tracing ?? this.config.tracing; const traceOverrides = { ...this.traceOverrides, ...(resolvedOptions.tracing?.apiKey !== undefined ? { tracingApiKey: resolvedOptions.tracing.apiKey } : {}), }; const effectiveOptions = { ...resolvedOptions, sessionInputCallback, callModelInputFilter, toolErrorFormatter, reasoningItemIdPolicy, }; const resumingFromState = input instanceof runState_1.RunState; const preserveTurnPersistenceOnResume = resumingFromState && input._currentTurnInProgress === true; const resumedConversationId = resumingFromState ? input._conversationId : undefined; const resumedPreviousResponseId = resumingFromState ? input._previousResponseId : undefined; const serverManagesConversation = Boolean(effectiveOptions.conversationId ?? resumedConversationId) || Boolean(effectiveOptions.previousResponseId ?? resumedPreviousResponseId); // 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 sessionPersistence = (0, sessionPersistence_1.createSessionPersistenceTracker)({ session, hasCallModelInputFilter, persistInput: sessionPersistence_1.saveStreamInputToSession, resumingFromState, }); let preparedInput = input; if (!(preparedInput instanceof runState_1.RunState)) { const prepared = await (0, sessionPersistence_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; } sessionPersistence?.setPreparedItems(prepared.sessionItems); } // Streaming runs persist the input asynchronously, so track a one-shot helper // that can be awaited from multiple branches without double-writing. const ensureStreamInputPersisted = sessionPersistence?.buildPersistInputOnce(serverManagesConversation); const executeRun = async () => { if (effectiveOptions.stream) { const streamResult = await this.#runIndividualStream(agent, preparedInput, effectiveOptions, ensureStreamInputPersisted, sessionPersistence?.recordTurnItems, preserveTurnPersistenceOnResume); return streamResult; } const runResult = await this.#runIndividualNonStream(agent, preparedInput, effectiveOptions, sessionPersistence?.recordTurnItems, preserveTurnPersistenceOnResume); // 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 (sessionPersistence && !serverManagesConversation) { await (0, sessionPersistence_1.saveToSession)(session, sessionPersistence.getItemsForPersistence(), runResult); } return runResult; }; if (preparedInput instanceof runState_1.RunState && preparedInput._trace) { const applied = (0, tracing_1.applyTraceOverrides)(preparedInput._trace, preparedInput._currentAgentSpan, traceOverrides); preparedInput._trace = applied.trace; preparedInput._currentAgentSpan = applied.currentSpan; 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, // Per-run tracing config overrides exporter defaults such as environment API key. tracingApiKey: tracingConfig?.apiKey, }); } // -------------------------------------------------------------- // 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); const selectedModel = (0, modelSettings_1.selectModel)(agent.model, this.config.model); const resolvedModelName = typeof selectedModel === 'string' ? selectedModel : undefined; const resolvedModel = typeof selectedModel === 'string' ? await this.config.modelProvider.getModel(selectedModel) : selectedModel; return { model: resolvedModel, explictlyModelSet, resolvedModelName }; } /** * @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, preserveTurnPersistenceOnResume) { 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 ?? constants_1.DEFAULT_MAX_TURNS); const resolvedReasoningItemIdPolicy = options.reasoningItemIdPolicy ?? (isResumedState ? state._reasoningItemIdPolicy : undefined) ?? this.config.reasoningItemIdPolicy; state.setReasoningItemIdPolicy(resolvedReasoningItemIdPolicy); const resolvedConversationId = options.conversationId ?? (isResumedState ? state._conversationId : undefined); const resolvedPreviousResponseId = options.previousResponseId ?? (isResumedState ? state._previousResponseId : undefined); if (!isResumedState) { state.setConversationContext(resolvedConversationId, resolvedPreviousResponseId); } const serverConversationTracker = resolvedConversationId || resolvedPreviousResponseId ? new conversation_1.ServerConversationTracker({ conversationId: resolvedConversationId, previousResponseId: resolvedPreviousResponseId, reasoningItemIdPolicy: resolvedReasoningItemIdPolicy, }) : undefined; if (serverConversationTracker && isResumedState) { serverConversationTracker.primeFromState({ originalInput: state._originalInput, generatedItems: state._generatedItems, modelResponses: state._modelResponses, }); state.setConversationContext(serverConversationTracker.conversationId, serverConversationTracker.previousResponseId); } const toolErrorFormatter = options.toolErrorFormatter ?? this.config.toolErrorFormatter; // Tracks when we resume an approval interruption so the next run-again step stays in the same turn. let continuingInterruptedTurn = false; 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 interruptedOutcome = await (0, runLoop_1.resumeInterruptedTurn)({ state, runner: this, toolErrorFormatter, }); // Don't reset counter here - resolveInterruptedTurn already adjusted it via rewind logic // The counter will be reset when _currentTurn is incremented (starting a new turn) const { shouldReturn, shouldContinue } = (0, runLoop_1.handleInterruptedOutcome)({ state, outcome: interruptedOutcome, setContinuingInterruptedTurn: (value) => { continuingInterruptedTurn = value; }, }); if (shouldReturn) { // we are still in an interruption, so we need to avoid an infinite loop return new result_1.RunResult(state); } if (shouldContinue) { continue; } } if (state._currentStep.type === 'next_step_run_again') { const wasContinuingInterruptedTurn = continuingInterruptedTurn; continuingInterruptedTurn = false; const guardrailTracker = (0, guardrails_1.createGuardrailTracker)(); const previousTurn = state._currentTurn; const previousPersistedCount = state._currentTurnPersistedItemCount; const previousGeneratedCount = state._generatedItems.length; const { artifacts, turnInput, parallelGuardrailPromise } = await (0, turnPreparation_1.prepareTurn)({ state, input: state._originalInput, generatedItems: state._generatedItems, isResumedState, preserveTurnPersistenceOnResume, continuingInterruptedTurn: wasContinuingInterruptedTurn, serverConversationTracker, inputGuardrailDefs: this.inputGuardrailDefs, guardrailHandlers: { onParallelStart: guardrailTracker.markPending, onParallelError: guardrailTracker.setError, }, emitAgentStart: (context, agent, inputItems) => { this.emit('agent_start', context, agent, inputItems); }, }); if (preserveTurnPersistenceOnResume && state._currentTurn > previousTurn && previousPersistedCount <= previousGeneratedCount) { // Preserve persisted offsets from a resumed run to avoid re-saving prior items. state._currentTurnPersistedItemCount = previousPersistedCount; } guardrailTracker.setPromise(parallelGuardrailPromise); const preparedCall = await this.#prepareModelCall(state, options, artifacts, turnInput, serverConversationTracker, sessionInputUpdate); guardrailTracker.throwIfError(); 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, toolsExplicitlyProvided: preparedCall.toolsExplicitlyProvided, outputType: (0, tools_1.convertAgentOutputTypeToSerializable)(state._currentAgent.outputType), handoffs: preparedCall.serializedHandoffs, tracing: (0, tracing_1.getTracing)(this.config.tracingDisabled, this.config.traceIncludeSensitiveData), signal: options.signal, }); if (serverConversationTracker) { serverConversationTracker.markInputAsSent(preparedCall.sourceItems, { filterApplied: preparedCall.filterApplied, allTurnItems: preparedCall.turnInput, }); } 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); if (serverConversationTracker) { state.setConversationContext(serverConversationTracker.conversationId, serverConversationTracker.previousResponseId); } const processedResponse = (0, modelOutputs_1.processModelResponse)(state._lastTurnResponse, state._currentAgent, preparedCall.tools, preparedCall.handoffs); state._lastProcessedResponse = processedResponse; await guardrailTracker.awaitCompletion(); const turnResult = await (0, turnResolution_1.resolveTurnAfterModelResponse)(state._currentAgent, state._originalInput, state._generatedItems, state._lastTurnResponse, state._lastProcessedResponse, this, state, toolErrorFormatter); (0, runLoop_1.applyTurnResult)({ state, turnResult, agent: state._currentAgent, toolsUsed: state._lastProcessedResponse?.toolsUsed ?? [], resetTurnPersistence: !isResumedState, }); } const currentStep = state._currentStep; if (!currentStep) { logger_1.default.debug('Running next loop'); continue; } switch (currentStep.type) { case 'next_step_final_output': await (0, guardrails_1.runOutputGuardrails)(state, this.outputGuardrailDefs, currentStep.output); state._currentTurnInProgress = false; this.emit('agent_end', state._context, state._currentAgent, currentStep.output); state._currentAgent.emit('agent_end', state._context, currentStep.output); return new result_1.RunResult(state); case 'next_step_handoff': state.setCurrentAgent(currentStep.newAgent); if (state._currentAgentSpan) { state._currentAgentSpan.end(); (0, context_1.resetCurrentSpan)(); state.setCurrentAgentSpan(undefined); } state._noActiveAgentRun = true; state._currentTurnInProgress = false; // We've processed the handoff, so we need to run the loop again. state._currentStep = { type: 'next_step_run_again' }; break; case 'next_step_interruption': // Interrupted. Don't run any guardrails. return new result_1.RunResult(state); case 'next_step_run_again': state._currentTurnInProgress = false; logger_1.default.debug('Running next loop'); break; default: logger_1.default.debug('Running next loop'); } } } catch (err) { state._currentTurnInProgress = false; const handledResult = await (0, errorHandlers_1.tryHandleRunError)({ error: err, state, errorHandlers: options.errorHandlers, outputGuardrailDefs: this.outputGuardrailDefs, emitAgentEnd: (context, agent, outputText) => { this.emit('agent_end', context, agent, outputText); agent.emit('agent_end', context, outputText); }, }); if (handledResult) { return handledResult; } if (state._currentAgentSpan) { state._currentAgentSpan.setError({ message: 'Error in agent run', data: { error: String(err) }, }); } throw err; } finally { if (state._currentStep?.type !== 'next_step_interruption') { try { await (0, tool_1.disposeResolvedComputers)({ runContext: state._context }); } catch (error) { logger_1.default.warn(`Failed to dispose computers after run: ${error}`); } } 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, preserveTurnPersistenceOnResume) { const resolvedReasoningItemIdPolicy = options.reasoningItemIdPolicy ?? (isResumedState ? result.state._reasoningItemIdPolicy : undefined) ?? this.config.reasoningItemIdPolicy; result.state.setReasoningItemIdPolicy(resolvedReasoningItemIdPolicy); const resolvedConversationId = options.conversationId ?? result.state._conversationId; const resolvedPreviousResponseId = options.previousResponseId ?? result.state._previousResponseId; const serverManagesConversation = Boolean(resolvedConversationId) || Boolean(resolvedPreviousResponseId); const serverConversationTracker = serverManagesConversation ? new conversation_1.ServerConversationTracker({ conversationId: resolvedConversationId, previousResponseId: resolvedPreviousResponseId, reasoningItemIdPolicy: resolvedReasoningItemIdPolicy, }) : undefined; if (serverConversationTracker) { result.state.setConversationContext(serverConversationTracker.conversationId, serverConversationTracker.previousResponseId); } let sentInputToModel = false; let streamInputPersisted = false; let guardrailTracker = (0, guardrails_1.createGuardrailTracker)(); const persistStreamInputIfNeeded = async () => { if (streamInputPersisted || !ensureStreamInputPersisted) { return; } // Both success and error paths call this helper, so guard against multiple writes. await ensureStreamInputPersisted(); streamInputPersisted = true; }; let parallelGuardrailPromise; const awaitGuardrailsAndPersistInput = async () => { await guardrailTracker.awaitCompletion(); if (guardrailTracker.failed) { throw guardrailTracker.error; } if (sentInputToModel && !streamInputPersisted && !guardrailTracker.failed) { await persistStreamInputIfNeeded(); } }; if (serverConversationTracker && isResumedState) { serverConversationTracker.primeFromState({ originalInput: result.state._originalInput, generatedItems: result.state._generatedItems, modelResponses: result.state._modelResponses, }); result.state.setConversationContext(serverConversationTracker.conversationId, serverConversationTracker.previousResponseId); } const toolErrorFormatter = options.toolErrorFormatter ?? this.config.toolErrorFormatter; // Tracks when we resume an approval interruption so the next run-again step stays in the same turn. let continuingInterruptedTurn = false; 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 interruptedOutcome = await (0, runLoop_1.resumeInterruptedTurn)({ state: result.state, runner: this, toolErrorFormatter, onStepItems: (turnResult) => { (0, streaming_1.addStepToRunResult)(result, turnResult); }, }); // Don't reset counter here - resolveInterruptedTurn already adjusted it via rewind logic // The counter will be reset when _currentTurn is incremented (starting a new turn) const { shouldReturn, shouldContinue } = (0, runLoop_1.handleInterruptedOutcome)({ state: result.state, outcome: interruptedOutcome, setContinuingInterruptedTurn: (value) => { continuingInterruptedTurn = value; }, }); if (shouldReturn) { // we are still in an interruption, so we need to avoid an infinite loop return; } if (shouldContinue) { continue; } } if (result.state._currentStep.type === 'next_step_run_again') { parallelGuardrailPromise = undefined; guardrailTracker = (0, guardrails_1.createGuardrailTracker)(); const wasContinuingInterruptedTurn = continuingInterruptedTurn; continuingInterruptedTurn = false; const previousTurn = result.state._currentTurn; const previousPersistedCount = result.state._currentTurnPersistedItemCount; const previousGeneratedCount = result.state._generatedItems.length; const preparedTurn = await (0, turnPreparation_1.prepareTurn)({ state: result.state, input: result.input, generatedItems: result.newItems, isResumedState, preserveTurnPersistenceOnResume, continuingInterruptedTurn: wasContinuingInterruptedTurn, serverConversationTracker, inputGuardrailDefs: this.inputGuardrailDefs, guardrailHandlers: { onParallelStart: () => { guardrailTracker.markPending(); }, onParallelError: (err) => { guardrailTracker.setError(err); }, }, emitAgentStart: (context, agent, inputItems) => { this.emit('agent_start', context, agent, inputItems); }, }); if (preserveTurnPersistenceOnResume && result.state._currentTurn > previousTurn && previousPersistedCount <= previousGeneratedCount) { // Preserve persisted offsets from a resumed run to avoid re-saving prior items. result.state._currentTurnPersistedItemCount = previousPersistedCount; } const { artifacts, turnInput } = preparedTurn; parallelGuardrailPromise = preparedTurn.parallelGuardrailPromise; guardrailTracker.setPromise(parallelGuardrailPromise); // If guardrails are still running, defer input persistence until they finish. const delayStreamInputPersistence = guardrailTracker.pending; const preparedCall = await this.#prepareModelCall(result.state, options, artifacts, turnInput, serverConversationTracker, sessionInputUpdate); guardrailTracker.throwIfError(); let finalResponse = undefined; let inputMarked = false; const markInputOnce = () => { if (inputMarked || !serverConversationTracker) { return; } // We only mark inputs as sent after receiving the first stream event, // which is the earliest reliable confirmation that the server accepted // the request. If the stream fails before any events, leave inputs // unmarked so a retry can resend safely. // Record the exact input that was sent so the server tracker can advance safely. serverConversationTracker.markInputAsSent(preparedCall.sourceItems, { filterApplied: preparedCall.filterApplied, allTurnItems: preparedCall.turnInput, }); inputMarked = true; }; sentInputToModel = true; if (!delayStreamInputPersistence) { await persistStreamInputIfNeeded(); } try { 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, toolsExplicitlyProvided: preparedCall.toolsExplicitlyProvided, handoffs: preparedCall.serializedHandoffs, outputType: (0, tools_1.convertAgentOutputTypeToSerializable)(currentAgent.outputType), tracing: (0, tracing_1.getTracing)(this.config.tracingDisabled, this.config.traceIncludeSensitiveData), signal: options.signal, })) { guardrailTracker.throwIfError(); markInputOnce(); 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, }; result.state._context.usage.add(finalResponse.usage); } 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 await awaitGuardrailsAndPersistInput(); return; } result._addItem(new events_1.RunRawModelStreamEvent(event)); } } catch (error) { if ((0, streaming_1.isAbortError)(error)) { if (sentInputToModel) { markInputOnce(); } await awaitGuardrailsAndPersistInput(); return; } throw error; } if (finalResponse) { markInputOnce(); } await awaitGuardrailsAndPersistInput(); if (result.cancelled) { return; } 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); if (serverConversationTracker) { result.state.setConversationContext(serverConversationTracker.conversationId, serverConversationTracker.previousResponseId); } result.state._modelResponses.push(result.state._lastTurnResponse); const processedResponse = (0, modelOutputs_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, streaming_1.streamStepItemsToRunResult)(result, processedResponse.newItems); } const turnResult = await (0, turnResolution_1.resolveTurnAfterModelResponse)(currentAgent, result.state._originalInput, result.state._generatedItems, result.state._lastTurnResponse, result.state._lastProcessedResponse, this, result.state, toolErrorFormatter); (0, runLoop_1.applyTurnResult)({ state: result.state, turnResult, agent: currentAgent, toolsUsed: processedResponse.toolsUsed, resetTurnPersistence: !isResumedState, onStepItems: (step) => { (0, streaming_1.addStepToRunResult)(result, step, { skipItems: preToolItems }); }, }); } const currentStep = result.state._currentStep; switch (currentStep.type) { case 'next_step_final_output': await (0, guardrails_1.runOutputGuardrails)(result.state, this.outputGuardrailDefs, currentStep.output); result.state._currentTurnInProgress = false; await persistStreamInputIfNeeded(); // Guardrails must succeed before persisting session memory to avoid storing blocked outputs. if (!serverManagesConversation) { await (0, sessionPersistence_1.saveStreamResultToSession)(options.session, result); } this.emit('agent_end', result.state._context, currentAgent, currentStep.output); currentAgent.emit('agent_end', result.state._context, currentStep.output); return; case 'next_step_interruption': // We are done for now. Don't run any output guardrails. await persistStreamInputIfNeeded(); if (!serverManagesConversation) { await (0, sessionPersistence_1.saveStreamResultToSession)(options.session, result); } return; case 'next_step_handoff': result.state.setCurrentAgent(currentStep.newAgent); if (result.state._currentAgentSpan) { result.state._currentAgentSpan.end(); (0, context_1.resetCurrentSpan)(); } result.state.setCurrentAgentSpan(undefined); result._addItem(new events_1.RunAgentUpdatedStreamEvent(result.state._currentAgent)); result.state._noActiveAgentRun = true; result.state._currentTurnInProgress = false; // We've processed the handoff, so we need to run the loop again. result.state._currentStep = { type: 'next_step_run_again', }; break; case 'next_step_run_again': result.state._currentTurnInProgress = false; logger_1.default.debug('Running next loop'); break; default: logger_1.default.debug('Running next loop'); } } } catch (error) { result.state._currentTurnInProgress = false; if (guardrailTracker.pending) { await guardrailTracker.awaitCompletion({ suppressErrors: true }); } if (sentInputToModel && !streamInputPersisted && !guardrailTracker.failed) { await persistStreamInputIfNeeded(); } const handledResult = await (0, errorHandlers_1.tryHandleRunError)({ error, state: result.state, errorHandlers: options.errorHandlers, outputGuardrailDefs: this.outputGuardrailDefs, emitAgentEnd: (context, agent, outputText) => { this.emit('agent_end', context, agent, outputText); agent.emit('agent_end', context, outputText); }, streamResult: result, }); if (handledResult) { await persistStreamInputIfNeeded(); if (!serverManagesConversation) { await (0, sessionPersistence_1.saveStreamResultToSession)(options.session, result); } return; } if (result.state._currentAgentSpan) { result.state._currentAgentSpan.setError({ message: 'Error in agent run', data: { error: String(error) }, }); } throw error; } finally { if (guardrailTracker.pending) { await guardrailTracker.awaitCompletion({ suppressErrors: true }); } if (sentInputToModel && !streamInputPersisted && !guardrailTracker.failed) { await persistStreamInputIfNeeded(); } if (result.state._currentStep?.type !== 'next_step_interruption') { try { await (0, tool_1.disposeResolvedComputers)({ runContext: result.state._context }); } catch (error) { logger_1.default.warn(`Failed to dispose computers after run: ${error}`); } } 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, preserveTurnPersistenceOnResume) { 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 ?? constants_1.DEFAULT_MAX_TURNS); const resolvedConversationId = options.conversationId ?? (isResumedState ? state._conversationId : undefined); const resolvedPreviousResponseId = options.previousResponseId ?? (isResumedState ? state._previousResponseId : undefined); if (!isResumedState) { state.setConversationContext(resolvedConversationId, resolvedPreviousResponseId); } // Initialize the streamed result with existing state const result = new result_1.StreamedRunResult({ signal: options.signal, state, }); const streamOptions = { ...options, signal: result._getAbortSignal(), }; // Setup defaults result.maxTurns = streamOptions.maxTurns ?? state._maxTurns; // Continue the stream loop without blocking const streamLoopPromise = this.#runStreamLoop(result, streamOptions, isResumedState, ensureStreamInputPersisted, sessionInputUpdate, preserveTurnPersistenceOnResume).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; }); } /** * @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, resolvedModelName } = await this.#resolveModelForAgent(state._currentAgent); let modelSettings = { ...this.config.modelSettings, ...state._currentAgent.modelSettings, }; modelSettings = (0, modelSettings_1.adjustModelSettingsForNonGPT5RunnerModel)(explictlyModelSet, state._currentAgent.modelSettings, model, modelSettings, resolvedModelName); modelSettings = (0, modelSettings_1.maybeResetToolChoice)(state._currentAgent, state._toolUseTracker, modelSettings); state._lastModelSettings = modelSettings; const systemInstructions = await state._currentAgent.getSystemPrompt(state._context); const prompt = await state._currentAgent.getPrompt(state._context); const { modelInput, sourceItems, persistedItems, filterApplied } = await (0, conversation_1.applyCallModelInputFilter)(state._currentAgent, options.callModelInputFilter, state._context, turnInput, systemInstructions); // 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.