ak-gemini 1.2.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +259 -294
- package/base.js +485 -0
- package/chat.js +87 -0
- package/code-agent.js +687 -0
- package/index.cjs +1928 -1213
- package/index.js +40 -1501
- package/json-helpers.js +352 -0
- package/message.js +170 -0
- package/package.json +14 -7
- package/tool-agent.js +312 -0
- package/transformer.js +502 -0
- package/types.d.ts +452 -241
- package/agent.js +0 -481
- package/tools.js +0 -134
package/tool-agent.js
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview ToolAgent class — AI agent with user-provided tools.
|
|
3
|
+
* Extends BaseGemini with automatic tool-use loops for both streaming
|
|
4
|
+
* and non-streaming conversations.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import BaseGemini from './base.js';
|
|
8
|
+
import log from './logger.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {import('./types').ToolAgentOptions} ToolAgentOptions
|
|
12
|
+
* @typedef {import('./types').AgentResponse} AgentResponse
|
|
13
|
+
* @typedef {import('./types').AgentStreamEvent} AgentStreamEvent
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* AI agent that uses user-provided tools to accomplish tasks.
|
|
18
|
+
* Automatically manages the tool-use loop: when the model decides to call
|
|
19
|
+
* a tool, the agent executes it via your toolExecutor, sends the result back,
|
|
20
|
+
* and continues until the model produces a final text response.
|
|
21
|
+
*
|
|
22
|
+
* Ships with zero built-in tools — you provide everything via the constructor.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```javascript
|
|
26
|
+
* import { ToolAgent } from 'ak-gemini';
|
|
27
|
+
*
|
|
28
|
+
* const agent = new ToolAgent({
|
|
29
|
+
* systemPrompt: 'You are a research assistant.',
|
|
30
|
+
* tools: [
|
|
31
|
+
* {
|
|
32
|
+
* name: 'http_get',
|
|
33
|
+
* description: 'Fetch a URL and return its contents',
|
|
34
|
+
* parametersJsonSchema: {
|
|
35
|
+
* type: 'object',
|
|
36
|
+
* properties: { url: { type: 'string', description: 'The URL to fetch' } },
|
|
37
|
+
* required: ['url']
|
|
38
|
+
* }
|
|
39
|
+
* }
|
|
40
|
+
* ],
|
|
41
|
+
* toolExecutor: async (toolName, args) => {
|
|
42
|
+
* if (toolName === 'http_get') {
|
|
43
|
+
* const res = await fetch(args.url);
|
|
44
|
+
* return { status: res.status, body: await res.text() };
|
|
45
|
+
* }
|
|
46
|
+
* throw new Error(`Unknown tool: ${toolName}`);
|
|
47
|
+
* }
|
|
48
|
+
* });
|
|
49
|
+
*
|
|
50
|
+
* const result = await agent.chat('Fetch https://api.example.com/data and summarize it');
|
|
51
|
+
* console.log(result.text); // Agent's summary
|
|
52
|
+
* console.log(result.toolCalls); // [{ name: 'http_get', args: {...}, result: {...} }]
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
class ToolAgent extends BaseGemini {
|
|
56
|
+
/**
|
|
57
|
+
* @param {ToolAgentOptions} [options={}]
|
|
58
|
+
*/
|
|
59
|
+
constructor(options = {}) {
|
|
60
|
+
if (options.systemPrompt === undefined) {
|
|
61
|
+
options = { ...options, systemPrompt: 'You are a helpful AI assistant.' };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
super(options);
|
|
65
|
+
|
|
66
|
+
// ── Tools ──
|
|
67
|
+
this.tools = options.tools || [];
|
|
68
|
+
this.toolExecutor = options.toolExecutor || null;
|
|
69
|
+
|
|
70
|
+
// Validate: if tools provided, executor is required (and vice versa)
|
|
71
|
+
if (this.tools.length > 0 && !this.toolExecutor) {
|
|
72
|
+
throw new Error("ToolAgent: tools provided without a toolExecutor. Provide a toolExecutor function to handle tool calls.");
|
|
73
|
+
}
|
|
74
|
+
if (this.toolExecutor && this.tools.length === 0) {
|
|
75
|
+
throw new Error("ToolAgent: toolExecutor provided without tools. Provide tool declarations so the model knows what tools are available.");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── Tool loop config ──
|
|
79
|
+
this.maxToolRounds = options.maxToolRounds || 10;
|
|
80
|
+
this.onToolCall = options.onToolCall || null;
|
|
81
|
+
this.onBeforeExecution = options.onBeforeExecution || null;
|
|
82
|
+
this.writeDir = options.writeDir || null;
|
|
83
|
+
this._stopped = false;
|
|
84
|
+
|
|
85
|
+
// ── Apply tools to chat config ──
|
|
86
|
+
if (this.tools.length > 0) {
|
|
87
|
+
this.chatConfig.tools = [{ functionDeclarations: this.tools }];
|
|
88
|
+
this.chatConfig.toolConfig = { functionCallingConfig: { mode: 'AUTO' } };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
log.debug(`ToolAgent created with ${this.tools.length} tools`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── Non-Streaming Chat ───────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Send a message and get a complete response (non-streaming).
|
|
98
|
+
* Automatically handles the tool-use loop.
|
|
99
|
+
*
|
|
100
|
+
* @param {string} message - The user's message
|
|
101
|
+
* @param {Object} [opts={}] - Per-message options
|
|
102
|
+
* @param {Record<string, string>} [opts.labels] - Per-message billing labels
|
|
103
|
+
* @returns {Promise<AgentResponse>} Response with text, toolCalls, and usage
|
|
104
|
+
*/
|
|
105
|
+
async chat(message, opts = {}) {
|
|
106
|
+
if (!this.chatSession) await this.init();
|
|
107
|
+
this._stopped = false;
|
|
108
|
+
|
|
109
|
+
const allToolCalls = [];
|
|
110
|
+
|
|
111
|
+
let response = await this.chatSession.sendMessage({ message });
|
|
112
|
+
|
|
113
|
+
for (let round = 0; round < this.maxToolRounds; round++) {
|
|
114
|
+
if (this._stopped) break;
|
|
115
|
+
|
|
116
|
+
const functionCalls = response.functionCalls;
|
|
117
|
+
if (!functionCalls || functionCalls.length === 0) break;
|
|
118
|
+
|
|
119
|
+
const toolResults = await Promise.all(
|
|
120
|
+
functionCalls.map(async (call) => {
|
|
121
|
+
// Fire onToolCall callback
|
|
122
|
+
if (this.onToolCall) {
|
|
123
|
+
try { this.onToolCall(call.name, call.args); }
|
|
124
|
+
catch (e) { log.warn(`onToolCall callback error: ${e.message}`); }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Check onBeforeExecution gate
|
|
128
|
+
if (this.onBeforeExecution) {
|
|
129
|
+
try {
|
|
130
|
+
const allowed = await this.onBeforeExecution(call.name, call.args);
|
|
131
|
+
if (allowed === false) {
|
|
132
|
+
const result = { error: 'Execution denied by onBeforeExecution callback' };
|
|
133
|
+
allToolCalls.push({ name: call.name, args: call.args, result });
|
|
134
|
+
return { id: call.id, name: call.name, result };
|
|
135
|
+
}
|
|
136
|
+
} catch (e) {
|
|
137
|
+
log.warn(`onBeforeExecution callback error: ${e.message}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let result;
|
|
142
|
+
try {
|
|
143
|
+
result = await this.toolExecutor(call.name, call.args);
|
|
144
|
+
} catch (err) {
|
|
145
|
+
log.warn(`Tool ${call.name} failed: ${err.message}`);
|
|
146
|
+
result = { error: err.message };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
allToolCalls.push({ name: call.name, args: call.args, result });
|
|
150
|
+
|
|
151
|
+
return { id: call.id, name: call.name, result };
|
|
152
|
+
})
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
// Send function responses back to the model
|
|
156
|
+
response = await this.chatSession.sendMessage({
|
|
157
|
+
message: toolResults.map(r => ({
|
|
158
|
+
functionResponse: {
|
|
159
|
+
id: r.id,
|
|
160
|
+
name: r.name,
|
|
161
|
+
response: { output: r.result }
|
|
162
|
+
}
|
|
163
|
+
}))
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
this._captureMetadata(response);
|
|
168
|
+
|
|
169
|
+
// Set cumulative usage
|
|
170
|
+
this._cumulativeUsage = {
|
|
171
|
+
promptTokens: this.lastResponseMetadata.promptTokens,
|
|
172
|
+
responseTokens: this.lastResponseMetadata.responseTokens,
|
|
173
|
+
totalTokens: this.lastResponseMetadata.totalTokens,
|
|
174
|
+
attempts: 1
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
text: response.text || '',
|
|
179
|
+
toolCalls: allToolCalls,
|
|
180
|
+
usage: this.getLastUsage()
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ── Streaming ────────────────────────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Send a message and stream the response as events.
|
|
188
|
+
* Automatically handles the tool-use loop between streamed rounds.
|
|
189
|
+
*
|
|
190
|
+
* Event types:
|
|
191
|
+
* - `text` — A chunk of the agent's text response
|
|
192
|
+
* - `tool_call` — The agent is about to call a tool
|
|
193
|
+
* - `tool_result` — A tool finished executing
|
|
194
|
+
* - `done` — The agent finished
|
|
195
|
+
*
|
|
196
|
+
* @param {string} message - The user's message
|
|
197
|
+
* @param {Object} [opts={}] - Per-message options
|
|
198
|
+
* @yields {AgentStreamEvent}
|
|
199
|
+
*/
|
|
200
|
+
async *stream(message, opts = {}) {
|
|
201
|
+
if (!this.chatSession) await this.init();
|
|
202
|
+
this._stopped = false;
|
|
203
|
+
|
|
204
|
+
const allToolCalls = [];
|
|
205
|
+
let fullText = '';
|
|
206
|
+
|
|
207
|
+
let streamResponse = await this.chatSession.sendMessageStream({ message });
|
|
208
|
+
|
|
209
|
+
for (let round = 0; round < this.maxToolRounds; round++) {
|
|
210
|
+
if (this._stopped) break;
|
|
211
|
+
|
|
212
|
+
let roundText = '';
|
|
213
|
+
const functionCalls = [];
|
|
214
|
+
|
|
215
|
+
// Consume the stream
|
|
216
|
+
for await (const chunk of streamResponse) {
|
|
217
|
+
if (chunk.functionCalls) {
|
|
218
|
+
functionCalls.push(...chunk.functionCalls);
|
|
219
|
+
} else if (chunk.candidates?.[0]?.content?.parts?.[0]?.text) {
|
|
220
|
+
const text = chunk.candidates[0].content.parts[0].text;
|
|
221
|
+
roundText += text;
|
|
222
|
+
fullText += text;
|
|
223
|
+
yield { type: 'text', text };
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// No tool calls — we're done
|
|
228
|
+
if (functionCalls.length === 0) {
|
|
229
|
+
yield {
|
|
230
|
+
type: 'done',
|
|
231
|
+
fullText,
|
|
232
|
+
usage: this.getLastUsage()
|
|
233
|
+
};
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Execute tools sequentially so we can yield events
|
|
238
|
+
const toolResults = [];
|
|
239
|
+
for (const call of functionCalls) {
|
|
240
|
+
if (this._stopped) break;
|
|
241
|
+
|
|
242
|
+
yield { type: 'tool_call', toolName: call.name, args: call.args };
|
|
243
|
+
|
|
244
|
+
// Fire onToolCall callback
|
|
245
|
+
if (this.onToolCall) {
|
|
246
|
+
try { this.onToolCall(call.name, call.args); }
|
|
247
|
+
catch (e) { log.warn(`onToolCall callback error: ${e.message}`); }
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Check onBeforeExecution gate
|
|
251
|
+
let denied = false;
|
|
252
|
+
if (this.onBeforeExecution) {
|
|
253
|
+
try {
|
|
254
|
+
const allowed = await this.onBeforeExecution(call.name, call.args);
|
|
255
|
+
if (allowed === false) denied = true;
|
|
256
|
+
} catch (e) {
|
|
257
|
+
log.warn(`onBeforeExecution callback error: ${e.message}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
let result;
|
|
262
|
+
if (denied) {
|
|
263
|
+
result = { error: 'Execution denied by onBeforeExecution callback' };
|
|
264
|
+
} else {
|
|
265
|
+
try {
|
|
266
|
+
result = await this.toolExecutor(call.name, call.args);
|
|
267
|
+
} catch (err) {
|
|
268
|
+
log.warn(`Tool ${call.name} failed: ${err.message}`);
|
|
269
|
+
result = { error: err.message };
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
allToolCalls.push({ name: call.name, args: call.args, result });
|
|
274
|
+
yield { type: 'tool_result', toolName: call.name, result };
|
|
275
|
+
|
|
276
|
+
toolResults.push({ id: call.id, name: call.name, result });
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Send function responses back and get next stream
|
|
280
|
+
streamResponse = await this.chatSession.sendMessageStream({
|
|
281
|
+
message: toolResults.map(r => ({
|
|
282
|
+
functionResponse: {
|
|
283
|
+
id: r.id,
|
|
284
|
+
name: r.name,
|
|
285
|
+
response: { output: r.result }
|
|
286
|
+
}
|
|
287
|
+
}))
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Max rounds reached or stopped
|
|
292
|
+
yield {
|
|
293
|
+
type: 'done',
|
|
294
|
+
fullText,
|
|
295
|
+
usage: this.getLastUsage(),
|
|
296
|
+
warning: this._stopped ? 'Agent was stopped' : 'Max tool rounds reached'
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
// ── Stop ────────────────────────────────────────────────────────────────
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Stop the agent before the next tool execution round.
|
|
303
|
+
* If called during a chat() or stream() loop, the agent will finish
|
|
304
|
+
* the current round and then stop.
|
|
305
|
+
*/
|
|
306
|
+
stop() {
|
|
307
|
+
this._stopped = true;
|
|
308
|
+
log.info('ToolAgent stopped');
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export default ToolAgent;
|