zyn-ai 1.3.3 → 1.3.5
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 +7 -11
- package/package.json +1 -1
- package/src/cli/commands.js +77 -7
- package/src/cli/runtime.js +3 -0
- package/src/config.js +28 -21
- package/src/core/agent.js +64 -11
- package/src/core/prompts.js +145 -29
- package/src/i18n.js +2 -2
- package/src/providers/catalog.js +27 -43
- package/src/providers/gemini/index.js +338 -0
- package/src/providers/scraperClient.js +3 -6
- package/src/tools/index.js +230 -0
- package/src/tui/app.mjs +17 -1
- package/src/utils/gmailAuth.js +427 -0
- package/src/utils/sessionStorage.js +16 -9
- package/src/web/public/index.html +3 -1
- package/src/web/server.js +10 -3
- package/src/web/webAgent.js +5 -3
- package/src/providers/ollama/index.js +0 -78
- package/src/providers/openaiCompatible/index.js +0 -97
package/README.md
CHANGED
|
@@ -31,7 +31,6 @@ Zyn is a local AI agent designed for terminal and web usage. It supports persist
|
|
|
31
31
|
- Node.js 18+
|
|
32
32
|
- npm
|
|
33
33
|
- Internet connection for remote providers
|
|
34
|
-
- Optional: Ollama for local models
|
|
35
34
|
|
|
36
35
|
---
|
|
37
36
|
|
|
@@ -133,6 +132,9 @@ Commands:
|
|
|
133
132
|
|---|---|
|
|
134
133
|
| `/tools` | List tools |
|
|
135
134
|
| `/skills` | List skills |
|
|
135
|
+
| `/gmail connect` | Connect Gmail with Google OAuth + PKCE |
|
|
136
|
+
| `/gmail status` | Show Gmail connection status |
|
|
137
|
+
| `/gmail disconnect` | Remove saved Gmail tokens |
|
|
136
138
|
| `/cwd` | Show working directory |
|
|
137
139
|
|
|
138
140
|
### Web & Export
|
|
@@ -164,16 +166,10 @@ Example:
|
|
|
164
166
|
```json
|
|
165
167
|
{
|
|
166
168
|
"models": {
|
|
167
|
-
"my-
|
|
168
|
-
"label": "
|
|
169
|
-
"provider": "
|
|
170
|
-
"
|
|
171
|
-
},
|
|
172
|
-
"my-remote-model": {
|
|
173
|
-
"label": "My remote model",
|
|
174
|
-
"provider": "openai-compatible",
|
|
175
|
-
"openaiModel": "gpt-4o-mini",
|
|
176
|
-
"baseUrl": "https://api.example.com/v1"
|
|
169
|
+
"my-gemini-flash": {
|
|
170
|
+
"label": "Gemini Flash",
|
|
171
|
+
"provider": "gemini",
|
|
172
|
+
"geminiModel": "gemini-flash"
|
|
177
173
|
}
|
|
178
174
|
}
|
|
179
175
|
}
|
package/package.json
CHANGED
package/src/cli/commands.js
CHANGED
|
@@ -4,14 +4,35 @@ const { spawn } = require('child_process');
|
|
|
4
4
|
|
|
5
5
|
const fsp = fs.promises;
|
|
6
6
|
const { listSkills, SKILLS_DIR } = require('../core/skills');
|
|
7
|
-
const { DEFAULT_LANGUAGE, DEFAULT_MODEL_KEY, MODELS, listProvidersFromModels } = require('../config');
|
|
7
|
+
const { DEFAULT_LANGUAGE, DEFAULT_MODEL_KEY, GEMINI_MODEL_WARNING, MODELS, listProvidersFromModels } = require('../config');
|
|
8
8
|
const { languageLabel, normalizeLanguage, t } = require('../i18n');
|
|
9
9
|
const { createNewSessionState, listSessions, loadSessionState, saveState } = require('../utils/sessionStorage');
|
|
10
10
|
const { listGitSecrets, removeGitSecret, upsertGitSecret } = require('../utils/secretStorage');
|
|
11
|
+
const { clearGmailAuth, getGmailAuthStatus, startGmailOAuthFlow } = require('../utils/gmailAuth');
|
|
11
12
|
const { exportTranscriptText, formatTranscriptPreview } = require('../utils/transcriptStorage');
|
|
12
13
|
const { resolveInputPath } = require('../utils/pathUtils');
|
|
13
14
|
const { printTools } = require('../tools');
|
|
14
15
|
|
|
16
|
+
|
|
17
|
+
function getModelWarning(key) {
|
|
18
|
+
const model = MODELS[key];
|
|
19
|
+
return model?.provider === 'gemini' ? GEMINI_MODEL_WARNING : '';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function printModelChanged(key) {
|
|
23
|
+
const warning = getModelWarning(key);
|
|
24
|
+
console.log(`Model: ${MODELS[key].label}`);
|
|
25
|
+
if (warning) console.log(`Warning: ${warning}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function printLanguageChanged(language) {
|
|
29
|
+
const normalized = normalizeLanguage(language);
|
|
30
|
+
const label = languageLabel(normalized);
|
|
31
|
+
console.log(normalized === 'es'
|
|
32
|
+
? `Actualizado al idioma ${label} (${normalized})`
|
|
33
|
+
: `Updated to language ${label} (${normalized})`);
|
|
34
|
+
}
|
|
35
|
+
|
|
15
36
|
const SLASH_COMMANDS = [
|
|
16
37
|
{ name: 'help', desc: 'full help', descEs: 'ayuda completa' },
|
|
17
38
|
{ name: 'status', desc: 'current status', descEs: 'estado actual' },
|
|
@@ -28,6 +49,7 @@ const SLASH_COMMANDS = [
|
|
|
28
49
|
{ name: 'models', desc: 'list models', descEs: 'listar modelos' },
|
|
29
50
|
{ name: 'providers', desc: 'list providers', descEs: 'listar proveedores' },
|
|
30
51
|
{ name: 'git', desc: 'configure git credentials', descEs: 'configurar credenciales git' },
|
|
52
|
+
{ name: 'gmail', desc: 'connect Gmail account', descEs: 'conectar cuenta Gmail' },
|
|
31
53
|
{ name: 'persona', desc: 'set response tone/personality', descEs: 'definir tono/persona' },
|
|
32
54
|
{ name: 'lang', desc: 'change language', descEs: 'cambiar idioma' },
|
|
33
55
|
{ name: 'language', desc: 'change language', descEs: 'cambiar idioma' },
|
|
@@ -121,6 +143,9 @@ function printHelp(state = {}) {
|
|
|
121
143
|
console.log(` ${b('/git set <provider> <token> [user] [apiBaseUrl:URL] [cloneBaseUrl:URL] [name:X]')}`);
|
|
122
144
|
console.log(` ${b('/git list')} List configured git profiles`);
|
|
123
145
|
console.log(` ${b('/git remove <provider> [name]')} Remove git credentials`);
|
|
146
|
+
console.log(` ${b('/gmail connect')} Connect Gmail with Google OAuth + PKCE`);
|
|
147
|
+
console.log(` ${b('/gmail status')} Show Gmail connection status`);
|
|
148
|
+
console.log(` ${b('/gmail disconnect')} Remove saved Gmail tokens`);
|
|
124
149
|
console.log(` ${b('/cwd')} Show current working directory`);
|
|
125
150
|
console.log(` ${b('/cwd <path>')} Change working directory`);
|
|
126
151
|
console.log('');
|
|
@@ -261,7 +286,7 @@ async function handleLocalCommand(input, state, deps) {
|
|
|
261
286
|
|
|
262
287
|
state.language = nextLanguage;
|
|
263
288
|
await saveState(state);
|
|
264
|
-
|
|
289
|
+
printLanguageChanged(nextLanguage);
|
|
265
290
|
return true;
|
|
266
291
|
}
|
|
267
292
|
|
|
@@ -401,7 +426,7 @@ async function handleLocalCommand(input, state, deps) {
|
|
|
401
426
|
}
|
|
402
427
|
state.language = nextLanguage;
|
|
403
428
|
await saveState(state);
|
|
404
|
-
|
|
429
|
+
printLanguageChanged(nextLanguage);
|
|
405
430
|
return true;
|
|
406
431
|
}
|
|
407
432
|
|
|
@@ -416,9 +441,9 @@ async function handleLocalCommand(input, state, deps) {
|
|
|
416
441
|
await saveState(state);
|
|
417
442
|
await appendTranscriptEntry(state.sessionId, {
|
|
418
443
|
type: 'system',
|
|
419
|
-
content: `Model switched to: ${MODELS[key].label}`,
|
|
444
|
+
content: `Model switched to: ${MODELS[key].label}${getModelWarning(key) ? `\nWarning: ${getModelWarning(key)}` : ''}`,
|
|
420
445
|
});
|
|
421
|
-
|
|
446
|
+
printModelChanged(key);
|
|
422
447
|
return true;
|
|
423
448
|
}
|
|
424
449
|
|
|
@@ -479,9 +504,9 @@ async function handleLocalCommand(input, state, deps) {
|
|
|
479
504
|
await saveState(state);
|
|
480
505
|
await appendTranscriptEntry(state.sessionId, {
|
|
481
506
|
type: 'system',
|
|
482
|
-
content: `Model switched to: ${MODELS[key].label}`,
|
|
507
|
+
content: `Model switched to: ${MODELS[key].label}${getModelWarning(key) ? `\nWarning: ${getModelWarning(key)}` : ''}`,
|
|
483
508
|
});
|
|
484
|
-
|
|
509
|
+
printModelChanged(key);
|
|
485
510
|
return true;
|
|
486
511
|
}
|
|
487
512
|
|
|
@@ -541,6 +566,51 @@ async function handleLocalCommand(input, state, deps) {
|
|
|
541
566
|
return true;
|
|
542
567
|
}
|
|
543
568
|
|
|
569
|
+
|
|
570
|
+
if (commandName === 'gmail') {
|
|
571
|
+
const [subRaw, ...rest] = String(args || 'status').trim().split(/\s+/).filter(Boolean);
|
|
572
|
+
const sub = (subRaw || 'status').toLowerCase();
|
|
573
|
+
|
|
574
|
+
if (sub === 'connect' || sub === 'login') {
|
|
575
|
+
const portArg = rest.find(part => /^\d{2,5}$/.test(part));
|
|
576
|
+
const flow = await startGmailOAuthFlow({ port: portArg ? Number(portArg) : 0, flow: 'code' });
|
|
577
|
+
console.log(flow.authUrl);
|
|
578
|
+
if (flow.flow === 'device') {
|
|
579
|
+
console.log(`Código: ${flow.userCode}`);
|
|
580
|
+
console.log('Abre el link, ingresa el código y autoriza Gmail.');
|
|
581
|
+
} else {
|
|
582
|
+
console.log('Abre el link, inicia sesión y vuelve aquí.');
|
|
583
|
+
}
|
|
584
|
+
flow.done
|
|
585
|
+
.then(auth => {
|
|
586
|
+
const email = auth?.profile?.email || 'cuenta conectada';
|
|
587
|
+
console.error(`Gmail conectado: ${email}`);
|
|
588
|
+
})
|
|
589
|
+
.catch(err => console.error(`Gmail OAuth fallo: ${err.message}`));
|
|
590
|
+
return true;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if (sub === 'status') {
|
|
594
|
+
const status = await getGmailAuthStatus();
|
|
595
|
+
if (!status.connected) {
|
|
596
|
+
console.log('Gmail: no conectado. Usa /gmail connect.');
|
|
597
|
+
} else {
|
|
598
|
+
console.log(`Gmail: conectado${status.email ? ` (${status.email})` : ''}`);
|
|
599
|
+
console.log(`Scopes: ${status.scopes.join(', ') || '-'}`);
|
|
600
|
+
console.log(`Expira: ${status.expiryDate ? new Date(status.expiryDate).toISOString() : '-'}`);
|
|
601
|
+
}
|
|
602
|
+
return true;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
if (sub === 'disconnect' || sub === 'logout' || sub === 'remove') {
|
|
606
|
+
await clearGmailAuth();
|
|
607
|
+
console.log('Gmail desconectado.');
|
|
608
|
+
return true;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
throw new Error('Use /gmail connect|status|disconnect');
|
|
612
|
+
}
|
|
613
|
+
|
|
544
614
|
if (commandName === 'web') {
|
|
545
615
|
let host = '127.0.0.1';
|
|
546
616
|
let port = 3000;
|
package/src/cli/runtime.js
CHANGED
|
@@ -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/config.js
CHANGED
|
@@ -34,23 +34,16 @@ const BUILTIN_MODELS = {
|
|
|
34
34
|
provider: 'zen',
|
|
35
35
|
zenModel: 'trinity-large-preview-free',
|
|
36
36
|
},
|
|
37
|
-
'
|
|
38
|
-
label: '
|
|
39
|
-
provider: '
|
|
40
|
-
|
|
41
|
-
},
|
|
42
|
-
'ollama-gemma': {
|
|
43
|
-
label: 'Ollama Gemma 3',
|
|
44
|
-
provider: 'ollama',
|
|
45
|
-
ollamaModel: 'gemma3:latest',
|
|
46
|
-
},
|
|
47
|
-
'openai-mini': {
|
|
48
|
-
label: 'OpenAI Compatible Mini',
|
|
49
|
-
provider: 'openai-compatible',
|
|
50
|
-
openaiModel: 'gpt-4o-mini',
|
|
37
|
+
'gemini-flash': {
|
|
38
|
+
label: 'Gemini Flash',
|
|
39
|
+
provider: 'gemini',
|
|
40
|
+
geminiModel: 'gemini-flash',
|
|
51
41
|
},
|
|
52
42
|
};
|
|
53
43
|
|
|
44
|
+
const SUPPORTED_MODEL_PROVIDERS = new Set(['qwen', 'zen', 'gemini']);
|
|
45
|
+
const GEMINI_MODEL_WARNING = 'It is not recommended for use in production; it is unstable and ineffective.';
|
|
46
|
+
|
|
54
47
|
function readJsonFile(filePath) {
|
|
55
48
|
try {
|
|
56
49
|
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
@@ -66,7 +59,7 @@ function loadExternalModels() {
|
|
|
66
59
|
if (Array.isArray(raw)) {
|
|
67
60
|
const output = {};
|
|
68
61
|
for (const item of raw) {
|
|
69
|
-
if (!item?.key || !item?.provider) continue;
|
|
62
|
+
if (!item?.key || !item?.provider || !SUPPORTED_MODEL_PROVIDERS.has(item.provider)) continue;
|
|
70
63
|
output[item.key] = {
|
|
71
64
|
label: item.label || item.key,
|
|
72
65
|
provider: item.provider,
|
|
@@ -76,11 +69,13 @@ function loadExternalModels() {
|
|
|
76
69
|
return output;
|
|
77
70
|
}
|
|
78
71
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
72
|
+
const rawModels = raw && typeof raw === 'object' && raw.models && typeof raw.models === 'object'
|
|
73
|
+
? raw.models
|
|
74
|
+
: (raw && typeof raw === 'object' ? raw : {});
|
|
82
75
|
|
|
83
|
-
return
|
|
76
|
+
return Object.fromEntries(
|
|
77
|
+
Object.entries(rawModels).filter(([, model]) => SUPPORTED_MODEL_PROVIDERS.has(model?.provider)),
|
|
78
|
+
);
|
|
84
79
|
}
|
|
85
80
|
|
|
86
81
|
const MODELS = {
|
|
@@ -99,8 +94,10 @@ const MAX_OUTPUT_CHARS = 12000;
|
|
|
99
94
|
const MAX_FILE_LINES = 5000;
|
|
100
95
|
const ACTION_LOG_LIMIT = 40;
|
|
101
96
|
const REQUEST_TIMEOUT_MS = Number(process.env.ZYN_REQUEST_TIMEOUT_MS || 180000);
|
|
102
|
-
const MAX_HISTORY_CHARS =
|
|
103
|
-
const KEEP_RECENT_MESSAGES =
|
|
97
|
+
const MAX_HISTORY_CHARS = 60000;
|
|
98
|
+
const KEEP_RECENT_MESSAGES = 50;
|
|
99
|
+
const PROVIDER_TIMEOUT_RETRY_DELAY_MS = Number(process.env.ZYN_PROVIDER_TIMEOUT_RETRY_DELAY_MS || 600000);
|
|
100
|
+
const PROVIDER_TIMEOUT_MAX_ATTEMPTS = 3;
|
|
104
101
|
const SESSION_ROOT = path.join(DATA_ROOT, 'chat');
|
|
105
102
|
const SESSIONS_DIR = path.join(SESSION_ROOT, 'sessions');
|
|
106
103
|
const CURRENT_SESSION_FILE = path.join(SESSION_ROOT, 'current-session.json');
|
|
@@ -110,6 +107,9 @@ const EXPORTS_DIR = path.join(SESSION_ROOT, 'exports');
|
|
|
110
107
|
const THINK_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
111
108
|
const USER_DATA_ROOT = path.join(os.homedir(), '.zyn');
|
|
112
109
|
const TASKS_FILE = path.join(USER_DATA_ROOT, 'tasks.json');
|
|
110
|
+
const GMAIL_CLIENT_ID = '871944347395-rnpsjsqgbnvlfb05hqk4dc9283olgnh2.apps.googleusercontent.com';
|
|
111
|
+
const GMAIL_CLIENT_SECRET = process.env.ZYN_GMAIL_CLIENT_SECRET || '';
|
|
112
|
+
const GMAIL_AUTH_FILE = path.join(USER_DATA_ROOT, 'gmail-auth.json');
|
|
113
113
|
const PROVIDERS_FILE = path.join(DATA_ROOT, 'providers.json');
|
|
114
114
|
|
|
115
115
|
function listProvidersFromModels(models = MODELS) {
|
|
@@ -142,6 +142,10 @@ module.exports = {
|
|
|
142
142
|
DATA_ROOT,
|
|
143
143
|
DEFAULT_LANGUAGE,
|
|
144
144
|
DEFAULT_MODEL_KEY,
|
|
145
|
+
GEMINI_MODEL_WARNING,
|
|
146
|
+
GMAIL_AUTH_FILE,
|
|
147
|
+
GMAIL_CLIENT_ID,
|
|
148
|
+
GMAIL_CLIENT_SECRET,
|
|
145
149
|
EXPORTS_DIR,
|
|
146
150
|
HOME_DIR,
|
|
147
151
|
KEEP_RECENT_MESSAGES,
|
|
@@ -152,6 +156,9 @@ module.exports = {
|
|
|
152
156
|
MODELS,
|
|
153
157
|
MODELS_FILE,
|
|
154
158
|
PROVIDERS_FILE,
|
|
159
|
+
SUPPORTED_MODEL_PROVIDERS,
|
|
160
|
+
PROVIDER_TIMEOUT_MAX_ATTEMPTS,
|
|
161
|
+
PROVIDER_TIMEOUT_RETRY_DELAY_MS,
|
|
155
162
|
QWEN_EMAIL,
|
|
156
163
|
QWEN_PASSWORD,
|
|
157
164
|
REQUEST_TIMEOUT_MS,
|
package/src/core/agent.js
CHANGED
|
@@ -2,7 +2,10 @@ const {
|
|
|
2
2
|
DEFAULT_MODEL_KEY,
|
|
3
3
|
KEEP_RECENT_MESSAGES,
|
|
4
4
|
MAX_HISTORY_CHARS,
|
|
5
|
+
MAX_TOOL_STEPS,
|
|
5
6
|
MODELS,
|
|
7
|
+
PROVIDER_TIMEOUT_MAX_ATTEMPTS,
|
|
8
|
+
PROVIDER_TIMEOUT_RETRY_DELAY_MS,
|
|
6
9
|
REQUEST_TIMEOUT_MS,
|
|
7
10
|
} = require('../config');
|
|
8
11
|
const { chat, chatSilent } = require('../providers/scraperClient');
|
|
@@ -24,6 +27,29 @@ const { normalizeText, shortText } = require('../utils/text');
|
|
|
24
27
|
const { detectLanguage } = require('../i18n');
|
|
25
28
|
|
|
26
29
|
|
|
30
|
+
|
|
31
|
+
function waitForRetry(ms, signal) {
|
|
32
|
+
if (!ms || ms <= 0) return Promise.resolve();
|
|
33
|
+
if (signal?.aborted) return Promise.reject(new Error('aborted'));
|
|
34
|
+
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
const timeout = setTimeout(cleanupAndResolve, ms);
|
|
37
|
+
const onAbort = () => {
|
|
38
|
+
clearTimeout(timeout);
|
|
39
|
+
cleanup();
|
|
40
|
+
reject(new Error('aborted'));
|
|
41
|
+
};
|
|
42
|
+
function cleanup() {
|
|
43
|
+
if (signal) signal.removeEventListener('abort', onAbort);
|
|
44
|
+
}
|
|
45
|
+
function cleanupAndResolve() {
|
|
46
|
+
cleanup();
|
|
47
|
+
resolve();
|
|
48
|
+
}
|
|
49
|
+
if (signal) signal.addEventListener('abort', onAbort, { once: true });
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
27
53
|
function looksLikeActionRequest(text) {
|
|
28
54
|
const sample = normalizeText(String(text || '')).toLowerCase();
|
|
29
55
|
if (!sample) return false;
|
|
@@ -37,7 +63,7 @@ async function requestModel(messages, state, ui, options = {}) {
|
|
|
37
63
|
signal,
|
|
38
64
|
} = options;
|
|
39
65
|
|
|
40
|
-
for (let attempt = 0; attempt <
|
|
66
|
+
for (let attempt = 0; attempt < PROVIDER_TIMEOUT_MAX_ATTEMPTS; attempt += 1) {
|
|
41
67
|
if (signal?.aborted) {
|
|
42
68
|
throw new Error(state.language === 'es' ? 'Agente detenido por el usuario (ESC x2)' : 'Agent stopped by the user (ESC x2)');
|
|
43
69
|
}
|
|
@@ -95,8 +121,21 @@ async function requestModel(messages, state, ui, options = {}) {
|
|
|
95
121
|
} catch (err) {
|
|
96
122
|
const externalAbort = Boolean(signal?.aborted);
|
|
97
123
|
const aborted = controller.signal.aborted || err?.name === 'AbortError';
|
|
98
|
-
if (aborted && timedOut && !externalAbort && attempt
|
|
99
|
-
|
|
124
|
+
if (aborted && timedOut && !externalAbort && attempt < PROVIDER_TIMEOUT_MAX_ATTEMPTS - 1) {
|
|
125
|
+
const waitMinutes = Math.round(PROVIDER_TIMEOUT_RETRY_DELAY_MS / 60000);
|
|
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
|
+
}
|
|
100
139
|
continue;
|
|
101
140
|
}
|
|
102
141
|
if (aborted) {
|
|
@@ -105,7 +144,7 @@ async function requestModel(messages, state, ui, options = {}) {
|
|
|
105
144
|
}
|
|
106
145
|
throw new Error(state.language === 'es' ? 'Tiempo agotado del proveedor' : 'Provider timeout exceeded');
|
|
107
146
|
}
|
|
108
|
-
if (!externalAbort && attempt
|
|
147
|
+
if (!externalAbort && attempt < PROVIDER_TIMEOUT_MAX_ATTEMPTS - 1) {
|
|
109
148
|
ui.logEvent(state, 'warn', state.language === 'es' ? 'Error transitorio, reenviando contexto y skills' : 'Transient error, resending context and skills');
|
|
110
149
|
continue;
|
|
111
150
|
}
|
|
@@ -130,10 +169,13 @@ async function summarizeMessages(state, ui, messages) {
|
|
|
130
169
|
{
|
|
131
170
|
role: 'system',
|
|
132
171
|
content: [
|
|
133
|
-
state.language === 'es' ? '
|
|
134
|
-
state.language === 'es' ? 'Escribe en
|
|
135
|
-
state.language === 'es'
|
|
136
|
-
|
|
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
|
+
state.language === 'es'
|
|
175
|
+
? 'Resume sin perder detalles críticos: objetivo del proyecto, progreso exacto, decisiones, archivos/rutas tocadas, comandos ejecutados, resultados, errores, credenciales/configuraciones no secretas, restricciones, próximos pasos y preferencias de idioma.'
|
|
176
|
+
: 'Summarize without losing critical details: project goal, exact progress, decisions, touched files/paths, executed commands, results, errors, non-secret credentials/configuration, constraints, next steps, and language preferences.',
|
|
177
|
+
state.language === 'es' ? 'No inventes; si algo está pendiente márcalo como Pendiente. Mantén nombres propios, rutas, APIs, límites y valores numéricos intactos.' : 'Do not invent; if something is pending mark it as Pending. Keep proper nouns, paths, APIs, limits, and numeric values intact.',
|
|
178
|
+
'Format: compact bullet list with sections Contexto/Progreso/Archivos/Comandos/Pendiente/Preferencias. Max 20 lines.',
|
|
137
179
|
].join('\n'),
|
|
138
180
|
},
|
|
139
181
|
{
|
|
@@ -204,9 +246,9 @@ async function answerFromToolResult(input, call, result, state, ui) {
|
|
|
204
246
|
|
|
205
247
|
const output = await requestModel(messages, state, ui, {
|
|
206
248
|
label: state.language === 'es' ? 'Resumiendo resultado' : 'Summarizing result',
|
|
207
|
-
streamOutput: true,
|
|
208
249
|
});
|
|
209
|
-
|
|
250
|
+
const parsed = parseAgentResponse(output);
|
|
251
|
+
return parsed.type === 'final' ? normalizeText(parsed.content) : normalizeText(output);
|
|
210
252
|
}
|
|
211
253
|
|
|
212
254
|
async function runAgentTurn(input, state, ui, options = {}) {
|
|
@@ -240,7 +282,7 @@ async function runAgentTurn(input, state, ui, options = {}) {
|
|
|
240
282
|
});
|
|
241
283
|
ui.logEvent(state, 'ok', state.language === 'es' ? 'Respuesta lista' : 'Response ready');
|
|
242
284
|
await persistSessionState(state, ui);
|
|
243
|
-
return { content: finalAnswer, rendered:
|
|
285
|
+
return { content: finalAnswer, rendered: false };
|
|
244
286
|
}
|
|
245
287
|
|
|
246
288
|
const turnMessages = [{ role: 'user', content: input }];
|
|
@@ -487,6 +529,17 @@ async function runAgentTurn(input, state, ui, options = {}) {
|
|
|
487
529
|
}
|
|
488
530
|
|
|
489
531
|
step += 1;
|
|
532
|
+
if (step >= MAX_TOOL_STEPS && MAX_TOOL_STEPS !== Number.POSITIVE_INFINITY) {
|
|
533
|
+
const limitMsg = state.language === 'es'
|
|
534
|
+
? `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.`
|
|
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');
|
|
540
|
+
await persistSessionState(state, ui);
|
|
541
|
+
return { content: limitMsg, rendered: false };
|
|
542
|
+
}
|
|
490
543
|
}
|
|
491
544
|
}
|
|
492
545
|
|