@openai/agents-core
Version:
The OpenAI Agents SDK is a lightweight yet powerful framework for building multi-agent workflows.
521 lines • 21 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.ScriptedModel = exports.ScriptedModelRequestAbortedError = exports.InvalidScriptedModelStepError = exports.UnconsumedModelStepsError = exports.UnexpectedModelCallError = void 0;
exports.modelResponse = modelResponse;
exports.modelError = modelError;
exports.modelResponder = modelResponder;
exports.modelStream = modelStream;
exports.modelStreamResponder = modelStreamResponder;
exports.assistantMessage = assistantMessage;
exports.functionCall = functionCall;
const usage_1 = require("../usage.js");
const rawUsage_1 = require("../utils/rawUsage.js");
const testingSnapshot_1 = require("../utils/testingSnapshot.js");
class UnexpectedModelCallError extends Error {
/** The zero-based position of the call in `ScriptedModel.calls`. */
callIndex;
streamed;
constructor(call) {
super(`ScriptedModel received unexpected ${call.streamed ? 'streaming' : 'non-streaming'} call #${displayIndex(call.index)}; no scripted steps remain.`);
this.name = 'UnexpectedModelCallError';
this.callIndex = call.index;
this.streamed = call.streamed;
}
}
exports.UnexpectedModelCallError = UnexpectedModelCallError;
class UnconsumedModelStepsError extends Error {
remainingSteps;
constructor(remainingSteps) {
super(`ScriptedModel has ${remainingSteps} unconsumed scripted step${remainingSteps === 1 ? '' : 's'}.`);
this.name = 'UnconsumedModelStepsError';
this.remainingSteps = remainingSteps;
}
}
exports.UnconsumedModelStepsError = UnconsumedModelStepsError;
class InvalidScriptedModelStepError extends Error {
reason;
/** The zero-based position of an invalid constructor or `enqueue()` input. */
inputIndex;
/** The zero-based position of a call in `ScriptedModel.calls`. */
callIndex;
stepType;
constructor(options) {
super(options.message);
this.name = 'InvalidScriptedModelStepError';
this.reason = options.reason;
this.inputIndex = options.inputIndex;
this.callIndex = options.callIndex;
this.stepType = options.stepType;
}
}
exports.InvalidScriptedModelStepError = InvalidScriptedModelStepError;
class ScriptedModelRequestAbortedError extends Error {
/** The zero-based position of a call in `ScriptedModel.calls`. */
callIndex;
constructor(callIndex) {
super('Scripted model request was aborted.');
this.name = 'AbortError';
this.callIndex = callIndex;
}
}
exports.ScriptedModelRequestAbortedError = ScriptedModelRequestAbortedError;
/** Creates a scripted normalized model response step. */
function modelResponse(response) {
return { type: 'response', response };
}
/** Creates a scripted model failure step. */
function modelError(error, retryAdvice) {
return { type: 'error', error, retryAdvice };
}
/** Creates a response step that derives its output from the recorded call. */
function modelResponder(respond) {
return { type: 'responder', respond };
}
/** Creates a raw normalized stream step. */
function modelStream(events) {
return { type: 'stream', events };
}
/** Creates a raw normalized stream derived from the recorded call. */
function modelStreamResponder(respond) {
return { type: 'stream_responder', respond };
}
/** Creates a normalized assistant text message. */
function assistantMessage(text, options = {}) {
return {
type: 'message',
role: 'assistant',
status: 'completed',
...(typeof options.id === 'undefined' ? {} : { id: options.id }),
...(typeof options.phase === 'undefined' ? {} : { phase: options.phase }),
content: [
{
type: 'output_text',
text,
},
],
...(typeof options.providerData === 'undefined'
? {}
: { providerData: options.providerData }),
};
}
/** Creates a normalized function call item. */
function functionCall(name, args, options) {
return {
type: 'function_call',
name,
callId: options.callId,
id: options.id ?? options.callId,
namespace: options.namespace,
status: 'completed',
arguments: typeof args === 'string' ? args : JSON.stringify(args),
providerData: options.providerData,
};
}
/**
* A deterministic model test double that consumes one scripted step per call.
*/
class ScriptedModel {
#steps;
#calls = [];
#retryAdvice = new Map();
constructor(inputs = []) {
this.#steps = Array.from(inputs, (input, index) => normalizeInput(input, index));
}
/** All calls recorded by this model in invocation order. */
get calls() {
return Object.freeze(this.#calls.map(snapshotRecordedModelCall));
}
/** The first recorded call, if one has occurred. */
get firstCall() {
const call = this.#calls[0];
return call ? snapshotRecordedModelCall(call) : undefined;
}
/** The most recent recorded call, if one has occurred. */
get lastCall() {
const call = this.#calls.at(-1);
return call ? snapshotRecordedModelCall(call) : undefined;
}
/** The number of scripted steps that have not been consumed. */
get remainingSteps() {
return this.#steps.length;
}
/** Adds steps to the end of the script. */
enqueue(...inputs) {
const steps = inputs.map((input, index) => normalizeInput(input, index));
this.#steps.push(...steps);
}
/** Throws when the script still contains unconsumed steps. */
assertComplete() {
if (this.#steps.length > 0) {
throw new UnconsumedModelStepsError(this.#steps.length);
}
}
async getResponse(request) {
throwIfAborted(request.signal);
const { call, step } = this.#recordAndTakeStep(request, false);
if (step.type === 'stream' || step.type === 'stream_responder') {
throw new InvalidScriptedModelStepError({
reason: 'incompatible_call',
callIndex: call.index,
stepType: step.type,
message: `ScriptedModel cannot use raw stream step for non-streaming call #${displayIndex(call.index)}.`,
});
}
if (step.type === 'error') {
this.#rememberRetryAdvice(step, request, false);
throw step.error;
}
const response = step.type === 'responder' ? await step.respond(call) : step.response;
throwIfAborted(request.signal, call.index);
return normalizeResponseForCall(response, call, false);
}
getStreamedResponse(request) {
throwIfAborted(request.signal);
const { call, step } = this.#recordAndTakeStep(request, true);
return this.#processStreamedStep(request, call, step);
}
async *#processStreamedStep(request, call, step) {
throwIfAborted(request.signal, call.index);
if (step.type === 'error') {
this.#rememberRetryAdvice(step, request, true);
throw step.error;
}
if (step.type === 'stream' || step.type === 'stream_responder') {
const events = step.type === 'stream' ? step.events : await step.respond(call);
throwIfAborted(request.signal, call.index);
if (step.type === 'stream_responder' && !isIterable(events)) {
throw new InvalidScriptedModelStepError({
reason: 'invalid_stream',
callIndex: call.index,
stepType: step.type,
message: `ScriptedModel stream responder returned an invalid stream for call #${displayIndex(call.index)}.`,
});
}
for await (const event of events) {
throwIfAborted(request.signal, call.index);
yield event;
}
throwIfAborted(request.signal, call.index);
return;
}
const scriptedResponse = step.type === 'responder' ? await step.respond(call) : step.response;
throwIfAborted(request.signal, call.index);
const response = normalizeResponseForCall(scriptedResponse, call, true);
yield* streamResponse(response, request.signal, call.index);
}
async getRetryAdvice(request) {
const records = this.#retryAdvice.get(request.error);
if (!records) {
return undefined;
}
let recordIndex = records.findIndex((record) => record.streamed === request.stream &&
record.request === request.request);
if (recordIndex === -1) {
recordIndex = records.findIndex((record) => record.streamed === request.stream &&
requestsShareCallInputs(record.request, request.request));
}
if (recordIndex === -1) {
recordIndex = records.findIndex((record) => record.streamed === request.stream);
}
if (recordIndex === -1) {
return undefined;
}
const [record] = records.splice(recordIndex, 1);
if (records.length === 0) {
this.#retryAdvice.delete(request.error);
}
const advice = record?.advice;
if (typeof advice === 'function') {
return await advice(request);
}
return advice;
}
#recordAndTakeStep(request, streamed) {
const recordedCall = Object.freeze({
index: this.#calls.length,
streamed,
request: snapshotRequest(request),
});
this.#calls.push(recordedCall);
const step = this.#steps.shift();
if (!step) {
throw new UnexpectedModelCallError(recordedCall);
}
return { call: snapshotRecordedModelCall(recordedCall), step };
}
#rememberRetryAdvice(step, request, streamed) {
const records = this.#retryAdvice.get(step.error) ?? [];
records.push({ request, streamed, advice: step.retryAdvice });
this.#retryAdvice.set(step.error, records);
}
}
exports.ScriptedModel = ScriptedModel;
function snapshotRequest(request) {
return Object.freeze((0, testingSnapshot_1.snapshotTestingValue)(request));
}
function snapshotRecordedModelCall(call) {
return Object.freeze({
index: call.index,
streamed: call.streamed,
request: snapshotRequest(call.request),
});
}
function normalizeInput(input, inputIndex) {
if (Array.isArray(input)) {
return { type: 'response', response: snapshotScriptedResponse(input) };
}
if (typeof input !== 'object' || input === null) {
throw invalidStep('invalid_input', inputIndex);
}
if ('type' in input) {
const stepType = typeof input.type === 'string' ? input.type : undefined;
switch (input.type) {
case 'response':
if (!('response' in input)) {
throw invalidStep('missing_response', inputIndex, stepType);
}
validateResponse(input.response, inputIndex);
return {
type: 'response',
response: snapshotScriptedResponse(input.response),
};
case 'error':
if (!('error' in input)) {
throw invalidStep('missing_error', inputIndex, stepType);
}
return {
type: 'error',
error: input.error,
retryAdvice: typeof input.retryAdvice === 'function'
? input.retryAdvice
: (0, testingSnapshot_1.snapshotTestingValue)(input.retryAdvice),
};
case 'responder':
if (typeof input.respond !== 'function') {
throw invalidStep('missing_responder', inputIndex, stepType);
}
return { type: 'responder', respond: input.respond };
case 'stream_responder':
if (typeof input.respond !== 'function') {
throw invalidStep('missing_responder', inputIndex, stepType);
}
return { type: 'stream_responder', respond: input.respond };
case 'stream':
if (!isIterable(input.events)) {
throw invalidStep('invalid_stream', inputIndex, stepType);
}
return {
type: 'stream',
events: snapshotScriptedStream(input.events),
};
default:
throw invalidStep('unknown_step_type', inputIndex, stepType);
}
}
if ('output' in input || 'usage' in input) {
validateResponse(input, inputIndex);
return { type: 'response', response: snapshotScriptedResponse(input) };
}
throw invalidStep('invalid_input', inputIndex);
}
function validateResponse(response, inputIndex, callIndex) {
if (Array.isArray(response)) {
return;
}
if (typeof response !== 'object' ||
response === null ||
!Array.isArray(response.output) ||
!(response.usage instanceof usage_1.Usage)) {
throw new InvalidScriptedModelStepError({
reason: 'invalid_response',
inputIndex,
callIndex,
message: typeof callIndex === 'number'
? `ScriptedModel responder returned an invalid response for call #${displayIndex(callIndex)}.`
: `ScriptedModel input #${displayIndex(inputIndex ?? 0)} has an invalid response envelope.`,
});
}
}
function invalidStep(reason, inputIndex, stepType) {
const position = inputIndex ?? 0;
const detail = reason === 'unknown_step_type' && stepType
? ` has unknown type "${stepType}"`
: ` is invalid (${reason.replace(/_/g, ' ')})`;
return new InvalidScriptedModelStepError({
reason,
inputIndex: position,
stepType,
message: `ScriptedModel input #${displayIndex(position)}${detail}.`,
});
}
function snapshotScriptedResponse(response) {
if (Array.isArray(response)) {
return (0, testingSnapshot_1.snapshotTestingValue)(response);
}
const snapshot = {
output: (0, testingSnapshot_1.snapshotTestingValue)(response.output),
usage: snapshotUsage(response.usage),
...(typeof response.responseId === 'undefined'
? {}
: { responseId: response.responseId }),
...(typeof response.requestId === 'undefined'
? {}
: { requestId: response.requestId }),
...(typeof response.providerData === 'undefined'
? {}
: { providerData: (0, testingSnapshot_1.snapshotTestingValue)(response.providerData) }),
};
const rawUsageDescriptor = Object.getOwnPropertyDescriptor(response, 'rawUsage');
if (rawUsageDescriptor) {
Object.defineProperty(snapshot, 'rawUsage', {
...rawUsageDescriptor,
...(Object.prototype.hasOwnProperty.call(rawUsageDescriptor, 'value')
? { value: (0, testingSnapshot_1.snapshotTestingValue)(rawUsageDescriptor.value) }
: {}),
});
}
return snapshot;
}
function snapshotUsage(usage) {
return new usage_1.Usage({
requests: usage.requests,
inputTokens: usage.inputTokens,
outputTokens: usage.outputTokens,
totalTokens: usage.totalTokens,
inputTokensDetails: (0, testingSnapshot_1.snapshotTestingValue)(usage.inputTokensDetails),
outputTokensDetails: (0, testingSnapshot_1.snapshotTestingValue)(usage.outputTokensDetails),
requestUsageEntries: usage.requestUsageEntries?.map((entry) => ({
inputTokens: entry.inputTokens,
outputTokens: entry.outputTokens,
totalTokens: entry.totalTokens,
inputTokensDetails: (0, testingSnapshot_1.snapshotTestingValue)(entry.inputTokensDetails),
outputTokensDetails: (0, testingSnapshot_1.snapshotTestingValue)(entry.outputTokensDetails),
endpoint: entry.endpoint,
})),
});
}
function snapshotScriptedStream(events) {
return Array.isArray(events) ? (0, testingSnapshot_1.snapshotTestingValue)(events) : events;
}
function displayIndex(index) {
return index + 1;
}
function isIterable(value) {
if (typeof value !== 'object' || value === null) {
return false;
}
const candidate = value;
return (typeof candidate[Symbol.iterator] === 'function' ||
typeof candidate[Symbol.asyncIterator] === 'function');
}
function requestsShareCallInputs(recorded, advised) {
return (recorded.input === advised.input &&
recorded.modelSettings === advised.modelSettings &&
recorded.tools === advised.tools &&
recorded.handoffs === advised.handoffs);
}
function normalizeResponse(response, request, callIndex, requireResponseId) {
validateResponse(response, undefined, callIndex);
if (Array.isArray(response)) {
return {
output: response,
usage: new usage_1.Usage({ requests: 1 }),
responseId: `scripted-response-${displayIndex(callIndex)}`,
};
}
let rawUsage;
const preserveRawUsage = request.modelSettings.preserveRawUsage === true;
if (preserveRawUsage) {
try {
rawUsage = (0, rawUsage_1.snapshotRawUsage)(response.rawUsage);
}
catch {
rawUsage = undefined;
}
}
const responseId = requireResponseId && typeof response.responseId === 'undefined'
? `scripted-response-${displayIndex(callIndex)}`
: response.responseId;
const hasRawUsage = 'rawUsage' in response;
if (responseId === response.responseId && !hasRawUsage) {
return response;
}
const responseWithoutRawUsage = {};
for (const key of Object.keys(response)) {
if (key !== 'rawUsage') {
Object.assign(responseWithoutRawUsage, { [key]: response[key] });
}
}
return {
...responseWithoutRawUsage,
...(typeof responseId === 'undefined' ? {} : { responseId }),
...(typeof rawUsage === 'undefined' ? {} : { rawUsage }),
};
}
function normalizeResponseForCall(response, call, requireResponseId) {
const normalized = normalizeResponse(response, call.request, call.index, requireResponseId);
throwIfAborted(call.request.signal, call.index);
return normalized;
}
async function* streamResponse(response, signal, callIndex) {
throwIfAborted(signal, callIndex);
yield { type: 'response_started' };
for (const item of response.output) {
if (item.type !== 'message' ||
item.role !== 'assistant' ||
!Array.isArray(item.content)) {
continue;
}
for (const part of item.content) {
if (part.type === 'output_text') {
throwIfAborted(signal, callIndex);
yield (0, testingSnapshot_1.snapshotTestingValue)({
type: 'output_text_delta',
itemId: item.id,
delta: part.text,
...(typeof part.providerData === 'undefined'
? {}
: { providerData: part.providerData }),
});
}
}
}
throwIfAborted(signal, callIndex);
yield (0, testingSnapshot_1.snapshotTestingValue)({
type: 'response_done',
response: {
id: response.responseId,
requestId: response.requestId,
usage: {
requests: response.usage.requests,
inputTokens: response.usage.inputTokens,
outputTokens: response.usage.outputTokens,
totalTokens: response.usage.totalTokens,
inputTokensDetails: response.usage.inputTokensDetails.length > 0
? response.usage.inputTokensDetails
: undefined,
outputTokensDetails: response.usage.outputTokensDetails.length > 0
? response.usage.outputTokensDetails
: undefined,
requestUsageEntries: response.usage.requestUsageEntries?.map((entry) => ({
inputTokens: entry.inputTokens,
outputTokens: entry.outputTokens,
totalTokens: entry.totalTokens,
inputTokensDetails: entry.inputTokensDetails,
outputTokensDetails: entry.outputTokensDetails,
endpoint: entry.endpoint,
})),
},
rawUsage: response.rawUsage,
providerData: response.providerData,
output: response.output,
},
});
}
function throwIfAborted(signal, callIndex) {
if (!signal?.aborted) {
return;
}
throw new ScriptedModelRequestAbortedError(callIndex);
}
//# sourceMappingURL=scriptedModel.js.map