zyn-ai 1.3.3 → 1.3.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zyn-ai",
3
- "version": "1.3.3",
3
+ "version": "1.3.4",
4
4
  "description": "Production-ready AI agent for CLI and web with real tool execution and automation",
5
5
  "author": "Maycol",
6
6
  "keywords": [
@@ -82,6 +82,9 @@ async function runSinglePrompt(prompt, options = {}) {
82
82
  try {
83
83
  const loaded = await loadOrCreateSessionState(rl, options);
84
84
  state = loaded.state;
85
+ if (!rl) {
86
+ state.autoApprove = true;
87
+ }
85
88
  const { resumed } = loaded;
86
89
  if (process.stdout.isTTY) {
87
90
  await printWelcome();
package/src/core/agent.js CHANGED
@@ -2,6 +2,7 @@ const {
2
2
  DEFAULT_MODEL_KEY,
3
3
  KEEP_RECENT_MESSAGES,
4
4
  MAX_HISTORY_CHARS,
5
+ MAX_TOOL_STEPS,
5
6
  MODELS,
6
7
  REQUEST_TIMEOUT_MS,
7
8
  } = require('../config');
@@ -487,6 +488,17 @@ async function runAgentTurn(input, state, ui, options = {}) {
487
488
  }
488
489
 
489
490
  step += 1;
491
+ if (step >= MAX_TOOL_STEPS && MAX_TOOL_STEPS !== Number.POSITIVE_INFINITY) {
492
+ const limitMsg = state.language === 'es'
493
+ ? `Se alcanzó el límite de ${MAX_TOOL_STEPS} pasos. No se pueden ejecutar más herramientas en este turno. Responde con un resumen de lo que lograste.`
494
+ : `Reached the limit of ${MAX_TOOL_STEPS} steps. No more tools can be executed this turn. Reply with a summary of what was accomplished.`;
495
+ turnMessages.push({ role: 'assistant', content: limitMsg });
496
+ state.history.push(...turnMessages);
497
+ await appendTranscriptEntry(state.sessionId, { type: 'assistant', content: limitMsg });
498
+ ui.logEvent(state, 'warn', state.language === 'es' ? 'Límite de pasos alcanzado' : 'Step limit reached');
499
+ await persistSessionState(state, ui);
500
+ return { content: limitMsg, rendered: false };
501
+ }
490
502
  }
491
503
  }
492
504
 
@@ -76,9 +76,6 @@ function buildSystemPrompt(cwd, state = {}, options = {}) {
76
76
  'No cierres con una conclusion si todavia no has probado nada.',
77
77
  'Si una tarea dura demasiado, usa run_command con un timeoutMs adecuado y confirma el resultado real.',
78
78
  'Para operaciones de Git usa la herramienta git con action="api" o action="clone".',
79
- 'Usa exclusivamente tools registradas en "Tool use". No inventes nombres de tools ni aliases.',
80
- 'Mantente acotado y determinista: entradas claras, salidas claras, sin razonamiento creativo salvo que el usuario lo pida.',
81
- 'Cuando aplique, entrega resumen ejecutivo corto + riesgos/banderas rojas + siguiente accion concreta.',
82
79
  'Para proyectos, usa combinaciones de tools según la fase: descubrir (list_dir/search_text), leer (read_file/fetch/webfetch), cambiar (write/replace), validar (run_command), documentar (final).',
83
80
  'No te limites a una sola tool por costumbre; elige la mejor secuencia técnica para el objetivo.',
84
81
  'Si el usuario pide logos, mockups o piezas visuales para un proyecto/frontend, usa create_canvas_image cuando corresponda, junto al resto de tools del flujo.',
@@ -94,19 +91,61 @@ function buildSystemPrompt(cwd, state = {}, options = {}) {
94
91
  'Do not end with a conclusion if you have not tested anything yet.',
95
92
  'If a task takes long, use run_command with an appropriate timeoutMs and verify the real result.',
96
93
  'For Git operations use the git tool with action="api" or action="clone".',
97
- 'Use only tools listed under "Tool use". Never invent tool names or aliases.',
98
- 'Stay bounded and deterministic: clear inputs, clear outputs, no creative reasoning unless explicitly requested.',
99
- 'When relevant, provide a short executive summary + obvious red flags + next concrete action.',
100
94
  'For project work, combine tools by phase: discover (list_dir/search_text), read (read_file/fetch/webfetch), change (write/replace), validate (run_command), then report.',
101
95
  'Do not over-focus on a single tool by habit; choose the best technical sequence for the goal.',
102
96
  'If the user asks for logos, mockups, or visual assets for a project/frontend, use create_canvas_image when appropriate together with the rest of the workflow.',
103
97
  ];
104
98
 
99
+ const toolUseEnforcement = language === 'es'
100
+ ? [
101
+ '',
102
+ '# Formato obligatorio de respuesta',
103
+ 'Solo existen DOS formatos de respuesta. NO inventes otros:',
104
+ '',
105
+ 'FORMATO 1 — Para USAR una herramienta:',
106
+ '{"type":"tool","tool":"NOMBRE_EXACTO","args":{"clave":"valor"}}',
107
+ '',
108
+ 'FORMATO 2 — Para responder AL USUARIO:',
109
+ '{"type":"final","content":"Tu respuesta aqui"}',
110
+ '',
111
+ 'REGLAS ESTRICTAS:',
112
+ '- USA EXCLUSIVAMENTE los nombres de herramientas listados en "# Tool use".',
113
+ '- NO inventes herramientas como "code_interpreter", "python", "bash", "shell", etc.',
114
+ '- NO uses formatos como <invoke>, function calls, ni tool_use de otros sistemas.',
115
+ '- Si una herramienta falla, INTENTA con otra herramienta diferente.',
116
+ '- Si una herramienta falla 2 VECES seguidas, no la repitas. Cambia de estrategia o usa type=final.',
117
+ '- LIMITE: Maximo 8 herramientas por turno. Despues de 8 pasos, responde con type=final.',
118
+ '- Si no puedes completar la tarea, responde con type=final explicando honestamente por que.',
119
+ '- Cada respuesta debe ser UNICAMENTE el JSON. Sin texto antes ni despues.',
120
+ ]
121
+ : [
122
+ '',
123
+ '# Strict response format',
124
+ 'Only TWO response formats are allowed. Do NOT use others:',
125
+ '',
126
+ 'FORMAT 1 — To USE a tool:',
127
+ '{"type":"tool","tool":"EXACT_NAME","args":{"key":"value"}}',
128
+ '',
129
+ 'FORMAT 2 — To REPLY to user:',
130
+ '{"type":"final","content":"Your answer here"}',
131
+ '',
132
+ 'STRICT RULES:',
133
+ '- ONLY use tool names listed in "# Tool use".',
134
+ '- Do NOT invent tools like "code_interpreter", "python", "bash", "shell", etc.',
135
+ '- Do NOT use <invoke>, function call, or tool_use formats from other systems.',
136
+ '- If a tool fails, TRY a different tool instead.',
137
+ '- If a tool fails 2 TIMES in a row, do not repeat it. Change strategy or use type=final.',
138
+ '- LIMIT: Maximum 8 tools per turn. After 8 steps, respond with type=final.',
139
+ '- If you cannot complete the task, use type=final and honestly explain why.',
140
+ '- Each response must be ONLY the JSON. No text before or after.',
141
+ ];
142
+
105
143
  const parts = [
106
144
  skills,
107
145
  '',
108
146
  '# Tool use',
109
147
  getToolPromptText(),
148
+ ...toolUseEnforcement,
110
149
  '',
111
150
  '# Environment',
112
151
  `- Working directory: ${cwd}`,
@@ -220,6 +259,9 @@ function classifyParsed(parsed) {
220
259
  if (parsed?.type === 'final') {
221
260
  return { type: 'final', content: typeof parsed.content === 'string' ? parsed.content : '' };
222
261
  }
262
+ if (parsed?.tool && KNOWN_TOOLS.has(parsed.tool)) {
263
+ return { type: 'tool', tool: parsed.tool, args: parsed.args ?? {} };
264
+ }
223
265
  return null;
224
266
  }
225
267
 
@@ -253,7 +295,7 @@ const LONG_VALUE_ARG = {
253
295
  };
254
296
 
255
297
  function fuzzyExtractTool(text) {
256
- const toolMatch = text.match(/"tool"\s*:\s*"(\w+)"/);
298
+ const toolMatch = text.match(/(?:"|')?tool(?:"|')?\s*:\s*"(\w+)"/i);
257
299
  if (!toolMatch) return null;
258
300
 
259
301
  const tool = toolMatch[1];
@@ -284,32 +326,57 @@ function findStringEnd(text, start) {
284
326
  return -1;
285
327
  }
286
328
 
329
+ function extractArgsContext(text) {
330
+ const argsMatch = text.match(/(?:"|')?args(?:"|')?\s*:\s*\{/i);
331
+ if (!argsMatch) return text;
332
+
333
+ let depth = 1;
334
+ let inStr = false;
335
+ let esc = false;
336
+ const start = argsMatch.index + argsMatch[0].length - 1;
337
+
338
+ for (let i = start + 1; i < text.length; i++) {
339
+ const ch = text[i];
340
+ if (esc) { esc = false; continue; }
341
+ if (ch === '\\' && inStr) { esc = true; continue; }
342
+ if (ch === '"') { inStr = !inStr; continue; }
343
+ if (inStr) continue;
344
+ if (ch === '{') depth++;
345
+ if (ch === '}') {
346
+ depth--;
347
+ if (depth === 0) return text.slice(start, i + 1);
348
+ }
349
+ }
350
+ return text;
351
+ }
352
+
287
353
  function extractLongValueTool(text, tool, longArg) {
354
+ const context = extractArgsContext(text);
288
355
  const args = {};
289
356
  const keys = TOOL_ARG_KEYS[tool] || [];
290
357
 
291
358
  for (const key of keys) {
292
359
  if (key === longArg) continue;
293
- const m = text.match(new RegExp(`"${key}"\\s*:\\s*"([^"]*?)"`));
360
+ const m = context.match(new RegExp(`(?:"|')?${key}(?:"|')?\\s*:\\s*"([^"]*?)"`));
294
361
  if (m) args[key] = unescapeJsonString(m[1]);
295
- const bm = text.match(new RegExp(`"${key}"\\s*:\\s*(true|false|\\d+)`));
362
+ const bm = context.match(new RegExp(`(?:"|')?${key}(?:"|')?\\s*:\\s*(true|false|\\d+)`));
296
363
  if (bm) args[key] = bm[1] === 'true' ? true : bm[1] === 'false' ? false : Number(bm[1]);
297
364
  }
298
365
 
299
- const marker = `"${longArg}"`;
300
- const argIdx = text.indexOf(marker);
301
- if (argIdx === -1) return null;
366
+ const longKeyRe = new RegExp(`(?:"|')?${longArg}(?:"|')?\\s*:`);
367
+ const longMatch = context.match(longKeyRe);
368
+ if (!longMatch) return null;
302
369
 
303
- let i = text.indexOf(':', argIdx + marker.length);
304
- if (i === -1) return null;
305
- i = text.indexOf('"', i);
306
- if (i === -1) return null;
307
- const valStart = i + 1;
370
+ const colonPos = context.indexOf(':', longMatch.index + longMatch[0].length - 1);
371
+ if (colonPos === -1) return null;
372
+ const quotePos = context.indexOf('"', colonPos);
373
+ if (quotePos === -1) return null;
374
+ const valStart = quotePos + 1;
308
375
 
309
- const valEnd = findStringEnd(text, valStart);
376
+ const valEnd = findStringEnd(context, valStart);
310
377
  if (valEnd === -1 || valEnd <= valStart) return null;
311
378
 
312
- const value = text.slice(valStart, valEnd);
379
+ const value = context.slice(valStart, valEnd);
313
380
  if (!value.trim()) return null;
314
381
 
315
382
  args[longArg] = unescapeJsonString(value);
@@ -321,9 +388,9 @@ function extractSimpleArgsTool(text, tool) {
321
388
  const keys = TOOL_ARG_KEYS[tool] || [];
322
389
 
323
390
  for (const key of keys) {
324
- const strM = text.match(new RegExp(`"${key}"\\s*:\\s*"([^"]*?)"`));
391
+ const strM = text.match(new RegExp(`(?:"|')?${key}(?:"|')?\\s*:\\s*"([^"]*?)"`));
325
392
  if (strM) { args[key] = unescapeJsonString(strM[1]); continue; }
326
- const numM = text.match(new RegExp(`"${key}"\\s*:\\s*(true|false|\\d+)`));
393
+ const numM = text.match(new RegExp(`(?:"|')?${key}(?:"|')?\\s*:\\s*(true|false|\\d+)`));
327
394
  if (numM) {
328
395
  const v = numM[1];
329
396
  args[key] = v === 'true' ? true : v === 'false' ? false : Number(v);
@@ -364,7 +431,7 @@ function parseAgentResponse(raw) {
364
431
  const fuzzy = fuzzyExtractTool(text);
365
432
  if (fuzzy) return fuzzy;
366
433
 
367
- return { type: 'final', content: text || raw.trim() };
434
+ return { type: 'final', content: text || (raw ? String(raw).trim() : '') };
368
435
  }
369
436
 
370
437
  function sanitizeArgsForModel(parsed) {
@@ -402,11 +469,15 @@ function buildConversationMessages(state, turnMessages, systemPrompt) {
402
469
  }
403
470
 
404
471
  function buildToolResultMessage(parsed, result) {
472
+ const maxResultChars = 8000;
473
+ const truncatedResult = typeof result === 'string' && result.length > maxResultChars
474
+ ? `${result.slice(0, maxResultChars)}\n... [resultado truncado, ${result.length} caracteres totales]`
475
+ : result;
405
476
  return [
406
477
  `Herramienta: ${parsed.tool}`,
407
478
  `Argumentos: ${JSON.stringify(sanitizeArgsForModel(parsed), null, 2)}`,
408
479
  'Resultado:',
409
- result,
480
+ truncatedResult,
410
481
  '',
411
482
  'Responde con la siguiente accion concreta o con el resultado final.',
412
483
  ].join('\n');
@@ -414,9 +485,10 @@ function buildToolResultMessage(parsed, result) {
414
485
 
415
486
  function buildToolErrorMessage(parsed, errorMessage) {
416
487
  return [
417
- `La herramienta ${parsed.tool} fallo.`,
488
+ `La herramienta "${parsed.tool}" NO existe.`,
418
489
  `Error: ${errorMessage}`,
419
- 'Corrige la llamada o explica brevemente el problema si no puedes continuar.',
490
+ `Las unicas herramientas disponibles son: ${TOOL_DEFINITIONS.map(t => t.name).join(', ')}.`,
491
+ 'Elige UNA de esas herramientas. Usa el formato exacto del nombre. No inventes herramientas.',
420
492
  ].join('\n');
421
493
  }
422
494