ak-gemini 1.2.0 → 2.0.0

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/index.cjs CHANGED
@@ -29,19 +29,25 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
29
29
  // index.js
30
30
  var index_exports = {};
31
31
  __export(index_exports, {
32
- AIAgent: () => agent_default,
32
+ BaseGemini: () => base_default,
33
+ Chat: () => chat_default,
34
+ CodeAgent: () => code_agent_default,
33
35
  HarmBlockThreshold: () => import_genai2.HarmBlockThreshold,
34
36
  HarmCategory: () => import_genai2.HarmCategory,
37
+ Message: () => message_default,
35
38
  ThinkingLevel: () => import_genai2.ThinkingLevel,
39
+ ToolAgent: () => tool_agent_default,
40
+ Transformer: () => transformer_default,
36
41
  attemptJSONRecovery: () => attemptJSONRecovery,
37
42
  default: () => index_default,
43
+ extractJSON: () => extractJSON,
38
44
  log: () => logger_default
39
45
  });
40
46
  module.exports = __toCommonJS(index_exports);
41
- var import_dotenv2 = __toESM(require("dotenv"), 1);
42
- var import_genai2 = require("@google/genai");
43
- var import_ak_tools = __toESM(require("ak-tools"), 1);
44
- var import_path = __toESM(require("path"), 1);
47
+
48
+ // base.js
49
+ var import_dotenv = __toESM(require("dotenv"), 1);
50
+ var import_genai = require("@google/genai");
45
51
 
46
52
  // logger.js
47
53
  var import_pino = __toESM(require("pino"), 1);
@@ -60,117 +66,249 @@ var logger = (0, import_pino.default)({
60
66
  });
61
67
  var logger_default = logger;
62
68
 
63
- // agent.js
64
- var import_dotenv = __toESM(require("dotenv"), 1);
65
- var import_genai = require("@google/genai");
66
-
67
- // tools.js
68
- var MAX_RESPONSE_LENGTH = 5e4;
69
- function parseBody(text) {
70
- const body = text.length > MAX_RESPONSE_LENGTH ? text.slice(0, MAX_RESPONSE_LENGTH) + "\n...[TRUNCATED]" : text;
69
+ // json-helpers.js
70
+ function isJSON(data) {
71
71
  try {
72
- return JSON.parse(body);
73
- } catch {
74
- return body;
72
+ const attempt = JSON.stringify(data);
73
+ if (attempt?.startsWith("{") || attempt?.startsWith("[")) {
74
+ if (attempt?.endsWith("}") || attempt?.endsWith("]")) {
75
+ return true;
76
+ }
77
+ }
78
+ return false;
79
+ } catch (e) {
80
+ return false;
75
81
  }
76
82
  }
77
- var BUILT_IN_DECLARATIONS = [
78
- {
79
- name: "http_get",
80
- description: "Make an HTTP GET request to any URL. Returns the response status and body as text. Use for fetching web pages, REST APIs, or any HTTP resource.",
81
- parametersJsonSchema: {
82
- type: "object",
83
- properties: {
84
- url: { type: "string", description: "The full URL to request (including https://)" },
85
- headers: {
86
- type: "object",
87
- description: "Optional HTTP headers as key-value pairs",
88
- additionalProperties: { type: "string" }
89
- }
90
- },
91
- required: ["url"]
92
- }
93
- },
94
- {
95
- name: "http_post",
96
- description: "Make an HTTP POST request to any URL with a JSON body. Returns the response status and body as text.",
97
- parametersJsonSchema: {
98
- type: "object",
99
- properties: {
100
- url: { type: "string", description: "The full URL to request (including https://)" },
101
- body: { type: "object", description: "The JSON body to send" },
102
- headers: {
103
- type: "object",
104
- description: "Optional HTTP headers as key-value pairs",
105
- additionalProperties: { type: "string" }
106
- }
107
- },
108
- required: ["url"]
109
- }
110
- },
111
- {
112
- name: "write_markdown",
113
- description: "Generate a structured markdown document such as a report, analysis, summary, or formatted findings. The content will be captured and returned to the caller.",
114
- parametersJsonSchema: {
115
- type: "object",
116
- properties: {
117
- filename: { type: "string", description: 'Suggested filename for the document (e.g. "report.md")' },
118
- title: { type: "string", description: "Document title" },
119
- content: { type: "string", description: "Full markdown content of the document" }
120
- },
121
- required: ["filename", "content"]
83
+ function isJSONStr(string) {
84
+ if (typeof string !== "string") return false;
85
+ try {
86
+ const result = JSON.parse(string);
87
+ const type = Object.prototype.toString.call(result);
88
+ return type === "[object Object]" || type === "[object Array]";
89
+ } catch (err) {
90
+ return false;
91
+ }
92
+ }
93
+ function attemptJSONRecovery(text, maxAttempts = 100) {
94
+ if (!text || typeof text !== "string") return null;
95
+ try {
96
+ return JSON.parse(text);
97
+ } catch (e) {
98
+ }
99
+ let workingText = text.trim();
100
+ let braces = 0;
101
+ let brackets = 0;
102
+ let inString = false;
103
+ let escapeNext = false;
104
+ for (let j = 0; j < workingText.length; j++) {
105
+ const char = workingText[j];
106
+ if (escapeNext) {
107
+ escapeNext = false;
108
+ continue;
109
+ }
110
+ if (char === "\\") {
111
+ escapeNext = true;
112
+ continue;
113
+ }
114
+ if (char === '"') {
115
+ inString = !inString;
116
+ continue;
117
+ }
118
+ if (!inString) {
119
+ if (char === "{") braces++;
120
+ else if (char === "}") braces--;
121
+ else if (char === "[") brackets++;
122
+ else if (char === "]") brackets--;
122
123
  }
123
124
  }
124
- ];
125
- async function executeBuiltInTool(name, args, options = {}) {
126
- const { httpTimeout = 3e4, onToolCall, onMarkdown } = options;
127
- if (onToolCall) {
125
+ if ((braces > 0 || brackets > 0 || inString) && workingText.length > 2) {
126
+ let fixedText = workingText;
127
+ if (inString) {
128
+ fixedText += '"';
129
+ }
130
+ while (braces > 0) {
131
+ fixedText += "}";
132
+ braces--;
133
+ }
134
+ while (brackets > 0) {
135
+ fixedText += "]";
136
+ brackets--;
137
+ }
128
138
  try {
129
- onToolCall(name, args);
139
+ const result = JSON.parse(fixedText);
140
+ if (logger_default.level !== "silent") {
141
+ logger_default.warn(`JSON response appears truncated (possibly hit maxOutputTokens limit). Recovered by adding closing characters.`);
142
+ }
143
+ return result;
130
144
  } catch (e) {
131
- logger_default.warn(`onToolCall callback error: ${e.message}`);
132
145
  }
133
146
  }
134
- switch (name) {
135
- case "http_get": {
136
- logger_default.debug(`http_get: ${args.url}`);
137
- const resp = await fetch(args.url, {
138
- method: "GET",
139
- headers: args.headers || {},
140
- signal: AbortSignal.timeout(httpTimeout)
141
- });
142
- const text = await resp.text();
143
- return { status: resp.status, statusText: resp.statusText, body: parseBody(text) };
144
- }
145
- case "http_post": {
146
- logger_default.debug(`http_post: ${args.url}`);
147
- const headers = { "Content-Type": "application/json", ...args.headers || {} };
148
- const resp = await fetch(args.url, {
149
- method: "POST",
150
- headers,
151
- body: args.body ? JSON.stringify(args.body) : void 0,
152
- signal: AbortSignal.timeout(httpTimeout)
153
- });
154
- const text = await resp.text();
155
- return { status: resp.status, statusText: resp.statusText, body: parseBody(text) };
147
+ for (let i = 0; i < maxAttempts && workingText.length > 2; i++) {
148
+ workingText = workingText.slice(0, -1);
149
+ let braces2 = 0;
150
+ let brackets2 = 0;
151
+ let inString2 = false;
152
+ let escapeNext2 = false;
153
+ for (let j = 0; j < workingText.length; j++) {
154
+ const char = workingText[j];
155
+ if (escapeNext2) {
156
+ escapeNext2 = false;
157
+ continue;
158
+ }
159
+ if (char === "\\") {
160
+ escapeNext2 = true;
161
+ continue;
162
+ }
163
+ if (char === '"') {
164
+ inString2 = !inString2;
165
+ continue;
166
+ }
167
+ if (!inString2) {
168
+ if (char === "{") braces2++;
169
+ else if (char === "}") braces2--;
170
+ else if (char === "[") brackets2++;
171
+ else if (char === "]") brackets2--;
172
+ }
156
173
  }
157
- case "write_markdown": {
158
- logger_default.debug(`write_markdown: ${args.filename}`);
159
- if (onMarkdown) {
160
- try {
161
- onMarkdown(args.filename, args.content);
162
- } catch (e) {
163
- logger_default.warn(`onMarkdown callback error: ${e.message}`);
174
+ if (braces2 === 0 && brackets2 === 0 && !inString2) {
175
+ try {
176
+ const result = JSON.parse(workingText);
177
+ if (logger_default.level !== "silent") {
178
+ logger_default.warn(`JSON response appears truncated (possibly hit maxOutputTokens limit). Recovered by removing ${i + 1} characters from the end.`);
179
+ }
180
+ return result;
181
+ } catch (e) {
182
+ }
183
+ }
184
+ if (i > 5) {
185
+ let fixedText = workingText;
186
+ if (inString2) {
187
+ fixedText += '"';
188
+ }
189
+ while (braces2 > 0) {
190
+ fixedText += "}";
191
+ braces2--;
192
+ }
193
+ while (brackets2 > 0) {
194
+ fixedText += "]";
195
+ brackets2--;
196
+ }
197
+ try {
198
+ const result = JSON.parse(fixedText);
199
+ if (logger_default.level !== "silent") {
200
+ logger_default.warn(`JSON response appears truncated (possibly hit maxOutputTokens limit). Recovered by adding closing characters.`);
201
+ }
202
+ return result;
203
+ } catch (e) {
204
+ }
205
+ }
206
+ }
207
+ return null;
208
+ }
209
+ function extractCompleteStructure(text, startPos) {
210
+ const startChar = text[startPos];
211
+ const endChar = startChar === "{" ? "}" : "]";
212
+ let depth = 0;
213
+ let inString = false;
214
+ let escaped = false;
215
+ for (let i = startPos; i < text.length; i++) {
216
+ const char = text[i];
217
+ if (escaped) {
218
+ escaped = false;
219
+ continue;
220
+ }
221
+ if (char === "\\" && inString) {
222
+ escaped = true;
223
+ continue;
224
+ }
225
+ if (char === '"' && !escaped) {
226
+ inString = !inString;
227
+ continue;
228
+ }
229
+ if (!inString) {
230
+ if (char === startChar) {
231
+ depth++;
232
+ } else if (char === endChar) {
233
+ depth--;
234
+ if (depth === 0) {
235
+ return text.substring(startPos, i + 1);
236
+ }
237
+ }
238
+ }
239
+ }
240
+ return null;
241
+ }
242
+ function findCompleteJSONStructures(text) {
243
+ const results = [];
244
+ const startChars = ["{", "["];
245
+ for (let i = 0; i < text.length; i++) {
246
+ if (startChars.includes(text[i])) {
247
+ const extracted = extractCompleteStructure(text, i);
248
+ if (extracted) {
249
+ results.push(extracted);
250
+ }
251
+ }
252
+ }
253
+ return results;
254
+ }
255
+ function extractJSON(text) {
256
+ if (!text || typeof text !== "string") {
257
+ throw new Error("No text provided for JSON extraction");
258
+ }
259
+ if (isJSONStr(text.trim())) {
260
+ return JSON.parse(text.trim());
261
+ }
262
+ const codeBlockPatterns = [
263
+ /```json\s*\n?([\s\S]*?)\n?\s*```/gi,
264
+ /```\s*\n?([\s\S]*?)\n?\s*```/gi
265
+ ];
266
+ for (const pattern of codeBlockPatterns) {
267
+ const matches = text.match(pattern);
268
+ if (matches) {
269
+ for (const match of matches) {
270
+ const jsonContent = match.replace(/```json\s*\n?/gi, "").replace(/```\s*\n?/gi, "").trim();
271
+ if (isJSONStr(jsonContent)) {
272
+ return JSON.parse(jsonContent);
273
+ }
274
+ }
275
+ }
276
+ }
277
+ const jsonPatterns = [
278
+ /\{[\s\S]*\}/g,
279
+ /\[[\s\S]*\]/g
280
+ ];
281
+ for (const pattern of jsonPatterns) {
282
+ const matches = text.match(pattern);
283
+ if (matches) {
284
+ for (const match of matches) {
285
+ const candidate = match.trim();
286
+ if (isJSONStr(candidate)) {
287
+ return JSON.parse(candidate);
164
288
  }
165
289
  }
166
- return { written: true, filename: args.filename, length: args.content.length };
167
290
  }
168
- default:
169
- throw new Error(`Unknown tool: ${name}`);
170
291
  }
292
+ const advancedExtract = findCompleteJSONStructures(text);
293
+ if (advancedExtract.length > 0) {
294
+ for (const candidate of advancedExtract) {
295
+ if (isJSONStr(candidate)) {
296
+ return JSON.parse(candidate);
297
+ }
298
+ }
299
+ }
300
+ const cleanedText = text.replace(/^\s*Sure,?\s*here\s+is\s+your?\s+.*?[:\n]/gi, "").replace(/^\s*Here\s+is\s+the\s+.*?[:\n]/gi, "").replace(/^\s*The\s+.*?is\s*[:\n]/gi, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "").trim();
301
+ if (isJSONStr(cleanedText)) {
302
+ return JSON.parse(cleanedText);
303
+ }
304
+ const recoveredJSON = attemptJSONRecovery(text);
305
+ if (recoveredJSON !== null) {
306
+ return recoveredJSON;
307
+ }
308
+ throw new Error(`Could not extract valid JSON from model response. Response preview: ${text.substring(0, 200)}...`);
171
309
  }
172
310
 
173
- // agent.js
311
+ // base.js
174
312
  import_dotenv.default.config();
175
313
  var { NODE_ENV = "unknown", LOG_LEVEL = "" } = process.env;
176
314
  var DEFAULT_SAFETY_SETTINGS = [
@@ -180,6 +318,7 @@ var DEFAULT_SAFETY_SETTINGS = [
180
318
  var DEFAULT_THINKING_CONFIG = {
181
319
  thinkingBudget: 0
182
320
  };
321
+ var DEFAULT_MAX_OUTPUT_TOKENS = 5e4;
183
322
  var THINKING_SUPPORTED_MODELS = [
184
323
  /^gemini-3-flash(-preview)?$/,
185
324
  /^gemini-3-pro(-preview|-image-preview)?$/,
@@ -188,20 +327,26 @@ var THINKING_SUPPORTED_MODELS = [
188
327
  /^gemini-2\.5-flash-lite(-preview)?$/,
189
328
  /^gemini-2\.0-flash$/
190
329
  ];
191
- var AIAgent = class {
330
+ var MODEL_PRICING = {
331
+ "gemini-2.5-flash": { input: 0.15, output: 0.6 },
332
+ "gemini-2.5-flash-lite": { input: 0.02, output: 0.1 },
333
+ "gemini-2.5-pro": { input: 2.5, output: 10 },
334
+ "gemini-3-pro": { input: 2, output: 12 },
335
+ "gemini-3-pro-preview": { input: 2, output: 12 },
336
+ "gemini-2.0-flash": { input: 0.1, output: 0.4 },
337
+ "gemini-2.0-flash-lite": { input: 0.02, output: 0.1 }
338
+ };
339
+ var BaseGemini = class {
192
340
  /**
193
- * Create a new AIAgent instance.
194
- * @param {AIAgentOptions} [options={}] - Configuration options (see AIAgentOptions in types.d.ts)
341
+ * @param {BaseGeminiOptions} [options={}]
195
342
  */
196
343
  constructor(options = {}) {
197
344
  this.modelName = options.modelName || "gemini-2.5-flash";
198
- this.systemPrompt = options.systemPrompt || "You are a helpful AI assistant.";
199
- this.maxToolRounds = options.maxToolRounds || 10;
200
- this.httpTimeout = options.httpTimeout || 3e4;
201
- this.maxRetries = options.maxRetries || 3;
202
- this.onToolCall = options.onToolCall || null;
203
- this.onMarkdown = options.onMarkdown || null;
204
- this.labels = options.labels || {};
345
+ if (options.systemPrompt !== void 0) {
346
+ this.systemPrompt = options.systemPrompt;
347
+ } else {
348
+ this.systemPrompt = null;
349
+ }
205
350
  this.vertexai = options.vertexai || false;
206
351
  this.project = options.project || process.env.GOOGLE_CLOUD_PROJECT || null;
207
352
  this.location = options.location || process.env.GOOGLE_CLOUD_LOCATION || void 0;
@@ -214,33 +359,34 @@ var AIAgent = class {
214
359
  throw new Error("Vertex AI requires a project ID. Provide via options.project or GOOGLE_CLOUD_PROJECT env var.");
215
360
  }
216
361
  this._configureLogLevel(options.logLevel);
362
+ this.labels = options.labels || {};
217
363
  this.chatConfig = {
218
364
  temperature: 0.7,
219
365
  topP: 0.95,
220
366
  topK: 64,
221
367
  safetySettings: DEFAULT_SAFETY_SETTINGS,
222
- systemInstruction: this.systemPrompt,
223
- maxOutputTokens: options.chatConfig?.maxOutputTokens || 5e4,
224
368
  ...options.chatConfig
225
369
  };
226
- this.chatConfig.systemInstruction = this.systemPrompt;
370
+ if (this.systemPrompt) {
371
+ this.chatConfig.systemInstruction = this.systemPrompt;
372
+ } else if (this.systemPrompt === null && options.systemPrompt === void 0) {
373
+ } else if (options.systemPrompt === null || options.systemPrompt === false) {
374
+ delete this.chatConfig.systemInstruction;
375
+ }
376
+ if (options.maxOutputTokens !== void 0) {
377
+ if (options.maxOutputTokens === null) {
378
+ delete this.chatConfig.maxOutputTokens;
379
+ } else {
380
+ this.chatConfig.maxOutputTokens = options.maxOutputTokens;
381
+ }
382
+ } else if (options.chatConfig?.maxOutputTokens !== void 0) {
383
+ if (options.chatConfig.maxOutputTokens === null) {
384
+ delete this.chatConfig.maxOutputTokens;
385
+ }
386
+ } else {
387
+ this.chatConfig.maxOutputTokens = DEFAULT_MAX_OUTPUT_TOKENS;
388
+ }
227
389
  this._configureThinking(options.thinkingConfig);
228
- this.chatConfig.tools = [{ functionDeclarations: BUILT_IN_DECLARATIONS }];
229
- this.chatConfig.toolConfig = { functionCallingConfig: { mode: "AUTO" } };
230
- this.genAIClient = null;
231
- this.chatSession = null;
232
- this.lastResponseMetadata = null;
233
- this._markdownFiles = [];
234
- logger_default.debug(`AIAgent created with model: ${this.modelName}`);
235
- }
236
- /**
237
- * Initialize the agent — creates the GenAI client and chat session.
238
- * Called automatically by chat() and stream() if not called explicitly.
239
- * Idempotent — safe to call multiple times.
240
- * @returns {Promise<void>}
241
- */
242
- async init() {
243
- if (this.chatSession) return;
244
390
  const clientOptions = this.vertexai ? {
245
391
  vertexai: true,
246
392
  project: this.project,
@@ -248,235 +394,165 @@ var AIAgent = class {
248
394
  ...this.googleAuthOptions && { googleAuthOptions: this.googleAuthOptions }
249
395
  } : { apiKey: this.apiKey };
250
396
  this.genAIClient = new import_genai.GoogleGenAI(clientOptions);
251
- this.chatSession = this.genAIClient.chats.create({
252
- model: this.modelName,
253
- config: {
254
- ...this.chatConfig,
255
- ...this.vertexai && Object.keys(this.labels).length > 0 && { labels: this.labels }
256
- },
257
- history: []
258
- });
259
- try {
260
- await this.genAIClient.models.list();
261
- logger_default.debug("AIAgent: Gemini API connection successful.");
262
- } catch (e) {
263
- throw new Error(`AIAgent initialization failed: ${e.message}`);
264
- }
265
- logger_default.debug("AIAgent: Chat session initialized.");
266
- }
267
- /**
268
- * Send a message and get a complete response (non-streaming).
269
- * Automatically handles the tool-use loop — if the model requests tool calls,
270
- * they are executed and results sent back until the model produces a final response.
271
- *
272
- * @param {string} message - The user's message
273
- * @returns {Promise<AgentResponse>} Response with text, toolCalls, markdownFiles, and usage
274
- * @example
275
- * const res = await agent.chat('Fetch https://api.example.com/users');
276
- * console.log(res.text); // Agent's summary
277
- * console.log(res.toolCalls); // [{name: 'http_get', args: {...}, result: {...}}]
278
- */
279
- async chat(message) {
280
- if (!this.chatSession) await this.init();
281
- this._markdownFiles = [];
282
- const allToolCalls = [];
283
- let response = await this.chatSession.sendMessage({ message });
284
- for (let round = 0; round < this.maxToolRounds; round++) {
285
- const functionCalls = response.functionCalls;
286
- if (!functionCalls || functionCalls.length === 0) break;
287
- const toolResults = await Promise.all(
288
- functionCalls.map(async (call) => {
289
- let result;
290
- try {
291
- result = await executeBuiltInTool(call.name, call.args, {
292
- httpTimeout: this.httpTimeout,
293
- onToolCall: this.onToolCall,
294
- onMarkdown: this.onMarkdown
295
- });
296
- } catch (err) {
297
- logger_default.warn(`Tool ${call.name} failed: ${err.message}`);
298
- result = { error: err.message };
299
- }
300
- allToolCalls.push({ name: call.name, args: call.args, result });
301
- if (call.name === "write_markdown" && call.args) {
302
- this._markdownFiles.push({
303
- filename: (
304
- /** @type {string} */
305
- call.args.filename
306
- ),
307
- content: (
308
- /** @type {string} */
309
- call.args.content
310
- )
311
- });
312
- }
313
- return { id: call.id, name: call.name, result };
314
- })
315
- );
316
- response = await this.chatSession.sendMessage({
317
- message: toolResults.map((r) => ({
318
- functionResponse: {
319
- id: r.id,
320
- name: r.name,
321
- response: { output: r.result }
322
- }
323
- }))
324
- });
325
- }
326
- this._captureMetadata(response);
327
- return {
328
- text: response.text || "",
329
- toolCalls: allToolCalls,
330
- markdownFiles: [...this._markdownFiles],
331
- usage: this.getLastUsage()
397
+ this.chatSession = null;
398
+ this.lastResponseMetadata = null;
399
+ this.exampleCount = 0;
400
+ this._cumulativeUsage = {
401
+ promptTokens: 0,
402
+ responseTokens: 0,
403
+ totalTokens: 0,
404
+ attempts: 0
332
405
  };
406
+ logger_default.debug(`${this.constructor.name} created with model: ${this.modelName}`);
333
407
  }
408
+ // ── Initialization ───────────────────────────────────────────────────────
334
409
  /**
335
- * Send a message and stream the response as events.
336
- * Automatically handles the tool-use loop between streamed rounds.
337
- *
338
- * Event types:
339
- * - `text` — A chunk of the agent's text response (yield as it arrives)
340
- * - `tool_call` — The agent is about to call a tool (includes toolName and args)
341
- * - `tool_result` — A tool finished executing (includes toolName and result)
342
- * - `markdown` — A markdown document was generated (includes filename and content)
343
- * - `done` — The agent finished (includes fullText, markdownFiles, usage)
344
- *
345
- * @param {string} message - The user's message
346
- * @yields {AgentStreamEvent}
347
- * @example
348
- * for await (const event of agent.stream('Analyze this API...')) {
349
- * if (event.type === 'text') process.stdout.write(event.text);
350
- * if (event.type === 'tool_call') console.log(`Calling: ${event.toolName}`);
351
- * if (event.type === 'done') console.log(`\nTokens: ${event.usage?.totalTokens}`);
352
- * }
410
+ * Initializes the chat session. Idempotent unless force=true.
411
+ * Subclasses can override `_getChatCreateOptions()` to customize.
412
+ * @param {boolean} [force=false]
413
+ * @returns {Promise<void>}
353
414
  */
354
- async *stream(message) {
355
- if (!this.chatSession) await this.init();
356
- this._markdownFiles = [];
357
- const allToolCalls = [];
358
- let fullText = "";
359
- let streamResponse = await this.chatSession.sendMessageStream({ message });
360
- for (let round = 0; round < this.maxToolRounds; round++) {
361
- let roundText = "";
362
- const functionCalls = [];
363
- for await (const chunk of streamResponse) {
364
- if (chunk.functionCalls) {
365
- functionCalls.push(...chunk.functionCalls);
366
- } else if (chunk.candidates?.[0]?.content?.parts?.[0]?.text) {
367
- const text = chunk.candidates[0].content.parts[0].text;
368
- roundText += text;
369
- fullText += text;
370
- yield { type: "text", text };
371
- }
372
- }
373
- if (functionCalls.length === 0) {
374
- yield {
375
- type: "done",
376
- fullText,
377
- markdownFiles: [...this._markdownFiles],
378
- usage: this.getLastUsage()
379
- };
380
- return;
381
- }
382
- const toolResults = [];
383
- for (const call of functionCalls) {
384
- yield { type: "tool_call", toolName: call.name, args: call.args };
385
- let result;
386
- try {
387
- result = await executeBuiltInTool(call.name, call.args, {
388
- httpTimeout: this.httpTimeout,
389
- onToolCall: this.onToolCall,
390
- onMarkdown: this.onMarkdown
391
- });
392
- } catch (err) {
393
- logger_default.warn(`Tool ${call.name} failed: ${err.message}`);
394
- result = { error: err.message };
395
- }
396
- allToolCalls.push({ name: call.name, args: call.args, result });
397
- yield { type: "tool_result", toolName: call.name, result };
398
- if (call.name === "write_markdown" && call.args) {
399
- const mdFilename = (
400
- /** @type {string} */
401
- call.args.filename
402
- );
403
- const mdContent = (
404
- /** @type {string} */
405
- call.args.content
406
- );
407
- this._markdownFiles.push({ filename: mdFilename, content: mdContent });
408
- yield { type: "markdown", filename: mdFilename, content: mdContent };
409
- }
410
- toolResults.push({ id: call.id, name: call.name, result });
411
- }
412
- streamResponse = await this.chatSession.sendMessageStream({
413
- message: toolResults.map((r) => ({
414
- functionResponse: {
415
- id: r.id,
416
- name: r.name,
417
- response: { output: r.result }
418
- }
419
- }))
420
- });
415
+ async init(force = false) {
416
+ if (this.chatSession && !force) return;
417
+ logger_default.debug(`Initializing ${this.constructor.name} chat session with model: ${this.modelName}...`);
418
+ const chatOptions = this._getChatCreateOptions();
419
+ this.chatSession = this.genAIClient.chats.create(chatOptions);
420
+ try {
421
+ await this.genAIClient.models.list();
422
+ logger_default.debug(`${this.constructor.name}: API connection successful.`);
423
+ } catch (e) {
424
+ throw new Error(`${this.constructor.name} initialization failed: ${e.message}`);
421
425
  }
422
- yield {
423
- type: "done",
424
- fullText,
425
- markdownFiles: [...this._markdownFiles],
426
- usage: this.getLastUsage(),
427
- warning: "Max tool rounds reached"
428
- };
426
+ logger_default.debug(`${this.constructor.name}: Chat session initialized.`);
429
427
  }
430
428
  /**
431
- * Clear conversation history while preserving tools and system prompt.
432
- * Useful for starting a new user session without re-initializing the agent.
433
- * @returns {Promise<void>}
429
+ * Builds the options object for `genAIClient.chats.create()`.
430
+ * Override in subclasses to add tools, grounding, etc.
431
+ * @returns {Object}
432
+ * @protected
434
433
  */
435
- async clearHistory() {
436
- this.chatSession = this.genAIClient.chats.create({
434
+ _getChatCreateOptions() {
435
+ return {
437
436
  model: this.modelName,
438
437
  config: {
439
438
  ...this.chatConfig,
440
439
  ...this.vertexai && Object.keys(this.labels).length > 0 && { labels: this.labels }
441
440
  },
442
441
  history: []
443
- });
444
- this._markdownFiles = [];
445
- this.lastResponseMetadata = null;
446
- logger_default.debug("AIAgent: Conversation history cleared.");
442
+ };
443
+ }
444
+ // ── Chat Session Management ──────────────────────────────────────────────
445
+ /**
446
+ * Creates a new chat session with the given history.
447
+ * Internal helper used by init, seed, clearHistory, reset.
448
+ * @param {Array} [history=[]]
449
+ * @returns {Object} The new chat session
450
+ * @protected
451
+ */
452
+ _createChatSession(history = []) {
453
+ const opts = this._getChatCreateOptions();
454
+ opts.history = history;
455
+ return this.genAIClient.chats.create(opts);
447
456
  }
448
457
  /**
449
- * Get conversation history.
458
+ * Retrieves the current conversation history.
450
459
  * @param {boolean} [curated=false]
451
- * @returns {any[]}
460
+ * @returns {Array<Object>}
452
461
  */
453
462
  getHistory(curated = false) {
454
- if (!this.chatSession) return [];
463
+ if (!this.chatSession) {
464
+ logger_default.warn("Chat session not initialized. No history available.");
465
+ return [];
466
+ }
455
467
  return this.chatSession.getHistory(curated);
456
468
  }
457
469
  /**
458
- * Get structured usage data from the last API call.
459
- * Returns null if no API call has been made yet.
460
- * @returns {UsageData|null} Usage data with promptTokens, responseTokens, totalTokens, etc.
470
+ * Clears conversation history. Recreates chat session with empty history.
471
+ * Subclasses may override to preserve seeded examples.
472
+ * @returns {Promise<void>}
461
473
  */
462
- getLastUsage() {
463
- if (!this.lastResponseMetadata) return null;
464
- const m = this.lastResponseMetadata;
465
- return {
466
- promptTokens: m.promptTokens,
467
- responseTokens: m.responseTokens,
468
- totalTokens: m.totalTokens,
469
- attempts: 1,
470
- modelVersion: m.modelVersion,
471
- requestedModel: this.modelName,
472
- timestamp: m.timestamp
473
- };
474
+ async clearHistory() {
475
+ if (!this.chatSession) {
476
+ logger_default.warn(`Cannot clear history: chat not initialized.`);
477
+ return;
478
+ }
479
+ this.chatSession = this._createChatSession([]);
480
+ this.lastResponseMetadata = null;
481
+ this._cumulativeUsage = { promptTokens: 0, responseTokens: 0, totalTokens: 0, attempts: 0 };
482
+ logger_default.debug(`${this.constructor.name}: Conversation history cleared.`);
474
483
  }
475
- // --- Private helpers ---
484
+ // ── Few-Shot Seeding ─────────────────────────────────────────────────────
476
485
  /**
477
- * Capture response metadata (model version, token counts) from an API response.
478
- * @param {import('@google/genai').GenerateContentResponse} response
479
- * @private
486
+ * Seeds the chat session with example input/output pairs for few-shot learning.
487
+ * @param {TransformationExample[]} examples - Array of example objects
488
+ * @param {Object} [opts={}] - Key configuration
489
+ * @param {string} [opts.promptKey='PROMPT'] - Key for input data in examples
490
+ * @param {string} [opts.answerKey='ANSWER'] - Key for output data in examples
491
+ * @param {string} [opts.contextKey='CONTEXT'] - Key for optional context
492
+ * @param {string} [opts.explanationKey='EXPLANATION'] - Key for optional explanations
493
+ * @param {string} [opts.systemPromptKey='SYSTEM'] - Key for system prompt overrides in examples
494
+ * @returns {Promise<Array>} The updated chat history
495
+ */
496
+ async seed(examples, opts = {}) {
497
+ await this.init();
498
+ if (!examples || !Array.isArray(examples) || examples.length === 0) {
499
+ logger_default.debug("No examples provided. Skipping seeding.");
500
+ return this.getHistory();
501
+ }
502
+ const promptKey = opts.promptKey || "PROMPT";
503
+ const answerKey = opts.answerKey || "ANSWER";
504
+ const contextKey = opts.contextKey || "CONTEXT";
505
+ const explanationKey = opts.explanationKey || "EXPLANATION";
506
+ const systemPromptKey = opts.systemPromptKey || "SYSTEM";
507
+ const instructionExample = examples.find((ex) => ex[systemPromptKey]);
508
+ if (instructionExample) {
509
+ logger_default.debug(`Found system prompt in examples; reinitializing chat.`);
510
+ this.systemPrompt = instructionExample[systemPromptKey];
511
+ this.chatConfig.systemInstruction = /** @type {string} */
512
+ this.systemPrompt;
513
+ await this.init(true);
514
+ }
515
+ logger_default.debug(`Seeding chat with ${examples.length} examples...`);
516
+ const historyToAdd = [];
517
+ for (const example of examples) {
518
+ const contextValue = example[contextKey] || "";
519
+ const promptValue = example[promptKey] || "";
520
+ const answerValue = example[answerKey] || "";
521
+ const explanationValue = example[explanationKey] || "";
522
+ let userText = "";
523
+ let modelResponse = {};
524
+ if (contextValue) {
525
+ let contextText = isJSON(contextValue) ? JSON.stringify(contextValue, null, 2) : contextValue;
526
+ userText += `CONTEXT:
527
+ ${contextText}
528
+
529
+ `;
530
+ }
531
+ if (promptValue) {
532
+ let promptText = isJSON(promptValue) ? JSON.stringify(promptValue, null, 2) : promptValue;
533
+ userText += promptText;
534
+ }
535
+ if (answerValue) modelResponse.data = answerValue;
536
+ if (explanationValue) modelResponse.explanation = explanationValue;
537
+ const modelText = JSON.stringify(modelResponse, null, 2);
538
+ if (userText.trim().length && modelText.trim().length > 0) {
539
+ historyToAdd.push({ role: "user", parts: [{ text: userText.trim() }] });
540
+ historyToAdd.push({ role: "model", parts: [{ text: modelText.trim() }] });
541
+ }
542
+ }
543
+ const currentHistory = this.chatSession?.getHistory() || [];
544
+ logger_default.debug(`Adding ${historyToAdd.length} items to chat history (${currentHistory.length} existing)...`);
545
+ this.chatSession = this._createChatSession([...currentHistory, ...historyToAdd]);
546
+ this.exampleCount = currentHistory.length + historyToAdd.length;
547
+ const newHistory = this.chatSession.getHistory();
548
+ logger_default.debug(`Chat session now has ${newHistory.length} history items.`);
549
+ return newHistory;
550
+ }
551
+ // ── Response Metadata ────────────────────────────────────────────────────
552
+ /**
553
+ * Captures response metadata (model version, token counts) from an API response.
554
+ * @param {Object} response - The API response object
555
+ * @protected
480
556
  */
481
557
  _captureMetadata(response) {
482
558
  this.lastResponseMetadata = {
@@ -488,7 +564,74 @@ var AIAgent = class {
488
564
  timestamp: Date.now()
489
565
  };
490
566
  }
491
- /** @private */
567
+ /**
568
+ * Returns structured usage data from the last API call for billing verification.
569
+ * Includes CUMULATIVE token counts across all retry attempts.
570
+ * @returns {UsageData|null} Usage data or null if no API call has been made.
571
+ */
572
+ getLastUsage() {
573
+ if (!this.lastResponseMetadata) return null;
574
+ const meta = this.lastResponseMetadata;
575
+ const cumulative = this._cumulativeUsage || { promptTokens: 0, responseTokens: 0, totalTokens: 0, attempts: 1 };
576
+ const useCumulative = cumulative.attempts > 0;
577
+ return {
578
+ promptTokens: useCumulative ? cumulative.promptTokens : meta.promptTokens,
579
+ responseTokens: useCumulative ? cumulative.responseTokens : meta.responseTokens,
580
+ totalTokens: useCumulative ? cumulative.totalTokens : meta.totalTokens,
581
+ attempts: useCumulative ? cumulative.attempts : 1,
582
+ modelVersion: meta.modelVersion,
583
+ requestedModel: meta.requestedModel,
584
+ timestamp: meta.timestamp
585
+ };
586
+ }
587
+ // ── Token Estimation ─────────────────────────────────────────────────────
588
+ /**
589
+ * Estimates INPUT token count for a payload before sending.
590
+ * Includes system prompt + chat history + your new message.
591
+ * @param {Object|string} nextPayload - The next message to estimate
592
+ * @returns {Promise<{ inputTokens: number }>}
593
+ */
594
+ async estimate(nextPayload) {
595
+ const contents = [];
596
+ if (this.systemPrompt) {
597
+ contents.push({ parts: [{ text: this.systemPrompt }] });
598
+ }
599
+ if (this.chatSession && typeof this.chatSession.getHistory === "function") {
600
+ const history = this.chatSession.getHistory();
601
+ if (Array.isArray(history) && history.length > 0) {
602
+ contents.push(...history);
603
+ }
604
+ }
605
+ const nextMessage = typeof nextPayload === "string" ? nextPayload : JSON.stringify(nextPayload, null, 2);
606
+ contents.push({ parts: [{ text: nextMessage }] });
607
+ const resp = await this.genAIClient.models.countTokens({
608
+ model: this.modelName,
609
+ contents
610
+ });
611
+ return { inputTokens: resp.totalTokens };
612
+ }
613
+ /**
614
+ * Estimates the INPUT cost of sending a payload based on model pricing.
615
+ * @param {Object|string} nextPayload - The next message to estimate
616
+ * @returns {Promise<Object>} Cost estimation
617
+ */
618
+ async estimateCost(nextPayload) {
619
+ const tokenInfo = await this.estimate(nextPayload);
620
+ const pricing = MODEL_PRICING[this.modelName] || { input: 0, output: 0 };
621
+ return {
622
+ inputTokens: tokenInfo.inputTokens,
623
+ model: this.modelName,
624
+ pricing,
625
+ estimatedInputCost: tokenInfo.inputTokens / 1e6 * pricing.input,
626
+ note: "Cost is for input tokens only; output cost depends on response length"
627
+ };
628
+ }
629
+ // ── Private Helpers ──────────────────────────────────────────────────────
630
+ /**
631
+ * Configures the log level based on options, env vars, or NODE_ENV.
632
+ * @param {string} [logLevel]
633
+ * @private
634
+ */
492
635
  _configureLogLevel(logLevel) {
493
636
  if (logLevel) {
494
637
  if (logLevel === "none") {
@@ -508,12 +651,17 @@ var AIAgent = class {
508
651
  logger_default.level = "info";
509
652
  }
510
653
  }
511
- /** @private */
654
+ /**
655
+ * Configures thinking settings based on model support.
656
+ * @param {Object|null|undefined} thinkingConfig
657
+ * @private
658
+ */
512
659
  _configureThinking(thinkingConfig) {
513
660
  const modelSupportsThinking = THINKING_SUPPORTED_MODELS.some((p) => p.test(this.modelName));
514
661
  if (thinkingConfig === void 0) return;
515
662
  if (thinkingConfig === null) {
516
663
  delete this.chatConfig.thinkingConfig;
664
+ logger_default.debug(`thinkingConfig set to null - removed from configuration`);
517
665
  return;
518
666
  }
519
667
  if (!modelSupportsThinking) {
@@ -528,16 +676,11 @@ var AIAgent = class {
528
676
  logger_default.debug(`Thinking config applied: ${JSON.stringify(config)}`);
529
677
  }
530
678
  };
531
- var agent_default = AIAgent;
679
+ var base_default = BaseGemini;
532
680
 
533
- // index.js
534
- var import_meta = {};
535
- import_dotenv2.default.config();
536
- var { NODE_ENV: NODE_ENV2 = "unknown", GEMINI_API_KEY, LOG_LEVEL: LOG_LEVEL2 = "" } = process.env;
537
- var DEFAULT_SAFETY_SETTINGS2 = [
538
- { category: import_genai2.HarmCategory.HARM_CATEGORY_HARASSMENT, threshold: import_genai2.HarmBlockThreshold.BLOCK_NONE },
539
- { category: import_genai2.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, threshold: import_genai2.HarmBlockThreshold.BLOCK_NONE }
540
- ];
681
+ // transformer.js
682
+ var import_promises = __toESM(require("fs/promises"), 1);
683
+ var import_path = __toESM(require("path"), 1);
541
684
  var DEFAULT_SYSTEM_INSTRUCTIONS = `
542
685
  You are an expert JSON transformation engine. Your task is to accurately convert data payloads from one format to another.
543
686
 
@@ -551,458 +694,210 @@ Always respond ONLY with a valid JSON object that strictly adheres to the expect
551
694
 
552
695
  Do not include any additional text, explanations, or formatting before or after the JSON object.
553
696
  `;
554
- var DEFAULT_THINKING_CONFIG2 = {
555
- thinkingBudget: 0
556
- };
557
- var DEFAULT_MAX_OUTPUT_TOKENS = 5e4;
558
- var THINKING_SUPPORTED_MODELS2 = [
559
- /^gemini-3-flash(-preview)?$/,
560
- /^gemini-3-pro(-preview|-image-preview)?$/,
561
- /^gemini-2\.5-pro/,
562
- /^gemini-2\.5-flash(-preview)?$/,
563
- /^gemini-2\.5-flash-lite(-preview)?$/,
564
- /^gemini-2\.0-flash$/
565
- // Experimental support, exact match only
566
- ];
567
- var DEFAULT_CHAT_CONFIG = {
568
- responseMimeType: "application/json",
569
- temperature: 0.2,
570
- topP: 0.95,
571
- topK: 64,
572
- systemInstruction: DEFAULT_SYSTEM_INSTRUCTIONS,
573
- safetySettings: DEFAULT_SAFETY_SETTINGS2
574
- };
575
- var AITransformer = class {
697
+ var Transformer = class extends base_default {
576
698
  /**
577
- * @param {AITransformerOptions} [options={}] - Configuration options for the transformer
578
- *
699
+ * @param {TransformerOptions} [options={}]
579
700
  */
580
701
  constructor(options = {}) {
581
- this.modelName = "";
582
- this.promptKey = "";
583
- this.answerKey = "";
584
- this.contextKey = "";
585
- this.explanationKey = "";
586
- this.systemInstructionKey = "";
587
- this.maxRetries = 3;
588
- this.retryDelay = 1e3;
589
- this.chatConfig = {};
590
- this.apiKey = GEMINI_API_KEY;
591
- this.onlyJSON = true;
592
- this.asyncValidator = null;
593
- this.logLevel = "info";
594
- this.lastResponseMetadata = null;
595
- this.exampleCount = 0;
596
- this._cumulativeUsage = {
597
- promptTokens: 0,
598
- responseTokens: 0,
599
- totalTokens: 0,
600
- attempts: 0
601
- };
602
- AITransformFactory.call(this, options);
603
- this.init = initChat.bind(this);
604
- this.seed = seedWithExamples.bind(this);
605
- this.rawMessage = rawMessage.bind(this);
606
- this.message = (payload, opts = {}, validatorFn = null) => {
607
- return prepareAndValidateMessage.call(this, payload, opts, validatorFn || this.asyncValidator);
608
- };
609
- this.rebuild = rebuildPayload.bind(this);
610
- this.reset = resetChat.bind(this);
611
- this.getHistory = getChatHistory.bind(this);
612
- this.messageAndValidate = prepareAndValidateMessage.bind(this);
613
- this.transformWithValidation = prepareAndValidateMessage.bind(this);
614
- this.estimate = estimateInputTokens.bind(this);
615
- this.updateSystemInstructions = updateSystemInstructions.bind(this);
616
- this.estimateCost = estimateCost.bind(this);
617
- this.clearConversation = clearConversation.bind(this);
618
- this.getLastUsage = getLastUsage.bind(this);
619
- }
620
- };
621
- var index_default = AITransformer;
622
- function AITransformFactory(options = {}) {
623
- this.modelName = options.modelName || "gemini-2.5-flash";
624
- if (options.systemInstructions === void 0) {
625
- this.systemInstructions = DEFAULT_SYSTEM_INSTRUCTIONS;
626
- } else {
627
- this.systemInstructions = options.systemInstructions;
628
- }
629
- if (options.logLevel) {
630
- this.logLevel = options.logLevel;
631
- if (this.logLevel === "none") {
632
- logger_default.level = "silent";
633
- } else {
634
- logger_default.level = this.logLevel;
635
- }
636
- } else if (LOG_LEVEL2) {
637
- this.logLevel = LOG_LEVEL2;
638
- logger_default.level = LOG_LEVEL2;
639
- } else if (NODE_ENV2 === "dev") {
640
- this.logLevel = "debug";
641
- logger_default.level = "debug";
642
- } else if (NODE_ENV2 === "test") {
643
- this.logLevel = "warn";
644
- logger_default.level = "warn";
645
- } else if (NODE_ENV2.startsWith("prod")) {
646
- this.logLevel = "error";
647
- logger_default.level = "error";
648
- } else {
649
- this.logLevel = "info";
650
- logger_default.level = "info";
651
- }
652
- this.vertexai = options.vertexai || false;
653
- this.project = options.project || process.env.GOOGLE_CLOUD_PROJECT || null;
654
- this.location = options.location || process.env.GOOGLE_CLOUD_LOCATION || void 0;
655
- this.googleAuthOptions = options.googleAuthOptions || null;
656
- this.apiKey = options.apiKey !== void 0 && options.apiKey !== null ? options.apiKey : GEMINI_API_KEY;
657
- if (!this.vertexai && !this.apiKey) {
658
- throw new Error("Missing Gemini API key. Provide via options.apiKey or GEMINI_API_KEY env var. For Vertex AI, set vertexai: true with project and location.");
659
- }
660
- if (this.vertexai && !this.project) {
661
- throw new Error("Vertex AI requires a project ID. Provide via options.project or GOOGLE_CLOUD_PROJECT env var.");
662
- }
663
- this.chatConfig = {
664
- ...DEFAULT_CHAT_CONFIG,
665
- ...options.chatConfig
666
- };
667
- if (this.systemInstructions) {
668
- this.chatConfig.systemInstruction = this.systemInstructions;
669
- } else if (options.systemInstructions !== void 0) {
670
- delete this.chatConfig.systemInstruction;
671
- }
672
- if (options.maxOutputTokens !== void 0) {
673
- if (options.maxOutputTokens === null) {
674
- delete this.chatConfig.maxOutputTokens;
675
- } else {
676
- this.chatConfig.maxOutputTokens = options.maxOutputTokens;
702
+ if (options.systemPrompt === void 0) {
703
+ options = { ...options, systemPrompt: DEFAULT_SYSTEM_INSTRUCTIONS };
677
704
  }
678
- } else if (options.chatConfig?.maxOutputTokens !== void 0) {
679
- if (options.chatConfig.maxOutputTokens === null) {
680
- delete this.chatConfig.maxOutputTokens;
681
- } else {
682
- this.chatConfig.maxOutputTokens = options.chatConfig.maxOutputTokens;
705
+ super(options);
706
+ this.chatConfig.responseMimeType = "application/json";
707
+ this.onlyJSON = options.onlyJSON !== void 0 ? options.onlyJSON : true;
708
+ if (options.responseSchema) {
709
+ this.chatConfig.responseSchema = options.responseSchema;
683
710
  }
684
- } else {
685
- this.chatConfig.maxOutputTokens = DEFAULT_MAX_OUTPUT_TOKENS;
686
- }
687
- const modelSupportsThinking = THINKING_SUPPORTED_MODELS2.some(
688
- (pattern) => pattern.test(this.modelName)
689
- );
690
- if (options.thinkingConfig !== void 0) {
691
- if (options.thinkingConfig === null) {
692
- delete this.chatConfig.thinkingConfig;
693
- if (logger_default.level !== "silent") {
694
- logger_default.debug(`thinkingConfig set to null - removed from configuration`);
695
- }
696
- } else if (modelSupportsThinking) {
697
- const thinkingConfig = {
698
- ...DEFAULT_THINKING_CONFIG2,
699
- ...options.thinkingConfig
700
- };
701
- if (options.thinkingConfig?.thinkingLevel !== void 0) {
702
- delete thinkingConfig.thinkingBudget;
703
- }
704
- this.chatConfig.thinkingConfig = thinkingConfig;
705
- if (logger_default.level !== "silent") {
706
- logger_default.debug(`Model ${this.modelName} supports thinking. Applied thinkingConfig: ${JSON.stringify(thinkingConfig)}`);
707
- }
708
- } else {
709
- if (logger_default.level !== "silent") {
710
- logger_default.warn(`Model ${this.modelName} does not support thinking features. Ignoring thinkingConfig.`);
711
- }
711
+ this.promptKey = options.promptKey || options.sourceKey || "PROMPT";
712
+ this.answerKey = options.answerKey || options.targetKey || "ANSWER";
713
+ this.contextKey = options.contextKey || "CONTEXT";
714
+ this.explanationKey = options.explanationKey || "EXPLANATION";
715
+ this.systemPromptKey = options.systemPromptKey || "SYSTEM";
716
+ if (this.promptKey === this.answerKey) {
717
+ throw new Error("Source and target keys cannot be the same. Please provide distinct keys.");
718
+ }
719
+ this.examplesFile = options.examplesFile || null;
720
+ this.exampleData = options.exampleData || null;
721
+ this.asyncValidator = options.asyncValidator || null;
722
+ this.maxRetries = options.maxRetries || 3;
723
+ this.retryDelay = options.retryDelay || 1e3;
724
+ this.enableGrounding = options.enableGrounding || false;
725
+ this.groundingConfig = options.groundingConfig || {};
726
+ logger_default.debug(`Transformer keys \u2014 Source: "${this.promptKey}", Target: "${this.answerKey}", Context: "${this.contextKey}"`);
727
+ }
728
+ // ── Chat Create Options Override ──────────────────────────────────────────
729
+ /** @protected */
730
+ _getChatCreateOptions() {
731
+ const opts = super._getChatCreateOptions();
732
+ if (this.enableGrounding) {
733
+ opts.config.tools = [{ googleSearch: this.groundingConfig }];
734
+ logger_default.debug(`Search grounding ENABLED (WARNING: costs $35/1k queries)`);
712
735
  }
736
+ return opts;
713
737
  }
714
- if (options.responseSchema) {
715
- this.chatConfig.responseSchema = options.responseSchema;
716
- }
717
- this.examplesFile = options.examplesFile || null;
718
- this.exampleData = options.exampleData || null;
719
- this.promptKey = options.promptKey || options.sourceKey || "PROMPT";
720
- this.answerKey = options.answerKey || options.targetKey || "ANSWER";
721
- this.contextKey = options.contextKey || "CONTEXT";
722
- this.explanationKey = options.explanationKey || "EXPLANATION";
723
- this.systemInstructionsKey = options.systemInstructionsKey || "SYSTEM";
724
- this.maxRetries = options.maxRetries || 3;
725
- this.retryDelay = options.retryDelay || 1e3;
726
- this.asyncValidator = options.asyncValidator || null;
727
- this.onlyJSON = options.onlyJSON !== void 0 ? options.onlyJSON : true;
728
- this.enableGrounding = options.enableGrounding || false;
729
- this.groundingConfig = options.groundingConfig || {};
730
- this.labels = options.labels || {};
731
- if (Object.keys(this.labels).length > 0 && logger_default.level !== "silent") {
732
- if (!this.vertexai) {
733
- logger_default.warn(`Billing labels are only supported with Vertex AI. Labels will be ignored.`);
734
- } else {
735
- logger_default.debug(`Billing labels configured: ${JSON.stringify(this.labels)}`);
736
- }
737
- }
738
- if (this.promptKey === this.answerKey) {
739
- throw new Error("Source and target keys cannot be the same. Please provide distinct keys.");
740
- }
741
- if (logger_default.level !== "silent") {
742
- logger_default.debug(`Creating AI Transformer with model: ${this.modelName}`);
743
- logger_default.debug(`Using keys - Source: "${this.promptKey}", Target: "${this.answerKey}", Context: "${this.contextKey}"`);
744
- logger_default.debug(`Max output tokens set to: ${this.chatConfig.maxOutputTokens}`);
745
- if (this.vertexai) {
746
- logger_default.debug(`Using Vertex AI - Project: ${this.project}, Location: ${this.location || "global (default)"}`);
747
- if (this.googleAuthOptions?.keyFilename) {
748
- logger_default.debug(`Auth: Service account key file: ${this.googleAuthOptions.keyFilename}`);
749
- } else if (this.googleAuthOptions?.credentials) {
750
- logger_default.debug(`Auth: Inline credentials provided`);
738
+ // ── Seeding ──────────────────────────────────────────────────────────────
739
+ /**
740
+ * Seeds the chat with transformation examples using the configured key mapping.
741
+ * Overrides base seed() to use Transformer-specific keys and support
742
+ * examplesFile/exampleData fallbacks.
743
+ *
744
+ * @param {TransformationExample[]} [examples] - Array of example objects
745
+ * @returns {Promise<Array>} The updated chat history
746
+ */
747
+ async seed(examples) {
748
+ await this.init();
749
+ if (!examples || !Array.isArray(examples) || examples.length === 0) {
750
+ if (this.examplesFile) {
751
+ logger_default.debug(`No examples provided, loading from file: ${this.examplesFile}`);
752
+ try {
753
+ const filePath = import_path.default.resolve(this.examplesFile);
754
+ const raw = await import_promises.default.readFile(filePath, "utf-8");
755
+ examples = JSON.parse(raw);
756
+ } catch (err) {
757
+ throw new Error(`Could not load examples from file: ${this.examplesFile}. ${err.message}`);
758
+ }
759
+ } else if (this.exampleData) {
760
+ logger_default.debug(`Using example data provided in options.`);
761
+ if (Array.isArray(this.exampleData)) {
762
+ examples = this.exampleData;
763
+ } else {
764
+ throw new Error(`Invalid example data provided. Expected an array of examples.`);
765
+ }
751
766
  } else {
752
- logger_default.debug(`Auth: Application Default Credentials (ADC)`);
767
+ logger_default.debug("No examples provided and no examples file specified. Skipping seeding.");
768
+ return this.getHistory();
753
769
  }
754
- } else {
755
- logger_default.debug(`Using Gemini API with key: ${this.apiKey.substring(0, 10)}...`);
756
- }
757
- logger_default.debug(`Grounding ${this.enableGrounding ? "ENABLED" : "DISABLED"} (costs $35/1k queries)`);
758
- }
759
- const clientOptions = this.vertexai ? {
760
- vertexai: true,
761
- project: this.project,
762
- ...this.location && { location: this.location },
763
- ...this.googleAuthOptions && { googleAuthOptions: this.googleAuthOptions }
764
- } : { apiKey: this.apiKey };
765
- const ai = new import_genai2.GoogleGenAI(clientOptions);
766
- this.genAIClient = ai;
767
- this.chat = null;
768
- }
769
- async function initChat(force = false) {
770
- if (this.chat && !force) return;
771
- logger_default.debug(`Initializing Gemini chat session with model: ${this.modelName}...`);
772
- const chatOptions = {
773
- model: this.modelName,
774
- // @ts-ignore
775
- config: {
776
- ...this.chatConfig,
777
- ...this.vertexai && Object.keys(this.labels).length > 0 && { labels: this.labels }
778
- },
779
- history: []
780
- };
781
- if (this.enableGrounding) {
782
- chatOptions.config.tools = [{
783
- googleSearch: this.groundingConfig
784
- }];
785
- logger_default.debug(`Search grounding ENABLED for this session (WARNING: costs $35/1k queries)`);
786
- }
787
- this.chat = await this.genAIClient.chats.create(chatOptions);
788
- try {
789
- await this.genAIClient.models.list();
790
- logger_default.debug("Gemini API connection successful.");
791
- } catch (e) {
792
- throw new Error(`Gemini chat initialization failed: ${e.message}`);
793
- }
794
- logger_default.debug("Gemini chat session initialized.");
795
- }
796
- async function seedWithExamples(examples) {
797
- await this.init();
798
- if (!examples || !Array.isArray(examples) || examples.length === 0) {
799
- if (this.examplesFile) {
800
- logger_default.debug(`No examples provided, loading from file: ${this.examplesFile}`);
801
- try {
802
- examples = await import_ak_tools.default.load(import_path.default.resolve(this.examplesFile), true);
803
- } catch (err) {
804
- throw new Error(`Could not load examples from file: ${this.examplesFile}. Please check the file path and format.`);
805
- }
806
- } else if (this.exampleData) {
807
- logger_default.debug(`Using example data provided in options.`);
808
- if (Array.isArray(this.exampleData)) {
809
- examples = this.exampleData;
810
- } else {
811
- throw new Error(`Invalid example data provided. Expected an array of examples.`);
812
- }
813
- } else {
814
- logger_default.debug("No examples provided and no examples file specified. Skipping seeding.");
815
- return;
816
- }
817
- }
818
- const instructionExample = examples.find((ex) => ex[this.systemInstructionsKey]);
819
- if (instructionExample) {
820
- logger_default.debug(`Found system instructions in examples; reinitializing chat with new instructions.`);
821
- this.systemInstructions = instructionExample[this.systemInstructionsKey];
822
- this.chatConfig.systemInstruction = this.systemInstructions;
823
- await this.init(true);
824
- }
825
- logger_default.debug(`Seeding chat with ${examples.length} transformation examples...`);
826
- const historyToAdd = [];
827
- for (const example of examples) {
828
- const contextValue = example[this.contextKey] || "";
829
- const promptValue = example[this.promptKey] || "";
830
- const answerValue = example[this.answerKey] || "";
831
- const explanationValue = example[this.explanationKey] || "";
832
- let userText = "";
833
- let modelResponse = {};
834
- if (contextValue) {
835
- let contextText = isJSON(contextValue) ? JSON.stringify(contextValue, null, 2) : contextValue;
836
- userText += `CONTEXT:
837
- ${contextText}
838
-
839
- `;
840
770
  }
841
- if (promptValue) {
842
- let promptText = isJSON(promptValue) ? JSON.stringify(promptValue, null, 2) : promptValue;
843
- userText += promptText;
844
- }
845
- if (answerValue) modelResponse.data = answerValue;
846
- if (explanationValue) modelResponse.explanation = explanationValue;
847
- const modelText = JSON.stringify(modelResponse, null, 2);
848
- if (userText.trim().length && modelText.trim().length > 0) {
849
- historyToAdd.push({ role: "user", parts: [{ text: userText.trim() }] });
850
- historyToAdd.push({ role: "model", parts: [{ text: modelText.trim() }] });
851
- }
852
- }
853
- const currentHistory = this?.chat?.getHistory() || [];
854
- logger_default.debug(`Adding ${historyToAdd.length} examples to chat history (${currentHistory.length} current examples)...`);
855
- this.chat = await this.genAIClient.chats.create({
856
- model: this.modelName,
857
- // @ts-ignore
858
- config: {
859
- ...this.chatConfig,
860
- ...this.vertexai && Object.keys(this.labels).length > 0 && { labels: this.labels }
861
- },
862
- history: [...currentHistory, ...historyToAdd]
863
- });
864
- this.exampleCount = currentHistory.length + historyToAdd.length;
865
- const newHistory = this.chat.getHistory();
866
- logger_default.debug(`Created new chat session with ${newHistory.length} examples.`);
867
- return newHistory;
868
- }
869
- async function rawMessage(sourcePayload, messageOptions = {}) {
870
- if (!this.chat) {
871
- throw new Error("Chat session not initialized.");
771
+ return await super.seed(examples, {
772
+ promptKey: this.promptKey,
773
+ answerKey: this.answerKey,
774
+ contextKey: this.contextKey,
775
+ explanationKey: this.explanationKey,
776
+ systemPromptKey: this.systemPromptKey
777
+ });
872
778
  }
873
- const actualPayload = typeof sourcePayload === "string" ? sourcePayload : JSON.stringify(sourcePayload, null, 2);
874
- const mergedLabels = { ...this.labels, ...messageOptions.labels || {} };
875
- const hasLabels = this.vertexai && Object.keys(mergedLabels).length > 0;
876
- try {
877
- const sendParams = { message: actualPayload };
878
- if (hasLabels) {
879
- sendParams.config = { labels: mergedLabels };
779
+ // ── Primary Send Method ──────────────────────────────────────────────────
780
+ /**
781
+ * Transforms a payload using the seeded examples and model.
782
+ * Includes validation and automatic retry with AI-powered error correction.
783
+ *
784
+ * @param {Object|string} payload - The source payload to transform
785
+ * @param {import('./types').SendOptions} [opts={}] - Per-message options
786
+ * @param {AsyncValidatorFunction|null} [validatorFn] - Validator for this call (overrides constructor validator)
787
+ * @returns {Promise<Object>} The transformed payload
788
+ */
789
+ async send(payload, opts = {}, validatorFn = null) {
790
+ if (!this.chatSession) {
791
+ throw new Error("Chat session not initialized. Please call init() first.");
880
792
  }
881
- const result = await this.chat.sendMessage(sendParams);
882
- this.lastResponseMetadata = {
883
- modelVersion: result.modelVersion || null,
884
- requestedModel: this.modelName,
885
- promptTokens: result.usageMetadata?.promptTokenCount || 0,
886
- responseTokens: result.usageMetadata?.candidatesTokenCount || 0,
887
- totalTokens: result.usageMetadata?.totalTokenCount || 0,
888
- timestamp: Date.now()
889
- };
890
- if (result.usageMetadata && logger_default.level !== "silent") {
891
- logger_default.debug(`API response metadata: ${JSON.stringify({
892
- modelVersion: result.modelVersion || "not-provided",
893
- requestedModel: this.modelName,
894
- promptTokens: result.usageMetadata.promptTokenCount,
895
- responseTokens: result.usageMetadata.candidatesTokenCount,
896
- totalTokens: result.usageMetadata.totalTokenCount
897
- })}`);
793
+ const validator = validatorFn || this.asyncValidator;
794
+ if (opts.stateless) {
795
+ return await this._statelessSend(payload, opts, validator);
898
796
  }
899
- const modelResponse = result.text;
900
- const extractedJSON = extractJSON(modelResponse);
901
- if (extractedJSON?.data) {
902
- return extractedJSON.data;
797
+ const maxRetries = opts.maxRetries ?? this.maxRetries;
798
+ const retryDelay = opts.retryDelay ?? this.retryDelay;
799
+ if (opts.enableGrounding !== void 0 && opts.enableGrounding !== this.enableGrounding) {
800
+ const originalGrounding = this.enableGrounding;
801
+ const originalConfig = this.groundingConfig;
802
+ try {
803
+ this.enableGrounding = opts.enableGrounding;
804
+ this.groundingConfig = opts.groundingConfig ?? this.groundingConfig;
805
+ await this.init(true);
806
+ } catch (error) {
807
+ this.enableGrounding = originalGrounding;
808
+ this.groundingConfig = originalConfig;
809
+ throw error;
810
+ }
811
+ opts._restoreGrounding = async () => {
812
+ this.enableGrounding = originalGrounding;
813
+ this.groundingConfig = originalConfig;
814
+ await this.init(true);
815
+ };
903
816
  }
904
- return extractedJSON;
905
- } catch (error) {
906
- if (this.onlyJSON && error.message.includes("Could not extract valid JSON")) {
907
- throw new Error(`Invalid JSON response from Gemini: ${error.message}`);
817
+ let lastPayload = this._preparePayload(payload);
818
+ const messageOptions = {};
819
+ if (opts.labels) messageOptions.labels = opts.labels;
820
+ this._cumulativeUsage = { promptTokens: 0, responseTokens: 0, totalTokens: 0, attempts: 0 };
821
+ let lastError = null;
822
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
823
+ try {
824
+ const transformedPayload = attempt === 0 ? await this.rawSend(lastPayload, messageOptions) : await this.rebuild(lastPayload, lastError.message);
825
+ if (this.lastResponseMetadata) {
826
+ this._cumulativeUsage.promptTokens += this.lastResponseMetadata.promptTokens || 0;
827
+ this._cumulativeUsage.responseTokens += this.lastResponseMetadata.responseTokens || 0;
828
+ this._cumulativeUsage.totalTokens += this.lastResponseMetadata.totalTokens || 0;
829
+ this._cumulativeUsage.attempts = attempt + 1;
830
+ }
831
+ lastPayload = transformedPayload;
832
+ if (validator) {
833
+ await validator(transformedPayload);
834
+ }
835
+ logger_default.debug(`Transformation succeeded on attempt ${attempt + 1}`);
836
+ if (opts._restoreGrounding) await opts._restoreGrounding();
837
+ return transformedPayload;
838
+ } catch (error) {
839
+ lastError = error;
840
+ logger_default.warn(`Attempt ${attempt + 1} failed: ${error.message}`);
841
+ if (attempt >= maxRetries) {
842
+ logger_default.error(`All ${maxRetries + 1} attempts failed.`);
843
+ if (opts._restoreGrounding) await opts._restoreGrounding();
844
+ throw new Error(`Transformation failed after ${maxRetries + 1} attempts. Last error: ${error.message}`);
845
+ }
846
+ const delay = retryDelay * Math.pow(2, attempt);
847
+ await new Promise((res) => setTimeout(res, delay));
848
+ }
908
849
  }
909
- throw new Error(`Transformation failed: ${error.message}`);
910
850
  }
911
- }
912
- async function prepareAndValidateMessage(sourcePayload, options = {}, validatorFn = null) {
913
- if (!this.chat) {
914
- throw new Error("Chat session not initialized. Please call init() first.");
915
- }
916
- if (options.stateless) {
917
- return await statelessMessage.call(this, sourcePayload, options, validatorFn);
918
- }
919
- const maxRetries = options.maxRetries ?? this.maxRetries;
920
- const retryDelay = options.retryDelay ?? this.retryDelay;
921
- const enableGroundingForMessage = options.enableGrounding ?? this.enableGrounding;
922
- const groundingConfigForMessage = options.groundingConfig ?? this.groundingConfig;
923
- if (enableGroundingForMessage !== this.enableGrounding) {
924
- const originalGrounding = this.enableGrounding;
925
- const originalConfig = this.groundingConfig;
926
- try {
927
- this.enableGrounding = enableGroundingForMessage;
928
- this.groundingConfig = groundingConfigForMessage;
929
- await this.init(true);
930
- if (enableGroundingForMessage) {
931
- logger_default.warn(`Search grounding ENABLED for this message (WARNING: costs $35/1k queries)`);
932
- } else {
933
- logger_default.debug(`Search grounding DISABLED for this message`);
934
- }
935
- } catch (error) {
936
- this.enableGrounding = originalGrounding;
937
- this.groundingConfig = originalConfig;
938
- throw error;
851
+ // ── Raw Send ─────────────────────────────────────────────────────────────
852
+ /**
853
+ * Sends a single prompt to the model and parses the JSON response.
854
+ * No validation or retry logic.
855
+ *
856
+ * @param {Object|string} payload - The source payload
857
+ * @param {Object} [messageOptions={}] - Per-message options (e.g., labels)
858
+ * @returns {Promise<Object>} The transformed payload
859
+ */
860
+ async rawSend(payload, messageOptions = {}) {
861
+ if (!this.chatSession) {
862
+ throw new Error("Chat session not initialized.");
939
863
  }
940
- const restoreGrounding = async () => {
941
- this.enableGrounding = originalGrounding;
942
- this.groundingConfig = originalConfig;
943
- await this.init(true);
944
- };
945
- options._restoreGrounding = restoreGrounding;
946
- }
947
- let lastError = null;
948
- let lastPayload = null;
949
- if (sourcePayload && isJSON(sourcePayload)) {
950
- lastPayload = JSON.stringify(sourcePayload, null, 2);
951
- } else if (typeof sourcePayload === "string") {
952
- lastPayload = sourcePayload;
953
- } else if (typeof sourcePayload === "boolean" || typeof sourcePayload === "number") {
954
- lastPayload = sourcePayload.toString();
955
- } else if (sourcePayload === null || sourcePayload === void 0) {
956
- lastPayload = JSON.stringify({});
957
- } else {
958
- throw new Error("Invalid source payload. Must be a JSON object or string.");
959
- }
960
- const messageOptions = {};
961
- if (options.labels) {
962
- messageOptions.labels = options.labels;
963
- }
964
- this._cumulativeUsage = {
965
- promptTokens: 0,
966
- responseTokens: 0,
967
- totalTokens: 0,
968
- attempts: 0
969
- };
970
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
864
+ const actualPayload = typeof payload === "string" ? payload : JSON.stringify(payload, null, 2);
865
+ const mergedLabels = { ...this.labels, ...messageOptions.labels || {} };
866
+ const hasLabels = this.vertexai && Object.keys(mergedLabels).length > 0;
971
867
  try {
972
- const transformedPayload = attempt === 0 ? await this.rawMessage(lastPayload, messageOptions) : await this.rebuild(lastPayload, lastError.message);
973
- if (this.lastResponseMetadata) {
974
- this._cumulativeUsage.promptTokens += this.lastResponseMetadata.promptTokens || 0;
975
- this._cumulativeUsage.responseTokens += this.lastResponseMetadata.responseTokens || 0;
976
- this._cumulativeUsage.totalTokens += this.lastResponseMetadata.totalTokens || 0;
977
- this._cumulativeUsage.attempts = attempt + 1;
868
+ const sendParams = { message: actualPayload };
869
+ if (hasLabels) {
870
+ sendParams.config = { labels: mergedLabels };
978
871
  }
979
- lastPayload = transformedPayload;
980
- if (validatorFn) {
981
- await validatorFn(transformedPayload);
872
+ const result = await this.chatSession.sendMessage(sendParams);
873
+ this._captureMetadata(result);
874
+ if (result.usageMetadata && logger_default.level !== "silent") {
875
+ logger_default.debug(`API response: model=${result.modelVersion || "unknown"}, tokens=${result.usageMetadata.totalTokenCount}`);
982
876
  }
983
- logger_default.debug(`Transformation succeeded on attempt ${attempt + 1}`);
984
- if (options._restoreGrounding) {
985
- await options._restoreGrounding();
877
+ const modelResponse = result.text;
878
+ const extractedJSON = extractJSON(modelResponse);
879
+ if (extractedJSON?.data) {
880
+ return extractedJSON.data;
986
881
  }
987
- return transformedPayload;
882
+ return extractedJSON;
988
883
  } catch (error) {
989
- lastError = error;
990
- logger_default.warn(`Attempt ${attempt + 1} failed: ${error.message}`);
991
- if (attempt >= maxRetries) {
992
- logger_default.error(`All ${maxRetries + 1} attempts failed.`);
993
- if (options._restoreGrounding) {
994
- await options._restoreGrounding();
995
- }
996
- throw new Error(`Transformation failed after ${maxRetries + 1} attempts. Last error: ${error.message}`);
884
+ if (this.onlyJSON && error.message.includes("Could not extract valid JSON")) {
885
+ throw new Error(`Invalid JSON response from Gemini: ${error.message}`);
997
886
  }
998
- const delay = retryDelay * Math.pow(2, attempt);
999
- await new Promise((res) => setTimeout(res, delay));
887
+ throw new Error(`Transformation failed: ${error.message}`);
1000
888
  }
1001
889
  }
1002
- }
1003
- async function rebuildPayload(lastPayload, serverError) {
1004
- await this.init();
1005
- const prompt = `
890
+ // ── Rebuild ──────────────────────────────────────────────────────────────
891
+ /**
892
+ * Asks the model to fix a payload that failed validation.
893
+ *
894
+ * @param {Object} lastPayload - The payload that failed
895
+ * @param {string} serverError - The error message
896
+ * @returns {Promise<Object>} Corrected payload
897
+ */
898
+ async rebuild(lastPayload, serverError) {
899
+ await this.init();
900
+ const prompt = `
1006
901
  The previous JSON payload (below) failed validation.
1007
902
  The server's error message is quoted afterward.
1008
903
 
@@ -1016,512 +911,952 @@ ${serverError}
1016
911
  Please return a NEW JSON payload that corrects the issue.
1017
912
  Respond with JSON only \u2013 no comments or explanations.
1018
913
  `;
1019
- let result;
1020
- try {
1021
- result = await this.chat.sendMessage({ message: prompt });
1022
- this.lastResponseMetadata = {
1023
- modelVersion: result.modelVersion || null,
1024
- requestedModel: this.modelName,
1025
- promptTokens: result.usageMetadata?.promptTokenCount || 0,
1026
- responseTokens: result.usageMetadata?.candidatesTokenCount || 0,
1027
- totalTokens: result.usageMetadata?.totalTokenCount || 0,
1028
- timestamp: Date.now()
1029
- };
1030
- if (result.usageMetadata && logger_default.level !== "silent") {
1031
- logger_default.debug(`Rebuild response metadata - tokens used: ${result.usageMetadata.totalTokenCount}`);
914
+ let result;
915
+ try {
916
+ result = await this.chatSession.sendMessage({ message: prompt });
917
+ this._captureMetadata(result);
918
+ } catch (err) {
919
+ throw new Error(`Gemini call failed while repairing payload: ${err.message}`);
920
+ }
921
+ try {
922
+ const text = result.text ?? result.response ?? "";
923
+ return typeof text === "object" ? text : JSON.parse(text);
924
+ } catch (parseErr) {
925
+ throw new Error(`Gemini returned non-JSON while repairing payload: ${parseErr.message}`);
1032
926
  }
1033
- } catch (err) {
1034
- throw new Error(`Gemini call failed while repairing payload: ${err.message}`);
1035
- }
1036
- try {
1037
- const text = result.text ?? result.response ?? "";
1038
- return typeof text === "object" ? text : JSON.parse(text);
1039
- } catch (parseErr) {
1040
- throw new Error(`Gemini returned non-JSON while repairing payload: ${parseErr.message}`);
1041
927
  }
1042
- }
1043
- async function estimateInputTokens(nextPayload) {
1044
- const contents = [];
1045
- if (this.systemInstructions) {
1046
- contents.push({ parts: [{ text: this.systemInstructions }] });
1047
- }
1048
- if (this.chat && typeof this.chat.getHistory === "function") {
1049
- const history = this.chat.getHistory();
1050
- if (Array.isArray(history) && history.length > 0) {
1051
- contents.push(...history);
1052
- }
1053
- }
1054
- const nextMessage = typeof nextPayload === "string" ? nextPayload : JSON.stringify(nextPayload, null, 2);
1055
- contents.push({ parts: [{ text: nextMessage }] });
1056
- const resp = await this.genAIClient.models.countTokens({
1057
- model: this.modelName,
1058
- contents
1059
- });
1060
- return { inputTokens: resp.totalTokens };
1061
- }
1062
- var MODEL_PRICING = {
1063
- "gemini-2.5-flash": { input: 0.15, output: 0.6 },
1064
- "gemini-2.5-flash-lite": { input: 0.02, output: 0.1 },
1065
- "gemini-2.5-pro": { input: 2.5, output: 10 },
1066
- "gemini-3-pro": { input: 2, output: 12 },
1067
- "gemini-3-pro-preview": { input: 2, output: 12 },
1068
- "gemini-2.0-flash": { input: 0.1, output: 0.4 },
1069
- "gemini-2.0-flash-lite": { input: 0.02, output: 0.1 }
1070
- };
1071
- async function estimateCost(nextPayload) {
1072
- const tokenInfo = await this.estimate(nextPayload);
1073
- const pricing = MODEL_PRICING[this.modelName] || { input: 0, output: 0 };
1074
- return {
1075
- inputTokens: tokenInfo.inputTokens,
1076
- model: this.modelName,
1077
- pricing,
1078
- estimatedInputCost: tokenInfo.inputTokens / 1e6 * pricing.input,
1079
- note: "Cost is for input tokens only; output cost depends on response length"
1080
- };
1081
- }
1082
- async function resetChat() {
1083
- if (this.chat) {
1084
- logger_default.debug("Resetting Gemini chat session...");
1085
- const chatOptions = {
928
+ // ── Stateless Send ───────────────────────────────────────────────────────
929
+ /**
930
+ * Sends a one-off message using generateContent (not chat).
931
+ * Does NOT affect chat history.
932
+ * @param {Object|string} payload
933
+ * @param {Object} [opts={}]
934
+ * @param {AsyncValidatorFunction|null} [validatorFn]
935
+ * @returns {Promise<Object>}
936
+ * @private
937
+ */
938
+ async _statelessSend(payload, opts = {}, validatorFn = null) {
939
+ if (!this.chatSession) {
940
+ throw new Error("Chat session not initialized. Please call init() first.");
941
+ }
942
+ const payloadStr = typeof payload === "string" ? payload : JSON.stringify(payload, null, 2);
943
+ const contents = [];
944
+ if (this.exampleCount > 0) {
945
+ const history = this.chatSession.getHistory();
946
+ const exampleHistory = history.slice(0, this.exampleCount);
947
+ contents.push(...exampleHistory);
948
+ }
949
+ contents.push({ role: "user", parts: [{ text: payloadStr }] });
950
+ const mergedLabels = { ...this.labels, ...opts.labels || {} };
951
+ const result = await this.genAIClient.models.generateContent({
1086
952
  model: this.modelName,
1087
- // @ts-ignore
953
+ contents,
1088
954
  config: {
1089
955
  ...this.chatConfig,
1090
- ...this.vertexai && Object.keys(this.labels).length > 0 && { labels: this.labels }
1091
- },
1092
- history: []
956
+ ...this.vertexai && Object.keys(mergedLabels).length > 0 && { labels: mergedLabels }
957
+ }
958
+ });
959
+ this._captureMetadata(result);
960
+ this._cumulativeUsage = {
961
+ promptTokens: this.lastResponseMetadata.promptTokens,
962
+ responseTokens: this.lastResponseMetadata.responseTokens,
963
+ totalTokens: this.lastResponseMetadata.totalTokens,
964
+ attempts: 1
1093
965
  };
1094
- if (this.enableGrounding) {
1095
- chatOptions.config.tools = [{
1096
- googleSearch: this.groundingConfig
1097
- }];
1098
- logger_default.debug(`Search grounding preserved during reset (WARNING: costs $35/1k queries)`);
966
+ const modelResponse = result.text;
967
+ const extractedJSON = extractJSON(modelResponse);
968
+ let transformedPayload = extractedJSON?.data ? extractedJSON.data : extractedJSON;
969
+ if (validatorFn) {
970
+ await validatorFn(transformedPayload);
1099
971
  }
1100
- this.chat = await this.genAIClient.chats.create(chatOptions);
1101
- logger_default.debug("Chat session reset.");
1102
- } else {
1103
- logger_default.warn("Cannot reset chat session: chat not yet initialized.");
1104
- }
1105
- }
1106
- function getChatHistory() {
1107
- if (!this.chat) {
1108
- logger_default.warn("Chat session not initialized. No history available.");
1109
- return [];
972
+ return transformedPayload;
1110
973
  }
1111
- return this.chat.getHistory();
1112
- }
1113
- async function updateSystemInstructions(newInstructions) {
1114
- if (!newInstructions || typeof newInstructions !== "string") {
1115
- throw new Error("System instructions must be a non-empty string");
1116
- }
1117
- this.systemInstructions = newInstructions.trim();
1118
- this.chatConfig.systemInstruction = this.systemInstructions;
1119
- logger_default.debug("Updating system instructions and reinitializing chat...");
1120
- await this.init(true);
1121
- }
1122
- async function clearConversation() {
1123
- if (!this.chat) {
1124
- logger_default.warn("Cannot clear conversation: chat not initialized.");
1125
- return;
1126
- }
1127
- const history = this.chat.getHistory();
1128
- const exampleHistory = history.slice(0, this.exampleCount || 0);
1129
- this.chat = await this.genAIClient.chats.create({
1130
- model: this.modelName,
1131
- // @ts-ignore
1132
- config: {
1133
- ...this.chatConfig,
1134
- ...this.vertexai && Object.keys(this.labels).length > 0 && { labels: this.labels }
1135
- },
1136
- history: exampleHistory
1137
- });
1138
- this.lastResponseMetadata = null;
1139
- this._cumulativeUsage = {
1140
- promptTokens: 0,
1141
- responseTokens: 0,
1142
- totalTokens: 0,
1143
- attempts: 0
1144
- };
1145
- logger_default.debug(`Conversation cleared. Preserved ${exampleHistory.length} example items.`);
1146
- }
1147
- function getLastUsage() {
1148
- if (!this.lastResponseMetadata) {
1149
- return null;
1150
- }
1151
- const meta = this.lastResponseMetadata;
1152
- const cumulative = this._cumulativeUsage || { promptTokens: 0, responseTokens: 0, totalTokens: 0, attempts: 1 };
1153
- const useCumulative = cumulative.attempts > 0;
1154
- return {
1155
- // Token breakdown for billing - CUMULATIVE across all retry attempts
1156
- promptTokens: useCumulative ? cumulative.promptTokens : meta.promptTokens,
1157
- responseTokens: useCumulative ? cumulative.responseTokens : meta.responseTokens,
1158
- totalTokens: useCumulative ? cumulative.totalTokens : meta.totalTokens,
1159
- // Number of attempts (1 = success on first try, 2+ = retries were needed)
1160
- attempts: useCumulative ? cumulative.attempts : 1,
1161
- // Model verification for billing cross-check
1162
- modelVersion: meta.modelVersion,
1163
- // Actual model that responded (e.g., 'gemini-2.5-flash-001')
1164
- requestedModel: meta.requestedModel,
1165
- // Model you requested (e.g., 'gemini-2.5-flash')
1166
- // Timestamp for audit trail
1167
- timestamp: meta.timestamp
1168
- };
1169
- }
1170
- async function statelessMessage(sourcePayload, options = {}, validatorFn = null) {
1171
- if (!this.chat) {
1172
- throw new Error("Chat session not initialized. Please call init() first.");
1173
- }
1174
- const payloadStr = typeof sourcePayload === "string" ? sourcePayload : JSON.stringify(sourcePayload, null, 2);
1175
- const contents = [];
1176
- if (this.exampleCount > 0) {
1177
- const history = this.chat.getHistory();
1178
- const exampleHistory = history.slice(0, this.exampleCount);
1179
- contents.push(...exampleHistory);
1180
- }
1181
- contents.push({ role: "user", parts: [{ text: payloadStr }] });
1182
- const mergedLabels = { ...this.labels, ...options.labels || {} };
1183
- const result = await this.genAIClient.models.generateContent({
1184
- model: this.modelName,
1185
- contents,
1186
- config: {
1187
- ...this.chatConfig,
1188
- ...this.vertexai && Object.keys(mergedLabels).length > 0 && { labels: mergedLabels }
1189
- }
1190
- });
1191
- this.lastResponseMetadata = {
1192
- modelVersion: result.modelVersion || null,
1193
- requestedModel: this.modelName,
1194
- promptTokens: result.usageMetadata?.promptTokenCount || 0,
1195
- responseTokens: result.usageMetadata?.candidatesTokenCount || 0,
1196
- totalTokens: result.usageMetadata?.totalTokenCount || 0,
1197
- timestamp: Date.now()
1198
- };
1199
- this._cumulativeUsage = {
1200
- promptTokens: this.lastResponseMetadata.promptTokens,
1201
- responseTokens: this.lastResponseMetadata.responseTokens,
1202
- totalTokens: this.lastResponseMetadata.totalTokens,
1203
- attempts: 1
1204
- };
1205
- if (result.usageMetadata && logger_default.level !== "silent") {
1206
- logger_default.debug(`Stateless message metadata: ${JSON.stringify({
1207
- modelVersion: result.modelVersion || "not-provided",
1208
- promptTokens: result.usageMetadata.promptTokenCount,
1209
- responseTokens: result.usageMetadata.candidatesTokenCount
1210
- })}`);
1211
- }
1212
- const modelResponse = result.text;
1213
- const extractedJSON = extractJSON(modelResponse);
1214
- let transformedPayload = extractedJSON?.data ? extractedJSON.data : extractedJSON;
1215
- if (validatorFn) {
1216
- await validatorFn(transformedPayload);
1217
- }
1218
- return transformedPayload;
1219
- }
1220
- function attemptJSONRecovery(text, maxAttempts = 100) {
1221
- if (!text || typeof text !== "string") return null;
1222
- try {
1223
- return JSON.parse(text);
1224
- } catch (e) {
974
+ // ── History Management ───────────────────────────────────────────────────
975
+ /**
976
+ * Clears conversation history while preserving seeded examples.
977
+ * @returns {Promise<void>}
978
+ */
979
+ async clearHistory() {
980
+ if (!this.chatSession) {
981
+ logger_default.warn("Cannot clear history: chat not initialized.");
982
+ return;
983
+ }
984
+ const history = this.chatSession.getHistory();
985
+ const exampleHistory = history.slice(0, this.exampleCount || 0);
986
+ this.chatSession = this._createChatSession(exampleHistory);
987
+ this.lastResponseMetadata = null;
988
+ this._cumulativeUsage = { promptTokens: 0, responseTokens: 0, totalTokens: 0, attempts: 0 };
989
+ logger_default.debug(`Conversation cleared. Preserved ${exampleHistory.length} example items.`);
1225
990
  }
1226
- let workingText = text.trim();
1227
- let braces = 0;
1228
- let brackets = 0;
1229
- let inString = false;
1230
- let escapeNext = false;
1231
- for (let j = 0; j < workingText.length; j++) {
1232
- const char = workingText[j];
1233
- if (escapeNext) {
1234
- escapeNext = false;
1235
- continue;
991
+ /**
992
+ * Fully resets the chat session, clearing all history including examples.
993
+ * @returns {Promise<void>}
994
+ */
995
+ async reset() {
996
+ if (this.chatSession) {
997
+ logger_default.debug("Resetting chat session...");
998
+ this.chatSession = this._createChatSession([]);
999
+ this.exampleCount = 0;
1000
+ logger_default.debug("Chat session reset.");
1001
+ } else {
1002
+ logger_default.warn("Cannot reset: chat not yet initialized.");
1236
1003
  }
1237
- if (char === "\\") {
1238
- escapeNext = true;
1239
- continue;
1004
+ }
1005
+ /**
1006
+ * Updates system prompt and reinitializes the chat session.
1007
+ * @param {string} newPrompt - The new system prompt
1008
+ * @returns {Promise<void>}
1009
+ */
1010
+ async updateSystemPrompt(newPrompt) {
1011
+ if (!newPrompt || typeof newPrompt !== "string") {
1012
+ throw new Error("System prompt must be a non-empty string");
1240
1013
  }
1241
- if (char === '"') {
1242
- inString = !inString;
1243
- continue;
1014
+ this.systemPrompt = newPrompt.trim();
1015
+ this.chatConfig.systemInstruction = this.systemPrompt;
1016
+ logger_default.debug("Updating system prompt and reinitializing chat...");
1017
+ await this.init(true);
1018
+ }
1019
+ // ── Private Helpers ──────────────────────────────────────────────────────
1020
+ /**
1021
+ * Normalizes a payload to a string for sending.
1022
+ * @param {*} payload
1023
+ * @returns {string}
1024
+ * @private
1025
+ */
1026
+ _preparePayload(payload) {
1027
+ if (payload && isJSON(payload)) {
1028
+ return JSON.stringify(payload, null, 2);
1029
+ } else if (typeof payload === "string") {
1030
+ return payload;
1031
+ } else if (typeof payload === "boolean" || typeof payload === "number") {
1032
+ return payload.toString();
1033
+ } else if (payload === null || payload === void 0) {
1034
+ return JSON.stringify({});
1035
+ } else {
1036
+ throw new Error("Invalid source payload. Must be a JSON object or string.");
1244
1037
  }
1245
- if (!inString) {
1246
- if (char === "{") braces++;
1247
- else if (char === "}") braces--;
1248
- else if (char === "[") brackets++;
1249
- else if (char === "]") brackets--;
1038
+ }
1039
+ };
1040
+ var transformer_default = Transformer;
1041
+
1042
+ // chat.js
1043
+ var Chat = class extends base_default {
1044
+ /**
1045
+ * @param {ChatOptions} [options={}]
1046
+ */
1047
+ constructor(options = {}) {
1048
+ if (options.systemPrompt === void 0) {
1049
+ options = { ...options, systemPrompt: "You are a helpful AI assistant." };
1250
1050
  }
1051
+ super(options);
1052
+ logger_default.debug(`Chat created with model: ${this.modelName}`);
1251
1053
  }
1252
- if ((braces > 0 || brackets > 0 || inString) && workingText.length > 2) {
1253
- let fixedText = workingText;
1254
- if (inString) {
1255
- fixedText += '"';
1054
+ /**
1055
+ * Send a text message and get a response. Adds to conversation history.
1056
+ *
1057
+ * @param {string} message - The user's message
1058
+ * @param {Object} [opts={}] - Per-message options
1059
+ * @param {Record<string, string>} [opts.labels] - Per-message billing labels
1060
+ * @returns {Promise<ChatResponse>} Response with text and usage data
1061
+ */
1062
+ async send(message, opts = {}) {
1063
+ if (!this.chatSession) await this.init();
1064
+ const mergedLabels = { ...this.labels, ...opts.labels || {} };
1065
+ const hasLabels = this.vertexai && Object.keys(mergedLabels).length > 0;
1066
+ const sendParams = { message };
1067
+ if (hasLabels) {
1068
+ sendParams.config = { labels: mergedLabels };
1256
1069
  }
1257
- while (braces > 0) {
1258
- fixedText += "}";
1259
- braces--;
1070
+ const result = await this.chatSession.sendMessage(sendParams);
1071
+ this._captureMetadata(result);
1072
+ this._cumulativeUsage = {
1073
+ promptTokens: this.lastResponseMetadata.promptTokens,
1074
+ responseTokens: this.lastResponseMetadata.responseTokens,
1075
+ totalTokens: this.lastResponseMetadata.totalTokens,
1076
+ attempts: 1
1077
+ };
1078
+ return {
1079
+ text: result.text || "",
1080
+ usage: this.getLastUsage()
1081
+ };
1082
+ }
1083
+ };
1084
+ var chat_default = Chat;
1085
+
1086
+ // message.js
1087
+ var Message = class extends base_default {
1088
+ /**
1089
+ * @param {MessageOptions} [options={}]
1090
+ */
1091
+ constructor(options = {}) {
1092
+ super(options);
1093
+ if (options.responseSchema) {
1094
+ this.chatConfig.responseSchema = options.responseSchema;
1260
1095
  }
1261
- while (brackets > 0) {
1262
- fixedText += "]";
1263
- brackets--;
1096
+ if (options.responseMimeType) {
1097
+ this.chatConfig.responseMimeType = options.responseMimeType;
1264
1098
  }
1099
+ this._isStructured = !!(options.responseSchema || options.responseMimeType === "application/json");
1100
+ logger_default.debug(`Message created (structured=${this._isStructured})`);
1101
+ }
1102
+ /**
1103
+ * Initialize the Message client.
1104
+ * Override: creates genAIClient only, NO chat session (stateless).
1105
+ * @param {boolean} [force=false]
1106
+ * @returns {Promise<void>}
1107
+ */
1108
+ async init(force = false) {
1109
+ if (this._initialized && !force) return;
1110
+ logger_default.debug(`Initializing ${this.constructor.name} with model: ${this.modelName}...`);
1265
1111
  try {
1266
- const result = JSON.parse(fixedText);
1267
- if (logger_default.level !== "silent") {
1268
- logger_default.warn(`JSON response appears truncated (possibly hit maxOutputTokens limit). Recovered by adding closing characters.`);
1269
- }
1270
- return result;
1112
+ await this.genAIClient.models.list();
1113
+ logger_default.debug(`${this.constructor.name}: API connection successful.`);
1271
1114
  } catch (e) {
1115
+ throw new Error(`${this.constructor.name} initialization failed: ${e.message}`);
1272
1116
  }
1117
+ this._initialized = true;
1118
+ logger_default.debug(`${this.constructor.name}: Initialized (stateless mode).`);
1273
1119
  }
1274
- for (let i = 0; i < maxAttempts && workingText.length > 2; i++) {
1275
- workingText = workingText.slice(0, -1);
1276
- let braces2 = 0;
1277
- let brackets2 = 0;
1278
- let inString2 = false;
1279
- let escapeNext2 = false;
1280
- for (let j = 0; j < workingText.length; j++) {
1281
- const char = workingText[j];
1282
- if (escapeNext2) {
1283
- escapeNext2 = false;
1284
- continue;
1285
- }
1286
- if (char === "\\") {
1287
- escapeNext2 = true;
1288
- continue;
1289
- }
1290
- if (char === '"') {
1291
- inString2 = !inString2;
1292
- continue;
1293
- }
1294
- if (!inString2) {
1295
- if (char === "{") braces2++;
1296
- else if (char === "}") braces2--;
1297
- else if (char === "[") brackets2++;
1298
- else if (char === "]") brackets2--;
1120
+ /**
1121
+ * Send a stateless message and get a response.
1122
+ * Each call is independent — no history is maintained.
1123
+ *
1124
+ * @param {Object|string} payload - The message or data to send
1125
+ * @param {Object} [opts={}] - Per-message options
1126
+ * @param {Record<string, string>} [opts.labels] - Per-message billing labels
1127
+ * @returns {Promise<MessageResponse>} Response with text, optional data, and usage
1128
+ */
1129
+ async send(payload, opts = {}) {
1130
+ if (!this._initialized) await this.init();
1131
+ const payloadStr = typeof payload === "string" ? payload : JSON.stringify(payload, null, 2);
1132
+ const contents = [{ role: "user", parts: [{ text: payloadStr }] }];
1133
+ const mergedLabels = { ...this.labels, ...opts.labels || {} };
1134
+ const result = await this.genAIClient.models.generateContent({
1135
+ model: this.modelName,
1136
+ contents,
1137
+ config: {
1138
+ ...this.chatConfig,
1139
+ ...this.vertexai && Object.keys(mergedLabels).length > 0 && { labels: mergedLabels }
1299
1140
  }
1141
+ });
1142
+ this._captureMetadata(result);
1143
+ this._cumulativeUsage = {
1144
+ promptTokens: this.lastResponseMetadata.promptTokens,
1145
+ responseTokens: this.lastResponseMetadata.responseTokens,
1146
+ totalTokens: this.lastResponseMetadata.totalTokens,
1147
+ attempts: 1
1148
+ };
1149
+ if (result.usageMetadata && logger_default.level !== "silent") {
1150
+ logger_default.debug(`Message response: model=${result.modelVersion || "unknown"}, tokens=${result.usageMetadata.totalTokenCount}`);
1300
1151
  }
1301
- if (braces2 === 0 && brackets2 === 0 && !inString2) {
1152
+ const text = result.text || "";
1153
+ const response = {
1154
+ text,
1155
+ usage: this.getLastUsage()
1156
+ };
1157
+ if (this._isStructured) {
1302
1158
  try {
1303
- const result = JSON.parse(workingText);
1304
- if (logger_default.level !== "silent") {
1305
- logger_default.warn(`JSON response appears truncated (possibly hit maxOutputTokens limit). Recovered by removing ${i + 1} characters from the end.`);
1306
- }
1307
- return result;
1308
- } catch (e) {
1309
- }
1310
- }
1311
- if (i > 5) {
1312
- let fixedText = workingText;
1313
- if (inString2) {
1314
- fixedText += '"';
1315
- }
1316
- while (braces2 > 0) {
1317
- fixedText += "}";
1318
- braces2--;
1319
- }
1320
- while (brackets2 > 0) {
1321
- fixedText += "]";
1322
- brackets2--;
1323
- }
1324
- try {
1325
- const result = JSON.parse(fixedText);
1326
- if (logger_default.level !== "silent") {
1327
- logger_default.warn(`JSON response appears truncated (possibly hit maxOutputTokens limit). Recovered by adding closing characters.`);
1328
- }
1329
- return result;
1159
+ response.data = extractJSON(text);
1330
1160
  } catch (e) {
1161
+ logger_default.warn(`Could not parse structured response: ${e.message}`);
1162
+ response.data = null;
1331
1163
  }
1332
1164
  }
1165
+ return response;
1333
1166
  }
1334
- return null;
1335
- }
1336
- function isJSON(data) {
1337
- try {
1338
- const attempt = JSON.stringify(data);
1339
- if (attempt?.startsWith("{") || attempt?.startsWith("[")) {
1340
- if (attempt?.endsWith("}") || attempt?.endsWith("]")) {
1341
- return true;
1342
- }
1343
- }
1344
- return false;
1345
- } catch (e) {
1346
- return false;
1167
+ // ── No-ops for stateless class ──
1168
+ /** @returns {Array} Always returns empty array (stateless). */
1169
+ getHistory() {
1170
+ return [];
1347
1171
  }
1348
- }
1349
- function isJSONStr(string) {
1350
- if (typeof string !== "string") return false;
1351
- try {
1352
- const result = JSON.parse(string);
1353
- const type = Object.prototype.toString.call(result);
1354
- return type === "[object Object]" || type === "[object Array]";
1355
- } catch (err) {
1356
- return false;
1172
+ /** No-op (stateless). */
1173
+ async clearHistory() {
1357
1174
  }
1358
- }
1359
- function extractJSON(text) {
1360
- if (!text || typeof text !== "string") {
1361
- throw new Error("No text provided for JSON extraction");
1175
+ /** Not supported on Message (stateless). */
1176
+ async seed() {
1177
+ logger_default.warn("Message is stateless \u2014 seed() has no effect. Use Transformer or Chat for few-shot learning.");
1178
+ return [];
1362
1179
  }
1363
- if (isJSONStr(text.trim())) {
1364
- return JSON.parse(text.trim());
1180
+ /**
1181
+ * Not supported on Message (stateless).
1182
+ * @param {any} [_nextPayload]
1183
+ * @returns {Promise<{inputTokens: number}>}
1184
+ */
1185
+ async estimate(_nextPayload) {
1186
+ throw new Error("Message is stateless \u2014 use estimate() on Chat or Transformer which have conversation context.");
1365
1187
  }
1366
- const codeBlockPatterns = [
1367
- /```json\s*\n?([\s\S]*?)\n?\s*```/gi,
1368
- /```\s*\n?([\s\S]*?)\n?\s*```/gi
1369
- ];
1370
- for (const pattern of codeBlockPatterns) {
1371
- const matches = text.match(pattern);
1372
- if (matches) {
1373
- for (const match of matches) {
1374
- const jsonContent = match.replace(/```json\s*\n?/gi, "").replace(/```\s*\n?/gi, "").trim();
1375
- if (isJSONStr(jsonContent)) {
1376
- return JSON.parse(jsonContent);
1377
- }
1378
- }
1188
+ };
1189
+ var message_default = Message;
1190
+
1191
+ // tool-agent.js
1192
+ var ToolAgent = class extends base_default {
1193
+ /**
1194
+ * @param {ToolAgentOptions} [options={}]
1195
+ */
1196
+ constructor(options = {}) {
1197
+ if (options.systemPrompt === void 0) {
1198
+ options = { ...options, systemPrompt: "You are a helpful AI assistant." };
1379
1199
  }
1200
+ super(options);
1201
+ this.tools = options.tools || [];
1202
+ this.toolExecutor = options.toolExecutor || null;
1203
+ if (this.tools.length > 0 && !this.toolExecutor) {
1204
+ throw new Error("ToolAgent: tools provided without a toolExecutor. Provide a toolExecutor function to handle tool calls.");
1205
+ }
1206
+ if (this.toolExecutor && this.tools.length === 0) {
1207
+ throw new Error("ToolAgent: toolExecutor provided without tools. Provide tool declarations so the model knows what tools are available.");
1208
+ }
1209
+ this.maxToolRounds = options.maxToolRounds || 10;
1210
+ this.onToolCall = options.onToolCall || null;
1211
+ this.onBeforeExecution = options.onBeforeExecution || null;
1212
+ this._stopped = false;
1213
+ if (this.tools.length > 0) {
1214
+ this.chatConfig.tools = [{ functionDeclarations: this.tools }];
1215
+ this.chatConfig.toolConfig = { functionCallingConfig: { mode: "AUTO" } };
1216
+ }
1217
+ logger_default.debug(`ToolAgent created with ${this.tools.length} tools`);
1380
1218
  }
1381
- const jsonPatterns = [
1382
- // Match complete JSON objects
1383
- /\{[\s\S]*\}/g,
1384
- // Match complete JSON arrays
1385
- /\[[\s\S]*\]/g
1386
- ];
1387
- for (const pattern of jsonPatterns) {
1388
- const matches = text.match(pattern);
1389
- if (matches) {
1390
- for (const match of matches) {
1391
- const candidate = match.trim();
1392
- if (isJSONStr(candidate)) {
1393
- return JSON.parse(candidate);
1219
+ // ── Non-Streaming Chat ───────────────────────────────────────────────────
1220
+ /**
1221
+ * Send a message and get a complete response (non-streaming).
1222
+ * Automatically handles the tool-use loop.
1223
+ *
1224
+ * @param {string} message - The user's message
1225
+ * @param {Object} [opts={}] - Per-message options
1226
+ * @param {Record<string, string>} [opts.labels] - Per-message billing labels
1227
+ * @returns {Promise<AgentResponse>} Response with text, toolCalls, and usage
1228
+ */
1229
+ async chat(message, opts = {}) {
1230
+ if (!this.chatSession) await this.init();
1231
+ this._stopped = false;
1232
+ const allToolCalls = [];
1233
+ let response = await this.chatSession.sendMessage({ message });
1234
+ for (let round = 0; round < this.maxToolRounds; round++) {
1235
+ if (this._stopped) break;
1236
+ const functionCalls = response.functionCalls;
1237
+ if (!functionCalls || functionCalls.length === 0) break;
1238
+ const toolResults = await Promise.all(
1239
+ functionCalls.map(async (call) => {
1240
+ if (this.onToolCall) {
1241
+ try {
1242
+ this.onToolCall(call.name, call.args);
1243
+ } catch (e) {
1244
+ logger_default.warn(`onToolCall callback error: ${e.message}`);
1245
+ }
1246
+ }
1247
+ if (this.onBeforeExecution) {
1248
+ try {
1249
+ const allowed = await this.onBeforeExecution(call.name, call.args);
1250
+ if (allowed === false) {
1251
+ const result2 = { error: "Execution denied by onBeforeExecution callback" };
1252
+ allToolCalls.push({ name: call.name, args: call.args, result: result2 });
1253
+ return { id: call.id, name: call.name, result: result2 };
1254
+ }
1255
+ } catch (e) {
1256
+ logger_default.warn(`onBeforeExecution callback error: ${e.message}`);
1257
+ }
1258
+ }
1259
+ let result;
1260
+ try {
1261
+ result = await this.toolExecutor(call.name, call.args);
1262
+ } catch (err) {
1263
+ logger_default.warn(`Tool ${call.name} failed: ${err.message}`);
1264
+ result = { error: err.message };
1265
+ }
1266
+ allToolCalls.push({ name: call.name, args: call.args, result });
1267
+ return { id: call.id, name: call.name, result };
1268
+ })
1269
+ );
1270
+ response = await this.chatSession.sendMessage({
1271
+ message: toolResults.map((r) => ({
1272
+ functionResponse: {
1273
+ id: r.id,
1274
+ name: r.name,
1275
+ response: { output: r.result }
1276
+ }
1277
+ }))
1278
+ });
1279
+ }
1280
+ this._captureMetadata(response);
1281
+ this._cumulativeUsage = {
1282
+ promptTokens: this.lastResponseMetadata.promptTokens,
1283
+ responseTokens: this.lastResponseMetadata.responseTokens,
1284
+ totalTokens: this.lastResponseMetadata.totalTokens,
1285
+ attempts: 1
1286
+ };
1287
+ return {
1288
+ text: response.text || "",
1289
+ toolCalls: allToolCalls,
1290
+ usage: this.getLastUsage()
1291
+ };
1292
+ }
1293
+ // ── Streaming ────────────────────────────────────────────────────────────
1294
+ /**
1295
+ * Send a message and stream the response as events.
1296
+ * Automatically handles the tool-use loop between streamed rounds.
1297
+ *
1298
+ * Event types:
1299
+ * - `text` — A chunk of the agent's text response
1300
+ * - `tool_call` — The agent is about to call a tool
1301
+ * - `tool_result` — A tool finished executing
1302
+ * - `done` — The agent finished
1303
+ *
1304
+ * @param {string} message - The user's message
1305
+ * @param {Object} [opts={}] - Per-message options
1306
+ * @yields {AgentStreamEvent}
1307
+ */
1308
+ async *stream(message, opts = {}) {
1309
+ if (!this.chatSession) await this.init();
1310
+ this._stopped = false;
1311
+ const allToolCalls = [];
1312
+ let fullText = "";
1313
+ let streamResponse = await this.chatSession.sendMessageStream({ message });
1314
+ for (let round = 0; round < this.maxToolRounds; round++) {
1315
+ if (this._stopped) break;
1316
+ let roundText = "";
1317
+ const functionCalls = [];
1318
+ for await (const chunk of streamResponse) {
1319
+ if (chunk.functionCalls) {
1320
+ functionCalls.push(...chunk.functionCalls);
1321
+ } else if (chunk.candidates?.[0]?.content?.parts?.[0]?.text) {
1322
+ const text = chunk.candidates[0].content.parts[0].text;
1323
+ roundText += text;
1324
+ fullText += text;
1325
+ yield { type: "text", text };
1394
1326
  }
1395
1327
  }
1328
+ if (functionCalls.length === 0) {
1329
+ yield {
1330
+ type: "done",
1331
+ fullText,
1332
+ usage: this.getLastUsage()
1333
+ };
1334
+ return;
1335
+ }
1336
+ const toolResults = [];
1337
+ for (const call of functionCalls) {
1338
+ if (this._stopped) break;
1339
+ yield { type: "tool_call", toolName: call.name, args: call.args };
1340
+ if (this.onToolCall) {
1341
+ try {
1342
+ this.onToolCall(call.name, call.args);
1343
+ } catch (e) {
1344
+ logger_default.warn(`onToolCall callback error: ${e.message}`);
1345
+ }
1346
+ }
1347
+ let denied = false;
1348
+ if (this.onBeforeExecution) {
1349
+ try {
1350
+ const allowed = await this.onBeforeExecution(call.name, call.args);
1351
+ if (allowed === false) denied = true;
1352
+ } catch (e) {
1353
+ logger_default.warn(`onBeforeExecution callback error: ${e.message}`);
1354
+ }
1355
+ }
1356
+ let result;
1357
+ if (denied) {
1358
+ result = { error: "Execution denied by onBeforeExecution callback" };
1359
+ } else {
1360
+ try {
1361
+ result = await this.toolExecutor(call.name, call.args);
1362
+ } catch (err) {
1363
+ logger_default.warn(`Tool ${call.name} failed: ${err.message}`);
1364
+ result = { error: err.message };
1365
+ }
1366
+ }
1367
+ allToolCalls.push({ name: call.name, args: call.args, result });
1368
+ yield { type: "tool_result", toolName: call.name, result };
1369
+ toolResults.push({ id: call.id, name: call.name, result });
1370
+ }
1371
+ streamResponse = await this.chatSession.sendMessageStream({
1372
+ message: toolResults.map((r) => ({
1373
+ functionResponse: {
1374
+ id: r.id,
1375
+ name: r.name,
1376
+ response: { output: r.result }
1377
+ }
1378
+ }))
1379
+ });
1396
1380
  }
1381
+ yield {
1382
+ type: "done",
1383
+ fullText,
1384
+ usage: this.getLastUsage(),
1385
+ warning: this._stopped ? "Agent was stopped" : "Max tool rounds reached"
1386
+ };
1397
1387
  }
1398
- const advancedExtract = findCompleteJSONStructures(text);
1399
- if (advancedExtract.length > 0) {
1400
- for (const candidate of advancedExtract) {
1401
- if (isJSONStr(candidate)) {
1402
- return JSON.parse(candidate);
1403
- }
1388
+ // ── Stop ────────────────────────────────────────────────────────────────
1389
+ /**
1390
+ * Stop the agent before the next tool execution round.
1391
+ * If called during a chat() or stream() loop, the agent will finish
1392
+ * the current round and then stop.
1393
+ */
1394
+ stop() {
1395
+ this._stopped = true;
1396
+ logger_default.info("ToolAgent stopped");
1397
+ }
1398
+ };
1399
+ var tool_agent_default = ToolAgent;
1400
+
1401
+ // code-agent.js
1402
+ var import_node_child_process = require("node:child_process");
1403
+ var import_promises2 = require("node:fs/promises");
1404
+ var import_node_path = require("node:path");
1405
+ var import_node_crypto = require("node:crypto");
1406
+ var MAX_OUTPUT_CHARS = 5e4;
1407
+ var MAX_FILE_TREE_LINES = 500;
1408
+ var IGNORE_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "coverage", ".next", "build", "__pycache__"]);
1409
+ var CodeAgent = class extends base_default {
1410
+ /**
1411
+ * @param {CodeAgentOptions} [options={}]
1412
+ */
1413
+ constructor(options = {}) {
1414
+ if (options.systemPrompt === void 0) {
1415
+ options = { ...options, systemPrompt: "" };
1404
1416
  }
1417
+ super(options);
1418
+ this.workingDirectory = options.workingDirectory || process.cwd();
1419
+ this.maxRounds = options.maxRounds || 10;
1420
+ this.timeout = options.timeout || 3e4;
1421
+ this.onBeforeExecution = options.onBeforeExecution || null;
1422
+ this.onCodeExecution = options.onCodeExecution || null;
1423
+ this._codebaseContext = null;
1424
+ this._contextGathered = false;
1425
+ this._stopped = false;
1426
+ this._activeProcess = null;
1427
+ this._userSystemPrompt = options.systemPrompt || "";
1428
+ this._allExecutions = [];
1429
+ this.chatConfig.tools = [{
1430
+ functionDeclarations: [{
1431
+ name: "execute_code",
1432
+ description: "Execute JavaScript code in a Node.js child process. The code has access to all Node.js built-in modules (fs, path, child_process, http, etc.). Use console.log() to produce output that will be returned to you. The code runs in the working directory with the same environment variables as the parent process.",
1433
+ parametersJsonSchema: {
1434
+ type: "object",
1435
+ properties: {
1436
+ code: {
1437
+ type: "string",
1438
+ description: "JavaScript code to execute. Use console.log() for output. You can import any built-in Node.js module."
1439
+ }
1440
+ },
1441
+ required: ["code"]
1442
+ }
1443
+ }]
1444
+ }];
1445
+ this.chatConfig.toolConfig = { functionCallingConfig: { mode: "AUTO" } };
1446
+ logger_default.debug(`CodeAgent created for directory: ${this.workingDirectory}`);
1405
1447
  }
1406
- const cleanedText = text.replace(/^\s*Sure,?\s*here\s+is\s+your?\s+.*?[:\n]/gi, "").replace(/^\s*Here\s+is\s+the\s+.*?[:\n]/gi, "").replace(/^\s*The\s+.*?is\s*[:\n]/gi, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "").trim();
1407
- if (isJSONStr(cleanedText)) {
1408
- return JSON.parse(cleanedText);
1448
+ // ── Init ─────────────────────────────────────────────────────────────────
1449
+ /**
1450
+ * Initialize the agent: gather codebase context, build system prompt,
1451
+ * and create the chat session.
1452
+ * @param {boolean} [force=false]
1453
+ */
1454
+ async init(force = false) {
1455
+ if (this.chatSession && !force) return;
1456
+ if (!this._contextGathered || force) {
1457
+ await this._gatherCodebaseContext();
1458
+ }
1459
+ const systemPrompt = this._buildSystemPrompt();
1460
+ this.chatConfig.systemInstruction = systemPrompt;
1461
+ await super.init(force);
1409
1462
  }
1410
- const recoveredJSON = attemptJSONRecovery(text);
1411
- if (recoveredJSON !== null) {
1412
- return recoveredJSON;
1463
+ // ── Context Gathering ────────────────────────────────────────────────────
1464
+ /**
1465
+ * Gather file tree and key file contents from the working directory.
1466
+ * @private
1467
+ */
1468
+ async _gatherCodebaseContext() {
1469
+ let fileTree = "";
1470
+ try {
1471
+ fileTree = await this._getFileTreeGit();
1472
+ } catch {
1473
+ logger_default.debug("git ls-files failed, falling back to readdir");
1474
+ fileTree = await this._getFileTreeReaddir(this.workingDirectory, 0, 3);
1475
+ }
1476
+ const lines = fileTree.split("\n");
1477
+ if (lines.length > MAX_FILE_TREE_LINES) {
1478
+ const truncated = lines.slice(0, MAX_FILE_TREE_LINES).join("\n");
1479
+ fileTree = `${truncated}
1480
+ ... (${lines.length - MAX_FILE_TREE_LINES} more files)`;
1481
+ }
1482
+ let npmPackages = [];
1483
+ try {
1484
+ const pkgPath = (0, import_node_path.join)(this.workingDirectory, "package.json");
1485
+ const pkg = JSON.parse(await (0, import_promises2.readFile)(pkgPath, "utf-8"));
1486
+ npmPackages = [
1487
+ ...Object.keys(pkg.dependencies || {}),
1488
+ ...Object.keys(pkg.devDependencies || {})
1489
+ ];
1490
+ } catch {
1491
+ }
1492
+ this._codebaseContext = { fileTree, npmPackages };
1493
+ this._contextGathered = true;
1413
1494
  }
1414
- throw new Error(`Could not extract valid JSON from model response. Response preview: ${text.substring(0, 200)}...`);
1415
- }
1416
- function findCompleteJSONStructures(text) {
1417
- const results = [];
1418
- const startChars = ["{", "["];
1419
- for (let i = 0; i < text.length; i++) {
1420
- if (startChars.includes(text[i])) {
1421
- const extracted = extractCompleteStructure(text, i);
1422
- if (extracted) {
1423
- results.push(extracted);
1495
+ /**
1496
+ * Get file tree using git ls-files.
1497
+ * @private
1498
+ * @returns {Promise<string>}
1499
+ */
1500
+ async _getFileTreeGit() {
1501
+ return new Promise((resolve, reject) => {
1502
+ (0, import_node_child_process.execFile)("git", ["ls-files"], {
1503
+ cwd: this.workingDirectory,
1504
+ timeout: 5e3,
1505
+ maxBuffer: 5 * 1024 * 1024
1506
+ }, (err, stdout) => {
1507
+ if (err) return reject(err);
1508
+ resolve(stdout.trim());
1509
+ });
1510
+ });
1511
+ }
1512
+ /**
1513
+ * Fallback file tree via recursive readdir.
1514
+ * @private
1515
+ * @param {string} dir
1516
+ * @param {number} depth
1517
+ * @param {number} maxDepth
1518
+ * @returns {Promise<string>}
1519
+ */
1520
+ async _getFileTreeReaddir(dir, depth, maxDepth) {
1521
+ if (depth >= maxDepth) return "";
1522
+ const entries = [];
1523
+ try {
1524
+ const items = await (0, import_promises2.readdir)(dir, { withFileTypes: true });
1525
+ for (const item of items) {
1526
+ if (IGNORE_DIRS.has(item.name)) continue;
1527
+ if (item.name.startsWith(".") && depth === 0 && item.isDirectory()) continue;
1528
+ const relativePath = (0, import_node_path.join)(dir, item.name).replace(this.workingDirectory + "/", "");
1529
+ if (item.isFile()) {
1530
+ entries.push(relativePath);
1531
+ } else if (item.isDirectory()) {
1532
+ entries.push(relativePath + "/");
1533
+ const subEntries = await this._getFileTreeReaddir((0, import_node_path.join)(dir, item.name), depth + 1, maxDepth);
1534
+ if (subEntries) entries.push(subEntries);
1535
+ }
1424
1536
  }
1537
+ } catch {
1425
1538
  }
1539
+ return entries.join("\n");
1426
1540
  }
1427
- return results;
1428
- }
1429
- function extractCompleteStructure(text, startPos) {
1430
- const startChar = text[startPos];
1431
- const endChar = startChar === "{" ? "}" : "]";
1432
- let depth = 0;
1433
- let inString = false;
1434
- let escaped = false;
1435
- for (let i = startPos; i < text.length; i++) {
1436
- const char = text[i];
1437
- if (escaped) {
1438
- escaped = false;
1439
- continue;
1541
+ /**
1542
+ * Build the full system prompt with codebase context.
1543
+ * @private
1544
+ * @returns {string}
1545
+ */
1546
+ _buildSystemPrompt() {
1547
+ const { fileTree, npmPackages } = this._codebaseContext || { fileTree: "", npmPackages: [] };
1548
+ let prompt = `You are a coding agent working in ${this.workingDirectory}.
1549
+
1550
+ ## Instructions
1551
+ - Use the execute_code tool to accomplish tasks by writing JavaScript code
1552
+ - Your code runs in a Node.js child process with access to all built-in modules
1553
+ - IMPORTANT: Your code runs as an ES module (.mjs). Use import syntax, NOT require():
1554
+ - import fs from 'fs';
1555
+ - import path from 'path';
1556
+ - import { execSync } from 'child_process';
1557
+ - Use console.log() to produce output \u2014 that's how results are returned to you
1558
+ - Write efficient scripts that do multiple things per execution when possible
1559
+ - For parallel async operations, use Promise.all():
1560
+ const [a, b] = await Promise.all([fetchA(), fetchB()]);
1561
+ - Read files with fs.readFileSync() when you need to understand their contents
1562
+ - Handle errors in your scripts with try/catch so you get useful error messages
1563
+ - Top-level await is supported
1564
+ - The working directory is: ${this.workingDirectory}`;
1565
+ if (fileTree) {
1566
+ prompt += `
1567
+
1568
+ ## File Tree
1569
+ \`\`\`
1570
+ ${fileTree}
1571
+ \`\`\``;
1440
1572
  }
1441
- if (char === "\\" && inString) {
1442
- escaped = true;
1443
- continue;
1573
+ if (npmPackages.length > 0) {
1574
+ prompt += `
1575
+
1576
+ ## Available Packages
1577
+ These npm packages are installed and can be imported: ${npmPackages.join(", ")}`;
1444
1578
  }
1445
- if (char === '"' && !escaped) {
1446
- inString = !inString;
1447
- continue;
1579
+ if (this._userSystemPrompt) {
1580
+ prompt += `
1581
+
1582
+ ## Additional Instructions
1583
+ ${this._userSystemPrompt}`;
1448
1584
  }
1449
- if (!inString) {
1450
- if (char === startChar) {
1451
- depth++;
1452
- } else if (char === endChar) {
1453
- depth--;
1454
- if (depth === 0) {
1455
- return text.substring(startPos, i + 1);
1585
+ return prompt;
1586
+ }
1587
+ // ── Code Execution ───────────────────────────────────────────────────────
1588
+ /**
1589
+ * Execute a JavaScript code string in a child process.
1590
+ * @private
1591
+ * @param {string} code - JavaScript code to execute
1592
+ * @returns {Promise<{stdout: string, stderr: string, exitCode: number, denied?: boolean}>}
1593
+ */
1594
+ async _executeCode(code) {
1595
+ if (this._stopped) {
1596
+ return { stdout: "", stderr: "Agent was stopped", exitCode: -1 };
1597
+ }
1598
+ if (this.onBeforeExecution) {
1599
+ try {
1600
+ const allowed = await this.onBeforeExecution(code);
1601
+ if (allowed === false) {
1602
+ return { stdout: "", stderr: "Execution denied by onBeforeExecution callback", exitCode: -1, denied: true };
1456
1603
  }
1604
+ } catch (e) {
1605
+ logger_default.warn(`onBeforeExecution callback error: ${e.message}`);
1457
1606
  }
1458
1607
  }
1459
- }
1460
- return null;
1461
- }
1462
- if (import_meta.url === new URL(`file://${process.argv[1]}`).href) {
1463
- logger_default.info("RUNNING AI Transformer as standalone script...");
1464
- (async () => {
1608
+ const tempFile = (0, import_node_path.join)(this.workingDirectory, `.code-agent-tmp-${(0, import_node_crypto.randomUUID)()}.mjs`);
1465
1609
  try {
1466
- logger_default.info("Initializing AI Transformer...");
1467
- const transformer = new AITransformer({
1468
- modelName: "gemini-2.5-flash",
1469
- sourceKey: "INPUT",
1470
- // Custom source key
1471
- targetKey: "OUTPUT",
1472
- // Custom target key
1473
- contextKey: "CONTEXT",
1474
- // Custom context key
1475
- maxRetries: 2
1610
+ await (0, import_promises2.writeFile)(tempFile, code, "utf-8");
1611
+ const result = await new Promise((resolve) => {
1612
+ const child = (0, import_node_child_process.execFile)("node", [tempFile], {
1613
+ cwd: this.workingDirectory,
1614
+ timeout: this.timeout,
1615
+ env: process.env,
1616
+ maxBuffer: 10 * 1024 * 1024
1617
+ }, (err, stdout, stderr) => {
1618
+ this._activeProcess = null;
1619
+ if (err) {
1620
+ resolve({
1621
+ stdout: err.stdout || stdout || "",
1622
+ stderr: (err.stderr || stderr || "") + (err.killed ? "\n[EXECUTION TIMED OUT]" : ""),
1623
+ exitCode: err.code || 1
1624
+ });
1625
+ } else {
1626
+ resolve({ stdout: stdout || "", stderr: stderr || "", exitCode: 0 });
1627
+ }
1628
+ });
1629
+ this._activeProcess = child;
1476
1630
  });
1477
- const examples = [
1478
- {
1479
- CONTEXT: "Generate professional profiles with emoji representations",
1480
- INPUT: { "name": "Alice" },
1481
- OUTPUT: { "name": "Alice", "profession": "data scientist", "life_as_told_by_emoji": ["\u{1F52C}", "\u{1F4A1}", "\u{1F4CA}", "\u{1F9E0}", "\u{1F31F}"] }
1482
- },
1483
- {
1484
- INPUT: { "name": "Bob" },
1485
- OUTPUT: { "name": "Bob", "profession": "product manager", "life_as_told_by_emoji": ["\u{1F4CB}", "\u{1F91D}", "\u{1F680}", "\u{1F4AC}", "\u{1F3AF}"] }
1486
- },
1487
- {
1488
- INPUT: { "name": "Eve" },
1489
- OUTPUT: { "name": "Even", "profession": "security analyst", "life_as_told_by_emoji": ["\u{1F575}\uFE0F\u200D\u2640\uFE0F", "\u{1F512}", "\u{1F4BB}", "\u{1F440}", "\u26A1\uFE0F"] }
1631
+ const totalLen = result.stdout.length + result.stderr.length;
1632
+ if (totalLen > MAX_OUTPUT_CHARS) {
1633
+ const half = Math.floor(MAX_OUTPUT_CHARS / 2);
1634
+ if (result.stdout.length > half) {
1635
+ result.stdout = result.stdout.slice(0, half) + "\n...[OUTPUT TRUNCATED]";
1490
1636
  }
1491
- ];
1492
- await transformer.init();
1493
- await transformer.seed(examples);
1494
- logger_default.info("AI Transformer initialized and seeded with examples.");
1495
- const normalResponse = await transformer.message({ "name": "AK" });
1496
- logger_default.info(`Normal Payload Transformed: ${JSON.stringify(normalResponse)}`);
1497
- const mockValidator = async (payload) => {
1498
- if (!payload.profession || !payload.life_as_told_by_emoji) {
1499
- throw new Error("Missing required fields: profession or life_as_told_by_emoji");
1637
+ if (result.stderr.length > half) {
1638
+ result.stderr = result.stderr.slice(0, half) + "\n...[STDERR TRUNCATED]";
1500
1639
  }
1501
- if (!Array.isArray(payload.life_as_told_by_emoji)) {
1502
- throw new Error("life_as_told_by_emoji must be an array");
1640
+ }
1641
+ this._allExecutions.push({ code, output: result.stdout, stderr: result.stderr, exitCode: result.exitCode });
1642
+ if (this.onCodeExecution) {
1643
+ try {
1644
+ this.onCodeExecution(code, result);
1645
+ } catch (e) {
1646
+ logger_default.warn(`onCodeExecution callback error: ${e.message}`);
1503
1647
  }
1504
- return payload;
1505
- };
1506
- const validatedResponse = await transformer.messageAndValidate(
1507
- { "name": "Lynn" },
1508
- {},
1509
- mockValidator
1510
- );
1511
- logger_default.info(`Validated Payload Transformed: ${JSON.stringify(validatedResponse)}`);
1512
- if (NODE_ENV2 === "dev") debugger;
1513
- } catch (error) {
1514
- logger_default.error(`Error in AI Transformer script: ${error?.message || error}`);
1515
- if (NODE_ENV2 === "dev") debugger;
1648
+ }
1649
+ return result;
1650
+ } finally {
1651
+ try {
1652
+ await (0, import_promises2.unlink)(tempFile);
1653
+ } catch {
1654
+ }
1516
1655
  }
1517
- })();
1518
- }
1656
+ }
1657
+ /**
1658
+ * Format execution result as a string for the model.
1659
+ * @private
1660
+ * @param {{stdout: string, stderr: string, exitCode: number}} result
1661
+ * @returns {string}
1662
+ */
1663
+ _formatOutput(result) {
1664
+ let output = "";
1665
+ if (result.stdout) output += result.stdout;
1666
+ if (result.stderr) output += (output ? "\n" : "") + `[STDERR]: ${result.stderr}`;
1667
+ if (result.exitCode !== 0) output += (output ? "\n" : "") + `[EXIT CODE]: ${result.exitCode}`;
1668
+ return output || "(no output)";
1669
+ }
1670
+ // ── Non-Streaming Chat ───────────────────────────────────────────────────
1671
+ /**
1672
+ * Send a message and get a complete response (non-streaming).
1673
+ * Automatically handles the code execution loop.
1674
+ *
1675
+ * @param {string} message - The user's message
1676
+ * @param {Object} [opts={}] - Per-message options
1677
+ * @param {Record<string, string>} [opts.labels] - Per-message billing labels
1678
+ * @returns {Promise<CodeAgentResponse>} Response with text, codeExecutions, and usage
1679
+ */
1680
+ async chat(message, opts = {}) {
1681
+ if (!this.chatSession) await this.init();
1682
+ this._stopped = false;
1683
+ const codeExecutions = [];
1684
+ let response = await this.chatSession.sendMessage({ message });
1685
+ for (let round = 0; round < this.maxRounds; round++) {
1686
+ if (this._stopped) break;
1687
+ const functionCalls = response.functionCalls;
1688
+ if (!functionCalls || functionCalls.length === 0) break;
1689
+ const results = [];
1690
+ for (const call of functionCalls) {
1691
+ if (this._stopped) break;
1692
+ const code = call.args?.code || "";
1693
+ const result = await this._executeCode(code);
1694
+ codeExecutions.push({
1695
+ code,
1696
+ output: result.stdout,
1697
+ stderr: result.stderr,
1698
+ exitCode: result.exitCode
1699
+ });
1700
+ results.push({
1701
+ id: call.id,
1702
+ name: call.name,
1703
+ result: this._formatOutput(result)
1704
+ });
1705
+ }
1706
+ if (this._stopped) break;
1707
+ response = await this.chatSession.sendMessage({
1708
+ message: results.map((r) => ({
1709
+ functionResponse: {
1710
+ id: r.id,
1711
+ name: r.name,
1712
+ response: { output: r.result }
1713
+ }
1714
+ }))
1715
+ });
1716
+ }
1717
+ this._captureMetadata(response);
1718
+ this._cumulativeUsage = {
1719
+ promptTokens: this.lastResponseMetadata.promptTokens,
1720
+ responseTokens: this.lastResponseMetadata.responseTokens,
1721
+ totalTokens: this.lastResponseMetadata.totalTokens,
1722
+ attempts: 1
1723
+ };
1724
+ return {
1725
+ text: response.text || "",
1726
+ codeExecutions,
1727
+ usage: this.getLastUsage()
1728
+ };
1729
+ }
1730
+ // ── Streaming ────────────────────────────────────────────────────────────
1731
+ /**
1732
+ * Send a message and stream the response as events.
1733
+ * Automatically handles the code execution loop between streamed rounds.
1734
+ *
1735
+ * Event types:
1736
+ * - `text` — A chunk of the agent's text response
1737
+ * - `code` — The agent is about to execute code
1738
+ * - `output` — Code finished executing
1739
+ * - `done` — The agent finished
1740
+ *
1741
+ * @param {string} message - The user's message
1742
+ * @param {Object} [opts={}] - Per-message options
1743
+ * @yields {CodeAgentStreamEvent}
1744
+ */
1745
+ async *stream(message, opts = {}) {
1746
+ if (!this.chatSession) await this.init();
1747
+ this._stopped = false;
1748
+ const codeExecutions = [];
1749
+ let fullText = "";
1750
+ let streamResponse = await this.chatSession.sendMessageStream({ message });
1751
+ for (let round = 0; round < this.maxRounds; round++) {
1752
+ if (this._stopped) break;
1753
+ const functionCalls = [];
1754
+ for await (const chunk of streamResponse) {
1755
+ if (chunk.functionCalls) {
1756
+ functionCalls.push(...chunk.functionCalls);
1757
+ } else if (chunk.candidates?.[0]?.content?.parts?.[0]?.text) {
1758
+ const text = chunk.candidates[0].content.parts[0].text;
1759
+ fullText += text;
1760
+ yield { type: "text", text };
1761
+ }
1762
+ }
1763
+ if (functionCalls.length === 0) {
1764
+ yield {
1765
+ type: "done",
1766
+ fullText,
1767
+ codeExecutions,
1768
+ usage: this.getLastUsage()
1769
+ };
1770
+ return;
1771
+ }
1772
+ const results = [];
1773
+ for (const call of functionCalls) {
1774
+ if (this._stopped) break;
1775
+ const code = call.args?.code || "";
1776
+ yield { type: "code", code };
1777
+ const result = await this._executeCode(code);
1778
+ codeExecutions.push({
1779
+ code,
1780
+ output: result.stdout,
1781
+ stderr: result.stderr,
1782
+ exitCode: result.exitCode
1783
+ });
1784
+ yield {
1785
+ type: "output",
1786
+ code,
1787
+ stdout: result.stdout,
1788
+ stderr: result.stderr,
1789
+ exitCode: result.exitCode
1790
+ };
1791
+ results.push({
1792
+ id: call.id,
1793
+ name: call.name,
1794
+ result: this._formatOutput(result)
1795
+ });
1796
+ }
1797
+ if (this._stopped) break;
1798
+ streamResponse = await this.chatSession.sendMessageStream({
1799
+ message: results.map((r) => ({
1800
+ functionResponse: {
1801
+ id: r.id,
1802
+ name: r.name,
1803
+ response: { output: r.result }
1804
+ }
1805
+ }))
1806
+ });
1807
+ }
1808
+ yield {
1809
+ type: "done",
1810
+ fullText,
1811
+ codeExecutions,
1812
+ usage: this.getLastUsage(),
1813
+ warning: this._stopped ? "Agent was stopped" : "Max tool rounds reached"
1814
+ };
1815
+ }
1816
+ // ── Dump ─────────────────────────────────────────────────────────────────
1817
+ /**
1818
+ * Returns all code scripts the agent has written across all chat/stream calls.
1819
+ * @returns {Array<{fileName: string, script: string}>}
1820
+ */
1821
+ dump() {
1822
+ return this._allExecutions.map((exec, i) => ({
1823
+ fileName: `script-${i + 1}.mjs`,
1824
+ script: exec.code
1825
+ }));
1826
+ }
1827
+ // ── Stop ─────────────────────────────────────────────────────────────────
1828
+ /**
1829
+ * Stop the agent before the next code execution.
1830
+ * If a child process is currently running, it will be killed.
1831
+ */
1832
+ stop() {
1833
+ this._stopped = true;
1834
+ if (this._activeProcess) {
1835
+ try {
1836
+ this._activeProcess.kill("SIGTERM");
1837
+ } catch {
1838
+ }
1839
+ }
1840
+ logger_default.info("CodeAgent stopped");
1841
+ }
1842
+ };
1843
+ var code_agent_default = CodeAgent;
1844
+
1845
+ // index.js
1846
+ var import_genai2 = require("@google/genai");
1847
+ var index_default = { Transformer: transformer_default, Chat: chat_default, Message: message_default, ToolAgent: tool_agent_default, CodeAgent: code_agent_default };
1519
1848
  // Annotate the CommonJS export names for ESM import in node:
1520
1849
  0 && (module.exports = {
1521
- AIAgent,
1850
+ BaseGemini,
1851
+ Chat,
1852
+ CodeAgent,
1522
1853
  HarmBlockThreshold,
1523
1854
  HarmCategory,
1855
+ Message,
1524
1856
  ThinkingLevel,
1857
+ ToolAgent,
1858
+ Transformer,
1525
1859
  attemptJSONRecovery,
1860
+ extractJSON,
1526
1861
  log
1527
1862
  });