UNPKG

@openai/agents-core

Version:

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

502 lines • 21 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.createGuardrailTracker = void 0; exports.getSerializedOutputGuardrailResults = getSerializedOutputGuardrailResults; exports.buildInputGuardrailDefinitions = buildInputGuardrailDefinitions; exports.splitInputGuardrails = splitInputGuardrails; exports.runInputGuardrails = runInputGuardrails; exports.runOutputGuardrails = runOutputGuardrails; exports.finalizeOutputGuardrails = finalizeOutputGuardrails; exports.sanitizeBlockedOutputGuardrailError = sanitizeBlockedOutputGuardrailError; exports.sanitizeBlockedOutputGuardrailErrorDetails = sanitizeBlockedOutputGuardrailErrorDetails; exports.sanitizeBlockedOutputGuardrailResults = sanitizeBlockedOutputGuardrailResults; exports.replaceSanitizedOutputGuardrailMessages = replaceSanitizedOutputGuardrailMessages; exports.sanitizeBlockedToolOutputGuardrailResults = sanitizeBlockedToolOutputGuardrailResults; const errors_1 = require("../errors.js"); const guardrail_1 = require("../guardrail.js"); const items_1 = require("./items.js"); const tracing_1 = require("../tracing/index.js"); const finalOutputError_1 = require("../utils/finalOutputError.js"); const createGuardrailTracker = () => { let pending = false; let failed = false; let error = undefined; let promise; const setError = (err) => { failed = true; error = err; }; const setPromise = (incoming) => { if (!incoming) { return; } pending = true; promise = incoming .then((results) => results) .catch((err) => { setError(err); // Swallow to keep downstream flow consistent; failure is signaled via `failed`. return []; }) .finally(() => { pending = false; }); }; const throwIfError = async () => { if (error) { if (promise) { await promise; } throw error; } }; const awaitCompletion = async (options) => { if (promise) { await promise; } if (error && !options?.suppressErrors) { throw error; } }; return { get pending() { return pending; }, get failed() { return failed; }, get error() { return error; }, markPending: () => { pending = true; }, setPromise, setError, throwIfError, awaitCompletion, }; }; exports.createGuardrailTracker = createGuardrailTracker; const currentResponseOutputGuardrailResults = new WeakMap(); /** Validates restored result ownership without publishing live ownership evidence. */ function getSerializedOutputGuardrailResults(state, blockedOutput) { const results = new Set(); for (const result of state._outputGuardrailResults) { if (result.agent !== state._currentAgent) { continue; } if (result.agentOutput !== blockedOutput || result.output.outputInfo !== undefined) { throw new errors_1.UserError('Cannot resume this serialized terminal output because previous output guardrail result ownership was not preserved. Continue the live RunState or start a new run from safe input.'); } results.add(result); } return results; } function getCurrentResponseOutputGuardrailResults(state, guardedTerminalToolOutput, redactedOutput) { const existing = currentResponseOutputGuardrailResults.get(state); if (existing && existing.response === state._lastTurnResponse) { return existing.results; } const results = guardedTerminalToolOutput && state._serializedCurrentStep === state._currentStep ? getSerializedOutputGuardrailResults(state, redactedOutput) : new Set(); currentResponseOutputGuardrailResults.set(state, { response: state._lastTurnResponse, results, }); return results; } async function runGuardrailsWithTripwire(options) { const { state, guardrails, guardrailArgs, resultsTarget, onTripwire, isTripwireError, onError, onErrorObserved, onResultObserved, } = options; const guardrailPromises = guardrails.map(async (guardrail) => { const observedResult = await (0, tracing_1.withGuardrailSpan)(async (span) => { const result = await guardrail.run(guardrailArgs); const tripwireTriggered = result.output.tripwireTriggered; span.spanData.triggered = tripwireTriggered; return { result, tripwireTriggered, }; }, { data: { name: guardrail.name } }, state._currentAgentSpan); onResultObserved?.(observedResult.result, observedResult.tripwireTriggered); return observedResult; }); let resultsPublished = false; try { const observedResults = await Promise.all(guardrailPromises); const results = observedResults.map(({ result }) => result); resultsTarget.push(...results); resultsPublished = true; for (const { result, tripwireTriggered } of observedResults) { if (tripwireTriggered) { if (state._currentAgentSpan) { state._currentAgentSpan.setError({ message: 'Guardrail tripwire triggered', data: { guardrail: result.guardrail.name }, }); } onTripwire(result); } } return results; } catch (error) { const finalError = isTripwireError(error) ? error : onError(error); onErrorObserved?.(finalError); // Promise.all rejects immediately, so drain the full batch before the // failure is surfaced to prevent sibling guardrails from outliving the run. const settledResults = await Promise.allSettled(guardrailPromises); if (!resultsPublished) { for (const result of settledResults) { if (result.status === 'fulfilled') { resultsTarget.push(result.value.result); } } } throw finalError; } } function buildInputGuardrailDefinitions(state, runnerGuardrails) { return runnerGuardrails.concat(state._currentAgent.inputGuardrails.map(guardrail_1.defineInputGuardrail)); } function splitInputGuardrails(guardrails) { const blocking = []; const parallel = []; for (const guardrail of guardrails) { if (guardrail.runInParallel === false) { blocking.push(guardrail); } else { parallel.push(guardrail); } } return { blocking, parallel }; } async function runInputGuardrails(state, guardrails, options = {}) { if (guardrails.length === 0) { return []; } const guardrailArgs = { agent: state._currentAgent, input: options.input ?? state._originalInput, context: state._context, }; return await runGuardrailsWithTripwire({ state, guardrails, guardrailArgs, resultsTarget: state._inputGuardrailResults, onTripwire: (result) => { throw new errors_1.InputGuardrailTripwireTriggered(`Input guardrail triggered: ${JSON.stringify(result.output.outputInfo)}`, result, state); }, isTripwireError: (error) => error instanceof errors_1.InputGuardrailTripwireTriggered, onError: (error) => { return new errors_1.GuardrailExecutionError(`Input guardrail failed to complete: ${error}`, error, state); }, onErrorObserved: options.onErrorObserved, }); } async function runOutputGuardrails(state, runnerOutputGuardrails, output, onResultObserved) { // Runner-level output guardrails are context-agnostic, so align them with the active run context type. const runnerGuardrails = runnerOutputGuardrails; const guardrails = runnerGuardrails.concat(state._currentAgent.outputGuardrails.map(guardrail_1.defineOutputGuardrail)); if (guardrails.length === 0) { return; } const agentOutput = state._currentAgent.outputType === 'text' ? state._currentAgent.processFinalOutput(output) : (0, finalOutputError_1.processFinalOutputWithRedaction)(() => state._currentAgent.processFinalOutput(output)); const runOutput = (0, items_1.getRunOutput)(state._generatedItems, state._reasoningItemIdPolicy); const guardrailArgs = { agent: state._currentAgent, agentOutput, context: state._context, details: { modelResponse: state._lastTurnResponse, output: runOutput, }, }; await runGuardrailsWithTripwire({ state, guardrails, guardrailArgs, resultsTarget: state._outputGuardrailResults, onTripwire: (result) => { throw new errors_1.OutputGuardrailTripwireTriggered(`Output guardrail triggered: ${JSON.stringify(result.output.outputInfo)}`, result, state); }, isTripwireError: (error) => error instanceof errors_1.OutputGuardrailTripwireTriggered, onError: (error) => { return new errors_1.GuardrailExecutionError(`Output guardrail failed to complete: ${error}`, error, state); }, onResultObserved, }); } async function finalizeOutputGuardrails(options) { const { state, runnerOutputGuardrails, output, redactedOutput, guardedTerminalToolOutput, signal, sanitizeRejectedOutput, persistBlockedOutput, persistUnblockedFailure, onBlockedOutput, releaseBlockedOutputPersistence, } = options; if (runnerOutputGuardrails.length === 0 && state._currentAgent.outputGuardrails.length === 0) { return; } const outputGuardrailResultStart = state._outputGuardrailResults.length; let completedTripwireResult; const observedResults = new Map(); const ownedGuardrailResults = getCurrentResponseOutputGuardrailResults(state, guardedTerminalToolOutput, redactedOutput); try { await runOutputGuardrails(state, runnerOutputGuardrails, output, (guardrailResult, tripwireTriggered) => { ownedGuardrailResults.add(guardrailResult); observedResults.set(guardrailResult, tripwireTriggered); if (tripwireTriggered && !completedTripwireResult) { completedTripwireResult = guardrailResult; } }); } catch (error) { const currentTripwireResult = state._outputGuardrailResults .slice(outputGuardrailResultStart) .find((result) => ownedGuardrailResults.has(result) && observedResults.get(result) === true) ?? completedTripwireResult; const outputBlockedResult = sanitizeRejectedOutput(state, outputGuardrailResultStart, currentTripwireResult, observedResults, error instanceof errors_1.OutputGuardrailTripwireTriggered ? error : undefined, ownedGuardrailResults); const outputBlockedMessage = typeof outputBlockedResult === 'string' || outputBlockedResult === false ? outputBlockedResult : await outputBlockedResult; signal?.throwIfAborted(); const outputBlocked = outputBlockedMessage !== false; const blockedMessage = outputBlockedMessage || redactedOutput; const guardrailError = outputBlocked ? sanitizeBlockedOutputGuardrailError(error, output, blockedMessage, state, currentTripwireResult) : error; const completedOutputTripwireError = completedTripwireResult !== undefined && guardrailError instanceof errors_1.OutputGuardrailTripwireTriggered; // Assistant output is unvetted whenever its guardrail batch fails, even // when no guardrail completed with a tripwire verdict. if (outputBlocked || completedOutputTripwireError || !guardedTerminalToolOutput) { onBlockedOutput?.(); if (persistBlockedOutput && !signal?.aborted) { try { await persistBlockedOutput(); } catch (persistenceError) { if (guardrailError instanceof errors_1.AgentsError) { guardrailError.state ??= state; } if (guardrailError instanceof errors_1.OutputGuardrailTripwireTriggered || !(0, finalOutputError_1.isDataRedactedError)(guardrailError)) { guardrailError.cause = outputBlocked ? sanitizeBlockedOutputGuardrailErrorDetails(persistenceError, output, blockedMessage, state) : persistenceError; } } } releaseBlockedOutputPersistence(state); } else if (persistUnblockedFailure && !signal?.aborted) { try { await persistUnblockedFailure(); } catch (persistenceError) { if (!(0, finalOutputError_1.isDataRedactedError)(guardrailError)) { guardrailError.cause = persistenceError; } } } throw guardrailError; } } function sanitizeBlockedOutputGuardrailError(error, rejectedOutput, redactedOutput, state, currentTripwireResult) { if (error instanceof errors_1.OutputGuardrailTripwireTriggered) { const completedResult = { guardrail: { type: 'output', name: 'output_guardrail' }, agent: state._currentAgent, agentOutput: redactedOutput, output: { tripwireTriggered: true, outputInfo: undefined, }, }; try { const guardrailName = currentTripwireResult?.guardrail.name; if (typeof guardrailName === 'string' && guardrailName.length > 0) { completedResult.guardrail.name = guardrailName; } } catch { // Keep the safe fallback name when caller-owned metadata is unreadable. } return new errors_1.OutputGuardrailTripwireTriggered('Output guardrail triggered.', completedResult, state); } if (!(error instanceof errors_1.GuardrailExecutionError)) { return error; } return new errors_1.GuardrailExecutionError(sanitizeBlockedOutputGuardrailErrorDetails(error, rejectedOutput, redactedOutput, state).message, sanitizeBlockedOutputGuardrailErrorDetails(error.error, rejectedOutput, redactedOutput, state), state); } function sanitizeBlockedOutputGuardrailErrorDetails(error, rejectedOutput, redactedOutput, state) { if (state._currentAgent.outputType !== 'text') { return new Error('Error details are redacted.'); } const message = error instanceof Error ? error.message : 'Error details are redacted.'; return new Error(rejectedOutput.length > 0 ? message.split(rejectedOutput).join(redactedOutput) : message); } function cloneSanitizedOutputGuardrailResult(result, redactedOutput, tripwireTriggered) { try { return { guardrail: { type: 'output', name: result.guardrail.name, }, agent: result.agent, agentOutput: redactedOutput, output: { tripwireTriggered, outputInfo: undefined, }, }; } catch { return undefined; } } function sanitizeBlockedOutputGuardrailResults(state, resultStartIndex, redactedOutput, observedTripwireResults, tripwire, ownedGuardrailResults = new Set()) { const results = state._outputGuardrailResults; const sanitizedResults = []; const replacements = new Map(); const sanitizedOwnedResults = new Set(); for (let index = 0; index < resultStartIndex; index += 1) { try { const result = results[index]; if (!ownedGuardrailResults.has(result)) { sanitizedResults.push(result); continue; } const replacement = cloneSanitizedOutputGuardrailResult(result, redactedOutput, false); if (replacement) { replacements.set(result, replacement); sanitizedOwnedResults.add(replacement); sanitizedResults.push(replacement); } } catch { // Omit unreadable prior-attempt results instead of retaining rejected data. } } for (let index = resultStartIndex; index < results.length; index += 1) { try { const result = results[index]; const tripwireTriggered = observedTripwireResults.get(result); if (typeof tripwireTriggered !== 'boolean') { continue; } let replacement = replacements.get(result); if (!replacement) { replacement = cloneSanitizedOutputGuardrailResult(result, redactedOutput, tripwireTriggered); if (!replacement) { continue; } replacements.set(result, replacement); } sanitizedOwnedResults.add(replacement); sanitizedResults.push(replacement); } catch { // Omit unreadable caller-owned results instead of retaining sensitive data. } } state._outputGuardrailResults = sanitizedResults; if (tripwire) { tripwire.result = replacements.get(tripwire.result) ?? cloneSanitizedOutputGuardrailResult(tripwire.result, redactedOutput, true) ?? { guardrail: { type: 'output', name: 'output_guardrail', }, agent: state._currentAgent, agentOutput: redactedOutput, output: { tripwireTriggered: true, outputInfo: undefined, }, }; sanitizedOwnedResults.add(tripwire.result); tripwire.message = 'Output guardrail triggered.'; tripwire.stack = `${tripwire.name}: ${tripwire.message}`; } currentResponseOutputGuardrailResults.set(state, { response: state._lastTurnResponse, results: sanitizedOwnedResults, }); return sanitizedOwnedResults; } function replaceSanitizedOutputGuardrailMessages(state, sanitizedOwnedResults, redactedOutput, tripwire) { const replacements = new Map(); state._outputGuardrailResults = state._outputGuardrailResults.map((result) => { if (!sanitizedOwnedResults.has(result)) { return result; } const replacement = cloneSanitizedOutputGuardrailResult(result, redactedOutput, result.output.tripwireTriggered); if (!replacement) { return result; } replacements.set(result, replacement); return replacement; }); if (tripwire && sanitizedOwnedResults.has(tripwire.result)) { tripwire.result = replacements.get(tripwire.result) ?? cloneSanitizedOutputGuardrailResult(tripwire.result, redactedOutput, true) ?? tripwire.result; } currentResponseOutputGuardrailResults.set(state, { response: state._lastTurnResponse, results: new Set(replacements.values()), }); } function sanitizeBlockedToolOutputGuardrailResults(state, resultStartIndex, redactedOutput) { const results = state._toolOutputGuardrailResults; const sanitizedResults = results.slice(0, resultStartIndex); for (let index = resultStartIndex; index < results.length; index += 1) { try { const result = results[index]; const behavior = result.output.behavior; const behaviorType = behavior.type; let sanitizedBehavior; if (behaviorType === 'rejectContent') { sanitizedBehavior = { type: 'rejectContent', message: redactedOutput, }; } else if (behaviorType === 'throwException') { sanitizedBehavior = { type: 'throwException' }; } else if (behaviorType === 'allow') { sanitizedBehavior = { type: 'allow' }; } else { continue; } sanitizedResults.push({ guardrail: { type: 'tool_output', name: result.guardrail.name, }, output: { outputInfo: undefined, behavior: sanitizedBehavior, }, }); } catch { // Omit unreadable caller-owned results instead of retaining sensitive data. } } state._toolOutputGuardrailResults = sanitizedResults; } //# sourceMappingURL=guardrails.js.map