2026-02-12 23:28:48 -08:00
|
|
|
# `Gemini CLI SDK`
|
|
|
|
|
|
2026-02-13 12:48:35 -08:00
|
|
|
> **Implementation Status:** Core agent loop, tool execution, and session
|
|
|
|
|
> context are implemented. Advanced features like hooks, skills, subagents, and
|
|
|
|
|
> ACP are currently missing.
|
|
|
|
|
|
2026-02-12 23:28:48 -08:00
|
|
|
# `Examples`
|
|
|
|
|
|
|
|
|
|
## `Simple Example`
|
|
|
|
|
|
2026-02-19 16:47:35 -08:00
|
|
|
> **Status:** Implemented. `GeminiCliAgent` supports `session()` and
|
|
|
|
|
> `resumeSession()`.
|
2026-02-13 12:48:35 -08:00
|
|
|
|
2026-02-12 23:28:48 -08:00
|
|
|
Equivalent to `gemini -p "what does this project do?"`. Loads all workspace and
|
|
|
|
|
user settings.
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
import { GeminiCliAgent } from '@google/gemini-cli-sdk';
|
|
|
|
|
|
|
|
|
|
const simpleAgent = new GeminiCliAgent({
|
|
|
|
|
cwd: '/path/to/some/dir',
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-19 16:47:35 -08:00
|
|
|
// Create a new empty session
|
|
|
|
|
const session = simpleAgent.session();
|
|
|
|
|
|
|
|
|
|
// Resume a specific session by ID
|
|
|
|
|
// const session = await simpleAgent.resumeSession('some-session-id');
|
|
|
|
|
|
|
|
|
|
for await (const chunk of session.sendStream('what does this project do?')) {
|
|
|
|
|
console.log(chunk); // equivalent to JSON streaming chunks
|
2026-02-12 23:28:48 -08:00
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Validation:
|
|
|
|
|
|
|
|
|
|
- Model receives call containing "what does this project do?" text.
|
|
|
|
|
|
|
|
|
|
## `System Instructions`
|
|
|
|
|
|
2026-02-13 12:48:35 -08:00
|
|
|
> **Status:** Implemented. Both static string instructions and dynamic functions
|
|
|
|
|
> (receiving `SessionContext`) are supported.
|
|
|
|
|
|
2026-02-12 23:28:48 -08:00
|
|
|
System instructions can be provided by a static string OR dynamically via a
|
|
|
|
|
function:
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
import { GeminiCliAgent } from "@google/gemini-cli-sdk";
|
|
|
|
|
|
|
|
|
|
const agent = new GeminiCliAgent({
|
|
|
|
|
instructions: "This is a static string instruction"; // this is valid
|
|
|
|
|
instructions: (ctx) => `The current time is ${new Date().toISOString()} in session ${ctx.sessionId}.`
|
|
|
|
|
});
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Validation:
|
|
|
|
|
|
|
|
|
|
- Static string instructions show up where GEMINI.md content normally would in
|
|
|
|
|
model call
|
|
|
|
|
- Dynamic instructions show up and contain dynamic content.
|
|
|
|
|
|
|
|
|
|
## `Custom Tools`
|
|
|
|
|
|
2026-02-13 12:48:35 -08:00
|
|
|
> **Status:** Implemented. `tool()` helper and `GeminiCliAgent` support custom
|
|
|
|
|
> tool definitions and execution.
|
|
|
|
|
|
2026-02-12 23:28:48 -08:00
|
|
|
```ts
|
|
|
|
|
import { GeminiCliAgent, tool, z } from "@google/gemini-cli-sdk";
|
|
|
|
|
|
|
|
|
|
const addTool = tool({
|
|
|
|
|
name: 'add',
|
|
|
|
|
description: 'add two numbers',
|
|
|
|
|
inputSchema: z.object({
|
|
|
|
|
a: z.number().describe('first number to add'),
|
|
|
|
|
b: z.number().describe('second number to add'),
|
|
|
|
|
}),
|
|
|
|
|
}, (({a, b}) => ({result: a + b}),);
|
|
|
|
|
|
|
|
|
|
const toolAgent = new GeminiCliAgent({
|
|
|
|
|
tools: [addTool],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const result = await toolAgent.send("what is 23 + 79?");
|
|
|
|
|
console.log(result.text);
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Validation:
|
|
|
|
|
|
|
|
|
|
- Model receives tool definition in prompt
|
|
|
|
|
- Model receives tool response after returning tool
|
|
|
|
|
|
|
|
|
|
## `Custom Hooks`
|
|
|
|
|
|
2026-02-13 12:48:35 -08:00
|
|
|
> **Status:** Not Implemented.
|
|
|
|
|
|
2026-02-12 23:28:48 -08:00
|
|
|
SDK users can provide programmatic custom hooks
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
import { GeminiCliAgent, hook, z } from '@google/gemini-cli-sdk';
|
|
|
|
|
import { reformat } from './reformat.js';
|
|
|
|
|
|
|
|
|
|
const myHook = hook(
|
|
|
|
|
{
|
|
|
|
|
event: 'AfterTool',
|
|
|
|
|
name: 'reformat',
|
|
|
|
|
matcher: 'write_file',
|
|
|
|
|
},
|
|
|
|
|
(hook, ctx) => {
|
|
|
|
|
const filePath = hook.toolInput.path;
|
|
|
|
|
|
|
|
|
|
// void return is a no-op
|
|
|
|
|
if (!filePath.endsWith('.ts')) return;
|
|
|
|
|
|
|
|
|
|
// ctx.fs gives us a filesystem interface that obeys Gemini CLI permissions/sandbox
|
|
|
|
|
const reformatted = await reformat(await ctx.fs.read(filePath));
|
|
|
|
|
await ctx.fs.write(filePath, reformatted);
|
|
|
|
|
|
|
|
|
|
// hooks return a payload instructing the agent how to proceed
|
|
|
|
|
return {
|
|
|
|
|
hookSpecificOutput: {
|
|
|
|
|
additionalContext: `Reformatted file ${filePath}, read again before modifying further.`,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
SDK Hooks can also run as standalone scripts to implement userland "command"
|
|
|
|
|
style hooks:
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
import { hook } from "@google/gemini-cli-sdk";
|
|
|
|
|
|
|
|
|
|
// define a hook as above
|
|
|
|
|
const myHook = hook({...}, (hook) => {...});
|
|
|
|
|
// calling runAsCommand parses stdin, calls action, uses appropriate exit code
|
|
|
|
|
// with output, but you get nice strong typings to guide your impl
|
|
|
|
|
myHook.runAsCommand();
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Validation (these are probably hardest to validate):
|
|
|
|
|
|
|
|
|
|
- Test each type of hook and check that model api receives injected content
|
|
|
|
|
- Check global halt scenarios
|
|
|
|
|
- Check specific return types for each type of hook
|
|
|
|
|
|
|
|
|
|
## `Custom Skills`
|
|
|
|
|
|
2026-02-13 18:09:31 -08:00
|
|
|
> **Status:** Implemented. `skillDir` helper and `GeminiCliAgent` support
|
|
|
|
|
> loading skills from filesystem.
|
2026-02-13 12:48:35 -08:00
|
|
|
|
2026-02-12 23:28:48 -08:00
|
|
|
Custom skills can be referenced by individual directories or by "skill roots"
|
|
|
|
|
(directories containing many skills).
|
|
|
|
|
|
2026-02-13 18:09:31 -08:00
|
|
|
### Directory Structure
|
2026-02-12 23:28:48 -08:00
|
|
|
|
2026-02-13 18:09:31 -08:00
|
|
|
```
|
|
|
|
|
skill-dir/
|
|
|
|
|
SKILL.md (Metadata and instructions)
|
|
|
|
|
tools/ (Optional directory for tools)
|
|
|
|
|
my-tool.js
|
2026-02-12 23:28:48 -08:00
|
|
|
```
|
|
|
|
|
|
2026-02-13 18:09:31 -08:00
|
|
|
### Usage
|
2026-02-12 23:28:48 -08:00
|
|
|
|
2026-02-13 18:09:31 -08:00
|
|
|
```typescript
|
|
|
|
|
import { GeminiCliAgent, skillDir } from '@google/gemini-sdk';
|
|
|
|
|
|
|
|
|
|
const agent = new GeminiCliAgent({
|
|
|
|
|
instructions: 'You are a helpful assistant.',
|
|
|
|
|
skills: [
|
|
|
|
|
// Load a single skill from a directory
|
|
|
|
|
skillDir('./my-skill'),
|
|
|
|
|
// Load all skills found in subdirectories of this root
|
|
|
|
|
skillDir('./skills-collection'),
|
|
|
|
|
],
|
2026-02-12 23:28:48 -08:00
|
|
|
});
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## `Subagents`
|
|
|
|
|
|
2026-02-13 12:48:35 -08:00
|
|
|
> **Status:** Not Implemented.
|
|
|
|
|
|
2026-02-12 23:28:48 -08:00
|
|
|
```ts
|
|
|
|
|
import { GeminiCliAgent, subagent } from "@google/gemini-cli";
|
|
|
|
|
|
|
|
|
|
const mySubagent = subagent({
|
|
|
|
|
name: "my-subagent",
|
|
|
|
|
description: "when the subagent should be used",
|
|
|
|
|
|
|
|
|
|
// simple prompt agent with static string or dynamic string
|
|
|
|
|
instructions: "the instructions",
|
|
|
|
|
instructions (prompt, ctx) => `can also be dynamic with context`,
|
|
|
|
|
|
|
|
|
|
// OR (in an ideal world)...
|
|
|
|
|
|
|
|
|
|
// pass a full standalone agent
|
|
|
|
|
agent: new GeminiCliAgent(...);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const agent = new GeminiCliAgent({
|
|
|
|
|
subagents: [mySubagent]
|
|
|
|
|
});
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## `Extensions`
|
|
|
|
|
|
2026-02-13 12:48:35 -08:00
|
|
|
> **Status:** Not Implemented.
|
|
|
|
|
|
2026-02-12 23:28:48 -08:00
|
|
|
Potentially the most important feature of the Gemini CLI SDK is support for
|
|
|
|
|
extensions, which modularly encapsulate all of the primitives listed above:
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
import { GeminiCliAgent, extension } from "@google/gemini-cli-sdk";
|
|
|
|
|
|
|
|
|
|
const myExtension = extension({
|
|
|
|
|
name: "my-extension",
|
|
|
|
|
description: "...",
|
|
|
|
|
instructions: "THESE ARE CONCATENATED WITH OTHER AGENT
|
|
|
|
|
INSTRUCTIONS",
|
|
|
|
|
tools: [...],
|
|
|
|
|
skills: [...],
|
|
|
|
|
hooks: [...],
|
|
|
|
|
subagents: [...],
|
|
|
|
|
});
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## `ACP Mode`
|
|
|
|
|
|
2026-02-13 12:48:35 -08:00
|
|
|
> **Status:** Not Implemented.
|
|
|
|
|
|
2026-02-12 23:28:48 -08:00
|
|
|
The SDK will include a wrapper utility to interact with the agent via ACP
|
|
|
|
|
instead of the SDK's natural API.
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
import { GeminiCliAgent } from "@google/gemini-cli-sdk";
|
|
|
|
|
import { GeminiCliAcpServer } from "@google/gemini-cli-sdk/acp";
|
|
|
|
|
|
|
|
|
|
const server = new GeminiCliAcpServer(new GeminiCliAgent({...}));
|
|
|
|
|
server.start(); // calling start runs a stdio ACP server
|
|
|
|
|
|
|
|
|
|
const client = server.connect({
|
|
|
|
|
onMessage: (message) => { /* updates etc received here */ },
|
|
|
|
|
});
|
|
|
|
|
client.send({...clientMessage}); // e.g. a "session/prompt" message
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## `Approvals / Policies`
|
|
|
|
|
|
2026-02-13 12:48:35 -08:00
|
|
|
> **Status:** Not Implemented.
|
|
|
|
|
|
2026-02-12 23:28:48 -08:00
|
|
|
TODO
|
|
|
|
|
|
|
|
|
|
# `Implementation Guidance`
|
|
|
|
|
|
|
|
|
|
## `Session Context`
|
|
|
|
|
|
2026-02-13 12:48:35 -08:00
|
|
|
> **Status:** Implemented. `SessionContext` interface exists and is passed to
|
|
|
|
|
> tools.
|
|
|
|
|
|
2026-02-12 23:28:48 -08:00
|
|
|
Whenever executing a tool, hook, command, or skill, a SessionContext object
|
|
|
|
|
should be passed as an additional argument after the arguments/payload. The
|
|
|
|
|
interface should look something like:
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
export interface SessionContext {
|
|
|
|
|
// translations of existing common hook payload info
|
|
|
|
|
sessionId: string;
|
|
|
|
|
transcript: Message[];
|
|
|
|
|
cwd: string;
|
|
|
|
|
timestamp: string;
|
|
|
|
|
|
|
|
|
|
// helpers to access files and run shell commands while adhering to policies/validation
|
|
|
|
|
fs: AgentFilesystem;
|
|
|
|
|
shell: AgentShell;
|
2026-02-19 16:47:35 -08:00
|
|
|
// the agent and session are passed as context
|
2026-02-12 23:28:48 -08:00
|
|
|
agent: GeminiCliAgent;
|
2026-02-19 16:47:35 -08:00
|
|
|
session: GeminiCliSession;
|
2026-02-12 23:28:48 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface AgentFilesystem {
|
2026-02-13 12:48:35 -08:00
|
|
|
readFile(path: string): Promise<string | null>;
|
|
|
|
|
writeFile(path: string, content: string): Promise<void>;
|
|
|
|
|
// consider others including delete, globbing, etc but read/write are bare minimum
|
|
|
|
|
}
|
2026-02-12 23:28:48 -08:00
|
|
|
|
|
|
|
|
export interface AgentShell {
|
|
|
|
|
// simple promise-based execution that blocks until complete
|
2026-02-13 12:48:35 -08:00
|
|
|
exec(
|
|
|
|
|
cmd: string,
|
|
|
|
|
options?: AgentShellOptions,
|
|
|
|
|
): Promise<{
|
|
|
|
|
exitCode: number;
|
|
|
|
|
output: string;
|
|
|
|
|
stdout: string;
|
|
|
|
|
stderr: string;
|
|
|
|
|
}>;
|
2026-02-12 23:28:48 -08:00
|
|
|
start(cmd: string, options?: AgentShellOptions): AgentShellProcess;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface AgentShellOptions {
|
2026-02-13 12:48:35 -08:00
|
|
|
env?: Record<string, string>;
|
2026-02-12 23:28:48 -08:00
|
|
|
timeoutSeconds?: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface AgentShellProcess {
|
|
|
|
|
// figure out how to have a streaming shell process here that supports stdin too
|
|
|
|
|
// investigate how Gemini CLI already does this
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
# `Notes`
|
|
|
|
|
|
|
|
|
|
- To validate the SDK, it would be useful to have a robust way to mock the
|
|
|
|
|
underlying model API so that the tests could be closer to end-to-end but still
|
|
|
|
|
deterministic.
|
|
|
|
|
- Need to work in both Gemini-CLI-triggered approvals and optional
|
|
|
|
|
developer-initiated user prompts / HITL stuff.
|
|
|
|
|
- Need to think about how subagents inherit message context \- e.g. do they have
|
|
|
|
|
the same session id?
|
|
|
|
|
- Presumably the transcript is kept updated in memory and also persisted to disk
|
|
|
|
|
by default?
|
2026-02-13 12:48:35 -08:00
|
|
|
|
|
|
|
|
# `Next Steps`
|
|
|
|
|
|
|
|
|
|
Based on the current implementation status, we can proceed with:
|
|
|
|
|
|
2026-02-13 18:09:31 -08:00
|
|
|
## Feature 3: Custom Hooks Support
|
2026-02-13 12:48:35 -08:00
|
|
|
|
2026-02-13 18:09:31 -08:00
|
|
|
Implement support for loading and registering custom hooks. This involves adding
|
|
|
|
|
a `hooks` option to `GeminiCliAgentOptions`.
|
2026-02-13 12:48:35 -08:00
|
|
|
|
|
|
|
|
**Tasks:**
|
|
|
|
|
|
2026-02-13 18:09:31 -08:00
|
|
|
1. Define `Hook` interface and helper functions.
|
|
|
|
|
2. Add `hooks` option to `GeminiCliAgentOptions`.
|
|
|
|
|
3. Implement hook registration logic in `GeminiCliAgent`.
|
|
|
|
|
|
|
|
|
|
IMPORTANT: Hook signatures should be strongly typed all the way through. You'll
|
|
|
|
|
need to create a mapping of the string event name to the request/response types.
|