chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 11:58:09 +08:00
commit c48e26cdd0
2909 changed files with 1591131 additions and 0 deletions
+18
View File
@@ -0,0 +1,18 @@
# Gemini CLI SDK (`@google/gemini-cli-sdk`)
Programmatic SDK for embedding Gemini CLI agent capabilities into other
applications.
## Architecture
- `src/agent.ts`: Agent creation and management.
- `src/session.ts`: Session lifecycle and state management.
- `src/tool.ts`: Tool definition and execution interface.
- `src/skills.ts`: Skill integration.
- `src/fs.ts` & `src/shell.ts`: File system and shell utilities.
- `src/types.ts`: Public type definitions.
## Testing
- Run tests: `npm test -w @google/gemini-cli-sdk`
- Integration tests use `*.integration.test.ts` naming convention.
+36
View File
@@ -0,0 +1,36 @@
# @google/gemini-cli-sdk
The Gemini CLI SDK provides a programmatic interface to interact with Gemini
models and tools.
## Installation
```bash
npm install @google/gemini-cli-sdk
```
## Usage
```typescript
import { GeminiCliAgent } from '@google/gemini-cli-sdk';
async function main() {
const agent = new GeminiCliAgent({
instructions: 'You are a helpful assistant.',
});
const controller = new AbortController();
const signal = controller.signal;
// Stream responses from the agent
const stream = agent.sendStream('Why is the sky blue?', signal);
for await (const chunk of stream) {
if (chunk.type === 'content') {
process.stdout.write(chunk.value.text || '');
}
}
}
main().catch(console.error);
```
+340
View File
@@ -0,0 +1,340 @@
# `Gemini CLI SDK`
> **Implementation Status:** Core agent loop, tool execution, and session
> context are implemented. Advanced features like hooks, skills, subagents, and
> ACP are currently missing.
# `Examples`
## `Simple Example`
> **Status:** Implemented. `GeminiCliAgent` supports `session()` and
> `resumeSession()`.
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',
});
// 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
}
```
Validation:
- Model receives call containing "what does this project do?" text.
## `System Instructions`
> **Status:** Implemented. Both static string instructions and dynamic functions
> (receiving `SessionContext`) are supported.
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`
> **Status:** Implemented. `tool()` helper and `GeminiCliAgent` support custom
> tool definitions and execution.
```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`
> **Status:** Not Implemented.
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`
> **Status:** Implemented. `skillDir` helper and `GeminiCliAgent` support
> loading skills from filesystem.
Custom skills can be referenced by individual directories or by "skill roots"
(directories containing many skills).
### Directory Structure
```
skill-dir/
SKILL.md (Metadata and instructions)
tools/ (Optional directory for tools)
my-tool.js
```
### Usage
```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'),
],
});
```
## `Subagents`
> **Status:** Not Implemented.
```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`
> **Status:** Not Implemented.
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`
> **Status:** Not Implemented.
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`
> **Status:** Not Implemented.
TODO
# `Implementation Guidance`
## `Session Context`
> **Status:** Implemented. `SessionContext` interface exists and is passed to
> tools.
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;
// the agent and session are passed as context
agent: GeminiCliAgent;
session: GeminiCliSession;
}
export interface AgentFilesystem {
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
}
export interface AgentShell {
// simple promise-based execution that blocks until complete
exec(
cmd: string,
options?: AgentShellOptions,
): Promise<{
exitCode: number;
output: string;
stdout: string;
stderr: string;
}>;
start(cmd: string, options?: AgentShellOptions): AgentShellProcess;
}
export interface AgentShellOptions {
env?: Record<string, string>;
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?
# `Next Steps`
Based on the current implementation status, we can proceed with:
## Feature 3: Custom Hooks Support
Implement support for loading and registering custom hooks. This involves adding
a `hooks` option to `GeminiCliAgentOptions`.
**Tasks:**
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.
+73
View File
@@ -0,0 +1,73 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { GeminiCliAgent, tool, z } from '../src/index.js';
async function main() {
const getContextTool = tool(
{
name: 'get_context',
description: 'Get information about the current session context.',
inputSchema: z.object({}),
},
async (_params, context) => {
if (!context) {
return { error: 'Context not available' };
}
console.log('Session Context Accessed:');
console.log(`- Session ID: ${context.sessionId}`);
console.log(`- CWD: ${context.cwd}`);
console.log(`- Timestamp: ${context.timestamp}`);
let fileContent = null;
try {
// Try to read a file (e.g., package.json in the CWD)
// Note: This relies on the agent running in a directory with package.json
fileContent = await context.fs.readFile('package.json');
} catch (e) {
console.log(`- Could not read package.json: ${e}`);
}
let shellOutput = null;
try {
// Try to run a simple shell command
const result = await context.shell.exec('echo "Hello from SDK Shell"');
shellOutput = result.output.trim();
} catch (e) {
console.log(`- Could not run shell command: ${e}`);
}
return {
sessionId: context.sessionId,
cwd: context.cwd,
hasFsAccess: !!context.fs,
hasShellAccess: !!context.shell,
packageJsonExists: !!fileContent,
shellEcho: shellOutput,
};
},
);
const agent = new GeminiCliAgent({
instructions:
'You are a helpful assistant. Use the get_context tool to tell me about my environment.',
tools: [getContextTool],
// Set CWD to the package root so package.json exists
cwd: process.cwd(),
});
console.log("Sending prompt: 'What is my current session context?'");
for await (const chunk of agent.sendStream(
'What is my current session context?',
)) {
if (chunk.type === 'content') {
process.stdout.write(chunk.value || '');
}
}
}
main().catch(console.error);
+38
View File
@@ -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
View File
@@ -0,0 +1,7 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
export * from './src/index.js';
+36
View File
@@ -0,0 +1,36 @@
{
"name": "@google/gemini-cli-sdk",
"version": "0.52.0-nightly.20260707.g27a3da3e8",
"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.25.76",
"zod-to-json-schema": "3.25.1"
},
"devDependencies": {
"typescript": "5.8.3",
"vitest": "3.2.4"
},
"engines": {
"node": ">=20"
}
}
+172
View File
@@ -0,0 +1,172 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { GeminiCliAgent } from './agent.js';
import * as path from 'node:path';
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Set this to true locally when you need to update snapshots
const RECORD_MODE = process.env['RECORD_NEW_RESPONSES'] === 'true';
const getGoldenPath = (name: string) =>
path.resolve(__dirname, '../test-data', `${name}.json`);
describe('GeminiCliAgent Integration', () => {
beforeEach(() => {
vi.stubEnv('GEMINI_API_KEY', 'test-api-key');
});
afterEach(() => {
vi.unstubAllEnvs();
});
it('handles static instructions', async () => {
const goldenFile = getGoldenPath('agent-static-instructions');
const agent = new GeminiCliAgent({
instructions: 'You are a pirate. Respond in pirate speak.',
model: 'gemini-2.0-flash',
recordResponses: RECORD_MODE ? goldenFile : undefined,
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 = session.sendStream('Say hello.');
for await (const event of stream) {
events.push(event);
}
const textEvents = events.filter((e) => e.type === 'content');
const responseText = textEvents
.map((e) => ('value' in e && typeof e.value === 'string' ? e.value : ''))
.join('');
// Expect pirate speak
expect(responseText.toLowerCase()).toMatch(/ahoy|matey|arrr/);
}, 30000);
it('handles dynamic instructions', async () => {
const goldenFile = getGoldenPath('agent-dynamic-instructions');
let callCount = 0;
const agent = new GeminiCliAgent({
instructions: (_ctx) => {
callCount++;
return `You are a helpful assistant. The secret number is ${callCount}. Always mention the secret number when asked.`;
},
model: 'gemini-2.0-flash',
recordResponses: RECORD_MODE ? goldenFile : undefined,
fakeResponses: RECORD_MODE ? undefined : goldenFile,
});
const session = agent.session();
// First turn
const events1 = [];
const stream1 = session.sendStream('What is the secret number?');
for await (const event of stream1) {
events1.push(event);
}
const responseText1 = events1
.filter((e) => e.type === 'content')
.map((e) => ('value' in e && typeof e.value === 'string' ? e.value : ''))
.join('');
expect(responseText1).toContain('1');
// Second turn
const events2 = [];
const stream2 = session.sendStream('What is the secret number now?');
for await (const event of stream2) {
events2.push(event);
}
const responseText2 = events2
.filter((e) => e.type === 'content')
.map((e) => ('value' in e && typeof e.value === 'string' ? e.value : ''))
.join('');
expect(responseText2).toContain('2');
}, 30000);
it('resumes a session', async () => {
const goldenFile = getGoldenPath('agent-resume-session');
// Create initial session
const agent = new GeminiCliAgent({
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,
});
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
}
// 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);
const events2 = [];
const stream2 = session2.sendStream('What is the word again?');
for await (const event of stream2) {
events2.push(event);
}
const responseText = events2
.filter((e) => e.type === 'content')
.map((e) => ('value' in e && typeof e.value === 'string' ? e.value : ''))
.join('');
expect(responseText).toContain('BANANA');
}, 30000);
it('throws on invalid instructions', () => {
// Missing instructions should be fine
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expect(() => new GeminiCliAgent({} as any).session()).not.toThrow();
expect(() =>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
new GeminiCliAgent({ instructions: 123 as any }).session(),
).toThrow('Instructions must be a string or a function.');
});
it('propagates errors from dynamic instructions', async () => {
const goldenFile = getGoldenPath('agent-static-instructions');
const agent = new GeminiCliAgent({
instructions: () => {
throw new Error('Dynamic instruction failure');
},
model: 'gemini-2.0-flash',
recordResponses: RECORD_MODE ? goldenFile : undefined,
fakeResponses: RECORD_MODE ? undefined : goldenFile,
});
const session = agent.session();
const stream = session.sendStream('Say hello.');
await expect(async () => {
for await (const _event of stream) {
// Just consume the stream
}
}).rejects.toThrow('Dynamic instruction failure');
}, 30000);
});
+125
View File
@@ -0,0 +1,125 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import * as path from 'node:path';
import {
Storage,
createSessionId,
type ResumedSessionData,
type ConversationRecord,
loadConversationRecord,
} from '@google/gemini-cli-core';
import { GeminiCliSession } from './session.js';
import type { GeminiCliAgentOptions } from './types.js';
/**
* The main entry point for the Gemini CLI SDK.
*
* An agent encapsulates configuration (instructions, tools, skills, model)
* and can create new sessions or resume existing ones.
*
* @example
* ```typescript
* const agent = new GeminiCliAgent({
* instructions: 'You are a helpful coding assistant.',
* tools: [myTool],
* });
*
* const session = agent.session();
* await session.initialize();
*
* for await (const event of session.sendStream('Hello!')) {
* console.log(event);
* }
* ```
*/
export class GeminiCliAgent {
private options: GeminiCliAgentOptions;
constructor(options: GeminiCliAgentOptions) {
this.options = options;
}
/**
* Create a new conversation session.
*
* @param options - Optional session configuration. Pass `{ sessionId }` to
* use a specific session ID; otherwise a new one is generated.
* @returns A new {@link GeminiCliSession} instance.
*/
session(options?: { sessionId?: string }): GeminiCliSession {
const sessionId = options?.sessionId || createSessionId();
return new GeminiCliSession(this.options, sessionId, this);
}
/**
* Resume a previously created session by its ID.
*
* Looks up the session's conversation history from storage and replays it
* so the agent can continue the conversation.
*
* @param sessionId - The ID of the session to resume.
* @returns A {@link GeminiCliSession} with the prior conversation loaded.
* @throws {Error} If no sessions exist or the specified ID is not found.
*/
async resumeSession(sessionId: string): Promise<GeminiCliSession> {
const cwd = this.options.cwd || process.cwd();
const storage = new Storage(cwd);
await storage.initialize();
let conversation: ConversationRecord | undefined;
let filePath: string | undefined;
const sessions = await storage.listProjectChatFiles();
if (sessions.length === 0) {
throw new Error(
`No sessions found in ${path.join(storage.getProjectTempDir(), 'chats')}`,
);
}
const truncatedId = sessionId.slice(0, 8);
// Optimization: filenames include first 8 chars of sessionId.
// Filter sessions that might match.
const candidates = sessions.filter((s) => s.filePath.includes(truncatedId));
// If optimization fails (e.g. old files), check all?
// Assuming filenames always follow convention if created by this tool.
// But we can fallback to checking all if needed, but let's stick to candidates first.
// If candidates is empty, maybe fallback to all.
const filesToCheck = candidates.length > 0 ? candidates : sessions;
for (const sessionFile of filesToCheck) {
const absolutePath = path.join(
storage.getProjectTempDir(),
sessionFile.filePath,
);
const loaded = await loadConversationRecord(absolutePath);
if (loaded && loaded.sessionId === sessionId) {
conversation = loaded;
filePath = path.join(storage.getProjectTempDir(), sessionFile.filePath);
break;
}
}
if (!conversation || !filePath) {
throw new Error(`Session with ID ${sessionId} not found`);
}
const resumedData: ResumedSessionData = {
conversation,
filePath,
};
return new GeminiCliSession(
this.options,
conversation.sessionId,
this,
resumedData,
);
}
}
+42
View File
@@ -0,0 +1,42 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type { Config as CoreConfig } from '@google/gemini-cli-core';
import type { AgentFilesystem } from './types.js';
import fs from 'node:fs/promises';
/**
* SDK implementation of {@link AgentFilesystem} that enforces path-based
* access policies from the core Config.
*
* Read operations return `null` when access is denied; write operations
* throw an error.
*/
export class SdkAgentFilesystem implements AgentFilesystem {
constructor(private readonly config: CoreConfig) {}
async readFile(path: string): Promise<string | null> {
const error = this.config.validatePathAccess(path, 'read');
if (error) {
// For now, if access is denied, we can either throw or return null.
// Returning null makes sense for "file not found or readable".
return null;
}
try {
return await fs.readFile(path, 'utf-8');
} catch {
return null;
}
}
async writeFile(path: string, content: string): Promise<void> {
const error = this.config.validatePathAccess(path, 'write');
if (error) {
throw new Error(error);
}
await fs.writeFile(path, content, 'utf-8');
}
}
+11
View File
@@ -0,0 +1,11 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
export * from './agent.js';
export * from './session.js';
export * from './tool.js';
export * from './skills.js';
export * from './types.js';
+332
View File
@@ -0,0 +1,332 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { GeminiCliSession } from './session.js';
import type { GeminiCliAgent } from './agent.js';
import type { GeminiCliAgentOptions } from './types.js';
// Mutable mock client so individual tests can override sendMessageStream
const mockClient = {
resumeChat: vi.fn().mockResolvedValue(undefined),
getHistory: vi.fn().mockReturnValue([]),
sendMessageStream: vi.fn().mockReturnValue((async function* () {})()),
updateSystemInstruction: vi.fn(),
};
// Mutable mock config so individual tests can spy on setUserMemory etc.
const mockConfig = {
initialize: vi.fn().mockResolvedValue(undefined),
refreshAuth: vi.fn().mockResolvedValue(undefined),
getSkillManager: vi.fn().mockReturnValue({
getSkills: vi.fn().mockReturnValue([]),
addSkills: vi.fn(),
}),
getToolRegistry: vi.fn().mockReturnValue({
getTool: vi.fn().mockReturnValue(null),
registerTool: vi.fn(),
unregisterTool: vi.fn(),
}),
getMessageBus: vi.fn().mockReturnValue({}),
getGeminiClient: vi.fn().mockReturnValue(mockClient),
getSessionId: vi.fn().mockReturnValue('mock-session-id'),
getWorkingDir: vi.fn().mockReturnValue('/tmp'),
setUserMemory: vi.fn(),
};
// Mock scheduleAgentTools at module level so tests can override it
const mockScheduleAgentTools = vi.fn().mockResolvedValue([]);
// Mock @google/gemini-cli-core to avoid heavy filesystem/auth/telemetry setup
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>();
return {
...actual,
Config: vi.fn().mockImplementation(() => mockConfig),
getAuthTypeFromEnv: vi.fn().mockReturnValue(null),
scheduleAgentTools: (...args: unknown[]) => mockScheduleAgentTools(...args),
loadSkillsFromDir: vi.fn().mockResolvedValue([]),
ActivateSkillTool: class {
static Name = 'activate_skill';
},
PolicyDecision: actual.PolicyDecision,
};
});
const mockAgent = {} as unknown as GeminiCliAgent;
const baseOptions: GeminiCliAgentOptions = {
instructions: 'You are a helpful assistant.',
};
beforeEach(() => {
vi.clearAllMocks();
// Reset sendMessageStream to empty stream by default
mockClient.sendMessageStream.mockReturnValue((async function* () {})());
mockScheduleAgentTools.mockResolvedValue([]);
});
describe('GeminiCliSession constructor', () => {
it('accepts string instructions', () => {
expect(
() => new GeminiCliSession(baseOptions, 'session-1', mockAgent),
).not.toThrow();
});
it('accepts function instructions', () => {
const options: GeminiCliAgentOptions = {
instructions: async () => 'dynamic instructions',
};
expect(
() => new GeminiCliSession(options, 'session-2', mockAgent),
).not.toThrow();
});
it('throws when instructions is an object (not string or function)', () => {
const options = {
instructions: { invalid: true },
} as unknown as GeminiCliAgentOptions;
expect(() => new GeminiCliSession(options, 'session-3', mockAgent)).toThrow(
'Instructions must be a string or a function.',
);
});
it('throws when instructions is a number', () => {
const options = {
instructions: 42,
} as unknown as GeminiCliAgentOptions;
expect(() => new GeminiCliSession(options, 'session-4', mockAgent)).toThrow(
'Instructions must be a string or a function.',
);
});
it('throws when instructions is an array', () => {
const options = {
instructions: ['step1', 'step2'],
} as unknown as GeminiCliAgentOptions;
expect(() => new GeminiCliSession(options, 'session-5', mockAgent)).toThrow(
'Instructions must be a string or a function.',
);
});
});
describe('GeminiCliSession id getter', () => {
it('returns the sessionId passed to the constructor', () => {
const session = new GeminiCliSession(
baseOptions,
'my-session-id',
mockAgent,
);
expect(session.id).toBe('my-session-id');
});
it('returns different ids for different sessions', () => {
const s1 = new GeminiCliSession(baseOptions, 'session-a', mockAgent);
const s2 = new GeminiCliSession(baseOptions, 'session-b', mockAgent);
expect(s1.id).not.toBe(s2.id);
});
});
describe('GeminiCliSession initialize()', () => {
it('initializes successfully with string instructions', async () => {
const session = new GeminiCliSession(
baseOptions,
'session-init-1',
mockAgent,
);
await expect(session.initialize()).resolves.toBeUndefined();
});
it('is idempotent — calling initialize() twice does not throw', async () => {
const session = new GeminiCliSession(
baseOptions,
'session-init-2',
mockAgent,
);
await session.initialize();
await expect(session.initialize()).resolves.toBeUndefined();
});
it('initializes with empty tools array', async () => {
const options: GeminiCliAgentOptions = { ...baseOptions, tools: [] };
const session = new GeminiCliSession(options, 'session-init-3', mockAgent);
await expect(session.initialize()).resolves.toBeUndefined();
});
it('initializes with empty skills array', async () => {
const options: GeminiCliAgentOptions = { ...baseOptions, skills: [] };
const session = new GeminiCliSession(options, 'session-init-4', mockAgent);
await expect(session.initialize()).resolves.toBeUndefined();
});
it('initializes with custom model', async () => {
const options: GeminiCliAgentOptions = {
...baseOptions,
model: 'gemini-2.0-flash',
};
const session = new GeminiCliSession(options, 'session-init-5', mockAgent);
await expect(session.initialize()).resolves.toBeUndefined();
});
it('initializes with custom cwd', async () => {
const options: GeminiCliAgentOptions = {
...baseOptions,
cwd: '/custom/working/dir',
};
const session = new GeminiCliSession(options, 'session-init-6', mockAgent);
await expect(session.initialize()).resolves.toBeUndefined();
});
});
// TODO(#24999): Mock uses getGeminiClient() method but session.ts expects geminiClient property.
describe.skip('GeminiCliSession sendStream()', () => {
it('auto-initializes if not yet initialized', async () => {
const session = new GeminiCliSession(
baseOptions,
'session-stream-1',
mockAgent,
);
const events = [];
for await (const event of session.sendStream('Hello')) {
events.push(event);
}
expect(events).toHaveLength(0);
});
it('completes cleanly when model returns no tool calls', async () => {
const session = new GeminiCliSession(
baseOptions,
'session-stream-2',
mockAgent,
);
await session.initialize();
const events = [];
for await (const event of session.sendStream('Hello')) {
events.push(event);
}
expect(events).toHaveLength(0);
});
it('accepts an AbortSignal without throwing', async () => {
const session = new GeminiCliSession(
baseOptions,
'session-stream-3',
mockAgent,
);
const controller = new AbortController();
const events = [];
for await (const event of session.sendStream('Hello', controller.signal)) {
events.push(event);
}
expect(events).toHaveLength(0);
});
it('executes tool call loop and sends function response back to model', async () => {
const { GeminiEventType } = await import('@google/gemini-cli-core');
// First call: yield a ToolCallRequest, then end
// Second call: empty stream (model is done after tool result)
let callCount = 0;
mockClient.sendMessageStream.mockImplementation(() => {
callCount++;
if (callCount === 1) {
return (async function* () {
yield {
type: GeminiEventType.ToolCallRequest,
value: {
callId: 'call-1',
name: 'testTool',
args: { input: 'value' },
},
};
})();
}
return (async function* () {})();
});
mockScheduleAgentTools.mockResolvedValue([
{
response: {
responseParts: [
{
functionResponse: {
name: 'testTool',
response: { result: 'done' },
},
},
],
},
},
]);
const session = new GeminiCliSession(
baseOptions,
'session-stream-4',
mockAgent,
);
const events = [];
for await (const event of session.sendStream('Use the tool')) {
events.push(event);
}
// The ToolCallRequest event should have been yielded to the caller
expect(events).toHaveLength(1);
expect(events[0].type).toBe(GeminiEventType.ToolCallRequest);
// scheduleAgentTools should have been called with the tool call
expect(mockScheduleAgentTools).toHaveBeenCalledOnce();
// sendMessageStream called twice: once for prompt, once with tool result
expect(mockClient.sendMessageStream).toHaveBeenCalledTimes(2);
});
it('calls setUserMemory and updateSystemInstruction when instructions is a function', async () => {
const dynamicInstructions = vi
.fn()
.mockResolvedValue('updated instructions');
const options: GeminiCliAgentOptions = {
instructions: dynamicInstructions,
};
const session = new GeminiCliSession(
options,
'session-stream-5',
mockAgent,
);
for await (const _event of session.sendStream('Hello')) {
// consume stream
}
// The instructions function should have been called with a SessionContext
expect(dynamicInstructions).toHaveBeenCalledOnce();
const context = dynamicInstructions.mock.calls[0][0];
expect(context).toHaveProperty('sessionId');
expect(context).toHaveProperty('transcript');
expect(context).toHaveProperty('cwd');
expect(context).toHaveProperty('timestamp');
// Config should have been updated with the new instructions
expect(mockConfig.setUserMemory).toHaveBeenCalledWith(
'updated instructions',
);
// Client system instruction should have been refreshed
expect(mockClient.updateSystemInstruction).toHaveBeenCalledOnce();
});
it('does not call setUserMemory when instructions is a string', async () => {
const session = new GeminiCliSession(
baseOptions,
'session-stream-6',
mockAgent,
);
for await (const _event of session.sendStream('Hello')) {
// consume stream
}
expect(mockConfig.setUserMemory).not.toHaveBeenCalled();
expect(mockClient.updateSystemInstruction).not.toHaveBeenCalled();
});
});
+316
View File
@@ -0,0 +1,316 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {
type AgentLoopContext,
Config,
type ConfigParameters,
AuthType,
PREVIEW_GEMINI_MODEL_AUTO,
GeminiEventType,
type ToolCallRequestInfo,
type ServerGeminiStreamEvent,
type GeminiClient,
type Content,
scheduleAgentTools,
getAuthTypeFromEnv,
type ToolRegistry,
loadSkillsFromDir,
ActivateSkillTool,
type ResumedSessionData,
PolicyDecision,
} from '@google/gemini-cli-core';
import { type Tool, SdkTool } from './tool.js';
import { SdkAgentFilesystem } from './fs.js';
import { SdkAgentShell } from './shell.js';
import type {
SessionContext,
GeminiCliAgentOptions,
SystemInstructions,
} from './types.js';
import type { SkillReference } from './skills.js';
import type { GeminiCliAgent } from './agent.js';
/**
* Represents an interactive conversation session with a Gemini CLI agent.
*
* A session manages the conversation lifecycle: initialization, sending messages
* via streaming, handling tool calls, and maintaining conversation history.
*
* Create a session via {@link GeminiCliAgent.session} or resume one with
* {@link GeminiCliAgent.resumeSession}.
*/
export class GeminiCliSession {
private readonly config: Config;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private readonly tools: Array<Tool<any>>;
private readonly skillRefs: SkillReference[];
private readonly instructions: SystemInstructions | undefined;
private client: GeminiClient | undefined;
private initialized = false;
constructor(
options: GeminiCliAgentOptions,
private readonly sessionId: string,
private readonly agent: GeminiCliAgent,
private readonly resumedData?: ResumedSessionData,
) {
this.instructions = options.instructions;
const cwd = options.cwd || process.cwd();
this.tools = options.tools || [];
this.skillRefs = options.skills || [];
let initialMemory = '';
if (typeof this.instructions === 'string') {
initialMemory = this.instructions;
} else if (this.instructions && typeof this.instructions !== 'function') {
throw new Error('Instructions must be a string or a function.');
}
const configParams: ConfigParameters = {
sessionId: this.sessionId,
targetDir: cwd,
cwd,
debugMode: options.debug ?? false,
model: options.model || PREVIEW_GEMINI_MODEL_AUTO,
userMemory: initialMemory,
// Minimal config
enableHooks: false,
mcpEnabled: false,
extensionsEnabled: false,
recordResponses: options.recordResponses,
fakeResponses: options.fakeResponses,
skillsSupport: true,
adminSkillsEnabled: true,
policyEngineConfig: {
// TODO: Revisit this default when we have a mechanism for wiring up approvals
defaultDecision: PolicyDecision.ALLOW,
},
};
this.config = new Config(configParams);
}
/**
* The unique identifier for this session.
*/
get id(): string {
return this.sessionId;
}
/**
* Initialize the session by setting up authentication, loading skills,
* and registering tools. Must be called before {@link sendStream}.
*
* This method is idempotent — calling it multiple times has no effect
* after the first successful initialization.
*/
async initialize(): Promise<void> {
if (this.initialized) return;
const authType = getAuthTypeFromEnv() || AuthType.COMPUTE_ADC;
await this.config.refreshAuth(authType);
await this.config.initialize();
// Load additional skills from options
if (this.skillRefs.length > 0) {
const skillManager = this.config.getSkillManager();
const loadPromises = this.skillRefs.map(async (ref) => {
try {
if (ref.type === 'dir') {
return await loadSkillsFromDir(ref.path);
}
} catch (e) {
// TODO: refactor this to use a proper logger interface
// eslint-disable-next-line no-console
console.error(`Failed to load skills from ${ref.path}:`, e);
}
return [];
});
const loadedSkills = (await Promise.all(loadPromises)).flat();
if (loadedSkills.length > 0) {
skillManager.addSkills(loadedSkills);
}
}
// Re-register ActivateSkillTool if we have skills
const skillManager = this.config.getSkillManager();
if (skillManager.getSkills().length > 0) {
const loopContext: AgentLoopContext = this.config;
const registry = loopContext.toolRegistry;
const toolName = ActivateSkillTool.Name;
if (registry.getTool(toolName)) {
registry.unregisterTool(toolName);
}
registry.registerTool(
new ActivateSkillTool(this.config, loopContext.messageBus),
);
}
// Register tools
const loopContext2: AgentLoopContext = this.config;
const registry = loopContext2.toolRegistry;
const messageBus = loopContext2.messageBus;
for (const toolDef of this.tools) {
const sdkTool = new SdkTool(toolDef, messageBus, this.agent, undefined);
registry.registerTool(sdkTool);
}
this.client = loopContext2.geminiClient;
if (this.resumedData) {
const history: Content[] = this.resumedData.conversation.messages.map(
(m) => {
const role = m.type === 'gemini' ? 'model' : 'user';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let parts: any[] = [];
if (Array.isArray(m.content)) {
parts = m.content;
} else if (m.content) {
parts = [{ text: String(m.content) }];
}
return { role, parts };
},
);
await this.client.resumeChat(history, this.resumedData);
}
this.initialized = true;
}
/**
* Send a prompt to the model and yield streaming events as they arrive.
*
* Handles the full agentic loop: sends the user prompt, streams model
* responses, executes any tool calls the model requests, and continues
* the loop until the model produces a final response with no tool calls.
*
* @param prompt - The user message to send.
* @param signal - Optional {@link AbortSignal} to cancel the stream.
* @yields {@link ServerGeminiStreamEvent} events as they are received from
* the model.
*
* @example
* ```typescript
* for await (const event of session.sendStream('Explain this code')) {
* if (event.type === GeminiEventType.ModelResponse) {
* process.stdout.write(event.value);
* }
* }
* ```
*/
async *sendStream(
prompt: string,
signal?: AbortSignal,
): AsyncGenerator<ServerGeminiStreamEvent> {
if (!this.initialized || !this.client) {
await this.initialize();
}
const client = this.client!;
const abortSignal = signal ?? new AbortController().signal;
const sessionId = this.config.getSessionId();
const fs = new SdkAgentFilesystem(this.config);
const shell = new SdkAgentShell(this.config);
let request: Parameters<GeminiClient['sendMessageStream']>[0] = [
{ text: prompt },
];
while (true) {
if (typeof this.instructions === 'function') {
const context: SessionContext = {
sessionId,
transcript: client.getHistory(),
cwd: this.config.getWorkingDir(),
timestamp: new Date().toISOString(),
fs,
shell,
agent: this.agent,
session: this,
};
const newInstructions = await this.instructions(context);
this.config.setUserMemory(newInstructions);
client.updateSystemInstruction();
}
const stream = client.sendMessageStream(request, abortSignal, sessionId);
const toolCallsToSchedule: ToolCallRequestInfo[] = [];
for await (const event of stream) {
yield event;
if (event.type === GeminiEventType.ToolCallRequest) {
const toolCall = event.value;
let args = toolCall.args;
if (typeof args === 'string') {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
args = JSON.parse(args);
}
toolCallsToSchedule.push({
...toolCall,
args,
isClientInitiated: false,
prompt_id: sessionId,
});
}
}
if (toolCallsToSchedule.length === 0) {
break;
}
const transcript: readonly Content[] = client.getHistory();
const context: SessionContext = {
sessionId,
transcript,
cwd: this.config.getWorkingDir(),
timestamp: new Date().toISOString(),
fs,
shell,
agent: this.agent,
session: this,
};
const loopContext: AgentLoopContext = this.config;
const originalRegistry = loopContext.toolRegistry;
const scopedRegistry: ToolRegistry = originalRegistry.clone();
const originalGetTool = scopedRegistry.getTool.bind(scopedRegistry);
scopedRegistry.getTool = (name: string) => {
const tool = originalGetTool(name);
if (tool instanceof SdkTool) {
return tool.bindContext(context);
}
return tool;
};
const completedCalls = await scheduleAgentTools(
this.config,
toolCallsToSchedule,
{
schedulerId: sessionId,
toolRegistry: scopedRegistry,
signal: abortSignal,
},
);
const functionResponses = completedCalls.flatMap(
(call) => call.response.responseParts,
);
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
request = functionResponses as unknown as Parameters<
GeminiClient['sendMessageStream']
>[0];
}
}
}
+86
View File
@@ -0,0 +1,86 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {
type AgentLoopContext,
ShellExecutionService,
ShellTool,
type Config as CoreConfig,
} from '@google/gemini-cli-core';
import type {
AgentShell,
AgentShellResult,
AgentShellOptions,
} from './types.js';
/**
* SDK implementation of {@link AgentShell} that executes commands via the
* core ShellExecutionService, subject to the agent's security policies.
*
* Commands that require interactive confirmation will be rejected since
* no interactive session is available in headless SDK mode.
*
* @remarks In this implementation, stderr is combined into stdout by the
* underlying ShellExecutionService. As a result, the stderr field of the
* returned {@link AgentShellResult} will be empty, and both output and
* stdout will contain the combined output.
*/
export class SdkAgentShell implements AgentShell {
constructor(private readonly config: CoreConfig) {}
async exec(
command: string,
options?: AgentShellOptions,
): Promise<AgentShellResult> {
const cwd = options?.cwd || this.config.getWorkingDir();
const abortController = new AbortController();
// Use ShellTool to check policy
const loopContext: AgentLoopContext = this.config;
const shellTool = new ShellTool(this.config, loopContext.messageBus);
try {
const invocation = shellTool.build({
command,
dir_path: cwd,
});
const confirmation = await invocation.shouldConfirmExecute(
abortController.signal,
);
if (confirmation) {
throw new Error(
'Command execution requires confirmation but no interactive session is available.',
);
}
} catch (error) {
return {
output: '',
stdout: '',
stderr: '',
exitCode: 1,
error: error instanceof Error ? error : new Error(String(error)),
};
}
const handle = await ShellExecutionService.execute(
command,
cwd,
() => {}, // No-op output event handler for now
abortController.signal,
false, // shouldUseNodePty: false for headless execution
this.config.getShellExecutionConfig(),
);
const result = await handle.result;
return {
output: result.output,
stdout: result.output, // ShellExecutionService combines stdout/stderr usually
stderr: '', // ShellExecutionService currently combines, so stderr is empty or mixed
exitCode: result.exitCode,
};
}
}
@@ -0,0 +1,92 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect } from 'vitest';
import { GeminiCliAgent } from './agent.js';
import { skillDir } from './skills.js';
import * as path from 'node:path';
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Set this to true locally when you need to update snapshots
const RECORD_MODE = process.env['RECORD_NEW_RESPONSES'] === 'true';
const getGoldenPath = (name: string) =>
path.resolve(__dirname, '../test-data', `${name}.json`);
const SKILL_DIR = path.resolve(__dirname, '../test-data/skills/pirate-skill');
const SKILL_ROOT = path.resolve(__dirname, '../test-data/skills');
describe('GeminiCliAgent Skills Integration', () => {
it('loads and activates a skill from a directory', async () => {
const goldenFile = getGoldenPath('skill-dir-success');
const agent = new GeminiCliAgent({
instructions: 'You are a helpful assistant.',
skills: [skillDir(SKILL_DIR)],
// If recording, use real model + record path.
// If testing, use auto model + fake path.
model: RECORD_MODE ? 'gemini-2.0-flash' : undefined,
recordResponses: RECORD_MODE ? goldenFile : undefined,
fakeResponses: RECORD_MODE ? undefined : goldenFile,
});
// 1. Ask to activate the skill
const events = [];
const session = agent.session();
// The prompt explicitly asks to activate the skill by name
const stream = session.sendStream(
'Activate the pirate-skill and then tell me a joke.',
);
for await (const event of stream) {
events.push(event);
}
const textEvents = events.filter((e) => e.type === 'content');
const responseText = textEvents
.map((e) => ('value' in e && typeof e.value === 'string' ? e.value : ''))
.join('');
// Expect pirate speak
expect(responseText.toLowerCase()).toContain('arrr');
}, 120000);
it('loads and activates a skill from a root', async () => {
const goldenFile = getGoldenPath('skill-root-success');
const agent = new GeminiCliAgent({
instructions: 'You are a helpful assistant.',
skills: [skillDir(SKILL_ROOT)],
// If recording, use real model + record path.
// If testing, use auto model + fake path.
model: RECORD_MODE ? 'gemini-2.0-flash' : undefined,
recordResponses: RECORD_MODE ? goldenFile : undefined,
fakeResponses: RECORD_MODE ? undefined : goldenFile,
});
// 1. Ask to activate the skill
const events = [];
const session = agent.session();
const stream = session.sendStream(
'Activate the pirate-skill and confirm it is active.',
);
for await (const event of stream) {
events.push(event);
}
const textEvents = events.filter((e) => e.type === 'content');
const responseText = textEvents
.map((e) => ('value' in e && typeof e.value === 'string' ? e.value : ''))
.join('');
// Expect confirmation or pirate speak
expect(responseText.toLowerCase()).toContain('arrr');
}, 120000);
});
+22
View File
@@ -0,0 +1,22 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* A reference to a skill directory that can be loaded by the agent.
*
* Skills extend the agent's capabilities by providing additional prompts,
* tools, and behaviors defined in a directory structure.
*/
export type SkillReference = { type: 'dir'; path: string };
/**
* Reference a directory containing skills.
*
* @param path Path to the skill directory
*/
export function skillDir(path: string): SkillReference {
return { type: 'dir', path };
}
+149
View File
@@ -0,0 +1,149 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect } from 'vitest';
import { GeminiCliAgent } from './agent.js';
import * as path from 'node:path';
import { z } from 'zod';
import { tool, ModelVisibleError } from './tool.js';
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Set this to true locally when you need to update snapshots
const RECORD_MODE = process.env['RECORD_NEW_RESPONSES'] === 'true';
const getGoldenPath = (name: string) =>
path.resolve(__dirname, '../test-data', `${name}.json`);
describe('GeminiCliAgent Tool Integration', () => {
it('handles tool execution success', async () => {
const goldenFile = getGoldenPath('tool-success');
const agent = new GeminiCliAgent({
instructions: 'You are a helpful assistant.',
// If recording, use real model + record path.
// If testing, use auto model + fake path.
model: RECORD_MODE ? 'gemini-2.0-flash' : undefined,
recordResponses: RECORD_MODE ? goldenFile : undefined,
fakeResponses: RECORD_MODE ? undefined : goldenFile,
tools: [
tool(
{
name: 'add',
description: 'Adds two numbers',
inputSchema: z.object({ a: z.number(), b: z.number() }),
},
async ({ a, b }) => a + b,
),
],
});
const events = [];
const session = agent.session();
const stream = session.sendStream('What is 5 + 3?');
for await (const event of stream) {
events.push(event);
}
const textEvents = events.filter((e) => e.type === 'content');
const responseText = textEvents
.map((e) => ('value' in e && typeof e.value === 'string' ? e.value : ''))
.join('');
expect(responseText).toContain('8');
}, 20000);
it('handles ModelVisibleError correctly', async () => {
const goldenFile = getGoldenPath('tool-error-recovery');
const agent = new GeminiCliAgent({
instructions: 'You are a helpful assistant.',
model: RECORD_MODE ? 'gemini-2.0-flash' : undefined,
recordResponses: RECORD_MODE ? goldenFile : undefined,
fakeResponses: RECORD_MODE ? undefined : goldenFile,
tools: [
tool(
{
name: 'failVisible',
description: 'Fails with a visible error if input is "fail"',
inputSchema: z.object({ input: z.string() }),
},
async ({ input }) => {
if (input === 'fail') {
throw new ModelVisibleError('Tool failed visibly');
}
return 'Success';
},
),
],
});
const events = [];
const session = agent.session();
// Force the model to trigger the error first, then hopefully recover or at least acknowledge it.
// The prompt is crafted to make the model try 'fail' first.
const stream = session.sendStream(
'Call the tool with "fail". If it fails, tell me the error message.',
);
for await (const event of stream) {
events.push(event);
}
const textEvents = events.filter((e) => e.type === 'content');
const responseText = textEvents
.map((e) => ('value' in e && typeof e.value === 'string' ? e.value : ''))
.join('');
// The model should see the error "Tool failed visibly" and report it back.
expect(responseText).toContain('Tool failed visibly');
}, 20000);
it('handles sendErrorsToModel: true correctly', async () => {
const goldenFile = getGoldenPath('tool-catchall-error');
const agent = new GeminiCliAgent({
instructions: 'You are a helpful assistant.',
model: RECORD_MODE ? 'gemini-2.0-flash' : undefined,
recordResponses: RECORD_MODE ? goldenFile : undefined,
fakeResponses: RECORD_MODE ? undefined : goldenFile,
tools: [
tool(
{
name: 'checkSystemStatus',
description: 'Checks the current system status',
inputSchema: z.object({}),
sendErrorsToModel: true,
},
async () => {
throw new Error('Standard error caught');
},
),
],
});
const events = [];
const session = agent.session();
const stream = session.sendStream(
'Check the system status and report any errors.',
);
for await (const event of stream) {
events.push(event);
}
const textEvents = events.filter((e) => e.type === 'content');
const responseText = textEvents
.map((e) => ('value' in e && typeof e.value === 'string' ? e.value : ''))
.join('');
// The model should report the caught standard error.
expect(responseText.toLowerCase()).toContain('error');
}, 20000);
});
+149
View File
@@ -0,0 +1,149 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect } from 'vitest';
import { z } from 'zod';
import { SdkTool, tool, ModelVisibleError } from './tool.js';
import type { MessageBus } from '@google/gemini-cli-core';
// Mock MessageBus
const mockMessageBus = {} as unknown as MessageBus;
describe('tool()', () => {
it('creates a tool definition with defaults', () => {
const definition = tool(
{
name: 'testTool',
description: 'A test tool',
inputSchema: z.object({ foo: z.string() }),
},
async () => 'result',
);
expect(definition.name).toBe('testTool');
expect(definition.description).toBe('A test tool');
expect(definition.sendErrorsToModel).toBeUndefined();
});
it('creates a tool definition with explicit configuration', () => {
const definition = tool(
{
name: 'testTool',
description: 'A test tool',
inputSchema: z.object({ foo: z.string() }),
sendErrorsToModel: true,
},
async () => 'result',
);
expect(definition.sendErrorsToModel).toBe(true);
});
});
describe('SdkTool Execution', () => {
it('executes successfully', async () => {
const definition = tool(
{
name: 'successTool',
description: 'Always succeeds',
inputSchema: z.object({ val: z.string() }),
},
async ({ val }) => `Success: ${val}`,
);
const sdkTool = new SdkTool(definition, mockMessageBus);
const invocation = sdkTool.createInvocationWithContext(
{ val: 'test' },
mockMessageBus,
undefined,
);
const result = await invocation.execute({
abortSignal: new AbortController().signal,
});
expect(result.llmContent).toBe('Success: test');
expect(result.error).toBeUndefined();
});
it('throws standard Error by default', async () => {
const definition = tool(
{
name: 'failTool',
description: 'Always fails',
inputSchema: z.object({}),
},
async () => {
throw new Error('Standard error');
},
);
const sdkTool = new SdkTool(definition, mockMessageBus);
const invocation = sdkTool.createInvocationWithContext(
{},
mockMessageBus,
undefined,
);
await expect(
invocation.execute({ abortSignal: new AbortController().signal }),
).rejects.toThrow('Standard error');
});
it('catches ModelVisibleError and returns ToolResult error', async () => {
const definition = tool(
{
name: 'visibleErrorTool',
description: 'Fails with visible error',
inputSchema: z.object({}),
},
async () => {
throw new ModelVisibleError('Visible error');
},
);
const sdkTool = new SdkTool(definition, mockMessageBus);
const invocation = sdkTool.createInvocationWithContext(
{},
mockMessageBus,
undefined,
);
const result = await invocation.execute({
abortSignal: new AbortController().signal,
});
expect(result.error).toBeDefined();
expect(result.error?.message).toBe('Visible error');
expect(result.llmContent).toContain('Error: Visible error');
});
it('catches standard Error when sendErrorsToModel is true', async () => {
const definition = tool(
{
name: 'catchAllTool',
description: 'Catches all errors',
inputSchema: z.object({}),
sendErrorsToModel: true,
},
async () => {
throw new Error('Standard error');
},
);
const sdkTool = new SdkTool(definition, mockMessageBus);
const invocation = sdkTool.createInvocationWithContext(
{},
mockMessageBus,
undefined,
);
const result = await invocation.execute({
abortSignal: new AbortController().signal,
});
expect(result.error).toBeDefined();
expect(result.error?.message).toBe('Standard error');
expect(result.llmContent).toContain('Error: Standard error');
});
});
+235
View File
@@ -0,0 +1,235 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';
import {
BaseDeclarativeTool,
BaseToolInvocation,
type ToolResult,
type ToolInvocation,
type ExecuteOptions,
Kind,
type MessageBus,
} from '@google/gemini-cli-core';
import type { SessionContext } from './types.js';
export { z };
/**
* An error that, when thrown from a tool's action, will be visible to the
* Gemini model in the conversation. Useful for providing feedback to the
* model about why a tool failed so it can retry or adjust its approach.
*/
export class ModelVisibleError extends Error {
constructor(message: string | Error) {
super(message instanceof Error ? message.message : message);
this.name = 'ModelVisibleError';
}
}
/**
* The declarative definition of a tool, including its name, description,
* Zod input schema, and optional error-handling behavior.
*
* @typeParam T - The Zod schema type that validates the tool's input parameters.
*/
export interface ToolDefinition<T extends z.ZodTypeAny> {
/**
* A unique name for the tool, used by the model to invoke it.
*/
name: string;
/**
* A human-readable description of what the tool does.
* This is sent to the model to help it decide when to use the tool.
*/
description: string;
/**
* A Zod schema that validates and type-checks the tool's input parameters.
*/
inputSchema: T;
/**
* When `true`, any errors thrown by the tool's action will be sent back
* to the model as part of the conversation. Defaults to `false`.
*/
sendErrorsToModel?: boolean;
}
/**
* A complete tool definition that combines a {@link ToolDefinition} with
* an executable action function.
*
* The action receives validated parameters (inferred from the Zod schema)
* and an optional {@link SessionContext}, and returns an arbitrary result
* that will be serialized and sent back to the model.
*
* @typeParam T - The Zod schema type that validates the tool's input parameters.
*/
export interface Tool<T extends z.ZodTypeAny> extends ToolDefinition<T> {
/**
* The function executed when the model invokes this tool.
*
* @param params - The validated input parameters, typed from the Zod schema.
* @param context - Optional session context providing access to filesystem,
* shell, and other session state.
* @returns A promise resolving to the tool's output, which will be
* serialized (to JSON if not already a string) and sent to the model.
*/
action: (params: z.infer<T>, context?: SessionContext) => Promise<unknown>;
}
class SdkToolInvocation<T extends z.ZodTypeAny> extends BaseToolInvocation<
z.infer<T>,
ToolResult
> {
constructor(
params: z.infer<T>,
messageBus: MessageBus,
private readonly action: (
params: z.infer<T>,
context?: SessionContext,
) => Promise<unknown>,
private readonly context: SessionContext | undefined,
toolName: string,
private readonly sendErrorsToModel: boolean = false,
) {
super(params, messageBus, toolName);
}
getDescription(): string {
return `Executing ${this._toolName}...`;
}
async execute({
abortSignal: _abortSignal,
updateOutput: _updateOutput,
}: ExecuteOptions): Promise<ToolResult> {
try {
const result = await this.action(this.params, this.context);
const output =
typeof result === 'string' ? result : JSON.stringify(result, null, 2);
return {
llmContent: output,
returnDisplay: output,
};
} catch (error) {
if (this.sendErrorsToModel || error instanceof ModelVisibleError) {
const errorMessage =
error instanceof Error ? error.message : String(error);
return {
llmContent: `Error: ${errorMessage}`,
returnDisplay: `Error: ${errorMessage}`,
error: {
message: errorMessage,
},
};
}
throw error;
}
}
}
/**
* A wrapper that integrates an SDK {@link Tool} into the core tool registry.
*
* Handles parameter validation, execution, error handling (including
* {@link ModelVisibleError}), and context binding for tool invocations.
*
* @typeParam T - The Zod schema type that validates the tool's input parameters.
*/
export class SdkTool<T extends z.ZodTypeAny> extends BaseDeclarativeTool<
z.infer<T>,
ToolResult
> {
constructor(
private readonly definition: Tool<T>,
messageBus: MessageBus,
_agent?: unknown,
private readonly context?: SessionContext,
) {
super(
definition.name,
definition.name,
definition.description,
Kind.Other,
zodToJsonSchema(definition.inputSchema),
messageBus,
);
}
bindContext(context: SessionContext): SdkTool<T> {
return new SdkTool(this.definition, this.messageBus, undefined, context);
}
createInvocationWithContext(
params: z.infer<T>,
messageBus: MessageBus,
context: SessionContext | undefined,
toolName?: string,
): ToolInvocation<z.infer<T>, ToolResult> {
return new SdkToolInvocation(
params,
messageBus,
this.definition.action,
context || this.context,
toolName || this.name,
this.definition.sendErrorsToModel,
);
}
protected createInvocation(
params: z.infer<T>,
messageBus: MessageBus,
toolName?: string,
): ToolInvocation<z.infer<T>, ToolResult> {
return new SdkToolInvocation(
params,
messageBus,
this.definition.action,
this.context,
toolName || this.name,
this.definition.sendErrorsToModel,
);
}
}
/**
* Helper function to create a {@link Tool} by combining a definition and an action.
*
* @typeParam T - The Zod schema type for the tool's input parameters.
* @param definition - The tool's name, description, and input schema.
* @param action - The async function to execute when the tool is invoked.
* @returns A complete {@link Tool} object ready to be passed to
* {@link GeminiCliAgentOptions.tools}.
*
* @example
* ```typescript
* import { z, tool } from '@google/gemini-cli-sdk';
*
* const myTool = tool(
* {
* name: 'get_weather',
* description: 'Get the current weather for a location',
* inputSchema: z.object({ city: z.string() }),
* },
* async (params) => {
* return `Weather in ${params.city}: Sunny, 25°C`;
* },
* );
* ```
*/
export function tool<T extends z.ZodTypeAny>(
definition: ToolDefinition<T>,
action: (params: z.infer<T>, context?: SessionContext) => Promise<unknown>,
): Tool<T> {
return {
...definition,
action,
};
}
+241
View File
@@ -0,0 +1,241 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type { Content } from '@google/gemini-cli-core';
import type { Tool } from './tool.js';
import type { SkillReference } from './skills.js';
import type { GeminiCliAgent } from './agent.js';
import type { GeminiCliSession } from './session.js';
/**
* System instructions for a Gemini CLI agent.
*
* Can be either a static string or a function that receives the current
* session context and returns a string (or a promise of one), allowing
* dynamic instructions that change based on conversation state.
*
* @issue-16272/packages/core/coverage/lcov-report/src/utils/security.ts.html WARNING: If using a dynamic function, ensure that any data from the
* session context is sanitized (e.g., removing newlines, ']', and escaping '<', '>')
* before being included in the returned instructions to prevent prompt injection.
*/
export type SystemInstructions =
| string
| ((context: SessionContext) => string | Promise<string>);
/**
* Configuration options for creating a {@link GeminiCliAgent}.
*/
export interface GeminiCliAgentOptions {
/**
* System instructions that define the agent's behavior.
* Can be a static string or a dynamic function that receives session context.
*
* @issue-16272/packages/core/coverage/lcov-report/src/utils/security.ts.html WARNING: If using a dynamic function, sanitize all input from the
* SessionContext (e.g., removing newlines, ']', and escaping '<', '>') to prevent prompt injection.
*/
instructions: SystemInstructions;
/**
* Custom tools to register with the agent.
* Each tool is defined using a Zod schema for input validation.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
tools?: Array<Tool<any>>;
/**
* Skill directories to load into the agent's skill set.
*/
skills?: SkillReference[];
/**
* The Gemini model to use for this agent.
* Defaults to the auto-selected model if not specified.
*/
model?: string;
/**
* The working directory for the agent.
* Defaults to `process.cwd()` if not specified.
*/
cwd?: string;
/**
* Whether to enable debug mode for verbose logging.
* Defaults to `false`.
*/
debug?: boolean;
/**
* File path to record agent responses to for debugging/replay.
*/
recordResponses?: string;
/**
* File path to load fake/resimulated responses from for testing.
*/
fakeResponses?: string;
}
/**
* A virtual filesystem interface available to agents during tool execution.
*
* Provides sandboxed read/write access to files, subject to the agent's
* configured path access policies.
*
* Note: Implementations must internally validate and sanitize file paths to
* prevent path traversal attacks (e.g., checking for '..' or null bytes)
* using robust functions like resolveToRealPath.
*/
export interface AgentFilesystem {
/**
* Read the contents of a file.
*
* @param path - Absolute or relative path to the file.
* @returns The file contents as a UTF-8 string, or `null` if the file
* does not exist or access is denied.
*/
readFile(path: string): Promise<string | null>;
/**
* Write content to a file.
*
* @param path - Absolute or relative path to the file.
* @param content - The content to write.
* @throws {Error} If write access is denied by the agent's policy.
*/
writeFile(path: string, content: string): Promise<void>;
}
/**
* Options for configuring shell command execution via {@link AgentShell.exec}.
*/
export interface AgentShellOptions {
/**
* Environment variables to set for the command execution.
* These are merged with the default environment.
*/
env?: Record<string, string>;
/**
* Maximum time in seconds to wait for the command to complete.
*/
timeoutSeconds?: number;
/**
* Working directory in which to execute the command.
* Defaults to the agent's configured working directory.
*/
cwd?: string;
}
/**
* The result of a shell command execution.
*/
export interface AgentShellResult {
/**
* The exit code of the process, or `null` if the process was killed
* or did not exit normally.
*/
exitCode: number | null;
/**
* The combined stdout and stderr output of the command.
*/
output: string;
/**
* The standard output stream content.
*/
stdout: string;
/**
* The standard error stream content.
*/
stderr: string;
/**
* An error object if the command failed to execute or was rejected
* by policy.
*/
error?: Error;
}
/**
* A shell interface for executing commands within an agent's sandboxed environment.
*
* Commands are subject to the agent's security policies and may be rejected
* if they require interactive confirmation.
*/
export interface AgentShell {
/**
* Execute a shell command.
*
* @issue-16272/packages/core/coverage/lcov-report/src/utils/security.ts.html WARNING: Ensure the command string is properly sanitized and does
* not contain unvalidated user or LLM input to prevent command injection.
*
* @param cmd - The command string to execute.
* @param options - Optional execution configuration.
* @returns A promise resolving to the command result.
*/
exec(cmd: string, options?: AgentShellOptions): Promise<AgentShellResult>;
}
/**
* Contextual information about the current session, passed to tools and
* dynamic system instruction functions.
*
* Provides access to session metadata, conversation history, filesystem,
* shell, and the parent agent/session instances.
*/
export interface SessionContext {
/**
* Unique identifier for the current session.
*/
sessionId: string;
/**
* Read-only transcript of the conversation so far, including user
* messages and model responses.
*/
transcript: readonly Content[];
/**
* The current working directory of the session.
*/
cwd: string;
/**
* ISO 8601 timestamp of when this context was created.
*/
timestamp: string;
/**
* Virtual filesystem for reading and writing files within the agent's
* sandbox.
*
* @issue-16272/packages/core/coverage/lcov-report/src/utils/security.ts.html WARNING: This provides full access to the agent's filesystem.
* Ensure tools using this are trusted and validate their inputs.
*/
fs: AgentFilesystem;
/**
* Shell interface for executing commands within the agent's sandbox.
*
* @issue-16272/packages/core/coverage/lcov-report/src/utils/security.ts.html WARNING: This provides full access to the agent's shell.
* Any tool receiving this context can execute arbitrary commands.
*/
shell: AgentShell;
/**
* The parent agent that owns this session.
*/
agent: GeminiCliAgent;
/**
* The current session instance.
*/
session: GeminiCliSession;
}
@@ -0,0 +1,4 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"The secret number is"}],"role":"model"}}],"usageMetadata":{"promptTokenCount":9831,"totalTokenCount":9831,"promptTokensDetails":[{"modality":"TEXT","tokenCount":9831}]}},{"candidates":[{"content":{"parts":[{"text":" 1.\n"}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7098,"candidatesTokenCount":8,"totalTokenCount":7106,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7098}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":8}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"The secret number is"}],"role":"model"}}],"usageMetadata":{"promptTokenCount":9848,"totalTokenCount":9848,"promptTokensDetails":[{"modality":"TEXT","tokenCount":9848}]}},{"candidates":[{"content":{"parts":[{"text":" 1.\n"}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7113,"candidatesTokenCount":8,"totalTokenCount":7121,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7113}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":8}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"The secret number is"}],"role":"model"}}],"usageMetadata":{"promptTokenCount":9853,"totalTokenCount":9853,"promptTokensDetails":[{"modality":"TEXT","tokenCount":9853}]}},{"candidates":[{"content":{"parts":[{"text":" 1.\n"}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7120,"candidatesTokenCount":8,"totalTokenCount":7128,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7120}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":8}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"The secret number is"}],"role":"model"}}],"usageMetadata":{"promptTokenCount":9870,"totalTokenCount":9870,"promptTokensDetails":[{"modality":"TEXT","tokenCount":9870}]}},{"candidates":[{"content":{"parts":[{"text":" 1.\n"}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7135,"candidatesTokenCount":8,"totalTokenCount":7143,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7135}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":8}]}}]}
@@ -0,0 +1,24 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"The secret number is"}],"role":"model"}}],"usageMetadata":{"promptTokenCount":9831,"totalTokenCount":9831,"promptTokensDetails":[{"modality":"TEXT","tokenCount":9831}]}},{"candidates":[{"content":{"parts":[{"text":" 1.\n"}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7098,"candidatesTokenCount":8,"totalTokenCount":7106,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7098}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":8}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"The secret number is"}],"role":"model"}}],"usageMetadata":{"promptTokenCount":9848,"totalTokenCount":9848,"promptTokensDetails":[{"modality":"TEXT","tokenCount":9848}]}},{"candidates":[{"content":{"parts":[{"text":" 2.\n"}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7113,"candidatesTokenCount":8,"totalTokenCount":7121,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7113}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":8}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"The secret number is"}],"role":"model"}}],"usageMetadata":{"promptTokenCount":9853,"totalTokenCount":9853,"promptTokensDetails":[{"modality":"TEXT","tokenCount":9853}]}},{"candidates":[{"content":{"parts":[{"text":" 1.\n"}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7120,"candidatesTokenCount":8,"totalTokenCount":7128,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7120}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":8}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"The secret number is"}],"role":"model"}}],"usageMetadata":{"promptTokenCount":9870,"totalTokenCount":9870,"promptTokensDetails":[{"modality":"TEXT","tokenCount":9870}]}},{"candidates":[{"content":{"parts":[{"text":" 2.\n"}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7135,"candidatesTokenCount":8,"totalTokenCount":7143,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7135}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":8}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"The secret number is"}],"role":"model"}}],"usageMetadata":{"promptTokenCount":10028,"totalTokenCount":10028,"promptTokensDetails":[{"modality":"TEXT","tokenCount":10028}]}},{"candidates":[{"content":{"parts":[{"text":" 1."}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7193,"candidatesTokenCount":7,"totalTokenCount":7200,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7193}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":7}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"The secret number is"}],"role":"model"}}],"usageMetadata":{"promptTokenCount":10044,"totalTokenCount":10044,"promptTokensDetails":[{"modality":"TEXT","tokenCount":10044}]}},{"candidates":[{"content":{"parts":[{"text":" 2."}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7207,"candidatesTokenCount":7,"totalTokenCount":7214,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7207}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":7}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"The secret number is"}],"role":"model"}}],"usageMetadata":{"promptTokenCount":10039,"totalTokenCount":10039,"promptTokensDetails":[{"modality":"TEXT","tokenCount":10039}]}},{"candidates":[{"content":{"parts":[{"text":" 1."}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7204,"candidatesTokenCount":7,"totalTokenCount":7211,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7204}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":7}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"The secret number is"}],"role":"model"}}],"usageMetadata":{"promptTokenCount":10055,"totalTokenCount":10055,"promptTokensDetails":[{"modality":"TEXT","tokenCount":10055}]}},{"candidates":[{"content":{"parts":[{"text":" 2."}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7218,"candidatesTokenCount":7,"totalTokenCount":7225,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7218}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":7}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"The"}],"role":"model"}}],"usageMetadata":{"promptTokenCount":10039,"totalTokenCount":10039,"promptTokensDetails":[{"modality":"TEXT","tokenCount":10039}]}},{"candidates":[{"content":{"parts":[{"text":" secret number is 1."}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7204,"candidatesTokenCount":7,"totalTokenCount":7211,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7204}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":7}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"The secret number is"}],"role":"model"}}],"usageMetadata":{"promptTokenCount":10055,"totalTokenCount":10055,"promptTokensDetails":[{"modality":"TEXT","tokenCount":10055}]}},{"candidates":[{"content":{"parts":[{"text":" 2."}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7218,"candidatesTokenCount":7,"totalTokenCount":7225,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7218}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":7}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"The"}],"role":"model"}}],"usageMetadata":{"promptTokenCount":10039,"totalTokenCount":10039,"promptTokensDetails":[{"modality":"TEXT","tokenCount":10039}]}},{"candidates":[{"content":{"parts":[{"text":" secret number is 1."}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7204,"candidatesTokenCount":7,"totalTokenCount":7211,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7204}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":7}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"The secret number is"}],"role":"model"}}],"usageMetadata":{"promptTokenCount":10055,"totalTokenCount":10055,"promptTokensDetails":[{"modality":"TEXT","tokenCount":10055}]}},{"candidates":[{"content":{"parts":[{"text":" 2.\n"}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7218,"candidatesTokenCount":8,"totalTokenCount":7226,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7218}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":8}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"The"}],"role":"model"}}],"usageMetadata":{"promptTokenCount":10039,"totalTokenCount":10039,"promptTokensDetails":[{"modality":"TEXT","tokenCount":10039}]}},{"candidates":[{"content":{"parts":[{"text":" secret number is 1."}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7204,"candidatesTokenCount":7,"totalTokenCount":7211,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7204}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":7}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"The secret number is"}],"role":"model"}}],"usageMetadata":{"promptTokenCount":10055,"totalTokenCount":10055,"promptTokensDetails":[{"modality":"TEXT","tokenCount":10055}]}},{"candidates":[{"content":{"parts":[{"text":" 2."}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7218,"candidatesTokenCount":7,"totalTokenCount":7225,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7218}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":7}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"The secret number is"}],"role":"model"}}],"usageMetadata":{"promptTokenCount":10039,"totalTokenCount":10039,"promptTokensDetails":[{"modality":"TEXT","tokenCount":10039}]}},{"candidates":[{"content":{"parts":[{"text":" 1."}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7204,"candidatesTokenCount":7,"totalTokenCount":7211,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7204}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":7}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"The secret number is"}],"role":"model"}}],"usageMetadata":{"promptTokenCount":10055,"totalTokenCount":10055,"promptTokensDetails":[{"modality":"TEXT","tokenCount":10055}]}},{"candidates":[{"content":{"parts":[{"text":" 2."}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7218,"candidatesTokenCount":7,"totalTokenCount":7225,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7218}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":7}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"The"}],"role":"model"}}],"usageMetadata":{"promptTokenCount":10039,"totalTokenCount":10039,"promptTokensDetails":[{"modality":"TEXT","tokenCount":10039}]}},{"candidates":[{"content":{"parts":[{"text":" secret number is 1."}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7204,"candidatesTokenCount":7,"totalTokenCount":7211,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7204}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":7}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"The secret number is"}],"role":"model"}}],"usageMetadata":{"promptTokenCount":10055,"totalTokenCount":10055,"promptTokensDetails":[{"modality":"TEXT","tokenCount":10055}]}},{"candidates":[{"content":{"parts":[{"text":" 2."}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7218,"candidatesTokenCount":7,"totalTokenCount":7225,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7218}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":7}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"The secret number is"}],"role":"model"}}],"usageMetadata":{"promptTokenCount":10123,"totalTokenCount":10123,"promptTokensDetails":[{"modality":"TEXT","tokenCount":10123}]}},{"candidates":[{"content":{"parts":[{"text":" 1.\n"}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7188,"candidatesTokenCount":8,"totalTokenCount":7196,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7188}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":8}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"The secret number is"}],"role":"model"}}],"usageMetadata":{"promptTokenCount":10140,"totalTokenCount":10140,"promptTokensDetails":[{"modality":"TEXT","tokenCount":10140}]}},{"candidates":[{"content":{"parts":[{"text":" 2.\n"}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7203,"candidatesTokenCount":8,"totalTokenCount":7211,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7203}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":8}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"The secret number is"}],"role":"model"}}],"usageMetadata":{"promptTokenCount":10123,"totalTokenCount":10123,"promptTokensDetails":[{"modality":"TEXT","tokenCount":10123}]}},{"candidates":[{"content":{"parts":[{"text":" 1.\n"}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7188,"candidatesTokenCount":8,"totalTokenCount":7196,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7188}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":8}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"The secret number is"}],"role":"model"}}],"usageMetadata":{"promptTokenCount":10140,"totalTokenCount":10140,"promptTokensDetails":[{"modality":"TEXT","tokenCount":10140}]}},{"candidates":[{"content":{"parts":[{"text":" 2.\n"}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7203,"candidatesTokenCount":8,"totalTokenCount":7211,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7203}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":8}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"The secret number is"}],"role":"model"}}],"usageMetadata":{"promptTokenCount":10112,"totalTokenCount":10112,"promptTokensDetails":[{"modality":"TEXT","tokenCount":10112}]}},{"candidates":[{"content":{"parts":[{"text":" 1."}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7177,"candidatesTokenCount":7,"totalTokenCount":7184,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7177}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":7}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"The secret number is"}],"role":"model"}}],"usageMetadata":{"promptTokenCount":10128,"totalTokenCount":10128,"promptTokensDetails":[{"modality":"TEXT","tokenCount":10128}]}},{"candidates":[{"content":{"parts":[{"text":" 2."}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7191,"candidatesTokenCount":7,"totalTokenCount":7198,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7191}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":7}]}}]}
@@ -0,0 +1,2 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"BAN"}],"role":"model"}}],"usageMetadata":{"promptTokenCount":10103,"totalTokenCount":10103,"promptTokensDetails":[{"modality":"TEXT","tokenCount":10103}]}},{"candidates":[{"content":{"parts":[{"text":"ANA\n"}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7168,"candidatesTokenCount":3,"totalTokenCount":7171,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7168}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":3}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"BANANA\n"}],"role":"model"}}],"usageMetadata":{"promptTokenCount":10124,"totalTokenCount":10124,"promptTokensDetails":[{"modality":"TEXT","tokenCount":10124}]}},{"candidates":[{"content":{"parts":[{"text":""}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7187,"candidatesTokenCount":3,"totalTokenCount":7190,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7187}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":3}]}}]}
@@ -0,0 +1,4 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"Ah"}],"role":"model"}}],"usageMetadata":{"promptTokenCount":9828,"totalTokenCount":9828,"promptTokensDetails":[{"modality":"TEXT","tokenCount":9828}]}},{"candidates":[{"content":{"parts":[{"text":"oy, matey! Ready to chart a course through the code?"}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7095,"candidatesTokenCount":15,"totalTokenCount":7110,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7095}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":15}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"Ah"}],"role":"model"}}],"usageMetadata":{"promptTokenCount":10109,"totalTokenCount":10109,"promptTokensDetails":[{"modality":"TEXT","tokenCount":10109}]}},{"candidates":[{"content":{"parts":[{"text":"oy, matey! Ready to plunder the codebase, are ye?\n"}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7174,"candidatesTokenCount":16,"totalTokenCount":7190,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7174}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":16}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"Ah"}],"role":"model"}}],"usageMetadata":{"promptTokenCount":10109,"totalTokenCount":10109,"promptTokensDetails":[{"modality":"TEXT","tokenCount":10109}]}},{"candidates":[{"content":{"parts":[{"text":"oy, matey! Ready to shiver some timbers and get to work?\n"}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7174,"candidatesTokenCount":18,"totalTokenCount":7192,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7174}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":18}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"Ah"}],"role":"model"}}],"usageMetadata":{"promptTokenCount":10098,"totalTokenCount":10098,"promptTokensDetails":[{"modality":"TEXT","tokenCount":10098}]}},{"candidates":[{"content":{"parts":[{"text":"oy, matey! Ready to chart a course through these here files, are we?\n"}],"role":"model"}}],"usageMetadata":{"promptTokenCount":10098,"totalTokenCount":10098,"promptTokensDetails":[{"modality":"TEXT","tokenCount":10098}]}},{"candidates":[{"content":{"parts":[{"text":""}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7163,"candidatesTokenCount":20,"totalTokenCount":7183,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7163}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":20}]}}]}
@@ -0,0 +1,8 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"activate_skill","args":{"name":"pirate-skill"}}}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7160,"candidatesTokenCount":8,"totalTokenCount":7168,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7160}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":8}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"Arrr, why"}],"role":"model"}}],"usageMetadata":{"promptTokenCount":10048,"totalTokenCount":10048,"promptTokensDetails":[{"modality":"TEXT","tokenCount":10048}]}},{"candidates":[{"content":{"parts":[{"text":" don't pirates play poker? Because they always have a hidden ace up their sleeves"}],"role":"model"}}],"usageMetadata":{"promptTokenCount":10048,"totalTokenCount":10048,"promptTokensDetails":[{"modality":"TEXT","tokenCount":10048}]}},{"candidates":[{"content":{"parts":[{"text":"!\\n"}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7284,"candidatesTokenCount":23,"totalTokenCount":7307,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7284}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":23}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"activate_skill","args":{"name":"pirate-skill"}}}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7185,"candidatesTokenCount":8,"totalTokenCount":7193,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7185}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":8}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I am"}],"role":"model"}}],"usageMetadata":{"promptTokenCount":10179,"totalTokenCount":10179,"promptTokensDetails":[{"modality":"TEXT","tokenCount":10179}]}},{"candidates":[{"content":{"parts":[{"text":" sorry, I cannot activate the skill at this time because it requires user confirmation."}],"role":"model"}}],"usageMetadata":{"promptTokenCount":10179,"totalTokenCount":10179,"promptTokensDetails":[{"modality":"TEXT","tokenCount":10179}]}},{"candidates":[{"content":{"parts":[{"text":" Would you like me to proceed with telling you a joke without activating the skill?"}],"role":"model"}}],"usageMetadata":{"promptTokenCount":10179,"totalTokenCount":10179,"promptTokensDetails":[{"modality":"TEXT","tokenCount":10179}]}},{"candidates":[{"content":{"parts":[{"text":"\n"}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7218,"candidatesTokenCount":35,"totalTokenCount":7253,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7218}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":35}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"activate_skill","args":{"name":"pirate-skill"}}}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7185,"candidatesTokenCount":8,"totalTokenCount":7193,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7185}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":8}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"Arrr, why"}],"role":"model"}}],"usageMetadata":{"promptTokenCount":10275,"totalTokenCount":10275,"promptTokensDetails":[{"modality":"TEXT","tokenCount":10275}]}},{"candidates":[{"content":{"parts":[{"text":" don't pirates play cards? Because someone's always standin' on the"}],"role":"model"}}],"usageMetadata":{"promptTokenCount":10275,"totalTokenCount":10275,"promptTokensDetails":[{"modality":"TEXT","tokenCount":10275}]}},{"candidates":[{"content":{"parts":[{"text":" deck!\n"}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7309,"candidatesTokenCount":24,"totalTokenCount":7333,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7309}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":24}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"activate_skill","args":{"name":"pirate-skill"}}}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7174,"candidatesTokenCount":8,"totalTokenCount":7182,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7174}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":8}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"Ar"}],"role":"model"}}],"usageMetadata":{"promptTokenCount":10264,"totalTokenCount":10264,"promptTokensDetails":[{"modality":"TEXT","tokenCount":10264}]}},{"candidates":[{"content":{"parts":[{"text":"rr, why don't pirates play cards? Because someone always be standin' on the"}],"role":"model"}}],"usageMetadata":{"promptTokenCount":10264,"totalTokenCount":10264,"promptTokensDetails":[{"modality":"TEXT","tokenCount":10264}]}},{"candidates":[{"content":{"parts":[{"text":" deck!\n"}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7298,"candidatesTokenCount":23,"totalTokenCount":7321,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7298}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":23}]}}]}
@@ -0,0 +1,8 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"activate_skill","args":{"name":"pirate-skill"}}}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7170,"candidatesTokenCount":8,"totalTokenCount":7178,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7170}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":8}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"Ar"}],"role":"model"}}],"usageMetadata":{"promptTokenCount":10058,"totalTokenCount":10058,"promptTokensDetails":[{"modality":"TEXT","tokenCount":10058}]}},{"candidates":[{"content":{"parts":[{"text":"rr, the pirate skill be active, matey!"}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7294,"candidatesTokenCount":12,"totalTokenCount":7306,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7294}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":12}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"activate_skill","args":{"name":"pirate-skill"}}}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7184,"candidatesTokenCount":8,"totalTokenCount":7192,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7184}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":8}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I"}],"role":"model"}}],"usageMetadata":{"promptTokenCount":10178,"totalTokenCount":10178,"promptTokensDetails":[{"modality":"TEXT","tokenCount":10178}]}},{"candidates":[{"content":{"parts":[{"text":" am unable to activate the skill without user confirmation. I will await further instructions.\n"}],"role":"model"}}],"usageMetadata":{"promptTokenCount":10178,"totalTokenCount":10178,"promptTokensDetails":[{"modality":"TEXT","tokenCount":10178}]}},{"candidates":[{"content":{"parts":[{"text":""}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7217,"candidatesTokenCount":18,"totalTokenCount":7235,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7217}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":18}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"activate_skill","args":{"name":"pirate-skill"}}}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7184,"candidatesTokenCount":8,"totalTokenCount":7192,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7184}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":8}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"Ar"}],"role":"model"}}],"usageMetadata":{"promptTokenCount":10274,"totalTokenCount":10274,"promptTokensDetails":[{"modality":"TEXT","tokenCount":10274}]}},{"candidates":[{"content":{"parts":[{"text":"rr, the pirate-skill be active, aye!\n"}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7308,"candidatesTokenCount":13,"totalTokenCount":7321,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7308}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":13}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"activate_skill","args":{"name":"pirate-skill"}}}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7173,"candidatesTokenCount":8,"totalTokenCount":7181,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7173}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":8}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"Ar"}],"role":"model"}}],"usageMetadata":{"promptTokenCount":10263,"totalTokenCount":10263,"promptTokensDetails":[{"modality":"TEXT","tokenCount":10263}]}},{"candidates":[{"content":{"parts":[{"text":"rr, pirate skill activated, aye!"}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7297,"candidatesTokenCount":9,"totalTokenCount":7306,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7297}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":9}]}}]}
@@ -0,0 +1,5 @@
---
name: pirate-skill
description: Speak like a pirate.
---
You are a pirate. Respond to everything in pirate speak. Always mention "Arrr".
@@ -0,0 +1,8 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"checkSystemStatus","args":{}}}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7070,"candidatesTokenCount":3,"totalTokenCount":7073,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7070}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":3}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"The system status check"}],"role":"model"}}],"usageMetadata":{"promptTokenCount":9850,"totalTokenCount":9850,"promptTokensDetails":[{"modality":"TEXT","tokenCount":9850}]}},{"candidates":[{"content":{"parts":[{"text":" returned an error. It says `Error: Standard error caught`."}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7082,"candidatesTokenCount":17,"totalTokenCount":7099,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7082}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":17}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"checkSystemStatus","args":{}}}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7184,"candidatesTokenCount":3,"totalTokenCount":7187,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7184}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":3}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I am unable to"}],"role":"model"}}],"usageMetadata":{"promptTokenCount":10183,"totalTokenCount":10183,"promptTokensDetails":[{"modality":"TEXT","tokenCount":10183}]}},{"candidates":[{"content":{"parts":[{"text":" check the system status because it requires user confirmation in interactive mode. Is there anything else I"}],"role":"model"}}],"usageMetadata":{"promptTokenCount":10183,"totalTokenCount":10183,"promptTokensDetails":[{"modality":"TEXT","tokenCount":10183}]}},{"candidates":[{"content":{"parts":[{"text":" can help with?"}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7213,"candidatesTokenCount":26,"totalTokenCount":7239,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7213}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":26}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"checkSystemStatus","args":{}}}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7184,"candidatesTokenCount":3,"totalTokenCount":7187,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7184}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":3}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"The system status check"}],"role":"model"}}],"usageMetadata":{"promptTokenCount":10164,"totalTokenCount":10164,"promptTokensDetails":[{"modality":"TEXT","tokenCount":10164}]}},{"candidates":[{"content":{"parts":[{"text":" reported an error: \"Standard error caught\".\n"}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7194,"candidatesTokenCount":14,"totalTokenCount":7208,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7194}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":14}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"checkSystemStatus","args":{}}}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7184,"candidatesTokenCount":3,"totalTokenCount":7187,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7184}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":3}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"There"}],"role":"model"}}],"usageMetadata":{"promptTokenCount":10164,"totalTokenCount":10164,"promptTokensDetails":[{"modality":"TEXT","tokenCount":10164}]}},{"candidates":[{"content":{"parts":[{"text":" is an error reported in the system status.\n"}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7194,"candidatesTokenCount":11,"totalTokenCount":7205,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7194}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":11}]}}]}
@@ -0,0 +1,8 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"failVisible","args":{"input":"fail"}}}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7073,"candidatesTokenCount":4,"totalTokenCount":7077,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7073}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":4}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"The"}],"role":"model"}}],"usageMetadata":{"promptTokenCount":9867,"totalTokenCount":9867,"promptTokensDetails":[{"modality":"TEXT","tokenCount":9867}]}},{"candidates":[{"content":{"parts":[{"text":" tool failed visibly with the error message: \"Error: Tool failed visibly\"."}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7085,"candidatesTokenCount":16,"totalTokenCount":7101,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7085}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":16}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"failVisible","args":{"input":"fail"}}}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7198,"candidatesTokenCount":4,"totalTokenCount":7202,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7198}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":4}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"The"}],"role":"model"}}],"usageMetadata":{"promptTokenCount":10210,"totalTokenCount":10210,"promptTokensDetails":[{"modality":"TEXT","tokenCount":10210}]}},{"candidates":[{"content":{"parts":[{"text":" tool execution for \"failVisible\" requires user confirmation, which is not supported in non-"}],"role":"model"}}],"usageMetadata":{"promptTokenCount":10210,"totalTokenCount":10210,"promptTokensDetails":[{"modality":"TEXT","tokenCount":10210}]}},{"candidates":[{"content":{"parts":[{"text":"interactive mode."}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7226,"candidatesTokenCount":22,"totalTokenCount":7248,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7226}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":22}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"failVisible","args":{"input":"fail"}}}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7198,"candidatesTokenCount":4,"totalTokenCount":7202,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7198}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":4}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"The"}],"role":"model"}}],"usageMetadata":{"promptTokenCount":10192,"totalTokenCount":10192,"promptTokensDetails":[{"modality":"TEXT","tokenCount":10192}]}},{"candidates":[{"content":{"parts":[{"text":" tool failed visibly. The error message is \"Tool failed visibly\"."}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7208,"candidatesTokenCount":14,"totalTokenCount":7222,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7208}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":14}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"failVisible","args":{"input":"fail"}}}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7187,"candidatesTokenCount":4,"totalTokenCount":7191,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7187}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":4}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"The tool failed with"}],"role":"model"}}],"usageMetadata":{"promptTokenCount":10181,"totalTokenCount":10181,"promptTokensDetails":[{"modality":"TEXT","tokenCount":10181}]}},{"candidates":[{"content":{"parts":[{"text":" the error message \"Tool failed visibly\"."}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7197,"candidatesTokenCount":12,"totalTokenCount":7209,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7197}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":12}]}}]}
+8
View File
@@ -0,0 +1,8 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"add","args":{"a":5,"b":3}}}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7045,"candidatesTokenCount":5,"totalTokenCount":7050,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7045}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":5}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"8"}],"role":"model"}}],"usageMetadata":{"promptTokenCount":9849,"totalTokenCount":9849,"promptTokensDetails":[{"modality":"TEXT","tokenCount":9849}]}},{"candidates":[{"content":{"parts":[{"text":""}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7053,"candidatesTokenCount":1,"totalTokenCount":7054,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7053}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":1}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"add","args":{"b":3,"a":5}}}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7179,"candidatesTokenCount":5,"totalTokenCount":7184,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7179}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":5}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I"}],"role":"model"}}],"usageMetadata":{"promptTokenCount":10204,"totalTokenCount":10204,"promptTokensDetails":[{"modality":"TEXT","tokenCount":10204}]}},{"candidates":[{"content":{"parts":[{"text":" am unable to execute the add tool in non-interactive mode.\n"}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7206,"candidatesTokenCount":15,"totalTokenCount":7221,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7206}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":15}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"add","args":{"a":5,"b":3}}}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7179,"candidatesTokenCount":5,"totalTokenCount":7184,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7179}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":5}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"8"}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7187,"candidatesTokenCount":1,"totalTokenCount":7188,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7187}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":1}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"add","args":{"a":5,"b":3}}}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7168,"candidatesTokenCount":5,"totalTokenCount":7173,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7168}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":5}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"8"}],"role":"model"}}],"usageMetadata":{"promptTokenCount":10174,"totalTokenCount":10174,"promptTokensDetails":[{"modality":"TEXT","tokenCount":10174}]}},{"candidates":[{"content":{"parts":[{"text":"\n"}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7176,"candidatesTokenCount":2,"totalTokenCount":7178,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7176}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":2}]}}]}
+12
View File
@@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"composite": true,
"lib": ["DOM", "DOM.Iterable", "ES2023"],
"types": ["node", "vitest/globals"]
},
"include": ["index.ts", "src/**/*.ts", "package.json"],
"exclude": ["node_modules", "dist"],
"references": [{ "path": "../core" }]
}
+14
View File
@@ -0,0 +1,14 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node',
},
});