zyn-ai 1.1.1 → 1.2.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/LICENSE +1 -3
- package/package.json +1 -1
- package/src/core/agent.js +35 -1
- package/src/core/prompts.js +7 -5
- package/src/i18n.js +19 -0
- package/src/tui/app.mjs +48 -1
- package/src/web/webAgent.js +26 -0
package/LICENSE
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Zyn Attribution License 1.0
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2026 Maycol
|
|
3
|
+
Copyright (c) 2026 Maycol and contributors.
|
|
4
4
|
|
|
5
5
|
Permission is granted to use, copy, modify, and redistribute this software,
|
|
6
6
|
including commercial use, provided that all of the following conditions are met:
|
|
@@ -12,6 +12,4 @@ including commercial use, provided that all of the following conditions are met:
|
|
|
12
12
|
the canonical URL changes.
|
|
13
13
|
4. Modified versions must clearly state that they were changed from the original.
|
|
14
14
|
|
|
15
|
-
Project link: <PROJECT_URL>
|
|
16
|
-
|
|
17
15
|
This software is provided "as is", without warranty of any kind.
|
package/package.json
CHANGED
package/src/core/agent.js
CHANGED
|
@@ -21,6 +21,14 @@ const {
|
|
|
21
21
|
const { appendTranscriptEntry } = require('../utils/transcriptStorage');
|
|
22
22
|
const { estimateHistoryChars, saveState } = require('../utils/sessionStorage');
|
|
23
23
|
const { normalizeText, shortText } = require('../utils/text');
|
|
24
|
+
const { detectLanguage } = require('../i18n');
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
function looksLikeActionRequest(text) {
|
|
28
|
+
const sample = normalizeText(String(text || '')).toLowerCase();
|
|
29
|
+
if (!sample) return false;
|
|
30
|
+
return /(instala|instalar|install|run|ejecuta|ejecutar|crea|crear|build|compile|compila|fix|arregla|corrige|update|actualiza|edita|edit|borra|elimina|remove|descarga|download|busca|search|prueba|test|verifica|check|configura|setup|mueve|move|importa|import|aplica|apply)/i.test(sample);
|
|
31
|
+
}
|
|
24
32
|
|
|
25
33
|
async function requestModel(messages, state, ui, options = {}) {
|
|
26
34
|
const {
|
|
@@ -192,6 +200,7 @@ async function runAgentTurn(input, state, ui, options = {}) {
|
|
|
192
200
|
const directAction = parseDirectAction(input);
|
|
193
201
|
if (directAction) {
|
|
194
202
|
await appendTranscriptEntry(state.sessionId, { type: 'user', content: input });
|
|
203
|
+
toolUsedThisTurn = true;
|
|
195
204
|
const result = await executeToolCall(directAction, state, ui);
|
|
196
205
|
await appendTranscriptEntry(state.sessionId, {
|
|
197
206
|
type: 'tool',
|
|
@@ -217,6 +226,9 @@ async function runAgentTurn(input, state, ui, options = {}) {
|
|
|
217
226
|
let lastFingerprint = '';
|
|
218
227
|
let repeatCount = 0;
|
|
219
228
|
let step = 0;
|
|
229
|
+
const turnLanguage = detectLanguage(input, state.language);
|
|
230
|
+
let toolUsedThisTurn = false;
|
|
231
|
+
let finalWithoutToolRetries = 0;
|
|
220
232
|
|
|
221
233
|
while (true) {
|
|
222
234
|
if (signal?.aborted) {
|
|
@@ -235,7 +247,7 @@ async function runAgentTurn(input, state, ui, options = {}) {
|
|
|
235
247
|
const messages = buildConversationMessages(
|
|
236
248
|
state,
|
|
237
249
|
turnMessages,
|
|
238
|
-
buildSystemPrompt(state.cwd, state),
|
|
250
|
+
buildSystemPrompt(state.cwd, state, { input, language: detectLanguage(input, state.language) }),
|
|
239
251
|
);
|
|
240
252
|
|
|
241
253
|
const primaryPromise = requestModel(messages, state, ui, {
|
|
@@ -343,6 +355,27 @@ async function runAgentTurn(input, state, ui, options = {}) {
|
|
|
343
355
|
|
|
344
356
|
if (parsed.type === 'final') {
|
|
345
357
|
const content = parsed.content.trim();
|
|
358
|
+
if (looksLikeActionRequest(input) && !toolUsedThisTurn && finalWithoutToolRetries < 2) {
|
|
359
|
+
finalWithoutToolRetries += 1;
|
|
360
|
+
ui.logEvent(state, 'warn', turnLanguage === 'es' ? 'Sin prueba real todavía' : 'No real attempt yet', turnLanguage === 'es' ? 'Primero intenta una herramienta antes de concluir.' : 'Try a real tool before concluding.');
|
|
361
|
+
turnMessages.push({ role: 'assistant', content: content || raw.trim() });
|
|
362
|
+
turnMessages.push({
|
|
363
|
+
role: 'user',
|
|
364
|
+
content: [
|
|
365
|
+
turnLanguage === 'es'
|
|
366
|
+
? 'Aun no has probado nada. No des una conclusion ni pasos teoricos.'
|
|
367
|
+
: 'You have not actually tried anything yet. Do not give a conclusion or theory steps.',
|
|
368
|
+
turnLanguage === 'es'
|
|
369
|
+
? 'Primero intenta una herramienta real adecuada para la tarea.'
|
|
370
|
+
: 'First try a real tool that fits the task.',
|
|
371
|
+
turnLanguage === 'es'
|
|
372
|
+
? 'Si ninguna herramienta aplica, dilo explicitamente con una sola frase corta y honesta.'
|
|
373
|
+
: 'If no tool applies, say so explicitly in one short honest sentence.',
|
|
374
|
+
].join(' '),
|
|
375
|
+
});
|
|
376
|
+
step += 1;
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
346
379
|
turnMessages.push({ role: 'assistant', content: content || raw.trim() });
|
|
347
380
|
state.history.push(...turnMessages);
|
|
348
381
|
await appendTranscriptEntry(state.sessionId, {
|
|
@@ -385,6 +418,7 @@ async function runAgentTurn(input, state, ui, options = {}) {
|
|
|
385
418
|
});
|
|
386
419
|
|
|
387
420
|
try {
|
|
421
|
+
toolUsedThisTurn = true;
|
|
388
422
|
const result = await executeToolCall(parsed, state, ui);
|
|
389
423
|
await appendTranscriptEntry(state.sessionId, {
|
|
390
424
|
type: 'tool',
|
package/src/core/prompts.js
CHANGED
|
@@ -2,7 +2,7 @@ const { normalizeText } = require('../utils/text');
|
|
|
2
2
|
const { buildSkillsPrompt } = require('./skills');
|
|
3
3
|
const { getToolPromptText } = require('../tools');
|
|
4
4
|
const { listProvidersFromModels, MODELS, DEFAULT_MODEL_KEY } = require('../config');
|
|
5
|
-
const { normalizeLanguage, languageLabel } = require('../i18n');
|
|
5
|
+
const { detectLanguage, normalizeLanguage, languageLabel } = require('../i18n');
|
|
6
6
|
|
|
7
7
|
const KNOWN_TOOLS = new Set([
|
|
8
8
|
'list_dir', 'read_file', 'search_text', 'glob_files', 'file_info',
|
|
@@ -11,8 +11,8 @@ const KNOWN_TOOLS = new Set([
|
|
|
11
11
|
]);
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
function buildSystemPrompt(cwd, state = {}) {
|
|
15
|
-
const language = normalizeLanguage(state.language);
|
|
14
|
+
function buildSystemPrompt(cwd, state = {}, options = {}) {
|
|
15
|
+
const language = normalizeLanguage(options.language || state.language || detectLanguage(options.input || '', state.language));
|
|
16
16
|
const platform = process.platform === 'linux' ? 'Linux'
|
|
17
17
|
: process.platform === 'darwin' ? 'macOS'
|
|
18
18
|
: process.platform;
|
|
@@ -35,7 +35,8 @@ function buildSystemPrompt(cwd, state = {}) {
|
|
|
35
35
|
'Responde solo con el resultado final o con la siguiente accion concreta.',
|
|
36
36
|
'Si el usuario pide editar, corregir, crear, mover, buscar o ejecutar, hazlo directamente.',
|
|
37
37
|
'Nunca finjas que hiciste algo si no usaste herramientas o no tienes el resultado real.',
|
|
38
|
-
'Si
|
|
38
|
+
'Si la tarea requiere comprobar algo, primero intenta una herramienta real y espera el resultado antes de concluir.',
|
|
39
|
+
'No cierres con una conclusion si todavia no has probado nada.',
|
|
39
40
|
]
|
|
40
41
|
: [
|
|
41
42
|
'Always respond in English.',
|
|
@@ -44,7 +45,8 @@ function buildSystemPrompt(cwd, state = {}) {
|
|
|
44
45
|
'Reply only with the final result or the next concrete action.',
|
|
45
46
|
'If the user asks to edit, fix, create, move, search, or execute, do it directly.',
|
|
46
47
|
'Never pretend you completed an action if you did not actually use tools or obtain a real result.',
|
|
47
|
-
'If
|
|
48
|
+
'If the task requires verification, try a real tool first and wait for its result before concluding.',
|
|
49
|
+
'Do not end with a conclusion if you have not tested anything yet.',
|
|
48
50
|
];
|
|
49
51
|
|
|
50
52
|
const parts = [
|
package/src/i18n.js
CHANGED
|
@@ -100,6 +100,24 @@ function normalizeLanguage(language) {
|
|
|
100
100
|
return 'en';
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
function detectLanguage(text, fallback = 'en') {
|
|
104
|
+
const value = String(text || '').toLowerCase();
|
|
105
|
+
const fallbackLang = normalizeLanguage(fallback);
|
|
106
|
+
|
|
107
|
+
const spanishScore = [
|
|
108
|
+
/[áéíóúñ¿¡]/,
|
|
109
|
+
/\b(el|la|los|las|de|del|y|que|para|con|por|una|un|no|si|instala|instalar|haz|hace|agrega|añade|corrige|arregla|muestra|dime|busca|abre|cierra)\b/i,
|
|
110
|
+
].reduce((score, re) => score + (re.test(value) ? 1 : 0), 0);
|
|
111
|
+
|
|
112
|
+
const englishScore = [
|
|
113
|
+
/\b(the|and|for|with|you|please|install|make|show|find|open|close|run|update|fix|create|give|need)\b/i,
|
|
114
|
+
].reduce((score, re) => score + (re.test(value) ? 1 : 0), 0);
|
|
115
|
+
|
|
116
|
+
if (spanishScore > englishScore) return 'es';
|
|
117
|
+
if (englishScore > spanishScore) return 'en';
|
|
118
|
+
return fallbackLang;
|
|
119
|
+
}
|
|
120
|
+
|
|
103
121
|
function languageLabel(language) {
|
|
104
122
|
return LABELS[normalizeLanguage(language)] || LABELS.en;
|
|
105
123
|
}
|
|
@@ -114,6 +132,7 @@ function t(language, key, params = {}) {
|
|
|
114
132
|
}
|
|
115
133
|
|
|
116
134
|
module.exports = {
|
|
135
|
+
detectLanguage,
|
|
117
136
|
languageLabel,
|
|
118
137
|
normalizeLanguage,
|
|
119
138
|
t,
|
package/src/tui/app.mjs
CHANGED
|
@@ -188,7 +188,7 @@ function useDimensions() {
|
|
|
188
188
|
|
|
189
189
|
function parseInline(text) {
|
|
190
190
|
const parts = [];
|
|
191
|
-
const regex = /(\*\*(.+?)\*\*)|(`(
|
|
191
|
+
const regex = /(\*\*(.+?)\*\*)|(`([^`]+?)`)|(\[([^\]]+)\]\(([^)]+)\))/g;
|
|
192
192
|
let lastIndex = 0;
|
|
193
193
|
let match;
|
|
194
194
|
while ((match = regex.exec(text)) !== null) {
|
|
@@ -197,6 +197,7 @@ function parseInline(text) {
|
|
|
197
197
|
}
|
|
198
198
|
if (match[2]) parts.push({ t: 'bold', v: match[2] });
|
|
199
199
|
else if (match[4]) parts.push({ t: 'code', v: match[4] });
|
|
200
|
+
else if (match[6] && match[7]) parts.push({ t: 'link', text: match[6], url: match[7] });
|
|
200
201
|
lastIndex = regex.lastIndex;
|
|
201
202
|
}
|
|
202
203
|
if (lastIndex < text.length) {
|
|
@@ -213,6 +214,10 @@ function InlineLine({ text, color }) {
|
|
|
213
214
|
...parts.map((p, i) => {
|
|
214
215
|
if (p.t === 'bold') return h(Text, { key: String(i), color: base, bold: true }, p.v);
|
|
215
216
|
if (p.t === 'code') return h(Text, { key: String(i), color: T.cyan, backgroundColor: T.codeBg }, ' ' + p.v + ' ');
|
|
217
|
+
if (p.t === 'link') return h(Box, { key: String(i), flexWrap: 'wrap' },
|
|
218
|
+
h(Text, { color: T.accent, underline: true }, p.text),
|
|
219
|
+
h(Text, { color: T.textMuted }, ' (' + p.url + ')'),
|
|
220
|
+
);
|
|
216
221
|
return h(Text, { key: String(i), color: base }, p.v);
|
|
217
222
|
}),
|
|
218
223
|
);
|
|
@@ -243,6 +248,29 @@ function parseMarkdownBlocks(text) {
|
|
|
243
248
|
i++;
|
|
244
249
|
continue;
|
|
245
250
|
}
|
|
251
|
+
const hrMatch = line.match(/^\s*(---+|\*\*\*+)\s*$/);
|
|
252
|
+
if (hrMatch) {
|
|
253
|
+
blocks.push({ type: 'hr' });
|
|
254
|
+
i++;
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
const quoteMatch = line.match(/^\s*>\s?(.*)$/);
|
|
258
|
+
if (quoteMatch) {
|
|
259
|
+
blocks.push({ type: 'quote', text: quoteMatch[1] });
|
|
260
|
+
i++;
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
const tableCandidate = line.includes('|') && i + 1 < lines.length && /^\s*\|?\s*:?-{3,}/.test(lines[i + 1]);
|
|
264
|
+
if (tableCandidate) {
|
|
265
|
+
const table = [line];
|
|
266
|
+
i += 2;
|
|
267
|
+
while (i < lines.length && lines[i].includes('|') && lines[i].trim()) {
|
|
268
|
+
table.push(lines[i]);
|
|
269
|
+
i++;
|
|
270
|
+
}
|
|
271
|
+
blocks.push({ type: 'table', rows: table });
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
246
274
|
const ulMatch = line.match(/^(\s*)[-*]\s+(.+)/);
|
|
247
275
|
if (ulMatch) {
|
|
248
276
|
blocks.push({ type: 'list', indent: Math.floor(ulMatch[1].length / 2), text: ulMatch[2] });
|
|
@@ -306,6 +334,25 @@ function MarkdownContent({ text, width }) {
|
|
|
306
334
|
return block.text.trim()
|
|
307
335
|
? h(Box, { key: String(i) }, h(InlineLine, { text: block.text }))
|
|
308
336
|
: h(Box, { key: String(i), height: 1 });
|
|
337
|
+
case 'quote':
|
|
338
|
+
return h(Box, { key: String(i), marginLeft: 1 },
|
|
339
|
+
h(Text, { color: T.textMuted }, '> '),
|
|
340
|
+
h(InlineLine, { text: block.text, color: T.textMuted }),
|
|
341
|
+
);
|
|
342
|
+
case 'hr':
|
|
343
|
+
return h(Text, { key: String(i), color: T.borderLight }, '─'.repeat(Math.max(10, Math.min(width || 80, 70))));
|
|
344
|
+
case 'table': {
|
|
345
|
+
const rows = block.rows.map(row => row.split('|').map(cell => cell.trim()).filter(Boolean));
|
|
346
|
+
const widthByCol = [];
|
|
347
|
+
for (const row of rows) {
|
|
348
|
+
row.forEach((cell, idx) => { widthByCol[idx] = Math.max(widthByCol[idx] || 0, cell.length); });
|
|
349
|
+
}
|
|
350
|
+
return h(Box, { key: String(i), flexDirection: 'column' },
|
|
351
|
+
...rows.map((row, rowIdx) => h(Text, { key: String(rowIdx), color: rowIdx === 0 ? T.accent : T.textMuted },
|
|
352
|
+
row.map((cell, idx) => String(cell).padEnd(widthByCol[idx] || cell.length)).join(' | ')
|
|
353
|
+
)),
|
|
354
|
+
);
|
|
355
|
+
}
|
|
309
356
|
default:
|
|
310
357
|
return null;
|
|
311
358
|
}
|
package/src/web/webAgent.js
CHANGED
|
@@ -48,6 +48,8 @@ function buildSystemPrompt(repoOwner, repoName, fileTree, state = {}) {
|
|
|
48
48
|
'- Do not ask "do you want" questions when you can investigate yourself.',
|
|
49
49
|
'- Do not narrate plans instead of acting.',
|
|
50
50
|
'- Use tools immediately when a file must be read, changed, or verified.',
|
|
51
|
+
'- Do not give a conclusion unless you have actually used a tool or verified the result.',
|
|
52
|
+
'- If you have not tested anything, say that clearly and keep investigating.',
|
|
51
53
|
'',
|
|
52
54
|
'Repository files:',
|
|
53
55
|
treeLines,
|
|
@@ -105,6 +107,13 @@ function looksLikePendingEdit(text) {
|
|
|
105
107
|
return PENDING_EDIT_RE.test(sample);
|
|
106
108
|
}
|
|
107
109
|
|
|
110
|
+
|
|
111
|
+
function looksLikeActionRequest(text) {
|
|
112
|
+
const sample = normalizeClassifierText(String(text || ''));
|
|
113
|
+
if (!sample) return false;
|
|
114
|
+
return /(instala|instalar|install|run|ejecuta|ejecutar|crea|crear|build|compile|compila|fix|arregla|corrige|update|actualiza|edita|edit|borra|elimina|remove|descarga|download|busca|search|prueba|test|verifica|check|configura|setup|mueve|move|importa|import|aplica|apply)/i.test(sample);
|
|
115
|
+
}
|
|
116
|
+
|
|
108
117
|
function buildForcedWritePrompt(state) {
|
|
109
118
|
const path = state.lastReadPath || 'archivo-leido';
|
|
110
119
|
const content = state.lastReadContent || '';
|
|
@@ -471,6 +480,7 @@ async function applyToolCall(parsed, answer, toolCtx, loopState, modelMessages)
|
|
|
471
480
|
loopState.lastReadPath = parsed.args?.path || loopState.lastReadPath;
|
|
472
481
|
}
|
|
473
482
|
|
|
483
|
+
loopState.toolCalls = (loopState.toolCalls || 0) + 1;
|
|
474
484
|
const toolResult = await executeTool(parsed.tool, parsed.args, toolCtx);
|
|
475
485
|
|
|
476
486
|
if (parsed.tool === 'read_file' && parsed.args?.path) {
|
|
@@ -714,6 +724,7 @@ async function runWebAgent({ chatData, user, onEvent, isAborted }) {
|
|
|
714
724
|
readOrder: [],
|
|
715
725
|
totalReads: 0,
|
|
716
726
|
writesDone: 0,
|
|
727
|
+
toolCalls: 0,
|
|
717
728
|
userWantsEdit,
|
|
718
729
|
fileTree,
|
|
719
730
|
};
|
|
@@ -960,6 +971,21 @@ async function runWebAgent({ chatData, user, onEvent, isAborted }) {
|
|
|
960
971
|
continue;
|
|
961
972
|
}
|
|
962
973
|
|
|
974
|
+
if (parsed.type === 'final' && looksLikeActionRequest(userLatest) && (loopState.toolCalls || 0) === 0) {
|
|
975
|
+
if (streamStarted) onEvent({ type: 'clear_stream' });
|
|
976
|
+
modelMessages.push({ role: 'assistant', content: answer });
|
|
977
|
+
modelMessages.push({
|
|
978
|
+
role: 'user',
|
|
979
|
+
content: [
|
|
980
|
+
'No has usado ninguna herramienta todavia.',
|
|
981
|
+
'No cierres con una conclusion ni con pasos teoricos.',
|
|
982
|
+
'Primero intenta una herramienta real adecuada para la tarea.',
|
|
983
|
+
'Si ninguna herramienta aplica, dilo de forma breve y honesta.',
|
|
984
|
+
].join(' '),
|
|
985
|
+
});
|
|
986
|
+
continue;
|
|
987
|
+
}
|
|
988
|
+
|
|
963
989
|
const fingerprint = normalizeReplyFingerprint(parsed.content || answer);
|
|
964
990
|
if (fingerprint) {
|
|
965
991
|
if (loopState.lastFingerprint === fingerprint) {
|