chore: import upstream snapshot with attribution
This commit is contained in:
@@ -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.
|
||||
@@ -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);
|
||||
```
|
||||
@@ -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.
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
export * from './src/index.js';
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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}]}}]}
|
||||
@@ -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}]}}]}
|
||||
@@ -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" }]
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user