UNPKG

@openai/agents-core

Version:

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

885 lines (884 loc) 71.3 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 = 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 tool_1 = require("./tool.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; #getInputGuardrailDefinitions(state) { return this.inputGuardrailDefs.concat(state._currentAgent.inputGuardrails.map(guardrail_1.defineInputGuardrail)); } #splitInputGuardrails(state) { const guardrails = this.#getInputGuardrailDefinitions(state); const blocking = []; const parallel = []; for (const guardrail of guardrails) { if (guardrail.runInParallel === false) { blocking.push(guardrail); } else { parallel.push(guardrail); } } return { blocking, parallel }; } /** * @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, }); } 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 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; // 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) if (turnResult.nextStep.type === 'next_step_interruption') { // we are still in an interruption, so we need to avoid an infinite loop state._currentStep = turnResult.nextStep; return new result_1.RunResult(state); } // If continuing from interruption with next_step_run_again, set step to undefined // so the loop treats it as a new step without incrementing the turn. // The counter has already been adjusted by resolveInterruptedTurn's rewind logic. if (turnResult.nextStep.type === 'next_step_run_again') { continuingInterruptedTurn = true; state._currentStep = undefined; continue; } continuingInterruptedTurn = false; state._currentStep = turnResult.nextStep; } if (state._currentStep.type === 'next_step_run_again') { const artifacts = await prepareAgentArtifacts(state); const isResumingFromInterruption = isResumedState && continuingInterruptedTurn; continuingInterruptedTurn = false; // Do not advance the turn when resuming from an interruption; the next model call is // still part of the same logical turn. if (!isResumingFromInterruption) { 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})`); let parallelGuardrailPromise; // Only run input guardrails on the first turn of a new run. if (state._currentTurn === 1 && !isResumingFromInterruption) { const guardrails = this.#splitInputGuardrails(state); if (guardrails.blocking.length > 0) { await this.#runInputGuardrails(state, guardrails.blocking); } if (guardrails.parallel.length > 0) { parallelGuardrailPromise = this.#runInputGuardrails(state, guardrails.parallel); parallelGuardrailPromise.catch(() => { }); } } 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, turnInput); this.emit('agent_start', state._context, state._currentAgent, turnInput); } 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, toolsExplicitlyProvided: preparedCall.toolsExplicitlyProvided, 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 (parallelGuardrailPromise) { await parallelGuardrailPromise; } } 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._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) { 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, }); } 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 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; // 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) if (turnResult.nextStep.type === 'next_step_interruption') { // we are still in an interruption, so we need to avoid an infinite loop result.state._currentStep = turnResult.nextStep; return; } // If continuing from interruption with next_step_run_again, set step to undefined // so the loop treats it as a new step without incrementing the turn. // The counter has already been adjusted by resolveInterruptedTurn's rewind logic. if (turnResult.nextStep.type === 'next_step_run_again') { continuingInterruptedTurn = true; result.state._currentStep = undefined; continue; } continuingInterruptedTurn = false; result.state._currentStep = turnResult.nextStep; } if (result.state._currentStep.type === 'next_step_run_again') { const artifacts = await prepareAgentArtifacts(result.state); const isResumingFromInterruption = isResumedState && continuingInterruptedTurn; continuingInterruptedTurn = false; // Do not advance the turn when resuming from an interruption; the next model call is // still part of the same logical turn. if (!isResumingFromInterruption) { 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})`); let guardrailError; let parallelGuardrailPromise; // Only run input guardrails on the first turn of a new run. if (result.state._currentTurn === 1 && !isResumingFromInterruption) { const guardrails = this.#splitInputGuardrails(result.state); if (guardrails.blocking.length > 0) { await this.#runInputGuardrails(result.state, guardrails.blocking); } if (guardrails.parallel.length > 0) { const promise = this.#runInputGuardrails(result.state, guardrails.parallel); parallelGuardrailPromise = promise.catch((err) => { guardrailError = err; return []; }); } } 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, turnInput); this.emit('agent_start', result.state._context, currentAgent, turnInput); } let finalResponse = undefined; const preparedCall = await this.#prepareModelCall(result.state, options, artifacts, turnInput, serverConversationTracker, sessionInputUpdate); if (guardrailError) { throw guardrailError; } 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, toolsExplicitlyProvided: preparedCall.toolsExplicitlyProvided, handoffs: preparedCall.serializedHandoffs, outputType: (0, tools_1.convertAgentOutputTypeToSerializable)(currentAgent.outputType), tracing: getTracing(this.config.tracingDisabled, this.config.traceIncludeSensitiveData), signal: options.signal, })) { if (guardrailError) { throw guardrailError; } 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 return; } result._addItem(new events_1.RunRawModelStreamEvent(event)); } if (parallelGuardrailPromise) { await parallelGuardrailPromise; if (guardrailError) { throw guardrailError; } } 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._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) { 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, guardrailsOverride) { const guardrails = guardrailsOverride ?? this.#getInputGuardrailDefinitions(state); 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); })); state._inputGuardrailResults.push(...results); 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); } } return results; } 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); } } return []; } 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 runOutput = getTurnInput([], state._generatedItems); const guardrailArgs = { agent: state._currentAgent, agentOutput, context: state._context, details: { modelResponse: state._lastTurnResponse, output: runOutput, }, }; 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({