Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
12 changes: 12 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,18 @@ export default tseslint.config(
],
},
},
{
files: ['packages/sdk/src/**/*.{ts,tsx}'],
rules: {
'no-restricted-imports': [
'error',
{
name: '@google/gemini-cli-sdk',
message: 'Please use relative imports within the @google/gemini-cli-sdk package.',
},
],
},
},
{
files: ['packages/*/src/**/*.test.{ts,tsx}'],
plugins: {
Expand Down
21 changes: 21 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

38 changes: 38 additions & 0 deletions packages/sdk/examples/simple.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import { GeminiCliAgent, tool, z } from '../src/index.js';

async function main() {
const myTool = tool(
{
name: 'add',
description: 'Add two numbers.',
inputSchema: z.object({
a: z.number().describe('the first number'),
b: z.number().describe('the second number'),
}),
},
async ({ a, b }) => {
console.log(`Tool 'add' called with a=${a}, b=${b}`);
return { result: a + b };
},
);

const agent = new GeminiCliAgent({
instructions: 'Make sure to always talk like a pirate.',
tools: [myTool],
});

console.log("Sending prompt: 'add 5 + 6'");
for await (const chunk of agent.sendStream(
'add 5 + 6 and tell me a story involving the result',
)) {
console.log(JSON.stringify(chunk, null, 2));
}
}

main().catch(console.error);
7 changes: 7 additions & 0 deletions packages/sdk/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

export * from './src/index.js';
36 changes: 36 additions & 0 deletions packages/sdk/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"name": "@google/gemini-cli-sdk",
"version": "0.29.0-nightly.20260203.71f46f116",
"description": "Gemini CLI SDK",
"license": "Apache-2.0",
"repository": {
"type": "git",
"url": "git+https://github.com/google-gemini/gemini-cli.git"
},
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "node ../../scripts/build_package.js",
"lint": "eslint . --ext .ts,.tsx",
"format": "prettier --write .",
"test": "vitest run",
"test:ci": "vitest run",
"typecheck": "tsc --noEmit"
},
"files": [
"dist"
],
"dependencies": {
"@google/gemini-cli-core": "file:../core",
"zod": "^3.23.8",
"zod-to-json-schema": "^3.23.1"
},
"devDependencies": {
"typescript": "^5.3.3",
"vitest": "^3.1.1"
},
"engines": {
"node": ">=20"
}
}
159 changes: 159 additions & 0 deletions packages/sdk/src/agent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import {
Config,
type ConfigParameters,
AuthType,
PREVIEW_GEMINI_MODEL_AUTO,
GeminiEventType,
type ToolCallRequestInfo,
type ServerGeminiStreamEvent,
type GeminiClient,
} from '@google/gemini-cli-core';

import { type Tool, SdkTool, type z } from './tool.js';

export interface GeminiCliAgentOptions {
instructions: string;
tools?: Array<Tool<z.ZodType>>;
model?: string;
cwd?: string;
debug?: boolean;
}

export class GeminiCliAgent {
private config: Config;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should mark this and tools as readonly

private tools: Array<Tool<z.ZodType>>;

constructor(options: GeminiCliAgentOptions) {
const cwd = options.cwd || process.cwd();
this.tools = options.tools || [];

const configParams: ConfigParameters = {
sessionId: `sdk-${Date.now()}`,
Comment thread
mbleigh marked this conversation as resolved.
targetDir: cwd,
cwd,
debugMode: options.debug ?? false,
model: options.model || PREVIEW_GEMINI_MODEL_AUTO,
userMemory: options.instructions,
Comment thread
mbleigh marked this conversation as resolved.
// Minimal config
enableHooks: false,
mcpEnabled: false,
extensionsEnabled: false,
};

this.config = new Config(configParams);
}

async *sendStream(prompt: string): AsyncGenerator<ServerGeminiStreamEvent> {
Comment thread
mbleigh marked this conversation as resolved.
Outdated
// Lazy initialization of auth and client
if (!this.config.getContentGenerator()) {
Comment thread
mbleigh marked this conversation as resolved.
// Simple auth detection
let authType = AuthType.COMPUTE_ADC;
if (process.env['GEMINI_API_KEY']) {
authType = AuthType.USE_GEMINI;
} else if (process.env['GOOGLE_API_KEY']) {
authType = AuthType.USE_VERTEX_AI;
}

await this.config.refreshAuth(authType);
await this.config.initialize();

// Register tools now that registry exists
const registry = this.config.getToolRegistry();
const messageBus = this.config.getMessageBus();

for (const toolDef of this.tools) {
const sdkTool = new SdkTool(toolDef, messageBus);
registry.registerTool(sdkTool);
}
}

const client = this.config.getGeminiClient();
const registry = this.config.getToolRegistry();

let request: Parameters<GeminiClient['sendMessageStream']>[0] = [
{ text: prompt },
];
// TODO: support AbortSignal cancellation properly
const signal = new AbortController().signal;
Comment thread
mbleigh marked this conversation as resolved.
Outdated
const sessionId = this.config.getSessionId();

while (true) {
Comment thread
mbleigh marked this conversation as resolved.
// sendMessageStream returns AsyncGenerator<ServerGeminiStreamEvent, Turn>
const stream = client.sendMessageStream(request, signal, sessionId);

const toolCalls: ToolCallRequestInfo[] = [];

for await (const event of stream) {
yield event;
if (event.type === GeminiEventType.ToolCallRequest) {
toolCalls.push(event.value);
}
}

if (toolCalls.length === 0) {
break;
}

const functionResponses: Array<Record<string, unknown>> = [];
for (const toolCall of toolCalls) {
const tool = registry.getTool(toolCall.name);
if (!tool) {
functionResponses.push({
functionResponse: {
name: toolCall.name,
response: { error: `Tool ${toolCall.name} not found` },
Comment thread
mbleigh marked this conversation as resolved.
Outdated
id: toolCall.callId,
},
});
continue;
}

try {
let args = toolCall.args;
if (typeof args === 'string') {
args = JSON.parse(args);
}

// Cast toolCall.args to object to satisfy AnyDeclarativeTool.build
const invocation = tool.build(args as object);

// Check if the tool execution requires confirmation according to policy
const confirmation = await invocation.shouldConfirmExecute(signal);
if (confirmation) {
throw new Error(
`Tool execution for '${toolCall.name}' requires confirmation, which is not supported in this SDK version.`,
);
}

const result = await invocation.execute(signal);
Comment thread
mbleigh marked this conversation as resolved.
Outdated

functionResponses.push({
functionResponse: {
name: toolCall.name,
response: { result: result.llmContent },
id: toolCall.callId,
},
});
} catch (e) {
functionResponses.push({
functionResponse: {
name: toolCall.name,
response: { error: e instanceof Error ? e.message : String(e) },
id: toolCall.callId,
},
});
}
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
request = functionResponses as unknown as Parameters<
GeminiClient['sendMessageStream']
>[0];
}
}
}
8 changes: 8 additions & 0 deletions packages/sdk/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

export * from './agent.js';
export * from './tool.js';
Loading
Loading