@openai/agents-core
Version:
The OpenAI Agents SDK is a lightweight yet powerful framework for building multi-agent workflows.
487 lines • 20 kB
JavaScript
import { AgentsError, GuardrailExecutionError, InputGuardrailTripwireTriggered, OutputGuardrailTripwireTriggered, UserError, } from "../errors.mjs";
import { defineInputGuardrail, defineOutputGuardrail, } from "../guardrail.mjs";
import { getRunOutput } from "./items.mjs";
import { withGuardrailSpan } from "../tracing/index.mjs";
import { isDataRedactedError, processFinalOutputWithRedaction, } from "../utils/finalOutputError.mjs";
export 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,
};
};
const currentResponseOutputGuardrailResults = new WeakMap();
/** Validates restored result ownership without publishing live ownership evidence. */
export 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 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 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;
}
}
export function buildInputGuardrailDefinitions(state, runnerGuardrails) {
return runnerGuardrails.concat(state._currentAgent.inputGuardrails.map(defineInputGuardrail));
}
export 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 };
}
export 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 InputGuardrailTripwireTriggered(`Input guardrail triggered: ${JSON.stringify(result.output.outputInfo)}`, result, state);
},
isTripwireError: (error) => error instanceof InputGuardrailTripwireTriggered,
onError: (error) => {
return new GuardrailExecutionError(`Input guardrail failed to complete: ${error}`, error, state);
},
onErrorObserved: options.onErrorObserved,
});
}
export 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(defineOutputGuardrail));
if (guardrails.length === 0) {
return;
}
const agentOutput = state._currentAgent.outputType === 'text'
? state._currentAgent.processFinalOutput(output)
: processFinalOutputWithRedaction(() => state._currentAgent.processFinalOutput(output));
const runOutput = 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 OutputGuardrailTripwireTriggered(`Output guardrail triggered: ${JSON.stringify(result.output.outputInfo)}`, result, state);
},
isTripwireError: (error) => error instanceof OutputGuardrailTripwireTriggered,
onError: (error) => {
return new GuardrailExecutionError(`Output guardrail failed to complete: ${error}`, error, state);
},
onResultObserved,
});
}
export 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 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 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 AgentsError) {
guardrailError.state ??= state;
}
if (guardrailError instanceof OutputGuardrailTripwireTriggered ||
!isDataRedactedError(guardrailError)) {
guardrailError.cause =
outputBlocked
? sanitizeBlockedOutputGuardrailErrorDetails(persistenceError, output, blockedMessage, state)
: persistenceError;
}
}
}
releaseBlockedOutputPersistence(state);
}
else if (persistUnblockedFailure && !signal?.aborted) {
try {
await persistUnblockedFailure();
}
catch (persistenceError) {
if (!isDataRedactedError(guardrailError)) {
guardrailError.cause =
persistenceError;
}
}
}
throw guardrailError;
}
}
export function sanitizeBlockedOutputGuardrailError(error, rejectedOutput, redactedOutput, state, currentTripwireResult) {
if (error instanceof 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 OutputGuardrailTripwireTriggered('Output guardrail triggered.', completedResult, state);
}
if (!(error instanceof GuardrailExecutionError)) {
return error;
}
return new GuardrailExecutionError(sanitizeBlockedOutputGuardrailErrorDetails(error, rejectedOutput, redactedOutput, state).message, sanitizeBlockedOutputGuardrailErrorDetails(error.error, rejectedOutput, redactedOutput, state), state);
}
export 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;
}
}
export 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;
}
export 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()),
});
}
export 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.mjs.map