Skip to content

Commit f55dac8

Browse files
committed
feat: fix #272 add memory feature
1 parent a08cc9c commit f55dac8

File tree

13 files changed

+798
-33
lines changed

13 files changed

+798
-33
lines changed

examples/memory/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
tmp/

examples/memory/file.ts

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
import type { AgentInputItem, Session } from '@openai/agents';
2+
import { protocol } from '@openai/agents';
3+
import * as fs from 'node:fs/promises';
4+
import * as path from 'node:path';
5+
import { randomUUID } from 'node:crypto';
6+
7+
export type FileSessionOptions = {
8+
/**
9+
* Directory where session files are stored. Defaults to `./.agents-sessions`.
10+
*/
11+
dir?: string;
12+
/**
13+
* Optional pre-existing session id to bind to.
14+
*/
15+
sessionId?: string;
16+
};
17+
18+
/**
19+
* A simple filesystem-backed Session implementation that stores history as a JSON array.
20+
*/
21+
export class FileSession implements Session {
22+
#dir: string;
23+
#sessionId?: string;
24+
25+
constructor(options: FileSessionOptions = {}) {
26+
this.#dir = options.dir ?? path.resolve(process.cwd(), '.agents-sessions');
27+
this.#sessionId = options.sessionId;
28+
}
29+
30+
/**
31+
* Get the current session id, creating one if necessary.
32+
*/
33+
async getSessionId(): Promise<string> {
34+
if (!this.#sessionId) {
35+
// Compact, URL-safe-ish id without dashes.
36+
this.#sessionId = randomUUID().replace(/-/g, '').slice(0, 24);
37+
}
38+
await this.#ensureDir();
39+
// Ensure the file exists.
40+
const file = this.#filePath(this.#sessionId);
41+
try {
42+
await fs.access(file);
43+
} catch {
44+
await fs.writeFile(file, '[]', 'utf8');
45+
}
46+
return this.#sessionId;
47+
}
48+
49+
/**
50+
* Retrieve items from the conversation history.
51+
*/
52+
async getItems(limit?: number): Promise<AgentInputItem[]> {
53+
const sessionId = await this.getSessionId();
54+
const items = await this.#readItems(sessionId);
55+
if (typeof limit === 'number' && limit >= 0) {
56+
return items.slice(-limit);
57+
}
58+
return items;
59+
}
60+
61+
/**
62+
* Append new items to the conversation history.
63+
*/
64+
async addItems(items: AgentInputItem[]): Promise<void> {
65+
if (!items.length) return;
66+
const sessionId = await this.getSessionId();
67+
const current = await this.#readItems(sessionId);
68+
const next = current.concat(items);
69+
await this.#writeItems(sessionId, next);
70+
}
71+
72+
/**
73+
* Remove and return the most recent item, if any.
74+
*/
75+
async popItem(): Promise<AgentInputItem | undefined> {
76+
const sessionId = await this.getSessionId();
77+
const items = await this.#readItems(sessionId);
78+
if (items.length === 0) return undefined;
79+
const popped = items.pop();
80+
await this.#writeItems(sessionId, items);
81+
return popped;
82+
}
83+
84+
/**
85+
* Delete all stored items and reset the session state.
86+
*/
87+
async clearSession(): Promise<void> {
88+
if (!this.#sessionId) return; // Nothing to clear.
89+
const file = this.#filePath(this.#sessionId);
90+
try {
91+
await fs.unlink(file);
92+
} catch {
93+
// Ignore if already removed or inaccessible.
94+
}
95+
this.#sessionId = undefined;
96+
}
97+
98+
// Internal helpers
99+
async #ensureDir(): Promise<void> {
100+
await fs.mkdir(this.#dir, { recursive: true });
101+
}
102+
103+
#filePath(sessionId: string): string {
104+
return path.join(this.#dir, `${sessionId}.json`);
105+
}
106+
107+
async #readItems(sessionId: string): Promise<AgentInputItem[]> {
108+
const file = this.#filePath(sessionId);
109+
try {
110+
const data = await fs.readFile(file, 'utf8');
111+
const parsed = JSON.parse(data);
112+
if (!Array.isArray(parsed)) return [];
113+
// Validate and coerce items to the protocol shape where possible.
114+
const result: AgentInputItem[] = [];
115+
for (const raw of parsed) {
116+
const check = protocol.ModelItem.safeParse(raw);
117+
if (check.success) {
118+
result.push(check.data as AgentInputItem);
119+
}
120+
// Silently skip invalid entries.
121+
}
122+
return result;
123+
} catch (err: any) {
124+
// On missing file, return empty list.
125+
if (err && (err.code === 'ENOENT' || err.code === 'ENOTDIR')) return [];
126+
// For other errors, rethrow.
127+
throw err;
128+
}
129+
}
130+
131+
async #writeItems(sessionId: string, items: AgentInputItem[]): Promise<void> {
132+
await this.#ensureDir();
133+
const file = this.#filePath(sessionId);
134+
// Keep JSON compact but deterministic.
135+
await fs.writeFile(file, JSON.stringify(items, null, 2), 'utf8');
136+
}
137+
}
138+
139+
import { Agent, run } from '@openai/agents';
140+
141+
async function main() {
142+
const agent = new Agent({
143+
name: 'Assistant',
144+
instructions: 'You are a helpful assistant. be VERY concise.',
145+
});
146+
147+
const session = new FileSession({ dir: './tmp/' });
148+
let result = await run(
149+
agent,
150+
'What is the largest country in South America?',
151+
{ session },
152+
);
153+
console.log(result.finalOutput); // e.g., Brazil
154+
155+
result = await run(agent, 'What is the capital of that country?', {
156+
session,
157+
});
158+
console.log(result.finalOutput); // e.g., Brasilia
159+
}
160+
161+
async function mainStream() {
162+
const agent = new Agent({
163+
name: 'Assistant',
164+
instructions: 'You are a helpful assistant. be VERY concise.',
165+
});
166+
167+
const session = new FileSession({ dir: './tmp/' });
168+
let result = await run(
169+
agent,
170+
'What is the largest country in South America?',
171+
{
172+
stream: true,
173+
session,
174+
},
175+
);
176+
177+
for await (const event of result) {
178+
if (
179+
event.type === 'raw_model_stream_event' &&
180+
event.data.type === 'output_text_delta'
181+
)
182+
process.stdout.write(event.data.delta);
183+
}
184+
console.log();
185+
186+
result = await run(agent, 'What is the capital of that country?', {
187+
stream: true,
188+
session,
189+
});
190+
191+
// toTextStream() automatically returns a readable stream of strings intended to be displayed
192+
// to the user
193+
for await (const event of result.toTextStream()) {
194+
process.stdout.write(event);
195+
}
196+
console.log();
197+
}
198+
199+
async function promptAndRun() {
200+
const readline = await import('node:readline/promises');
201+
const rl = readline.createInterface({
202+
input: process.stdin,
203+
output: process.stdout,
204+
});
205+
const isStream = await rl.question('Run in stream mode? (y/n): ');
206+
rl.close();
207+
if (isStream.trim().toLowerCase() === 'y') {
208+
await mainStream();
209+
} else {
210+
await main();
211+
}
212+
}
213+
214+
if (require.main === module) {
215+
promptAndRun().catch(console.error);
216+
}

examples/memory/oai.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { Agent, OpenAIConversationsSession, run } from '@openai/agents';
2+
3+
async function main() {
4+
const agent = new Agent({
5+
name: 'Assistant',
6+
instructions: 'You are a helpful assistant. be VERY concise.',
7+
});
8+
9+
const session = new OpenAIConversationsSession();
10+
let result = await run(
11+
agent,
12+
'What is the largest country in South America?',
13+
{ session },
14+
);
15+
console.log(result.finalOutput); // e.g., Brazil
16+
17+
result = await run(agent, 'What is the capital of that country?', {
18+
session,
19+
});
20+
console.log(result.finalOutput); // e.g., Brasilia
21+
}
22+
23+
async function mainStream() {
24+
const agent = new Agent({
25+
name: 'Assistant',
26+
instructions: 'You are a helpful assistant. be VERY concise.',
27+
});
28+
29+
const session = new OpenAIConversationsSession();
30+
let result = await run(
31+
agent,
32+
'What is the largest country in South America?',
33+
{
34+
stream: true,
35+
session,
36+
},
37+
);
38+
39+
for await (const event of result) {
40+
if (
41+
event.type === 'raw_model_stream_event' &&
42+
event.data.type === 'output_text_delta'
43+
)
44+
process.stdout.write(event.data.delta);
45+
}
46+
console.log();
47+
48+
result = await run(agent, 'What is the capital of that country?', {
49+
stream: true,
50+
session,
51+
});
52+
53+
// toTextStream() automatically returns a readable stream of strings intended to be displayed
54+
// to the user
55+
for await (const event of result.toTextStream()) {
56+
process.stdout.write(event);
57+
}
58+
console.log();
59+
}
60+
61+
async function promptAndRun() {
62+
const readline = await import('node:readline/promises');
63+
const rl = readline.createInterface({
64+
input: process.stdin,
65+
output: process.stdout,
66+
});
67+
const isStream = await rl.question('Run in stream mode? (y/n): ');
68+
rl.close();
69+
if (isStream.trim().toLowerCase() === 'y') {
70+
await mainStream();
71+
} else {
72+
await main();
73+
}
74+
}
75+
76+
if (require.main === module) {
77+
promptAndRun().catch(console.error);
78+
}

examples/memory/package.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"private": true,
3+
"name": "memory",
4+
"dependencies": {
5+
"@openai/agents": "workspace:*"
6+
},
7+
"scripts": {
8+
"build-check": "tsc --noEmit",
9+
"start:oai": "tsx oai.ts",
10+
"start:file": "tsx file.ts"
11+
}
12+
}

examples/memory/tsconfig.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"extends": "../../tsconfig.examples.json"
3+
}

packages/agents-core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ export type {
161161
StreamEventGenericItem,
162162
} from './types';
163163
export { Usage } from './usage';
164+
export type { Session } from './memory/session';
164165

165166
/**
166167
* Exporting the whole protocol as an object here. This contains both the types
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import type { AgentInputItem } from '../types';
2+
3+
/**
4+
* Interface representing a persistent session store for conversation history.
5+
*/
6+
export interface Session {
7+
/**
8+
* Ensure and return the identifier for this session.
9+
*/
10+
getSessionId(): Promise<string>;
11+
12+
/**
13+
* Retrieve items from the conversation history.
14+
*
15+
* @param limit - The maximum number of items to return. When provided the most
16+
* recent {@link limit} items should be returned in chronological order.
17+
*/
18+
getItems(limit?: number): Promise<AgentInputItem[]>;
19+
20+
/**
21+
* Append new items to the conversation history.
22+
*
23+
* @param items - Items to add to the session history.
24+
*/
25+
addItems(items: AgentInputItem[]): Promise<void>;
26+
27+
/**
28+
* Remove and return the most recent item from the conversation history if it
29+
* exists.
30+
*/
31+
popItem(): Promise<AgentInputItem | undefined>;
32+
33+
/**
34+
* Remove all items that belong to the session and reset its state.
35+
*/
36+
clearSession(): Promise<void>;
37+
}

0 commit comments

Comments
 (0)