File size: 4,826 Bytes
7421850
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
/**
 * @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);
});