zyn-ai 1.3.5 → 1.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/package.json +1 -2
- package/src/cli/commands.js +46 -0
- package/src/cli/print.js +3 -3
- package/src/core/agent.js +171 -307
- package/src/core/prompts.js +59 -44
- package/src/core/skills.js +59 -67
- package/src/i18n.js +14 -0
- package/src/providers/catalog.js +26 -39
- package/src/providers/gemini/index.js +63 -45
- package/src/providers/qwen/index.js +0 -3
- package/src/providers/zen/index.js +0 -3
- package/src/tui/app.mjs +359 -118
- package/src/web/public/assets/index.css +1 -0
- package/src/web/public/assets/index.js +151 -0
- package/src/web/public/index.html +16 -1054
- package/src/web/server.js +8 -2
package/src/core/agent.js
CHANGED
|
@@ -6,7 +6,6 @@ const {
|
|
|
6
6
|
MODELS,
|
|
7
7
|
PROVIDER_TIMEOUT_MAX_ATTEMPTS,
|
|
8
8
|
PROVIDER_TIMEOUT_RETRY_DELAY_MS,
|
|
9
|
-
REQUEST_TIMEOUT_MS,
|
|
10
9
|
} = require('../config');
|
|
11
10
|
const { chat, chatSilent } = require('../providers/scraperClient');
|
|
12
11
|
const {
|
|
@@ -26,8 +25,6 @@ const { estimateHistoryChars, saveState } = require('../utils/sessionStorage');
|
|
|
26
25
|
const { normalizeText, shortText } = require('../utils/text');
|
|
27
26
|
const { detectLanguage } = require('../i18n');
|
|
28
27
|
|
|
29
|
-
|
|
30
|
-
|
|
31
28
|
function waitForRetry(ms, signal) {
|
|
32
29
|
if (!ms || ms <= 0) return Promise.resolve();
|
|
33
30
|
if (signal?.aborted) return Promise.reject(new Error('aborted'));
|
|
@@ -53,7 +50,7 @@ function waitForRetry(ms, signal) {
|
|
|
53
50
|
function looksLikeActionRequest(text) {
|
|
54
51
|
const sample = normalizeText(String(text || '')).toLowerCase();
|
|
55
52
|
if (!sample) return false;
|
|
56
|
-
return /(instala|
|
|
53
|
+
return /(instala|install|run|ejecuta|crea|build|compile|compila|fix|arregla|corrige|update|actualiza|edita|edit|borra|delete|remove|descarga|download|busca|search|prueba|test|verifica|check|configura|setup|mueve|move|import|aplica|apply|deploy|despliega|init|npm|git|docker|qemu)/i.test(sample);
|
|
57
54
|
}
|
|
58
55
|
|
|
59
56
|
async function requestModel(messages, state, ui, options = {}) {
|
|
@@ -65,13 +62,11 @@ async function requestModel(messages, state, ui, options = {}) {
|
|
|
65
62
|
|
|
66
63
|
for (let attempt = 0; attempt < PROVIDER_TIMEOUT_MAX_ATTEMPTS; attempt += 1) {
|
|
67
64
|
if (signal?.aborted) {
|
|
68
|
-
throw new Error(state.language === 'es' ? 'Agente detenido por el usuario
|
|
65
|
+
throw new Error(state.language === 'es' ? 'Agente detenido por el usuario' : 'Agent stopped by user');
|
|
69
66
|
}
|
|
70
67
|
const stopThinking = ui.startThinkingIndicator(state, attempt === 0 ? label : `${label} (${state.language === 'es' ? 'reintento' : 'retry'})`);
|
|
71
68
|
let answerStarted = false;
|
|
72
69
|
let thinkingStarted = false;
|
|
73
|
-
let timedOut = false;
|
|
74
|
-
let timeout = null;
|
|
75
70
|
const controller = new AbortController();
|
|
76
71
|
const onExternalAbort = () => controller.abort();
|
|
77
72
|
|
|
@@ -79,14 +74,6 @@ async function requestModel(messages, state, ui, options = {}) {
|
|
|
79
74
|
if (signal.aborted) controller.abort();
|
|
80
75
|
else signal.addEventListener('abort', onExternalAbort, { once: true });
|
|
81
76
|
}
|
|
82
|
-
const refreshTimeout = () => {
|
|
83
|
-
if (timeout) clearTimeout(timeout);
|
|
84
|
-
timeout = setTimeout(() => {
|
|
85
|
-
timedOut = true;
|
|
86
|
-
controller.abort();
|
|
87
|
-
}, REQUEST_TIMEOUT_MS);
|
|
88
|
-
};
|
|
89
|
-
refreshTimeout();
|
|
90
77
|
|
|
91
78
|
try {
|
|
92
79
|
const result = await chat({
|
|
@@ -94,7 +81,6 @@ async function requestModel(messages, state, ui, options = {}) {
|
|
|
94
81
|
modelKey: state?.activeModel || DEFAULT_MODEL_KEY,
|
|
95
82
|
signal: controller.signal,
|
|
96
83
|
onChunk: (delta, phase) => {
|
|
97
|
-
refreshTimeout();
|
|
98
84
|
if (phase === 'thinking') {
|
|
99
85
|
if (!thinkingStarted) {
|
|
100
86
|
stopThinking();
|
|
@@ -116,104 +102,173 @@ async function requestModel(messages, state, ui, options = {}) {
|
|
|
116
102
|
if (streamOutput) ui.writeAssistantDelta(state, delta);
|
|
117
103
|
},
|
|
118
104
|
});
|
|
119
|
-
ui.pushAction(state, 'ok', 'Respuesta
|
|
105
|
+
ui.pushAction(state, 'ok', 'Respuesta recibida');
|
|
120
106
|
return result.answer ?? '';
|
|
121
107
|
} catch (err) {
|
|
122
108
|
const externalAbort = Boolean(signal?.aborted);
|
|
123
109
|
const aborted = controller.signal.aborted || err?.name === 'AbortError';
|
|
124
|
-
if (aborted
|
|
125
|
-
|
|
126
|
-
ui.logEvent(
|
|
127
|
-
state,
|
|
128
|
-
'warn',
|
|
129
|
-
state.language === 'es' ? 'Tiempo agotado del proveedor' : 'Provider timeout',
|
|
130
|
-
state.language === 'es'
|
|
131
|
-
? `Esperando ${waitMinutes} minutos antes de reenviar (${attempt + 2}/${PROVIDER_TIMEOUT_MAX_ATTEMPTS})`
|
|
132
|
-
: `Waiting ${waitMinutes} minutes before resending (${attempt + 2}/${PROVIDER_TIMEOUT_MAX_ATTEMPTS})`,
|
|
133
|
-
);
|
|
134
|
-
try {
|
|
135
|
-
await waitForRetry(PROVIDER_TIMEOUT_RETRY_DELAY_MS, signal);
|
|
136
|
-
} catch {
|
|
137
|
-
throw new Error(state.language === 'es' ? 'Agente detenido por el usuario (ESC x2)' : 'Agent stopped by the user (ESC x2)');
|
|
138
|
-
}
|
|
139
|
-
continue;
|
|
140
|
-
}
|
|
141
|
-
if (aborted) {
|
|
142
|
-
if (externalAbort) {
|
|
143
|
-
throw new Error(state.language === 'es' ? 'Agente detenido por el usuario (ESC x2)' : 'Agent stopped by the user (ESC x2)');
|
|
144
|
-
}
|
|
145
|
-
throw new Error(state.language === 'es' ? 'Tiempo agotado del proveedor' : 'Provider timeout exceeded');
|
|
146
|
-
}
|
|
147
|
-
if (!externalAbort && attempt < PROVIDER_TIMEOUT_MAX_ATTEMPTS - 1) {
|
|
148
|
-
ui.logEvent(state, 'warn', state.language === 'es' ? 'Error transitorio, reenviando contexto y skills' : 'Transient error, resending context and skills');
|
|
149
|
-
continue;
|
|
150
|
-
}
|
|
110
|
+
if (aborted) throw new Error(state.language === 'es' ? 'Tiempo agotado' : 'Timeout');
|
|
111
|
+
if (!externalAbort && attempt < PROVIDER_TIMEOUT_MAX_ATTEMPTS - 1) continue;
|
|
151
112
|
throw err;
|
|
152
113
|
} finally {
|
|
153
|
-
clearTimeout(timeout);
|
|
154
114
|
if (signal) signal.removeEventListener('abort', onExternalAbort);
|
|
155
115
|
stopThinking();
|
|
156
116
|
if (thinkingStarted) ui.endThinkingStream(state);
|
|
157
117
|
if (streamOutput && answerStarted) ui.endAssistantStream(state);
|
|
158
118
|
}
|
|
159
119
|
}
|
|
160
|
-
throw new Error(
|
|
120
|
+
throw new Error('Provider unreachable');
|
|
161
121
|
}
|
|
162
122
|
|
|
163
|
-
|
|
123
|
+
function normalizeCompactMode(mode) {
|
|
124
|
+
const value = String(mode || '').trim().toLowerCase();
|
|
125
|
+
if (value === 'low' || value === 'medium' || value === 'high') return value;
|
|
126
|
+
return 'medium';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function compactTextLossless(text) {
|
|
130
|
+
return String(text || '')
|
|
131
|
+
.split('\r\n').join('\n')
|
|
132
|
+
.split('\r').join('\n')
|
|
133
|
+
.replaceAll('\t', ' ')
|
|
134
|
+
.replace(/[ \t]+$/gm, '')
|
|
135
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
136
|
+
.trim();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function compactMessageContent(message, mode) {
|
|
140
|
+
const content = compactTextLossless(message?.content ?? '');
|
|
141
|
+
if (mode === 'low') return content;
|
|
142
|
+
const lines = content.split('\n');
|
|
143
|
+
const filtered = lines.filter((line, idx, arr) => !(line.trim() === '' && arr[idx - 1]?.trim() === ''));
|
|
144
|
+
if (mode === 'high') {
|
|
145
|
+
return filtered.slice(0, Math.max(8, Math.min(filtered.length, 24))).join('\n');
|
|
146
|
+
}
|
|
147
|
+
return filtered.join('\n');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function mergeMemorySummary(previous, next) {
|
|
151
|
+
const parts = [compactTextLossless(previous), compactTextLossless(next)].filter(Boolean);
|
|
152
|
+
if (parts.length === 0) return '';
|
|
153
|
+
if (parts.length === 1) return parts[0];
|
|
154
|
+
return parts.join('\n\n');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function summarizeMessages(state, ui, messages, mode = 'medium') {
|
|
158
|
+
const compactMode = normalizeCompactMode(mode);
|
|
164
159
|
const transcript = messages
|
|
165
|
-
.map(message => `${message.role.toUpperCase()}:\n${message
|
|
160
|
+
.map(message => `${message.role.toUpperCase()}:\n${compactMessageContent(message, compactMode)}`)
|
|
166
161
|
.join('\n\n');
|
|
167
162
|
|
|
163
|
+
if (compactMode === 'low') {
|
|
164
|
+
return compactTextLossless([
|
|
165
|
+
state.memorySummary ? `Memoria previa:\n${state.memorySummary}` : '',
|
|
166
|
+
transcript,
|
|
167
|
+
].filter(Boolean).join('\n\n'));
|
|
168
|
+
}
|
|
169
|
+
|
|
168
170
|
const prompt = [
|
|
169
171
|
{
|
|
170
172
|
role: 'system',
|
|
171
173
|
content: [
|
|
172
|
-
state.language === 'es' ? 'Compacta la conversacion para memoria persistente del agente.' : 'Compact the conversation for the agent persistent memory.',
|
|
173
|
-
state.language === 'es' ? 'Escribe en español y conserva los idiomas/preferencias indicados por el usuario.' : 'Write in English and preserve any languages/preferences requested by the user.',
|
|
174
174
|
state.language === 'es'
|
|
175
|
-
? '
|
|
176
|
-
: '
|
|
177
|
-
state.language === 'es'
|
|
178
|
-
|
|
175
|
+
? 'Eres un sistema de compresion de memoria tecnica profesional.'
|
|
176
|
+
: 'You are a professional technical memory compression system.',
|
|
177
|
+
state.language === 'es'
|
|
178
|
+
? 'Resume la sesion manteniendo: Objetivos de desarrollo, estructura del proyecto actual, archivos modificados, comandos ejecutados con exito, errores encontrados y estado de los servicios (Docker/QEMU/Backend).'
|
|
179
|
+
: 'Summarize the session while preserving development goals, current project structure, modified files, successful commands, encountered errors, and service state (Docker/QEMU/Backend).',
|
|
180
|
+
state.language === 'es'
|
|
181
|
+
? 'Se extremadamente conciso. Usa listas de puntos. No pierdas rutas de archivos ni valores de variables de entorno.'
|
|
182
|
+
: 'Be extremely concise. Use bullet points. Do not lose file paths or environment variable values.',
|
|
183
|
+
state.language === 'es'
|
|
184
|
+
? 'Si algo no se termino, marcalo como [BLOQUEADO] o [PENDIENTE].'
|
|
185
|
+
: 'If something was not finished, mark it as [BLOCKED] or [PENDING].',
|
|
186
|
+
state.language === 'es'
|
|
187
|
+
? 'Formato: Contexto | Progreso Tecnico | Archivos | Pendientes. Max 20 lineas.'
|
|
188
|
+
: 'Format: Context | Technical Progress | Files | Pending. Max 20 lines.',
|
|
179
189
|
].join('\n'),
|
|
180
190
|
},
|
|
181
191
|
{
|
|
182
192
|
role: 'user',
|
|
183
193
|
content: [
|
|
184
|
-
state.memorySummary ?
|
|
185
|
-
'Conversacion a
|
|
194
|
+
state.memorySummary ? `${state.language === 'es' ? 'Memoria previa' : 'Previous memory'}:\n${state.memorySummary}\n` : '',
|
|
195
|
+
state.language === 'es' ? 'Conversacion a resumir:' : 'Conversation to summarize:',
|
|
186
196
|
transcript,
|
|
187
197
|
].join('\n'),
|
|
188
198
|
},
|
|
189
199
|
];
|
|
190
200
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
201
|
+
const label = compactMode === 'high'
|
|
202
|
+
? (state.language === 'es' ? 'Compactando memoria alta' : 'High memory compaction')
|
|
203
|
+
: (state.language === 'es' ? 'Consolidando memoria técnica' : 'Consolidating technical memory');
|
|
204
|
+
|
|
205
|
+
return normalizeText(await requestModel(prompt, state, ui, { label }));
|
|
194
206
|
}
|
|
195
207
|
|
|
196
208
|
async function compactHistoryIfNeeded(state, ui) {
|
|
197
|
-
if (estimateHistoryChars(state.history) <= MAX_HISTORY_CHARS)
|
|
198
|
-
|
|
209
|
+
if (estimateHistoryChars(state.history) <= MAX_HISTORY_CHARS) return;
|
|
210
|
+
if (state.history.length <= KEEP_RECENT_MESSAGES) return;
|
|
211
|
+
|
|
212
|
+
const splitIndex = Math.max(2, state.history.length - KEEP_RECENT_MESSAGES);
|
|
213
|
+
const oldMessages = state.history.slice(0, splitIndex);
|
|
214
|
+
const recentMessages = state.history.slice(splitIndex);
|
|
215
|
+
const summary = await summarizeMessages(state, ui, oldMessages, 'medium');
|
|
216
|
+
|
|
217
|
+
state.memorySummary = mergeMemorySummary(state.memorySummary, summary);
|
|
218
|
+
state.history = recentMessages;
|
|
219
|
+
ui?.logEvent?.(state, 'info', state.language === 'es' ? 'Memoria compactada' : 'Memory compacted', shortText(summary, 120));
|
|
220
|
+
await appendTranscriptEntry(state.sessionId, {
|
|
221
|
+
type: 'system',
|
|
222
|
+
content: `${state.language === 'es' ? 'Memoria técnica actualizada' : 'Technical memory updated'}:
|
|
223
|
+
${summary}`,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function compactMemory(state, ui, mode = 'medium', options = {}) {
|
|
228
|
+
const compactMode = normalizeCompactMode(mode);
|
|
229
|
+
const force = Boolean(options.force);
|
|
230
|
+
if (compactMode === 'high' && !force) {
|
|
231
|
+
const confirm = typeof state.tuiConfirm === 'function'
|
|
232
|
+
? await state.tuiConfirm(
|
|
233
|
+
state.language === 'es' ? 'Confirmación de compactación alta' : 'High compaction confirmation',
|
|
234
|
+
t(state.language, 'compactWarningHigh'),
|
|
235
|
+
)
|
|
236
|
+
: true;
|
|
237
|
+
if (!confirm || String(confirm).toLowerCase() === 'n') return { changed: false, mode: compactMode, warned: true };
|
|
199
238
|
}
|
|
200
239
|
|
|
201
|
-
if (
|
|
202
|
-
|
|
240
|
+
if (compactMode === 'low') {
|
|
241
|
+
const nextSummary = compactTextLossless([
|
|
242
|
+
state.memorySummary,
|
|
243
|
+
state.history.map(message => `${message.role.toUpperCase()}: ${compactMessageContent(message, 'low')}`).join('\n'),
|
|
244
|
+
].filter(Boolean).join('\n\n'));
|
|
245
|
+
const changed = nextSummary !== state.memorySummary;
|
|
246
|
+
state.memorySummary = nextSummary;
|
|
247
|
+
if (changed) {
|
|
248
|
+
await saveState(state);
|
|
249
|
+
ui?.logEvent?.(state, 'info', t(state.language, 'compactDone'), t(state.language, 'compactCommand', { mode: compactMode }));
|
|
250
|
+
} else {
|
|
251
|
+
ui?.logEvent?.(state, 'info', t(state.language, 'compactNoChange'), '');
|
|
252
|
+
}
|
|
253
|
+
return { changed, mode: compactMode };
|
|
203
254
|
}
|
|
204
255
|
|
|
205
|
-
const splitIndex = Math.max(
|
|
256
|
+
const splitIndex = Math.max(1, state.history.length - KEEP_RECENT_MESSAGES);
|
|
206
257
|
const oldMessages = state.history.slice(0, splitIndex);
|
|
207
258
|
const recentMessages = state.history.slice(splitIndex);
|
|
208
|
-
const summary = await summarizeMessages(state, ui, oldMessages);
|
|
209
|
-
|
|
210
|
-
state.memorySummary
|
|
259
|
+
const summary = await summarizeMessages(state, ui, oldMessages, compactMode);
|
|
260
|
+
const nextSummary = mergeMemorySummary(state.memorySummary, summary);
|
|
261
|
+
const changed = nextSummary !== state.memorySummary || recentMessages.length !== state.history.length;
|
|
262
|
+
state.memorySummary = nextSummary;
|
|
211
263
|
state.history = recentMessages;
|
|
212
|
-
ui
|
|
264
|
+
ui?.logEvent?.(state, 'info', t(state.language, 'compactDone'), t(state.language, 'compactCommand', { mode: compactMode }));
|
|
213
265
|
await appendTranscriptEntry(state.sessionId, {
|
|
214
266
|
type: 'system',
|
|
215
|
-
content:
|
|
267
|
+
content: `${state.language === 'es' ? 'Memoria técnica actualizada' : 'Technical memory updated'}:
|
|
268
|
+
${summary}`,
|
|
216
269
|
});
|
|
270
|
+
await saveState(state);
|
|
271
|
+
return { changed, mode: compactMode };
|
|
217
272
|
}
|
|
218
273
|
|
|
219
274
|
async function persistSessionState(state, ui) {
|
|
@@ -225,27 +280,16 @@ async function answerFromToolResult(input, call, result, state, ui) {
|
|
|
225
280
|
const messages = [
|
|
226
281
|
{
|
|
227
282
|
role: 'system',
|
|
228
|
-
content:
|
|
229
|
-
'You are Zyn.',
|
|
230
|
-
state.language === 'es' ? 'Responde en espanol, directo y solo con la respuesta final.' : 'Respond in English, direct and only with the final answer.',
|
|
231
|
-
state.language === 'es' ? 'Usa solo los datos del resultado de herramienta dado.' : 'Use only the data from the provided tool result.',
|
|
232
|
-
`Directorio actual: ${state.cwd}`,
|
|
233
|
-
].join('\n'),
|
|
283
|
+
content: `Eres Zyn. Responde de forma técnica y directa basada únicamente en el resultado de la herramienta. Directorio: ${state.cwd}`,
|
|
234
284
|
},
|
|
235
285
|
{
|
|
236
286
|
role: 'user',
|
|
237
|
-
content:
|
|
238
|
-
'Solicitud original del usuario:',
|
|
239
|
-
input,
|
|
240
|
-
'',
|
|
241
|
-
`Resultado de la herramienta ${call.tool}:`,
|
|
242
|
-
result,
|
|
243
|
-
].join('\n'),
|
|
287
|
+
content: `Solicitud: ${input}\n\nResultado de ${call.tool}:\n${result}`,
|
|
244
288
|
},
|
|
245
289
|
];
|
|
246
290
|
|
|
247
291
|
const output = await requestModel(messages, state, ui, {
|
|
248
|
-
label: state.language === 'es' ? '
|
|
292
|
+
label: state.language === 'es' ? 'Procesando resultado' : 'Processing result',
|
|
249
293
|
});
|
|
250
294
|
const parsed = parseAgentResponse(output);
|
|
251
295
|
return parsed.type === 'final' ? normalizeText(parsed.content) : normalizeText(output);
|
|
@@ -257,30 +301,15 @@ async function runAgentTurn(input, state, ui, options = {}) {
|
|
|
257
301
|
if (state.turnCount === 1 && state.title === 'New session') {
|
|
258
302
|
state.title = shortText(input, 60) || state.title;
|
|
259
303
|
}
|
|
260
|
-
ui.logEvent(state, 'info', `${state.language === 'es' ? 'Turno' : 'Turn'} ${state.turnCount}`);
|
|
261
|
-
|
|
262
|
-
let toolUsedThisTurn = false;
|
|
263
|
-
let finalWithoutToolRetries = 0;
|
|
264
304
|
|
|
265
305
|
const directAction = parseDirectAction(input);
|
|
266
306
|
if (directAction) {
|
|
267
307
|
await appendTranscriptEntry(state.sessionId, { type: 'user', content: input });
|
|
268
|
-
toolUsedThisTurn = true;
|
|
269
308
|
const result = await executeToolCall(directAction, state, ui);
|
|
270
|
-
await appendTranscriptEntry(state.sessionId, {
|
|
271
|
-
type: 'tool',
|
|
272
|
-
tool: directAction.tool,
|
|
273
|
-
args: directAction.args,
|
|
274
|
-
result,
|
|
275
|
-
});
|
|
309
|
+
await appendTranscriptEntry(state.sessionId, { type: 'tool', tool: directAction.tool, args: directAction.args, result });
|
|
276
310
|
const finalAnswer = await answerFromToolResult(input, directAction, result, state, ui);
|
|
277
|
-
state.history.push({ role: 'user', content: input });
|
|
278
|
-
state.
|
|
279
|
-
await appendTranscriptEntry(state.sessionId, {
|
|
280
|
-
type: 'assistant',
|
|
281
|
-
content: finalAnswer,
|
|
282
|
-
});
|
|
283
|
-
ui.logEvent(state, 'ok', state.language === 'es' ? 'Respuesta lista' : 'Response ready');
|
|
311
|
+
state.history.push({ role: 'user', content: input }, { role: 'assistant', content: finalAnswer });
|
|
312
|
+
await appendTranscriptEntry(state.sessionId, { type: 'assistant', content: finalAnswer });
|
|
284
313
|
await persistSessionState(state, ui);
|
|
285
314
|
return { content: finalAnswer, rendered: false };
|
|
286
315
|
}
|
|
@@ -288,24 +317,19 @@ async function runAgentTurn(input, state, ui, options = {}) {
|
|
|
288
317
|
const turnMessages = [{ role: 'user', content: input }];
|
|
289
318
|
await appendTranscriptEntry(state.sessionId, { type: 'user', content: input });
|
|
290
319
|
|
|
320
|
+
let step = 0;
|
|
321
|
+
let toolUsedThisTurn = false;
|
|
322
|
+
let finalWithoutToolRetries = 0;
|
|
291
323
|
let lastFingerprint = '';
|
|
292
324
|
let repeatCount = 0;
|
|
293
|
-
const toolPathUsage = new Map();
|
|
294
|
-
let step = 0;
|
|
295
325
|
const turnLanguage = detectLanguage(input, state.language);
|
|
296
326
|
|
|
297
327
|
while (true) {
|
|
298
|
-
if (signal?.aborted)
|
|
299
|
-
throw new Error(state.language === 'es' ? 'Agente detenido por el usuario' : 'Agent stopped by the user');
|
|
300
|
-
}
|
|
328
|
+
if (signal?.aborted) throw new Error('Aborted');
|
|
301
329
|
|
|
302
|
-
const injected = typeof state.getQueuedMessages === 'function'
|
|
303
|
-
? state.getQueuedMessages()
|
|
304
|
-
: [];
|
|
330
|
+
const injected = typeof state.getQueuedMessages === 'function' ? state.getQueuedMessages() : [];
|
|
305
331
|
for (const msg of injected) {
|
|
306
|
-
|
|
307
|
-
turnMessages.push({ role: 'user', content: note });
|
|
308
|
-
ui.logEvent(state, 'info', state.language === 'es' ? 'Mensaje recibido en vivo' : 'Live message received', shortText(msg, 60));
|
|
332
|
+
turnMessages.push({ role: 'user', content: `INPUT_ADICIONAL:\n${msg}` });
|
|
309
333
|
}
|
|
310
334
|
|
|
311
335
|
const messages = buildConversationMessages(
|
|
@@ -314,173 +338,49 @@ async function runAgentTurn(input, state, ui, options = {}) {
|
|
|
314
338
|
buildSystemPrompt(state.cwd, state, { input, language: turnLanguage }),
|
|
315
339
|
);
|
|
316
340
|
|
|
317
|
-
const
|
|
318
|
-
label: step === 0 ?
|
|
341
|
+
const raw = await requestModel(messages, state, ui, {
|
|
342
|
+
label: step === 0 ? 'Analizando' : `Paso ${step + 1}`,
|
|
319
343
|
signal,
|
|
320
344
|
});
|
|
321
345
|
|
|
322
|
-
let secondaryResults = [];
|
|
323
|
-
if (state.concuerdo) {
|
|
324
|
-
const activeKey = state.activeModel || DEFAULT_MODEL_KEY;
|
|
325
|
-
const otherKeys = Object.keys(MODELS).filter(k => k !== activeKey);
|
|
326
|
-
const CONCUERDO_TIMEOUT = 30000;
|
|
327
|
-
const withTimeout = (promise) => Promise.race([
|
|
328
|
-
promise,
|
|
329
|
-
new Promise(r => setTimeout(() => r(null), CONCUERDO_TIMEOUT)),
|
|
330
|
-
]);
|
|
331
|
-
const secondaryPromises = otherKeys.map(k =>
|
|
332
|
-
withTimeout(chatSilent({ messages, modelKey: k, signal }).catch(() => null))
|
|
333
|
-
);
|
|
334
|
-
secondaryResults = secondaryPromises.map((p, i) => ({ promise: p, key: otherKeys[i] }));
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
const raw = await primaryPromise;
|
|
338
346
|
let parsed = parseAgentResponse(raw);
|
|
339
347
|
|
|
340
|
-
if (
|
|
341
|
-
const
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
const altParsed = parseAgentResponse(val.answer);
|
|
355
|
-
|
|
356
|
-
if (altParsed.type === 'tool') {
|
|
357
|
-
toolSuggestions.push({ parsed: altParsed, label });
|
|
358
|
-
ui.logEvent(state, 'info', `${label} ${state.language === 'es' ? 'sugiere' : 'suggests'} ${altParsed.tool}`);
|
|
359
|
-
} else if (altParsed.type === 'final' && altParsed.content?.trim()) {
|
|
360
|
-
extras.push({ content: altParsed.content, label });
|
|
361
|
-
ui.logEvent(state, 'info', `${label} ${state.language === 'es' ? 'respondió' : 'responded'}`);
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
if (parsed.type === 'final' && toolSuggestions.length >= 2) {
|
|
366
|
-
parsed = toolSuggestions[0].parsed;
|
|
367
|
-
ui.logEvent(state, 'info', `${toolSuggestions.length} ${state.language === 'es' ? 'modelos concuerdan' : 'models agree'}: ${parsed.tool}`);
|
|
368
|
-
} else if (parsed.type === 'final' && extras.length > 0) {
|
|
369
|
-
const activeLabel = MODELS[state.activeModel || DEFAULT_MODEL_KEY]?.label || (state.language === 'es' ? 'Primario' : 'Primary');
|
|
370
|
-
ui.logEvent(state, 'info', `${state.language === 'es' ? 'Sintetizando' : 'Synthesizing'}: ${[activeLabel, ...extras.map(e => e.label)].join(' + ')}`);
|
|
371
|
-
|
|
372
|
-
const synthMessages = [
|
|
373
|
-
{
|
|
374
|
-
role: 'system',
|
|
375
|
-
content: [
|
|
376
|
-
state.language === 'es' ? 'Eres Zyn. Varios modelos IA analizaron la misma pregunta del usuario.' : 'You are Zyn. Several AI models analyzed the same user request.',
|
|
377
|
-
state.language === 'es' ? 'Tu trabajo: crear UNA SOLA respuesta final unificada.' : 'Your job: create ONE unified final answer.',
|
|
378
|
-
'Rules:',
|
|
379
|
-
state.language === 'es' ? '- NO repitas informacion que ya este cubierta por otro modelo' : '- Do not repeat information already covered by another model',
|
|
380
|
-
state.language === 'es' ? '- Integra las perspectivas unicas de cada uno naturalmente' : "- Blend each model's unique insights naturally",
|
|
381
|
-
state.language === 'es' ? '- Si todos dicen lo mismo, da UNA respuesta limpia sin redundancia' : '- If they all say the same thing, give one clean non-redundant answer',
|
|
382
|
-
state.language === 'es' ? '- Se directo y conciso' : '- Be direct and concise',
|
|
383
|
-
state.language === 'es' ? '- Responde en español' : '- Respond in English',
|
|
384
|
-
state.language === 'es' ? '- NO menciones que estas sintetizando ni que hay multiples modelos' : '- Do not mention that you are synthesizing or that multiple models are involved',
|
|
385
|
-
state.language === 'es' ? '- NO uses separadores --- ni secciones por modelo' : '- Do not use --- separators or per-model sections',
|
|
386
|
-
state.language === 'es' ? '- Responde como si fueras un solo agente dando la mejor respuesta posible' : '- Respond like a single agent giving the best possible answer',
|
|
387
|
-
].join('\n'),
|
|
388
|
-
},
|
|
389
|
-
{
|
|
390
|
-
role: 'user',
|
|
391
|
-
content: [
|
|
392
|
-
`Respuesta de ${activeLabel}:\n${parsed.content}`,
|
|
393
|
-
'',
|
|
394
|
-
...extras.map(e => `Respuesta de ${e.label}:\n${e.content}`),
|
|
395
|
-
'',
|
|
396
|
-
'Crea la respuesta final unificada:',
|
|
397
|
-
].join('\n'),
|
|
398
|
-
},
|
|
399
|
-
];
|
|
400
|
-
|
|
401
|
-
try {
|
|
402
|
-
const synthesis = await requestModel(synthMessages, state, ui, {
|
|
403
|
-
label: 'Concuerdo — unificando',
|
|
404
|
-
signal,
|
|
405
|
-
});
|
|
406
|
-
if (synthesis?.trim()) {
|
|
407
|
-
parsed = { type: 'final', content: synthesis.trim() };
|
|
408
|
-
ui.logEvent(state, 'info', state.language === 'es' ? '🤝 Respuesta unificada lista' : '🤝 Unified response ready');
|
|
409
|
-
}
|
|
410
|
-
} catch {
|
|
411
|
-
}
|
|
412
|
-
} else if (parsed.type === 'tool' && toolSuggestions.length > 0) {
|
|
413
|
-
const matching = toolSuggestions.filter(t => t.parsed.tool === parsed.tool);
|
|
414
|
-
if (matching.length > 0) {
|
|
415
|
-
ui.logEvent(state, 'info', `🤝 ${matching.length + 1} modelos concuerdan: ${parsed.tool}`);
|
|
416
|
-
}
|
|
348
|
+
if (state.concuerdo && step === 0) {
|
|
349
|
+
const activeKey = state.activeModel || DEFAULT_MODEL_KEY;
|
|
350
|
+
const otherKeys = Object.keys(MODELS).filter(k => k !== activeKey);
|
|
351
|
+
const secondaryResults = await Promise.all(otherKeys.map(k => chatSilent({ messages, modelKey: k, signal }).catch(() => null)));
|
|
352
|
+
|
|
353
|
+
const suggestions = secondaryResults
|
|
354
|
+
.filter(r => r?.answer)
|
|
355
|
+
.map(r => parseAgentResponse(r.answer));
|
|
356
|
+
|
|
357
|
+
const toolSugg = suggestions.filter(s => s.type === 'tool');
|
|
358
|
+
if (parsed.type === 'final' && toolSugg.length >= 2) {
|
|
359
|
+
parsed = toolSugg[0];
|
|
417
360
|
}
|
|
418
361
|
}
|
|
419
362
|
|
|
420
363
|
if (parsed.type === 'final') {
|
|
421
|
-
const content = parsed.content.trim();
|
|
422
364
|
if (looksLikeActionRequest(input) && !toolUsedThisTurn && finalWithoutToolRetries < 2) {
|
|
423
|
-
finalWithoutToolRetries
|
|
424
|
-
|
|
425
|
-
turnMessages.push({ role: '
|
|
426
|
-
|
|
427
|
-
role: 'user',
|
|
428
|
-
content: [
|
|
429
|
-
turnLanguage === 'es'
|
|
430
|
-
? 'Aun no has probado nada. No des una conclusion ni pasos teoricos.'
|
|
431
|
-
: 'You have not actually tried anything yet. Do not give a conclusion or theory steps.',
|
|
432
|
-
turnLanguage === 'es'
|
|
433
|
-
? 'Primero intenta una herramienta real adecuada para la tarea.'
|
|
434
|
-
: 'First try a real tool that fits the task.',
|
|
435
|
-
turnLanguage === 'es'
|
|
436
|
-
? 'Si ninguna herramienta aplica, dilo explicitamente con una sola frase corta y honesta.'
|
|
437
|
-
: 'If no tool applies, say so explicitly in one short honest sentence.',
|
|
438
|
-
].join(' '),
|
|
439
|
-
});
|
|
440
|
-
step += 1;
|
|
365
|
+
finalWithoutToolRetries++;
|
|
366
|
+
turnMessages.push({ role: 'assistant', content: parsed.content });
|
|
367
|
+
turnMessages.push({ role: 'user', content: 'No has ejecutado ninguna herramienta técnica para esta solicitud de acción. Por favor, usa las herramientas necesarias para obtener resultados reales antes de concluir.' });
|
|
368
|
+
step++;
|
|
441
369
|
continue;
|
|
442
370
|
}
|
|
443
|
-
turnMessages.push({ role: 'assistant', content: content
|
|
371
|
+
turnMessages.push({ role: 'assistant', content: parsed.content });
|
|
444
372
|
state.history.push(...turnMessages);
|
|
445
|
-
await appendTranscriptEntry(state.sessionId, {
|
|
446
|
-
type: 'assistant',
|
|
447
|
-
content,
|
|
448
|
-
});
|
|
449
|
-
ui.logEvent(state, 'ok', state.language === 'es' ? 'Respuesta lista' : 'Response ready');
|
|
373
|
+
await appendTranscriptEntry(state.sessionId, { type: 'assistant', content: parsed.content });
|
|
450
374
|
await persistSessionState(state, ui);
|
|
451
|
-
return { content, rendered: false };
|
|
375
|
+
return { content: parsed.content, rendered: false };
|
|
452
376
|
}
|
|
453
377
|
|
|
454
|
-
const
|
|
455
|
-
const contentSample = typeof parsed.args?.content === 'string'
|
|
456
|
-
? parsed.args.content.slice(0, 120)
|
|
457
|
-
: '';
|
|
458
|
-
const fingerprint = `${parsed.tool}:${targetPath}:${contentSample}`;
|
|
459
|
-
if (targetPath) {
|
|
460
|
-
const key = `${parsed.tool}:${targetPath}`;
|
|
461
|
-
const nextCount = (toolPathUsage.get(key) || 0) + 1;
|
|
462
|
-
toolPathUsage.set(key, nextCount);
|
|
463
|
-
if (nextCount >= 20 && ['write_file', 'append_file', 'replace_in_file'].includes(parsed.tool)) {
|
|
464
|
-
ui.logEvent(state, 'warn', state.language === 'es' ? 'Posible loop detectado' : 'Possible loop detected', `${parsed.tool} → ${targetPath} x${nextCount}`);
|
|
465
|
-
turnMessages.push({
|
|
466
|
-
role: 'user',
|
|
467
|
-
content: state.language === 'es'
|
|
468
|
-
? `ALTO: ya editaste ${targetPath} varias veces en este turno. No repitas ediciones; valida y responde con type=final.`
|
|
469
|
-
: `STOP: you already edited ${targetPath} multiple times in this turn. Do not repeat edits; verify and answer with type=final.`,
|
|
470
|
-
});
|
|
471
|
-
step += 1;
|
|
472
|
-
continue;
|
|
473
|
-
}
|
|
474
|
-
}
|
|
378
|
+
const fingerprint = `${parsed.tool}:${parsed.args?.path || ''}:${shortText(JSON.stringify(parsed.args || {}), 50)}`;
|
|
475
379
|
if (fingerprint === lastFingerprint) {
|
|
476
|
-
repeatCount
|
|
477
|
-
if (repeatCount >=
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
role: 'user',
|
|
481
|
-
content: 'ATENCION: Estas repitiendo la misma operacion. La operacion anterior ya fue exitosa. Responde con type=final confirmando lo que hiciste.',
|
|
482
|
-
});
|
|
483
|
-
step += 1;
|
|
380
|
+
repeatCount++;
|
|
381
|
+
if (repeatCount >= 3) {
|
|
382
|
+
turnMessages.push({ role: 'user', content: 'Estas en un bucle con la misma herramienta y argumentos. Cambia de estrategia o finaliza con el estado actual.' });
|
|
383
|
+
step++;
|
|
484
384
|
continue;
|
|
485
385
|
}
|
|
486
386
|
} else {
|
|
@@ -488,61 +388,25 @@ async function runAgentTurn(input, state, ui, options = {}) {
|
|
|
488
388
|
repeatCount = 0;
|
|
489
389
|
}
|
|
490
390
|
|
|
491
|
-
turnMessages.push({
|
|
492
|
-
role: 'assistant',
|
|
493
|
-
content: JSON.stringify(
|
|
494
|
-
{
|
|
495
|
-
type: 'tool',
|
|
496
|
-
tool: parsed.tool,
|
|
497
|
-
args: sanitizeArgsForModel(parsed),
|
|
498
|
-
},
|
|
499
|
-
null,
|
|
500
|
-
2,
|
|
501
|
-
),
|
|
502
|
-
});
|
|
391
|
+
turnMessages.push({ role: 'assistant', content: JSON.stringify({ type: 'tool', tool: parsed.tool, args: sanitizeArgsForModel(parsed) }) });
|
|
503
392
|
|
|
504
393
|
try {
|
|
505
394
|
toolUsedThisTurn = true;
|
|
506
395
|
const result = await executeToolCall(parsed, state, ui);
|
|
507
|
-
await appendTranscriptEntry(state.sessionId, {
|
|
508
|
-
|
|
509
|
-
tool: parsed.tool,
|
|
510
|
-
args: parsed.args,
|
|
511
|
-
result,
|
|
512
|
-
});
|
|
513
|
-
turnMessages.push({
|
|
514
|
-
role: 'user',
|
|
515
|
-
content: `TOOL_RESULT\n${buildToolResultMessage(parsed, result)}`,
|
|
516
|
-
});
|
|
396
|
+
await appendTranscriptEntry(state.sessionId, { type: 'tool', tool: parsed.tool, args: parsed.args, result });
|
|
397
|
+
turnMessages.push({ role: 'user', content: `TOOL_RESULT\n${buildToolResultMessage(parsed, result)}` });
|
|
517
398
|
} catch (err) {
|
|
518
|
-
|
|
519
|
-
await appendTranscriptEntry(state.sessionId, {
|
|
520
|
-
type: 'tool_error',
|
|
521
|
-
tool: parsed.tool,
|
|
522
|
-
args: parsed.args,
|
|
523
|
-
error: err.message,
|
|
524
|
-
});
|
|
525
|
-
turnMessages.push({
|
|
526
|
-
role: 'user',
|
|
527
|
-
content: buildToolErrorMessage(parsed, err.message),
|
|
528
|
-
});
|
|
399
|
+
turnMessages.push({ role: 'user', content: buildToolErrorMessage(parsed, err.message) });
|
|
529
400
|
}
|
|
530
401
|
|
|
531
|
-
step
|
|
532
|
-
if (step >= MAX_TOOL_STEPS
|
|
533
|
-
const limitMsg =
|
|
534
|
-
|
|
535
|
-
: `Reached the limit of ${MAX_TOOL_STEPS} steps. No more tools can be executed this turn. Reply with a summary of what was accomplished.`;
|
|
536
|
-
turnMessages.push({ role: 'assistant', content: limitMsg });
|
|
537
|
-
state.history.push(...turnMessages);
|
|
538
|
-
await appendTranscriptEntry(state.sessionId, { type: 'assistant', content: limitMsg });
|
|
539
|
-
ui.logEvent(state, 'warn', state.language === 'es' ? 'Límite de pasos alcanzado' : 'Step limit reached');
|
|
402
|
+
step++;
|
|
403
|
+
if (step >= MAX_TOOL_STEPS) {
|
|
404
|
+
const limitMsg = 'Límite de pasos alcanzado. Resumiendo estado actual del sistema.';
|
|
405
|
+
state.history.push(...turnMessages, { role: 'assistant', content: limitMsg });
|
|
540
406
|
await persistSessionState(state, ui);
|
|
541
407
|
return { content: limitMsg, rendered: false };
|
|
542
408
|
}
|
|
543
409
|
}
|
|
544
410
|
}
|
|
545
411
|
|
|
546
|
-
module.exports = {
|
|
547
|
-
runAgentTurn,
|
|
548
|
-
};
|
|
412
|
+
module.exports = { compactMemory, normalizeCompactMode, runAgentTurn };
|