@openai/agents-core
Version:
The OpenAI Agents SDK is a lightweight yet powerful framework for building multi-agent workflows.
330 lines • 11.7 kB
JavaScript
import logger, { logModelAndToolActionError } from "../logger.mjs";
import { timer as _timer, isTracingLoopRunningByDefault, } from '@openai/agents-core/_shims';
import { tracing } from "../config.mjs";
import { combineAbortSignals } from "../utils/abortSignals.mjs";
import { NOOP_TRACE_OR_SPAN_ID } from "./utils.mjs";
function isNoopSpan(span) {
return (span.traceId === NOOP_TRACE_OR_SPAN_ID ||
span.spanId === NOOP_TRACE_OR_SPAN_ID);
}
/**
* Prints the traces and spans to the console
*/
export class ConsoleSpanExporter {
async export(items) {
if (tracing.disabled) {
logger.debug('Tracing is disabled. Skipping export');
return;
}
for (const item of items) {
if (logger.dontLogModelData || logger.dontLogToolData) {
console.log(item.type === 'trace'
? '[Exporter] Export trace. Trace data is redacted.'
: '[Exporter] Export span. Span data is redacted.');
continue;
}
if (item.type === 'trace') {
console.log(`[Exporter] Export trace traceId=${item.traceId} name=${item.name}${item.groupId ? ` groupId=${item.groupId}` : ''}`);
}
else {
console.log(`[Exporter] Export span: ${JSON.stringify(item)}`);
}
}
}
}
export class BatchTraceProcessor {
#maxQueueSize;
#maxBatchSize;
#scheduleDelay;
#exportTriggerSize;
#exporter;
#buffer = [];
#timer;
#timeout = null;
#exportInProgress = false;
#timeoutAbortController = null;
#activeExportAbortControllers = new Set();
#activeExportPromises = new Set();
constructor(exporter, { maxQueueSize = 1000, maxBatchSize = 100, scheduleDelay = 5000, // 5 seconds
exportTriggerRatio = 0.8, } = {}) {
this.#maxQueueSize = maxQueueSize;
this.#maxBatchSize = maxBatchSize;
this.#scheduleDelay = scheduleDelay;
this.#exportTriggerSize = maxQueueSize * exportTriggerRatio;
this.#exporter = exporter;
this.#timer = _timer;
if (isTracingLoopRunningByDefault()) {
this.start();
}
else {
logger.debug('Automatic trace export loop is not supported in this environment. You need to manually call `getGlobalTraceProvider().forceFlush()` to export traces.');
}
}
start() {
this.#timeoutAbortController = new AbortController();
this.#runExportLoop();
}
async #safeAddItem(item) {
if (this.#buffer.length + 1 > this.#maxQueueSize) {
logger.error('Dropping trace because buffer is full');
return;
}
// add the item to the buffer
this.#buffer.push(item);
if (this.#buffer.length > this.#exportTriggerSize) {
// start exporting immediately
await this.#exportBatches();
}
}
#runExportLoop() {
this.#timeout = this.#timer.setTimeout(async () => {
// scheduled export
await this.#exportBatches();
this.#runExportLoop();
}, this.#scheduleDelay);
// We set this so that Node no longer considers this part of the event loop and keeps the
// process alive until the timer is done.
if (typeof this.#timeout.unref === 'function') {
this.#timeout.unref();
}
}
async #exportBatches(force = false, signal) {
if (this.#buffer.length === 0) {
return;
}
logger.debug(`Exporting batches. Force: ${force}. Buffer size: ${this.#buffer.length}`);
const exportBatch = async (batch) => {
this.#exportInProgress = true;
const activeExportAbortController = new AbortController();
this.#activeExportAbortControllers.add(activeExportAbortController);
const combinedSignal = combineAbortSignals(signal, activeExportAbortController.signal);
const exportPromise = (async () => {
try {
await this.#exporter.export(batch, combinedSignal.signal);
}
catch (error) {
logModelAndToolActionError(logger, 'Tracing exporter failed to export batch', error);
}
finally {
combinedSignal.cleanup();
this.#activeExportAbortControllers.delete(activeExportAbortController);
this.#exportInProgress = this.#activeExportAbortControllers.size > 0;
}
})();
this.#activeExportPromises.add(exportPromise);
try {
await exportPromise;
}
finally {
this.#activeExportPromises.delete(exportPromise);
}
};
if (force || this.#buffer.length < this.#maxBatchSize) {
const toExport = [...this.#buffer];
this.#buffer = [];
await exportBatch(toExport);
}
else if (this.#buffer.length > 0) {
const batch = this.#buffer.splice(0, this.#maxBatchSize);
await exportBatch(batch);
}
}
#abortActiveExports() {
for (const controller of this.#activeExportAbortControllers) {
controller.abort();
}
}
async #waitForShutdownProgress(signal) {
await new Promise((resolve) => {
if (signal?.aborted) {
resolve();
return;
}
const state = {};
const cleanup = () => {
if (state.timeout) {
this.#timer.clearTimeout(state.timeout);
}
signal?.removeEventListener('abort', onAbort);
};
const done = () => {
cleanup();
resolve();
};
const onAbort = () => done();
state.timeout = this.#timer.setTimeout(done, 500);
signal?.addEventListener('abort', onAbort, { once: true });
});
}
async onTraceStart(trace) {
await this.#safeAddItem(trace);
}
async onTraceEnd(_trace) {
// We don't send traces on end because we already send them on start
}
async onSpanStart(_span) {
// We don't send spans on start because we send them at the end
}
async onSpanEnd(span) {
await this.#safeAddItem(span);
}
async shutdown(timeout) {
let shutdownTimeout;
const shutdownAbortController = timeout
? (this.#timeoutAbortController ?? new AbortController())
: undefined;
if (timeout) {
this.#timeoutAbortController = shutdownAbortController ?? null;
shutdownTimeout = this.#timer.setTimeout(() => {
// Force shutdown the HTTP request.
shutdownAbortController?.abort();
this.#abortActiveExports();
}, timeout);
if (typeof shutdownTimeout.unref === 'function') {
shutdownTimeout.unref();
}
}
try {
logger.debug('Shutting down gracefully');
while (this.#buffer.length > 0 || this.#exportInProgress) {
logger.debug(`Waiting for buffer to empty. Items left: ${this.#buffer.length}`);
if (!this.#exportInProgress && this.#buffer.length > 0) {
// No current export in progress. Forcing all items to be exported.
await this.#exportBatches(true, shutdownAbortController?.signal);
}
if (shutdownAbortController?.signal.aborted) {
logger.debug('Timeout reached, force flushing');
break;
}
// Using setTimeout to add to the event loop and keep this alive until done.
await this.#waitForShutdownProgress(shutdownAbortController?.signal);
}
logger.debug('Buffer empty. Exiting');
}
finally {
if (shutdownTimeout) {
this.#timer.clearTimeout(shutdownTimeout);
}
if (this.#timer && this.#timeout) {
// Making sure there are no more requests.
this.#timer.clearTimeout(this.#timeout);
}
}
}
async forceFlush() {
const inFlightExports = [...this.#activeExportPromises];
if (this.#buffer.length > 0) {
await this.#exportBatches(true);
}
if (inFlightExports.length > 0) {
await Promise.all(inFlightExports);
}
}
}
export class MultiTracingProcessor {
#processors = [];
start() {
for (const processor of this.#processors) {
if (processor.start) {
processor.start();
}
}
}
addTraceProcessor(processor) {
this.#processors.push(processor);
}
setProcessors(processors) {
logger.debug('Shutting down old processors');
for (const processor of this.#processors) {
processor.shutdown();
}
this.#processors = processors;
}
async onTraceStart(trace) {
for (const processor of this.#processors) {
await processor.onTraceStart(trace);
}
}
async onTraceEnd(trace) {
for (const processor of this.#processors) {
await processor.onTraceEnd(trace);
}
}
async onSpanStart(span) {
for (const processor of this.#processors) {
await processor.onSpanStart(span);
}
}
async onSpanEnd(span) {
for (const processor of this.#processors) {
await processor.onSpanEnd(span);
}
}
/**
* Dispatches a completed trace lifecycle to every registered processor without
* calling Trace.start() or Trace.end().
*/
async dispatchTrace(trace) {
if (trace.traceId === NOOP_TRACE_OR_SPAN_ID) {
return;
}
await this.onTraceStart(trace);
await this.onTraceEnd(trace);
}
/**
* Dispatches a completed span lifecycle to every registered processor without
* calling Span.start() or Span.end().
*/
async dispatchSpan(span) {
if (isNoopSpan(span)) {
return;
}
await this.onSpanStart(span);
await this.onSpanEnd(span);
}
/**
* Dispatches a span start event to every registered processor without calling
* Span.start().
*/
async dispatchSpanStart(span) {
if (isNoopSpan(span)) {
return;
}
await this.onSpanStart(span);
}
/**
* Dispatches a span end event to every registered processor without calling
* Span.end().
*/
async dispatchSpanEnd(span) {
if (isNoopSpan(span)) {
return;
}
await this.onSpanEnd(span);
}
async shutdown(timeout) {
for (const processor of this.#processors) {
await processor.shutdown(timeout);
}
}
async forceFlush() {
for (const processor of this.#processors) {
await processor.forceFlush();
}
}
}
let _defaultExporter = null;
let _defaultProcessor = null;
export function defaultExporter() {
if (!_defaultExporter) {
_defaultExporter = new ConsoleSpanExporter();
}
return _defaultExporter;
}
export function defaultProcessor() {
if (!_defaultProcessor) {
_defaultProcessor = new BatchTraceProcessor(defaultExporter());
}
return _defaultProcessor;
}
//# sourceMappingURL=processor.mjs.map