Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
2 changes: 1 addition & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ export { OAuthUtils } from './mcp/oauth-utils.js';

// Export telemetry functions
export * from './telemetry/index.js';
export { sessionId } from './utils/session.js';
export { sessionId, createSessionId } from './utils/session.js';
export * from './utils/browser.js';
export { Storage } from './config/storage.js';

Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/utils/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,7 @@
import { randomUUID } from 'node:crypto';

export const sessionId = randomUUID();

export function createSessionId(): string {
return randomUUID();
}
18 changes: 12 additions & 6 deletions packages/sdk/SDK_DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@

## `Simple Example`

> **Status:** Implemented. `GeminiCliAgent` supports `cwd` and `sendStream`.
> **Status:** Implemented. `GeminiCliAgent` supports `session()` and
> `resumeSession()`.

Equivalent to `gemini -p "what does this project do?"`. Loads all workspace and
user settings.
Expand All @@ -20,10 +21,14 @@ const simpleAgent = new GeminiCliAgent({
cwd: '/path/to/some/dir',
});

for await (const chunk of simpleAgent.sendStream(
'what does this project do?',
)) {
console.log(chunk); // equivalent to JSON streaming chunks (probably?) for now
// Create a new empty session
const session = simpleAgent.session();

// Resume most recent session (optional: pass a specific session ID)
// const session = await simpleAgent.resumeSession();

for await (const chunk of session.sendStream('what does this project do?')) {
console.log(chunk); // equivalent to JSON streaming chunks
}
```

Expand Down Expand Up @@ -268,8 +273,9 @@ export interface SessionContext {
// helpers to access files and run shell commands while adhering to policies/validation
fs: AgentFilesystem;
shell: AgentShell;
// the agent itself is passed as context
// the agent and session are passed as context
agent: GeminiCliAgent;
session: GeminiCliSession;
}

export interface AgentFilesystem {
Expand Down
78 changes: 29 additions & 49 deletions packages/sdk/src/agent.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,12 @@ describe('GeminiCliAgent Integration', () => {
fakeResponses: RECORD_MODE ? undefined : goldenFile,
});

const session = agent.session();
expect(session.id).toMatch(
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
);
const events = [];
const stream = agent.sendStream('Say hello.');
const stream = session.sendStream('Say hello.');

for await (const event of stream) {
events.push(event);
Expand Down Expand Up @@ -60,9 +64,11 @@ describe('GeminiCliAgent Integration', () => {
fakeResponses: RECORD_MODE ? undefined : goldenFile,
});

const session = agent.session();

// First turn
const stream1 = agent.sendStream('What is the secret number?');
const events1 = [];
const stream1 = session.sendStream('What is the secret number?');
for await (const event of stream1) {
events1.push(event);
}
Expand All @@ -72,11 +78,10 @@ describe('GeminiCliAgent Integration', () => {
.join('');

expect(responseText1).toContain('1');
expect(callCount).toBe(1);

// Second turn
const stream2 = agent.sendStream('What is the secret number now?');
const events2 = [];
const stream2 = session.sendStream('What is the secret number now?');
for await (const event of stream2) {
events2.push(event);
}
Expand All @@ -85,70 +90,45 @@ describe('GeminiCliAgent Integration', () => {
.map((e) => (typeof e.value === 'string' ? e.value : ''))
.join('');

// Should still be 1 because instructions are only loaded once per session
expect(responseText2).toContain('1');
expect(callCount).toBe(1);
expect(responseText2).toContain('2');
}, 30000);

it('handles async dynamic instructions', async () => {
const goldenFile = getGoldenPath('agent-async-instructions');
it('resumes a session', async () => {
const goldenFile = getGoldenPath('agent-resume-session');

let callCount = 0;
// Create initial session
const agent = new GeminiCliAgent({
instructions: async (_ctx) => {
await new Promise((resolve) => setTimeout(resolve, 10)); // Simulate async work
callCount++;
return `You are a helpful assistant. The secret number is ${callCount}. Always mention the secret number when asked.`;
},
instructions: 'You are a memory test. Remember the word "BANANA".',
model: 'gemini-2.0-flash',
recordResponses: RECORD_MODE ? goldenFile : undefined,
fakeResponses: RECORD_MODE ? undefined : goldenFile,
});

// First turn
const stream1 = agent.sendStream('What is the secret number?');
const events1 = [];
for await (const event of stream1) {
events1.push(event);
const session1 = agent.session({ sessionId: 'resume-test-fixed-id' });
const sessionId = session1.id;
const stream1 = session1.sendStream('What is the word?');
for await (const _ of stream1) {
// consume stream
}
const responseText1 = events1
.filter((e) => e.type === 'content')
.map((e) => (typeof e.value === 'string' ? e.value : ''))
.join('');

expect(responseText1).toContain('1');
expect(callCount).toBe(1);
// Resume session
// Allow some time for async writes if any
await new Promise((resolve) => setTimeout(resolve, 500));

const session2 = await agent.resumeSession(sessionId);
expect(session2.id).toBe(sessionId);

// Second turn
const stream2 = agent.sendStream('What is the secret number now?');
const events2 = [];
const stream2 = session2.sendStream('What is the word again?');
for await (const event of stream2) {
events2.push(event);
}
const responseText2 = events2

const responseText = events2
.filter((e) => e.type === 'content')
.map((e) => (typeof e.value === 'string' ? e.value : ''))
.join('');

// Should still be 1 because instructions are only loaded once per session
expect(responseText2).toContain('1');
expect(callCount).toBe(1);
expect(responseText).toContain('BANANA');
}, 30000);

it('throws when dynamic instructions fail', async () => {
const agent = new GeminiCliAgent({
instructions: () => {
throw new Error('Dynamic instruction failure');
},
model: 'gemini-2.0-flash',
});

const stream = agent.sendStream('Say hello.');

await expect(async () => {
for await (const _event of stream) {
// Just consume the stream
}
}).rejects.toThrow('Dynamic instruction failure');
});
});
Loading
Loading