Skip to content

Commit 9052aff

Browse files
committed
fix local codex review comment
- [P1] Set message type when wrapping string input — packages/agents-core/src/runImplementation.ts:1713-1718 When prepareInputItemsWithSession receives a string, this helper synthesizes the AgentInputItem that we hand to the model and also persist in session storage. Here we return { role: 'user', content: ... } without a type. The Responses API (and our own AgentInputItem contract) requires type: 'message', so as soon as session memory is enabled with a string input we start sending malformed payloads and will get a 400 from the model while also storing invalid history. Please set type: 'message' on the generated item before casting it to AgentInputItem.
1 parent 7892cdc commit 9052aff

File tree

3 files changed

+138
-16
lines changed

3 files changed

+138
-16
lines changed

packages/agents-core/src/runImplementation.ts

Lines changed: 60 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,17 @@ import { getLastTextFromOutputMessage } from './utils/messages';
3939
import { withFunctionSpan, withHandoffSpan } from './tracing/createSpans';
4040
import { getSchemaAndParserFromInputType } from './utils/tools';
4141
import { encodeUint8ArrayToBase64 } from './utils/base64';
42+
import {
43+
isArrayBufferView,
44+
isNodeBuffer,
45+
isSerializedBufferSnapshot,
46+
toSmartString,
47+
} from './utils/smartString';
4248
import { safeExecute } from './utils/safeExecute';
4349
import { addErrorToCurrentSpan } from './tracing/context';
4450
import { RunItemStreamEvent, RunItemStreamEventName } from './events';
4551
import { RunResult, StreamedRunResult } from './result';
4652
import { z } from 'zod';
47-
import { toSmartString } from './utils/smartString';
4853
import * as protocol from './types/protocol';
4954
import { Computer } from './computer';
5055
import { RunState } from './runState';
@@ -1858,21 +1863,12 @@ export async function prepareInputItemsWithSession(
18581863
);
18591864
}
18601865

1861-
const historyCounts = new Map<string, number>();
1862-
for (const item of history) {
1863-
const key = toSmartString(item);
1864-
historyCounts.set(key, (historyCounts.get(key) ?? 0) + 1);
1865-
}
1866-
1867-
const newInputCounts = new Map<string, number>();
1868-
for (const item of newInputItems) {
1869-
const key = toSmartString(item);
1870-
newInputCounts.set(key, (newInputCounts.get(key) ?? 0) + 1);
1871-
}
1866+
const historyCounts = buildItemFrequencyMap(history);
1867+
const newInputCounts = buildItemFrequencyMap(newInputItems);
18721868

18731869
const appended: AgentInputItem[] = [];
18741870
for (const item of combined) {
1875-
const key = toSmartString(item);
1871+
const key = sessionItemKey(item);
18761872
const historyRemaining = historyCounts.get(key) ?? 0;
18771873
if (historyRemaining > 0) {
18781874
historyCounts.set(key, historyRemaining - 1);
@@ -1894,3 +1890,54 @@ export async function prepareInputItemsWithSession(
18941890
sessionItems: appended.length > 0 ? appended : [],
18951891
};
18961892
}
1893+
1894+
function buildItemFrequencyMap(items: AgentInputItem[]): Map<string, number> {
1895+
const counts = new Map<string, number>();
1896+
for (const item of items) {
1897+
const key = sessionItemKey(item);
1898+
counts.set(key, (counts.get(key) ?? 0) + 1);
1899+
}
1900+
return counts;
1901+
}
1902+
1903+
function sessionItemKey(item: AgentInputItem): string {
1904+
return JSON.stringify(item, sessionSerializationReplacer);
1905+
}
1906+
1907+
function sessionSerializationReplacer(_key: string, value: unknown): unknown {
1908+
if (value instanceof ArrayBuffer) {
1909+
return {
1910+
__type: 'ArrayBuffer',
1911+
data: encodeUint8ArrayToBase64(new Uint8Array(value)),
1912+
};
1913+
}
1914+
1915+
if (isArrayBufferView(value)) {
1916+
const view = value as ArrayBufferView;
1917+
return {
1918+
__type: view.constructor.name,
1919+
data: encodeUint8ArrayToBase64(
1920+
new Uint8Array(view.buffer, view.byteOffset, view.byteLength),
1921+
),
1922+
};
1923+
}
1924+
1925+
if (isNodeBuffer(value)) {
1926+
const view = value as Uint8Array;
1927+
return {
1928+
__type: 'Buffer',
1929+
data: encodeUint8ArrayToBase64(
1930+
new Uint8Array(view.buffer, view.byteOffset, view.byteLength),
1931+
),
1932+
};
1933+
}
1934+
1935+
if (isSerializedBufferSnapshot(value)) {
1936+
return {
1937+
__type: 'Buffer',
1938+
data: encodeUint8ArrayToBase64(Uint8Array.from(value.data)),
1939+
};
1940+
}
1941+
1942+
return value;
1943+
}

packages/agents-core/src/utils/smartString.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const BYTE_PREVIEW_LIMIT = 20;
22

33
export function toSmartString(value: unknown): string {
4+
// Produce a human-friendly string representation while preserving enough detail for debugging workflows.
45
if (value === null || value === undefined) {
56
return String(value);
67
}
@@ -31,7 +32,8 @@ export function toSmartString(value: unknown): string {
3132
return String(value);
3233
}
3334

34-
function isArrayBufferLike(value: unknown): value is ArrayBufferLike {
35+
export function isArrayBufferLike(value: unknown): value is ArrayBufferLike {
36+
// Detect raw ArrayBuffer-backed payloads so callers can generate full previews rather than truncated hashes.
3537
if (value instanceof ArrayBuffer) {
3638
return true;
3739
}
@@ -47,13 +49,15 @@ function isArrayBufferLike(value: unknown): value is ArrayBufferLike {
4749
);
4850
}
4951

50-
function isArrayBufferView(value: unknown): value is ArrayBufferView {
52+
export function isArrayBufferView(value: unknown): value is ArrayBufferView {
53+
// Treat typed array views as binary data for consistent serialization.
5154
return typeof ArrayBuffer !== 'undefined' && ArrayBuffer.isView(value);
5255
}
5356

54-
function isSerializedBufferSnapshot(
57+
export function isSerializedBufferSnapshot(
5558
value: unknown,
5659
): value is { type: 'Buffer'; data: number[] } {
60+
// Support serialized Buffer snapshots (e.g., from JSON.parse) emitted by some tool outputs.
5761
return (
5862
typeof value === 'object' &&
5963
value !== null &&
@@ -62,6 +66,22 @@ function isSerializedBufferSnapshot(
6266
);
6367
}
6468

69+
export function isNodeBuffer(
70+
value: unknown,
71+
): value is Uint8Array & { toString(encoding: string): string } {
72+
// Detect runtime Buffers without importing node-specific shims, handling browser builds gracefully.
73+
const bufferCtor = (
74+
globalThis as {
75+
Buffer?: { isBuffer(input: unknown): boolean };
76+
}
77+
).Buffer;
78+
return Boolean(
79+
bufferCtor &&
80+
typeof bufferCtor.isBuffer === 'function' &&
81+
bufferCtor.isBuffer(value),
82+
);
83+
}
84+
6585
function formatByteArray(bytes: Uint8Array): string {
6686
if (bytes.length === 0) {
6787
return '[byte array (0 bytes)]';

packages/agents-core/test/run.test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -848,6 +848,61 @@ describe('Runner.run', () => {
848848
expect(getFirstTextContent(persistedUsers[0])).toBe('Fresh input');
849849
});
850850

851+
it('persists binary payloads that share prefixes with history', async () => {
852+
const model = new RecordingModel([
853+
{
854+
...TEST_MODEL_RESPONSE_BASIC,
855+
output: [fakeModelMessage('binary response')],
856+
},
857+
]);
858+
const historyPayload = new Uint8Array(32);
859+
const newPayload = new Uint8Array(32);
860+
for (let i = 0; i < 32; i++) {
861+
const value = i < 20 ? 0xaa : i;
862+
historyPayload[i] = value;
863+
newPayload[i] = value;
864+
}
865+
historyPayload[31] = 0xbb;
866+
newPayload[31] = 0xcc;
867+
868+
const session = new MemorySession([
869+
user('History with binary', { payload: historyPayload }),
870+
]);
871+
const agent = new Agent({ name: 'BinarySession', model });
872+
const runner = new Runner();
873+
874+
await runner.run(agent, [user('Binary input')], {
875+
session,
876+
sessionInputCallback: (history, newItems) => {
877+
const clonedHistory = history.map((item) => structuredClone(item));
878+
const updatedNewItems = newItems.map((item) => {
879+
const cloned = structuredClone(item);
880+
cloned.providerData = { payload: newPayload };
881+
return cloned;
882+
});
883+
return clonedHistory.concat(updatedNewItems);
884+
},
885+
});
886+
887+
expect(session.added).toHaveLength(1);
888+
const [persistedItems] = session.added;
889+
const persistedPayloads = persistedItems
890+
.filter(
891+
(item): item is protocol.UserMessageItem =>
892+
item.type === 'message' &&
893+
'role' in item &&
894+
item.role === 'user' &&
895+
item.providerData?.payload,
896+
)
897+
.map((item) =>
898+
Array.from(item.providerData?.payload as Uint8Array | number[]),
899+
);
900+
expect(persistedPayloads).toContainEqual(Array.from(newPayload));
901+
expect(persistedPayloads).not.toContainEqual(
902+
Array.from(historyPayload),
903+
);
904+
});
905+
851906
it('throws when session input callback returns invalid data', async () => {
852907
const model = new RecordingModel([
853908
{

0 commit comments

Comments
 (0)