Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/fiery-ravens-refuse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@openai/agents-openai': patch
'@openai/agents-core': patch
---

fix: Add usage data integration to #760 feature addition
7 changes: 6 additions & 1 deletion examples/memory/oai-compact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ async function main() {
console.log(`Assistant: ${event.item.content.trim()}`);
}
}
console.log(
'Usage for the turn:',
result.state.usage.requestUsageEntries,
);
}

const compactedHistory = await session.getItems();
Expand All @@ -77,7 +81,8 @@ async function main() {
}

// You can manually run compaction this way:
await session.runCompaction({ force: true });
const compactionResult = await session.runCompaction({ force: true });
console.log('Manual compaction result:', compactionResult);

const finalHistory = await session.getItems();
console.log('\nStored history after final compaction:');
Expand Down
1 change: 1 addition & 0 deletions packages/agents-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ export type {
SessionInputCallback,
OpenAIResponsesCompactionArgs,
OpenAIResponsesCompactionAwareSession,
OpenAIResponsesCompactionResult,
} from './memory/session';
export { isOpenAIResponsesCompactionAwareSession } from './memory/session';
export { MemorySession } from './memory/memorySession';
Expand Down
12 changes: 11 additions & 1 deletion packages/agents-core/src/memory/session.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { AgentInputItem } from '../types';
import type { RequestUsage } from '../usage';

/**
* A function that combines session history with new input items before the model call.
Expand Down Expand Up @@ -60,6 +61,10 @@ export type OpenAIResponsesCompactionArgs = {
force?: boolean;
};

export type OpenAIResponsesCompactionResult = {
usage: RequestUsage;
};

export interface OpenAIResponsesCompactionAwareSession extends Session {
/**
* Invoked by the runner after it persists a completed turn into the session.
Expand All @@ -70,7 +75,12 @@ export interface OpenAIResponsesCompactionAwareSession extends Session {
* This hook is best-effort. Implementations should consider handling transient failures and
* deciding whether to retry or skip compaction for the current turn.
*/
runCompaction(args?: OpenAIResponsesCompactionArgs): Promise<void> | void;
runCompaction(
args?: OpenAIResponsesCompactionArgs,
):
| Promise<OpenAIResponsesCompactionResult | null>
| OpenAIResponsesCompactionResult
| null;
}

export function isOpenAIResponsesCompactionAwareSession(
Expand Down
27 changes: 22 additions & 5 deletions packages/agents-core/src/runImplementation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ import {
type Session,
type SessionInputCallback,
} from './memory/session';
import { Usage } from './usage';

// Represents a single handoff function call that still needs to be executed after the model turn.
type ToolRunHandoff = {
Expand Down Expand Up @@ -2296,14 +2297,30 @@ function shouldStripIdForType(type: string): boolean {
async function runCompactionOnSession(
session: Session | undefined,
responseId: string | undefined,
state: RunState<any, any>,
): Promise<void> {
if (!isOpenAIResponsesCompactionAwareSession(session)) {
return;
}
// Called after a completed turn is persisted so compaction can consider the latest stored state.
await session.runCompaction(
const compactionResult = await session.runCompaction(
typeof responseId === 'undefined' ? undefined : { responseId },
);
if (!compactionResult) {
return;
}
const usage = compactionResult.usage;
state._context.usage.add(
new Usage({
requests: 1,
inputTokens: usage.inputTokens,
outputTokens: usage.outputTokens,
totalTokens: usage.totalTokens,
inputTokensDetails: usage.inputTokensDetails,
outputTokensDetails: usage.outputTokensDetails,
requestUsageEntries: [usage],
}),
);
}

/**
Expand Down Expand Up @@ -2335,12 +2352,12 @@ export async function saveToSession(
if (itemsToSave.length === 0) {
state._currentTurnPersistedItemCount =
alreadyPersisted + newRunItems.length;
await runCompactionOnSession(session, result.lastResponseId);
await runCompactionOnSession(session, result.lastResponseId, state);
return;
}
const sanitizedItems = normalizeItemsForSessionPersistence(itemsToSave);
await session.addItems(sanitizedItems);
await runCompactionOnSession(session, result.lastResponseId);
await runCompactionOnSession(session, result.lastResponseId, state);
state._currentTurnPersistedItemCount = alreadyPersisted + newRunItems.length;
}

Expand Down Expand Up @@ -2382,12 +2399,12 @@ export async function saveStreamResultToSession(
if (itemsToSave.length === 0) {
state._currentTurnPersistedItemCount =
alreadyPersisted + newRunItems.length;
await runCompactionOnSession(session, result.lastResponseId);
await runCompactionOnSession(session, result.lastResponseId, state);
return;
}
const sanitizedItems = normalizeItemsForSessionPersistence(itemsToSave);
await session.addItems(sanitizedItems);
await runCompactionOnSession(session, result.lastResponseId);
await runCompactionOnSession(session, result.lastResponseId, state);
state._currentTurnPersistedItemCount = alreadyPersisted + newRunItems.length;
}

Expand Down
2 changes: 2 additions & 0 deletions packages/agents-core/src/runState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ const requestUsageSchema = z.object({
totalTokens: z.number(),
inputTokensDetails: z.record(z.string(), z.number()).optional(),
outputTokensDetails: z.record(z.string(), z.number()).optional(),
endpoint: z.string().optional(),
});

const usageSchema = z.object({
Expand Down Expand Up @@ -470,6 +471,7 @@ export class RunState<TContext, TAgent extends Agent<any, any>> {
totalTokens: entry.totalTokens,
inputTokensDetails: entry.inputTokensDetails,
outputTokensDetails: entry.outputTokensDetails,
...(entry.endpoint ? { endpoint: entry.endpoint } : {}),
}),
),
}
Expand Down
1 change: 1 addition & 0 deletions packages/agents-core/src/types/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -769,6 +769,7 @@ export const RequestUsageData = z.object({
totalTokens: z.number(),
inputTokensDetails: z.record(z.string(), z.number()).optional(),
outputTokensDetails: z.record(z.string(), z.number()).optional(),
endpoint: z.string().optional(),
});

export type RequestUsageData = z.infer<typeof RequestUsageData>;
Expand Down
9 changes: 9 additions & 0 deletions packages/agents-core/src/usage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ type RequestUsageInput = Partial<
total_tokens: number;
input_tokens_details: object;
output_tokens_details: object;
endpoint?: string;
}
>;

Expand Down Expand Up @@ -56,6 +57,11 @@ export class RequestUsage {
*/
public outputTokensDetails: Record<string, number>;

/**
* The endpoint that produced this usage entry (e.g., responses.create, responses.compact).
*/
public endpoint?: 'responses.create' | 'responses.compact' | (string & {});

constructor(input?: RequestUsageInput) {
this.inputTokens = input?.inputTokens ?? input?.input_tokens ?? 0;
this.outputTokens = input?.outputTokens ?? input?.output_tokens ?? 0;
Expand All @@ -73,6 +79,9 @@ export class RequestUsage {
this.outputTokensDetails = outputTokensDetails
? (outputTokensDetails as Record<string, number>)
: {};
if (typeof input?.endpoint !== 'undefined') {
this.endpoint = input.endpoint;
}
}
}

Expand Down
125 changes: 121 additions & 4 deletions packages/agents-core/test/runImplementation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ import {
import { handoff } from '../src/handoff';
import { ModelBehaviorError, UserError } from '../src/errors';
import { Computer } from '../src/computer';
import { Usage } from '../src/usage';
import { RequestUsage, Usage } from '../src/usage';
import { setTracingDisabled, withTrace } from '../src';

import {
Expand All @@ -70,7 +70,10 @@ import { RunContext } from '../src/runContext';
import { setDefaultModelProvider } from '../src';
import { Logger } from '../src/logger';
import type { UnknownContext } from '../src/types';
import type { Session } from '../src/memory/session';
import type {
OpenAIResponsesCompactionResult,
Session,
} from '../src/memory/session';
import type { AgentInputItem } from '../src/types';

beforeAll(() => {
Expand Down Expand Up @@ -580,8 +583,9 @@ describe('saveToSession', () => {

async runCompaction(args: {
responseId: string | undefined;
}): Promise<void> {
}): Promise<OpenAIResponsesCompactionResult | null> {
this.events.push(`runCompaction:${args.responseId}`);
return null;
}
}

Expand Down Expand Up @@ -662,8 +666,11 @@ describe('saveToSession', () => {
this.items = [];
}

async runCompaction(args?: { responseId?: string }): Promise<void> {
async runCompaction(args?: {
responseId?: string;
}): Promise<OpenAIResponsesCompactionResult | null> {
this.events.push(`runCompaction:${String(args?.responseId)}`);
return null;
}
}

Expand Down Expand Up @@ -713,6 +720,116 @@ describe('saveToSession', () => {
expect(session.events).toEqual(['addItems:2', 'runCompaction:undefined']);
expect(session.items).toHaveLength(2);
});

it('aggregates compaction usage into the run usage', async () => {
class TrackingSession implements Session {
items: AgentInputItem[] = [];
events: string[] = [];

async getSessionId(): Promise<string> {
return 'session';
}

async getItems(): Promise<AgentInputItem[]> {
return [...this.items];
}

async addItems(items: AgentInputItem[]): Promise<void> {
this.events.push(`addItems:${items.length}`);
this.items.push(...items);
}

async popItem(): Promise<AgentInputItem | undefined> {
return undefined;
}

async clearSession(): Promise<void> {
this.items = [];
}

async runCompaction(): Promise<OpenAIResponsesCompactionResult | null> {
this.events.push('runCompaction:resp_123');
return {
usage: new RequestUsage({
inputTokens: 4,
outputTokens: 6,
totalTokens: 10,
endpoint: 'responses.compact',
}),
};
}
}

const textAgent = new Agent<UnknownContext, 'text'>({
name: 'Recorder',
outputType: 'text',
instructions: 'capture',
});
const agent = textAgent as unknown as Agent<
UnknownContext,
AgentOutputType
>;
const session = new TrackingSession();
const context = new RunContext<UnknownContext>(undefined as UnknownContext);
const state = new RunState<
UnknownContext,
Agent<UnknownContext, AgentOutputType>
>(context, 'hello', agent, 10);

const modelUsage = new Usage({
requests: 1,
inputTokens: 2,
outputTokens: 3,
totalTokens: 5,
requestUsageEntries: [
new RequestUsage({
inputTokens: 2,
outputTokens: 3,
totalTokens: 5,
endpoint: 'responses.create',
}),
],
});
state._modelResponses.push({
output: [],
usage: modelUsage,
responseId: 'resp_123',
});
state._context.usage.add(modelUsage);
state._generatedItems = [
new MessageOutputItem(
{
type: 'message',
role: 'assistant',
id: 'msg_123',
status: 'completed',
content: [
{
type: 'output_text',
text: 'here is the reply',
},
],
providerData: {},
},
textAgent,
),
];
state._currentStep = {
type: 'next_step_final_output',
output: 'here is the reply',
};

const result = new RunResult(state);
await saveToSession(session, toInputItemList(state._originalInput), result);

expect(session.events).toEqual(['addItems:2', 'runCompaction:resp_123']);
expect(state.usage.inputTokens).toBe(6);
expect(state.usage.outputTokens).toBe(9);
expect(state.usage.totalTokens).toBe(15);
expect(
state.usage.requestUsageEntries?.map((entry) => entry.endpoint),
).toEqual(['responses.create', 'responses.compact']);
});
});

describe('prepareInputItemsWithSession', () => {
Expand Down
32 changes: 32 additions & 0 deletions packages/agents-core/test/usage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,4 +163,36 @@ describe('Usage', () => {
},
]);
});

it('preserves endpoint metadata on request usage entries', () => {
const aggregated = new Usage();

aggregated.add(
new Usage({
requests: 1,
inputTokens: 3,
outputTokens: 4,
totalTokens: 7,
requestUsageEntries: [
new RequestUsage({
inputTokens: 3,
outputTokens: 4,
totalTokens: 7,
endpoint: 'responses.create',
}),
],
}),
);

expect(aggregated.requestUsageEntries).toEqual([
{
inputTokens: 3,
outputTokens: 4,
totalTokens: 7,
inputTokensDetails: {},
outputTokensDetails: {},
endpoint: 'responses.create',
},
]);
});
});
Loading