zyn-ai 1.2.1 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +17 -11
- package/README.md +8 -4
- package/package.json +6 -4
- package/src/cli/commands.js +57 -0
- package/src/cli/runtime.js +4 -2
- package/src/config.js +5 -3
- package/src/core/agent.js +102 -59
- package/src/core/prompts.js +36 -5
- package/src/i18n.js +6 -0
- package/src/tools/index.js +593 -41
- package/src/tui/app.mjs +83 -39
- package/src/utils/secretStorage.js +223 -0
- package/src/utils/sessionStorage.js +42 -4
- package/src/utils/taskStorage.js +192 -0
- package/src/web/webAgent.js +2 -2
package/LICENSE
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
|
-
|
|
1
|
+
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2026 Maycol
|
|
3
|
+
Copyright (c) 2026 Maycol
|
|
4
4
|
|
|
5
|
-
Permission is granted
|
|
6
|
-
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
7
11
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
distributed derivative work.
|
|
11
|
-
3. You include the project link below, or a clearly documented replacement if
|
|
12
|
-
the canonical URL changes.
|
|
13
|
-
4. Modified versions must clearly state that they were changed from the original.
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
# Zyn
|
|
1
|
+
# Zyn Agent
|
|
2
2
|
|
|
3
3
|
<p align="center">
|
|
4
4
|
<img src="http://cdn.soymaycol.icu/files/logo_zyn.png" alt="Zyn logo" width="180" />
|
|
5
5
|
</p>
|
|
6
6
|
|
|
7
|
-
<p>
|
|
7
|
+
<p align="center">
|
|
8
8
|
<img src="https://img.shields.io/npm/v/zyn-ai?label=npm&color=%23CB3837" alt="NPM Version"/>
|
|
9
|
-
|
|
9
|
+
|
|
10
10
|
<img src="https://img.shields.io/github/v/release/SoyMaycol/Zyn?include_prereleases&sort=semver" alt="Latest Release"/>
|
|
11
|
-
|
|
11
|
+
|
|
12
|
+
<img src="https://img.shields.io/npm/dt/zyn-ai" alt="Downloads"/>
|
|
13
|
+
|
|
14
|
+
<img src="https://badge.socket.dev/npm/package/zyn-ai/latest" alt="Security"/>
|
|
15
|
+
|
|
12
16
|
<img src="https://img.shields.io/github/forks/SoyMaycol/Zyn" alt="Forks"/>
|
|
13
17
|
</p>
|
|
14
18
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zyn-ai",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "Production-ready AI agent for CLI and web with real tool execution and automation",
|
|
5
|
-
"author": "Maycol
|
|
5
|
+
"author": "Maycol",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"ai-agent",
|
|
8
8
|
"cli-ai",
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"zyn"
|
|
19
19
|
],
|
|
20
20
|
"bin": {
|
|
21
|
-
"zyn": "./zyn.js"
|
|
21
|
+
"zyn": "./zyn.js",
|
|
22
|
+
"Zyn": "./zyn.js"
|
|
22
23
|
},
|
|
23
24
|
"type": "commonjs",
|
|
24
25
|
"scripts": {
|
|
@@ -36,7 +37,8 @@
|
|
|
36
37
|
"ink": "^6.8.0",
|
|
37
38
|
"keypress": "^0.2.1",
|
|
38
39
|
"react": "^19.0.0",
|
|
39
|
-
"session-file-store": "^1.5.0"
|
|
40
|
+
"session-file-store": "^1.5.0",
|
|
41
|
+
"jimp": "^1.6.1"
|
|
40
42
|
},
|
|
41
43
|
"repository": {
|
|
42
44
|
"type": "git",
|
package/src/cli/commands.js
CHANGED
|
@@ -7,6 +7,7 @@ const { listSkills, SKILLS_DIR } = require('../core/skills');
|
|
|
7
7
|
const { DEFAULT_LANGUAGE, DEFAULT_MODEL_KEY, MODELS, listProvidersFromModels } = require('../config');
|
|
8
8
|
const { languageLabel, normalizeLanguage, t } = require('../i18n');
|
|
9
9
|
const { createNewSessionState, listSessions, loadSessionState, saveState } = require('../utils/sessionStorage');
|
|
10
|
+
const { listGitSecrets, removeGitSecret, upsertGitSecret } = require('../utils/secretStorage');
|
|
10
11
|
const { exportTranscriptText, formatTranscriptPreview } = require('../utils/transcriptStorage');
|
|
11
12
|
const { resolveInputPath } = require('../utils/pathUtils');
|
|
12
13
|
const { printTools } = require('../tools');
|
|
@@ -24,6 +25,8 @@ const SLASH_COMMANDS = [
|
|
|
24
25
|
{ name: 'model', desc: 'view/change model' },
|
|
25
26
|
{ name: 'models', desc: 'list models' },
|
|
26
27
|
{ name: 'providers', desc: 'list providers' },
|
|
28
|
+
{ name: 'git', desc: 'configure git credentials' },
|
|
29
|
+
{ name: 'persona', desc: 'set response tone/personality' },
|
|
27
30
|
{ name: 'lang', desc: 'change language' },
|
|
28
31
|
{ name: 'language', desc: 'change language' },
|
|
29
32
|
{ name: 'auto', desc: 'auto-approval' },
|
|
@@ -194,9 +197,62 @@ async function handleLocalCommand(input, state, deps) {
|
|
|
194
197
|
return true;
|
|
195
198
|
}
|
|
196
199
|
|
|
200
|
+
if (commandName === 'git') {
|
|
201
|
+
const [sub, ...rest] = args.split(' ').filter(Boolean);
|
|
202
|
+
if (!sub || sub === 'help') {
|
|
203
|
+
console.log('Uso: /git list | /git set <provider> <token> [username] | /git remove <provider>');
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
if (sub === 'list') {
|
|
207
|
+
const secrets = listGitSecrets();
|
|
208
|
+
if (!secrets.length) console.log('No hay credenciales git guardadas.');
|
|
209
|
+
else secrets.forEach(s => console.log(`${s.key} user:${s.username || '-'} api:${s.apiBaseUrl || '-'}`));
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
if (sub === 'set') {
|
|
213
|
+
const [provider, token, username] = rest;
|
|
214
|
+
if (!provider || !token) throw new Error('Uso: /git set <provider> <token> [username]');
|
|
215
|
+
upsertGitSecret(provider, { provider, token, username });
|
|
216
|
+
console.log(`Credencial guardada para ${provider}`);
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
if (sub === 'remove') {
|
|
220
|
+
const [provider] = rest;
|
|
221
|
+
if (!provider) throw new Error('Uso: /git remove <provider>');
|
|
222
|
+
const removed = removeGitSecret(provider);
|
|
223
|
+
console.log(removed ? `Credencial eliminada: ${provider}` : `No existe credencial para ${provider}`);
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
throw new Error('Subcomando git no reconocido. Usa /git help');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (commandName === 'persona') {
|
|
230
|
+
const [sub, ...rest] = args.split(' ');
|
|
231
|
+
if (!sub || sub === 'show') {
|
|
232
|
+
console.log(state.personaPrompt ? `Persona activa:\n${state.personaPrompt}` : 'Persona por defecto activa.');
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
if (sub === 'reset' || sub === 'default') {
|
|
236
|
+
state.personaPrompt = '';
|
|
237
|
+
await saveState(state);
|
|
238
|
+
console.log('Persona restaurada al estado por defecto.');
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
if (sub === 'set') {
|
|
242
|
+
const text = rest.join(' ').trim();
|
|
243
|
+
if (!text) throw new Error('Uso: /persona set <descripcion>');
|
|
244
|
+
state.personaPrompt = text;
|
|
245
|
+
await saveState(state);
|
|
246
|
+
console.log('Persona actualizada (solo estilo).');
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
throw new Error('Uso: /persona show | /persona set <texto> | /persona reset');
|
|
250
|
+
}
|
|
251
|
+
|
|
197
252
|
if (commandName === 'new') {
|
|
198
253
|
const nextState = await createNewSessionState(state.rl);
|
|
199
254
|
applyLoadedState(state, nextState);
|
|
255
|
+
if (typeof state.clearQueuedMessages === 'function') state.clearQueuedMessages();
|
|
200
256
|
global.__zynActiveModel = state.activeModel || DEFAULT_MODEL_KEY;
|
|
201
257
|
printBanner(state);
|
|
202
258
|
console.log(`${t(state.language, 'newSessionCreated')}: ${state.sessionId}`);
|
|
@@ -215,6 +271,7 @@ async function handleLocalCommand(input, state, deps) {
|
|
|
215
271
|
}
|
|
216
272
|
|
|
217
273
|
applyLoadedState(state, loaded);
|
|
274
|
+
if (typeof state.clearQueuedMessages === 'function') state.clearQueuedMessages();
|
|
218
275
|
global.__zynActiveModel = state.activeModel || DEFAULT_MODEL_KEY;
|
|
219
276
|
await saveState(state);
|
|
220
277
|
printBanner(state);
|
package/src/cli/runtime.js
CHANGED
|
@@ -28,6 +28,7 @@ const {
|
|
|
28
28
|
loadOrCreateSessionState,
|
|
29
29
|
} = require('../utils/sessionStorage');
|
|
30
30
|
const { appendTranscriptEntry } = require('../utils/transcriptStorage');
|
|
31
|
+
const { t } = require('../i18n');
|
|
31
32
|
|
|
32
33
|
async function readPromptFromStdin() {
|
|
33
34
|
if (process.stdin.isTTY) {
|
|
@@ -119,6 +120,7 @@ async function runInteractiveChatClassic(options = {}) {
|
|
|
119
120
|
let currentAbort = null;
|
|
120
121
|
|
|
121
122
|
state.getQueuedMessages = () => messageQueue.splice(0);
|
|
123
|
+
state.clearQueuedMessages = () => { messageQueue.length = 0; };
|
|
122
124
|
state.abortCurrentTurn = () => {
|
|
123
125
|
if (currentAbort && !currentAbort.signal.aborted) {
|
|
124
126
|
currentAbort.abort();
|
|
@@ -175,7 +177,7 @@ async function runInteractiveChatClassic(options = {}) {
|
|
|
175
177
|
if (!input) continue;
|
|
176
178
|
|
|
177
179
|
if (input === '/exit' || input === '/quit') {
|
|
178
|
-
logEvent(state, 'info',
|
|
180
|
+
logEvent(state, 'info', t(state.language, 'goodbye'));
|
|
179
181
|
break;
|
|
180
182
|
}
|
|
181
183
|
|
|
@@ -202,7 +204,7 @@ async function runInteractiveChatClassic(options = {}) {
|
|
|
202
204
|
rl.removeListener('line', lineHandler);
|
|
203
205
|
|
|
204
206
|
if (pendingExit) {
|
|
205
|
-
logEvent(state, 'info',
|
|
207
|
+
logEvent(state, 'info', t(state.language, 'goodbye'));
|
|
206
208
|
break;
|
|
207
209
|
}
|
|
208
210
|
}
|
package/src/config.js
CHANGED
|
@@ -89,10 +89,10 @@ const MODELS = {
|
|
|
89
89
|
};
|
|
90
90
|
|
|
91
91
|
const DEFAULT_MODEL_KEY = process.env.ZYN_DEFAULT_MODEL || 'qwen';
|
|
92
|
-
const DEFAULT_LANGUAGE = normalizeLanguage(process.env.ZYN_DEFAULT_LANG || process.env.ZYN_LANGUAGE || 'en');
|
|
92
|
+
const DEFAULT_LANGUAGE = normalizeLanguage(process.env.ZYN_DEFAULT_LANG || process.env.ZYN_LANGUAGE || process.env.LANG || 'en');
|
|
93
93
|
|
|
94
|
-
const QWEN_EMAIL = process.env.ZYN_QWEN_EMAIL ||
|
|
95
|
-
const QWEN_PASSWORD = process.env.ZYN_QWEN_PASSWORD ||
|
|
94
|
+
const QWEN_EMAIL = process.env.ZYN_QWEN_EMAIL || 'danielalejandrobasado@gmail.com';
|
|
95
|
+
const QWEN_PASSWORD = process.env.ZYN_QWEN_PASSWORD || 'zyzz1234';
|
|
96
96
|
|
|
97
97
|
const MAX_TOOL_STEPS = Number.POSITIVE_INFINITY;
|
|
98
98
|
const MAX_OUTPUT_CHARS = 12000;
|
|
@@ -104,6 +104,7 @@ const KEEP_RECENT_MESSAGES = 12;
|
|
|
104
104
|
const SESSION_ROOT = path.join(DATA_ROOT, 'chat');
|
|
105
105
|
const SESSIONS_DIR = path.join(SESSION_ROOT, 'sessions');
|
|
106
106
|
const CURRENT_SESSION_FILE = path.join(SESSION_ROOT, 'current-session.json');
|
|
107
|
+
const PERSISTENT_CONFIG_FILE = path.join(SESSION_ROOT, 'persistent-config.json');
|
|
107
108
|
const TRANSCRIPTS_DIR = path.join(SESSION_ROOT, 'transcripts');
|
|
108
109
|
const EXPORTS_DIR = path.join(SESSION_ROOT, 'exports');
|
|
109
110
|
const THINK_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
@@ -134,6 +135,7 @@ module.exports = {
|
|
|
134
135
|
APP_ROOT,
|
|
135
136
|
BUILTIN_MODELS,
|
|
136
137
|
CURRENT_SESSION_FILE,
|
|
138
|
+
PERSISTENT_CONFIG_FILE,
|
|
137
139
|
DATA_ROOT,
|
|
138
140
|
DEFAULT_LANGUAGE,
|
|
139
141
|
DEFAULT_MODEL_KEY,
|
package/src/core/agent.js
CHANGED
|
@@ -37,68 +37,88 @@ async function requestModel(messages, state, ui, options = {}) {
|
|
|
37
37
|
signal,
|
|
38
38
|
} = options;
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
40
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
41
|
+
if (signal?.aborted) {
|
|
42
|
+
throw new Error(state.language === 'es' ? 'Agente detenido por el usuario (ESC x2)' : 'Agent stopped by the user (ESC x2)');
|
|
43
|
+
}
|
|
44
|
+
const stopThinking = ui.startThinkingIndicator(state, attempt === 0 ? label : `${label} (${state.language === 'es' ? 'reintento' : 'retry'})`);
|
|
45
|
+
let answerStarted = false;
|
|
46
|
+
let thinkingStarted = false;
|
|
47
|
+
let timedOut = false;
|
|
48
|
+
let timeout = null;
|
|
49
|
+
const controller = new AbortController();
|
|
50
|
+
const onExternalAbort = () => controller.abort();
|
|
51
|
+
|
|
52
|
+
if (signal) {
|
|
53
|
+
if (signal.aborted) controller.abort();
|
|
54
|
+
else signal.addEventListener('abort', onExternalAbort, { once: true });
|
|
55
|
+
}
|
|
56
|
+
const refreshTimeout = () => {
|
|
57
|
+
if (timeout) clearTimeout(timeout);
|
|
58
|
+
timeout = setTimeout(() => {
|
|
59
|
+
timedOut = true;
|
|
60
|
+
controller.abort();
|
|
61
|
+
}, REQUEST_TIMEOUT_MS);
|
|
62
|
+
};
|
|
63
|
+
refreshTimeout();
|
|
52
64
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (
|
|
65
|
+
try {
|
|
66
|
+
const result = await chat({
|
|
67
|
+
messages,
|
|
68
|
+
modelKey: state?.activeModel || DEFAULT_MODEL_KEY,
|
|
69
|
+
signal: controller.signal,
|
|
70
|
+
onChunk: (delta, phase) => {
|
|
71
|
+
refreshTimeout();
|
|
72
|
+
if (phase === 'thinking') {
|
|
73
|
+
if (!thinkingStarted) {
|
|
74
|
+
stopThinking();
|
|
75
|
+
ui.beginThinkingStream(state);
|
|
76
|
+
thinkingStarted = true;
|
|
77
|
+
}
|
|
78
|
+
ui.writeThinkingDelta(state, delta);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (thinkingStarted) {
|
|
82
|
+
ui.endThinkingStream(state);
|
|
83
|
+
thinkingStarted = false;
|
|
84
|
+
}
|
|
85
|
+
if (streamOutput && !answerStarted) {
|
|
61
86
|
stopThinking();
|
|
62
|
-
ui.
|
|
63
|
-
|
|
87
|
+
ui.beginAssistantStream(state);
|
|
88
|
+
answerStarted = true;
|
|
64
89
|
}
|
|
65
|
-
ui.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (streamOutput) {
|
|
81
|
-
ui.writeAssistantDelta(state, delta);
|
|
90
|
+
if (streamOutput) ui.writeAssistantDelta(state, delta);
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
ui.pushAction(state, 'ok', 'Respuesta del modelo recibida');
|
|
94
|
+
return result.answer ?? '';
|
|
95
|
+
} catch (err) {
|
|
96
|
+
const externalAbort = Boolean(signal?.aborted);
|
|
97
|
+
const aborted = controller.signal.aborted || err?.name === 'AbortError';
|
|
98
|
+
if (aborted && timedOut && !externalAbort && attempt === 0) {
|
|
99
|
+
ui.logEvent(state, 'warn', state.language === 'es' ? 'Proveedor lento, reenviando mensaje' : 'Provider stalled, resending message');
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (aborted) {
|
|
103
|
+
if (externalAbort) {
|
|
104
|
+
throw new Error(state.language === 'es' ? 'Agente detenido por el usuario (ESC x2)' : 'Agent stopped by the user (ESC x2)');
|
|
82
105
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
stopThinking();
|
|
97
|
-
if (thinkingStarted) ui.endThinkingStream(state);
|
|
98
|
-
if (streamOutput && answerStarted) {
|
|
99
|
-
ui.endAssistantStream(state);
|
|
106
|
+
throw new Error(state.language === 'es' ? 'Tiempo agotado del proveedor' : 'Provider timeout exceeded');
|
|
107
|
+
}
|
|
108
|
+
if (!externalAbort && attempt === 0) {
|
|
109
|
+
ui.logEvent(state, 'warn', state.language === 'es' ? 'Error transitorio, reenviando contexto y skills' : 'Transient error, resending context and skills');
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
throw err;
|
|
113
|
+
} finally {
|
|
114
|
+
clearTimeout(timeout);
|
|
115
|
+
if (signal) signal.removeEventListener('abort', onExternalAbort);
|
|
116
|
+
stopThinking();
|
|
117
|
+
if (thinkingStarted) ui.endThinkingStream(state);
|
|
118
|
+
if (streamOutput && answerStarted) ui.endAssistantStream(state);
|
|
100
119
|
}
|
|
101
120
|
}
|
|
121
|
+
throw new Error(state.language === 'es' ? 'No se pudo obtener respuesta del proveedor' : 'Could not get provider response');
|
|
102
122
|
}
|
|
103
123
|
|
|
104
124
|
async function summarizeMessages(state, ui, messages) {
|
|
@@ -197,6 +217,9 @@ async function runAgentTurn(input, state, ui, options = {}) {
|
|
|
197
217
|
}
|
|
198
218
|
ui.logEvent(state, 'info', `${state.language === 'es' ? 'Turno' : 'Turn'} ${state.turnCount}`);
|
|
199
219
|
|
|
220
|
+
let toolUsedThisTurn = false;
|
|
221
|
+
let finalWithoutToolRetries = 0;
|
|
222
|
+
|
|
200
223
|
const directAction = parseDirectAction(input);
|
|
201
224
|
if (directAction) {
|
|
202
225
|
await appendTranscriptEntry(state.sessionId, { type: 'user', content: input });
|
|
@@ -225,10 +248,10 @@ async function runAgentTurn(input, state, ui, options = {}) {
|
|
|
225
248
|
|
|
226
249
|
let lastFingerprint = '';
|
|
227
250
|
let repeatCount = 0;
|
|
251
|
+
const toolPathUsage = new Map();
|
|
228
252
|
let step = 0;
|
|
229
253
|
const turnLanguage = detectLanguage(input, state.language);
|
|
230
|
-
|
|
231
|
-
let finalWithoutToolRetries = 0;
|
|
254
|
+
state.language = turnLanguage;
|
|
232
255
|
|
|
233
256
|
while (true) {
|
|
234
257
|
if (signal?.aborted) {
|
|
@@ -387,7 +410,27 @@ async function runAgentTurn(input, state, ui, options = {}) {
|
|
|
387
410
|
return { content, rendered: false };
|
|
388
411
|
}
|
|
389
412
|
|
|
390
|
-
const
|
|
413
|
+
const targetPath = parsed.args?.path || '';
|
|
414
|
+
const contentSample = typeof parsed.args?.content === 'string'
|
|
415
|
+
? parsed.args.content.slice(0, 120)
|
|
416
|
+
: '';
|
|
417
|
+
const fingerprint = `${parsed.tool}:${targetPath}:${contentSample}`;
|
|
418
|
+
if (targetPath) {
|
|
419
|
+
const key = `${parsed.tool}:${targetPath}`;
|
|
420
|
+
const nextCount = (toolPathUsage.get(key) || 0) + 1;
|
|
421
|
+
toolPathUsage.set(key, nextCount);
|
|
422
|
+
if (nextCount >= 3 && ['write_file', 'append_file', 'replace_in_file'].includes(parsed.tool)) {
|
|
423
|
+
ui.logEvent(state, 'warn', state.language === 'es' ? 'Posible loop detectado' : 'Possible loop detected', `${parsed.tool} → ${targetPath} x${nextCount}`);
|
|
424
|
+
turnMessages.push({
|
|
425
|
+
role: 'user',
|
|
426
|
+
content: state.language === 'es'
|
|
427
|
+
? `ALTO: ya editaste ${targetPath} varias veces en este turno. No repitas ediciones; valida y responde con type=final.`
|
|
428
|
+
: `STOP: you already edited ${targetPath} multiple times in this turn. Do not repeat edits; verify and answer with type=final.`,
|
|
429
|
+
});
|
|
430
|
+
step += 1;
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
391
434
|
if (fingerprint === lastFingerprint) {
|
|
392
435
|
repeatCount += 1;
|
|
393
436
|
if (repeatCount >= 2) {
|
package/src/core/prompts.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
const { normalizeText } = require('../utils/text');
|
|
2
2
|
const { buildSkillsPrompt } = require('./skills');
|
|
3
|
-
const { getToolPromptText } = require('../tools');
|
|
3
|
+
const { getToolPromptText, TOOL_DEFINITIONS } = require('../tools');
|
|
4
4
|
const { listProvidersFromModels, MODELS, DEFAULT_MODEL_KEY } = require('../config');
|
|
5
5
|
const { detectLanguage, normalizeLanguage, languageLabel } = require('../i18n');
|
|
6
6
|
|
|
7
7
|
const KNOWN_TOOLS = new Set([
|
|
8
|
-
|
|
9
|
-
'
|
|
10
|
-
'
|
|
8
|
+
...TOOL_DEFINITIONS.map(tool => tool.name),
|
|
9
|
+
'task_create', 'task_list', 'task_update', 'task_complete', 'task_delete', 'task_clear',
|
|
10
|
+
'git_secret_set', 'git_secret_list', 'git_secret_remove',
|
|
11
|
+
'git_clone_repo', 'git_api_request',
|
|
11
12
|
]);
|
|
12
13
|
|
|
13
14
|
|
|
@@ -37,6 +38,14 @@ function buildSystemPrompt(cwd, state = {}, options = {}) {
|
|
|
37
38
|
'Nunca finjas que hiciste algo si no usaste herramientas o no tienes el resultado real.',
|
|
38
39
|
'Si la tarea requiere comprobar algo, primero intenta una herramienta real y espera el resultado antes de concluir.',
|
|
39
40
|
'No cierres con una conclusion si todavia no has probado nada.',
|
|
41
|
+
'Si una tarea dura demasiado, usa run_command con un timeoutMs adecuado y confirma el resultado real.',
|
|
42
|
+
'Para GitHub, GitLab o un Git personalizado usa git_secret_set para guardar credenciales y git_clone_repo o git_api_request para operar sin exponer secretos.',
|
|
43
|
+
'Usa exclusivamente tools registradas en "Tool use". No inventes nombres de tools ni aliases.',
|
|
44
|
+
'Para tareas empresariales, mantente acotado y determinista: entradas claras, salidas claras, sin razonamiento creativo salvo que el usuario lo pida.',
|
|
45
|
+
'Cuando aplique, entrega resumen ejecutivo corto + riesgos/banderas rojas + siguiente accion concreta.',
|
|
46
|
+
'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).',
|
|
47
|
+
'No te limites a una sola tool por costumbre; elige la mejor secuencia técnica para el objetivo.',
|
|
48
|
+
'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.',
|
|
40
49
|
]
|
|
41
50
|
: [
|
|
42
51
|
'Always respond in English.',
|
|
@@ -47,6 +56,14 @@ function buildSystemPrompt(cwd, state = {}, options = {}) {
|
|
|
47
56
|
'Never pretend you completed an action if you did not actually use tools or obtain a real result.',
|
|
48
57
|
'If the task requires verification, try a real tool first and wait for its result before concluding.',
|
|
49
58
|
'Do not end with a conclusion if you have not tested anything yet.',
|
|
59
|
+
'If a task takes long, use run_command with an appropriate timeoutMs and verify the real result.',
|
|
60
|
+
'For GitHub, GitLab, or a custom Git host, use git_secret_set to store credentials and git_clone_repo or git_api_request to operate without exposing secrets.',
|
|
61
|
+
'Use only tools listed under "Tool use". Never invent tool names or aliases.',
|
|
62
|
+
'For business tasks, stay bounded and deterministic: clear inputs, clear outputs, no creative reasoning unless explicitly requested.',
|
|
63
|
+
'When relevant, provide a short executive summary + obvious red flags + next concrete action.',
|
|
64
|
+
'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.',
|
|
65
|
+
'Do not over-focus on a single tool by habit; choose the best technical sequence for the goal.',
|
|
66
|
+
'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.',
|
|
50
67
|
];
|
|
51
68
|
|
|
52
69
|
const parts = [
|
|
@@ -68,6 +85,15 @@ function buildSystemPrompt(cwd, state = {}, options = {}) {
|
|
|
68
85
|
providerGroups,
|
|
69
86
|
];
|
|
70
87
|
|
|
88
|
+
if (state.personaPrompt && state.personaPrompt.trim()) {
|
|
89
|
+
parts.push(
|
|
90
|
+
'',
|
|
91
|
+
'# Persona style (tone only)',
|
|
92
|
+
'Apply this only to communication style. Do NOT change tool choice, safety rules, or technical decisions.',
|
|
93
|
+
state.personaPrompt.trim(),
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
71
97
|
if (state.concuerdo) {
|
|
72
98
|
const activeKey = state.activeModel || DEFAULT_MODEL_KEY;
|
|
73
99
|
const otherKeys = Object.keys(MODELS).filter(k => k !== activeKey);
|
|
@@ -173,8 +199,13 @@ const TOOL_ARG_KEYS = {
|
|
|
173
199
|
append_file: ['path', 'content'],
|
|
174
200
|
replace_in_file: ['path', 'search', 'replace', 'all'],
|
|
175
201
|
fetch_url: ['url', 'selector', 'attribute', 'limit'],
|
|
176
|
-
|
|
202
|
+
fetch: ['url', 'method', 'headers', 'query', 'json', 'data', 'form', 'files', 'timeoutMs'],
|
|
203
|
+
fetch_http: ['url', 'method', 'headers', 'query', 'json', 'data', 'form', 'files', 'timeoutMs'],
|
|
204
|
+
webfetch: ['url', 'headers', 'timeoutMs'],
|
|
205
|
+
scrape_site: ['url', 'selectors', 'limit', 'headers'],
|
|
206
|
+
web_search: ['query', 'lang', 'limit'],
|
|
177
207
|
web_read: ['url'],
|
|
208
|
+
create_canvas_image: ['width', 'height', 'background', 'elements', 'format', 'outputPath'],
|
|
178
209
|
};
|
|
179
210
|
|
|
180
211
|
const LONG_VALUE_ARG = {
|
package/src/i18n.js
CHANGED
|
@@ -47,6 +47,9 @@ const STRINGS = {
|
|
|
47
47
|
transcriptLabel: 'transcript',
|
|
48
48
|
fromLabel: 'created',
|
|
49
49
|
updatedLabel: 'updated',
|
|
50
|
+
queuedLabel: 'queued',
|
|
51
|
+
processingQueuedMessage: 'processing queued message',
|
|
52
|
+
goodbye: 'Goodbye',
|
|
50
53
|
},
|
|
51
54
|
es: {
|
|
52
55
|
helpTitle: 'Ayuda',
|
|
@@ -89,6 +92,9 @@ const STRINGS = {
|
|
|
89
92
|
transcriptLabel: 'transcript',
|
|
90
93
|
fromLabel: 'desde',
|
|
91
94
|
updatedLabel: 'actualizado',
|
|
95
|
+
queuedLabel: 'en cola',
|
|
96
|
+
processingQueuedMessage: 'procesando mensaje en cola',
|
|
97
|
+
goodbye: 'Hasta luego',
|
|
92
98
|
},
|
|
93
99
|
};
|
|
94
100
|
|