zames_pro 2.32.0 → 2.35.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.
@@ -4,7 +4,7 @@ import { getGitContext, formatGitContext } from './gitTools.js';
4
4
  import { parseXmlToolCalls } from './xml-toolcall.js';
5
5
  import { translate } from './i18n.js';
6
6
  import { normText } from './browser.js';
7
- export async function runAgentLoop({ browser, tools, task, workdir, maxIterations = 0, freshChat = false, sendSystemPrompt = false, transcript = null, attachments = [], onThinking = () => { }, onSendPause = () => { }, onNotice = () => { }, onAssistantThought = () => { }, onToolCall = () => { }, onToolResult = () => { }, onAssistantMessage = () => { }, onChatReady = () => { }, onWarning = () => { }, debugLog = false, locale = 'ru', askDeadlineMs = 240_000, maxAfterToolRetries = 6, onAutoCompact = null, autoCompactPct = 95, contextLimit = 1_000_000, getTokenUsage = null, }) {
7
+ export async function runAgentLoop({ browser, tools, task, workdir, maxIterations = 0, freshChat = false, sendSystemPrompt = false, transcript = null, attachments = [], onThinking = () => { }, onSendPause = () => { }, onSendState = () => { }, onNotice = () => { }, onAssistantThought = () => { }, onToolCall = () => { }, onToolResult = () => { }, onAssistantMessage = () => { }, onChatReady = () => { }, onWarning = () => { }, debugLog = false, locale = 'ru', askDeadlineMs = 240_000, maxAfterToolRetries = 6, onAutoCompact = null, autoCompactPct = 95, contextLimit = 1_000_000, getTokenUsage = null, }) {
8
8
  // UI callbacks must NEVER break the agent loop. A rendering error (a huge
9
9
  // tool result, a broken markdown frame, a closed terminal) used to throw
10
10
  // out of the loop right after a tool call — the session looked "stopped
@@ -30,6 +30,9 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
30
30
  // seconds, the UI shows an animated status instead of a frozen line.
31
31
  const safeSendPause = safe(onSendPause);
32
32
  browser.onSendPause = safeSendPause;
33
+ // Coarse lifecycle of the send (generating/paused/settled) for the status
34
+ // line. Same best-effort wiring as the other hooks.
35
+ browser.onSendState = safe(onSendState);
33
36
  // Service notices (rate limit, server busy, resend) must reach the OPERATOR.
34
37
  // While a LineEditor is active a raw console.error is overwritten by the
35
38
  // editor's repaint, so the operator only saw the spinner. `onNotice` prints
package/dist/browser.js CHANGED
@@ -400,6 +400,12 @@ export class DeepSeekBrowser {
400
400
  // remaining seconds. Lets the UI animate the pause status instead of
401
401
  // printing a static line (the dots used to be frozen during the pause).
402
402
  onSendPause;
403
+ // Coarse lifecycle of one send: 'generating' right before the message is
404
+ // typed, 'paused' while waiting out a throttle/Continue gap, 'settled'
405
+ // once the answer stopped changing. Lets the UI show an explicit phase
406
+ // instead of only an on/off spinner (a long reasoning turn and a rate-limit
407
+ // wait looked identical). Firing this hook does not alter any send timing.
408
+ onSendState;
403
409
  // A service notice for the OPERATOR (rate limit, server busy, resend, …).
404
410
  // IMPORTANT: these must NOT go through `console.error` directly — while a
405
411
  // LineEditor is active it repaints its own status line and the raw stderr
@@ -458,6 +464,7 @@ export class DeepSeekBrowser {
458
464
  this._netHookInstalled = false;
459
465
  this.onSendStart = null;
460
466
  this.onSendPause = null;
467
+ this.onSendState = null;
461
468
  this.onNotice = null;
462
469
  // An explicit UA (config/options) is respected as-is. Otherwise it stays
463
470
  // empty here and is derived from the real engine UA after launch — see
@@ -482,6 +489,16 @@ export class DeepSeekBrowser {
482
489
  }
483
490
  console.error(text);
484
491
  }
492
+ // Report the coarse lifecycle of one send to the UI hook (if wired).
493
+ // Best-effort: a broken UI callback must never break the send loop.
494
+ _emitSendState(state) {
495
+ if (this.onSendState) {
496
+ try {
497
+ this.onSendState(state);
498
+ }
499
+ catch { }
500
+ }
501
+ }
485
502
  async launch() {
486
503
  // NOTE: we deliberately do NOT kill stray chrome processes here. Per the
487
504
  // project rules, an agent must never kill a browser it did not start —
@@ -1606,7 +1623,18 @@ export class DeepSeekBrowser {
1606
1623
  // at the start burns the whole askRetries budget and ask() gives up early.
1607
1624
  while (attempt < this.askRetries) {
1608
1625
  try {
1609
- return await this._askOnce(prompt, { timeout, agent, attachments });
1626
+ const answer = await this._askOnce(prompt, {
1627
+ timeout,
1628
+ agent,
1629
+ attachments,
1630
+ });
1631
+ // The answer has settled (the finish loop returned it): mark the
1632
+ // lifecycle state so the status line shows "settled" instead of
1633
+ // still looking like generation.
1634
+ if (answer !== '(прервано пользователем)') {
1635
+ this._emitSendState('settled');
1636
+ }
1637
+ return answer;
1610
1638
  }
1611
1639
  catch (e) {
1612
1640
  lastErr = e;
@@ -1892,6 +1920,7 @@ export class DeepSeekBrowser {
1892
1920
  catch { }
1893
1921
  }
1894
1922
  };
1923
+ this._emitSendState('paused');
1895
1924
  tick(totalSec * 1000);
1896
1925
  return await this._sleepInterruptible(totalSec * 1000, tick);
1897
1926
  }
@@ -1937,6 +1966,7 @@ export class DeepSeekBrowser {
1937
1966
  }
1938
1967
  };
1939
1968
  report(gap);
1969
+ this._emitSendState('paused');
1940
1970
  await this._sleepInterruptible(gap, report);
1941
1971
  }
1942
1972
  // Sleep in small slices so Esc/Ctrl+C can cancel the wait promptly.
@@ -2022,6 +2052,7 @@ export class DeepSeekBrowser {
2022
2052
  }
2023
2053
  catch { }
2024
2054
  }
2055
+ this._emitSendState('generating');
2025
2056
  this._netCapture = '';
2026
2057
  this._netCaptureAt = 0;
2027
2058
  this._netNoAnswer = false;
package/dist/commands.js CHANGED
@@ -35,12 +35,15 @@ export function summarizeTranscript(entries) {
35
35
  toolCounts: {},
36
36
  durationMs: 0,
37
37
  startedAt: null,
38
+ autoCompacts: 0,
38
39
  };
39
40
  for (const e of entries) {
40
41
  if (!e || typeof e !== 'object')
41
42
  continue;
42
43
  if (e.type === 'user_task')
43
44
  stats.turns++;
45
+ if (e.type === 'auto_compact_done')
46
+ stats.autoCompacts++;
44
47
  if (e.type === 'tool_call') {
45
48
  stats.toolCalls++;
46
49
  const tool = String(e.tool ?? '?');
@@ -71,17 +74,20 @@ export function parseTranscript(body) {
71
74
  }
72
75
  return out;
73
76
  }
74
- export function formatDuration(ms) {
77
+ export const EN_DURATION_UNITS = { h: 'h', m: 'm', s: 's' };
78
+ // Human-readable duration: "8s", "5m 12s", "1h 01m 01s". Seconds are padded
79
+ // only when a larger unit is present, so a lone "45s" is not "45s" padded.
80
+ export function formatDuration(ms, units = EN_DURATION_UNITS) {
75
81
  const total = Math.max(0, Math.round(ms / 1000));
76
82
  const h = Math.floor(total / 3600);
77
83
  const m = Math.floor((total % 3600) / 60);
78
84
  const s = total % 60;
79
85
  const pad = (n) => String(n).padStart(2, '0');
80
86
  if (h > 0)
81
- return h + 'h ' + pad(m) + 'm ' + pad(s) + 's';
87
+ return h + units.h + ' ' + pad(m) + units.m + ' ' + pad(s) + units.s;
82
88
  if (m > 0)
83
- return m + 'm ' + pad(s) + 's';
84
- return s + 's';
89
+ return m + units.m + ' ' + pad(s) + units.s;
90
+ return s + units.s;
85
91
  }
86
92
  export function renderCost(stats, transcriptFile, tokenUsage = null) {
87
93
  const lines = [];
@@ -94,6 +100,9 @@ export function renderCost(stats, transcriptFile, tokenUsage = null) {
94
100
  }
95
101
  lines.push(' tasks: ' + stats.turns);
96
102
  lines.push(' tool calls: ' + stats.toolCalls);
103
+ if (stats.autoCompacts > 0) {
104
+ lines.push(' auto-compacts: ' + stats.autoCompacts);
105
+ }
97
106
  const top = Object.entries(stats.toolCounts).sort((a, b) => b[1] - a[1]);
98
107
  if (top.length) {
99
108
  lines.push(' by tool:');
@@ -116,6 +125,15 @@ export function formatExport(entries, meta = {}) {
116
125
  if (meta.chatId)
117
126
  out.push('- chat: ' + meta.chatId);
118
127
  out.push('- exported: ' + new Date().toISOString());
128
+ // A self-contained summary, so the exported Markdown stands on its own as a
129
+ // report without opening the transcript or a /cost run.
130
+ const stats = summarizeTranscript(entries);
131
+ out.push('- tasks: ' + stats.turns);
132
+ out.push('- tool calls: ' + stats.toolCalls);
133
+ out.push('- duration: ' + formatDuration(stats.durationMs));
134
+ if (stats.autoCompacts > 0) {
135
+ out.push('- auto-compacts: ' + stats.autoCompacts);
136
+ }
119
137
  out.push('');
120
138
  for (const e of entries) {
121
139
  const type = String(e.type ?? '?');
@@ -178,6 +196,9 @@ export function renderDoctor(d) {
178
196
  if (typeof d.minSendIntervalMs === 'number') {
179
197
  row(d.minSendIntervalMs > 0, 'send pause', Math.round(d.minSendIntervalMs / 1000) + 's between agent sends');
180
198
  }
199
+ if (typeof d.sshRemote === 'boolean') {
200
+ row(true, 'session', d.sshRemote ? 'SSH remote' : 'local terminal');
201
+ }
181
202
  return 'Doctor:' + NL + rows.join(NL);
182
203
  }
183
204
  export function renderPermissions(p) {
@@ -430,6 +451,95 @@ export function isSlashCommand(text) {
430
451
  .trim()
431
452
  .startsWith('/');
432
453
  }
454
+ export function parseLiveToggle(text) {
455
+ const m = /^\/(thinking|web|websearch|search)(?:\s+(\S+))?\s*$/i.exec(String(text ?? '').trim());
456
+ if (!m)
457
+ return null;
458
+ const name = m[1].toLowerCase();
459
+ const kind = name === 'thinking' ? 'thinking' : 'search';
460
+ const arg = (m[2] || '').toLowerCase();
461
+ let mode = 'toggle';
462
+ if (arg === 'on' || arg === '1' || arg === 'true' || arg === 'вкл')
463
+ mode = 'on';
464
+ else if (arg === 'off' || arg === '0' || arg === 'false' || arg === 'выкл')
465
+ mode = 'off';
466
+ else if (arg !== '')
467
+ return null;
468
+ return { kind, mode };
469
+ }
470
+ // Prepend a long-lived session goal to a task message, so the model keeps the
471
+ // big picture even when the goal was set many turns ago. The task is appended
472
+ // AFTER the goal; when there is no goal the task is returned unchanged. The
473
+ // text is agent-facing (English), like the system prompt.
474
+ export function withGoal(goal, task) {
475
+ const g = String(goal ?? '').trim();
476
+ if (!g)
477
+ return task;
478
+ const NL = String.fromCharCode(10);
479
+ return ('Session goal (long-lived — keep it in mind for this and every following task):' +
480
+ NL +
481
+ g +
482
+ NL +
483
+ NL +
484
+ 'Current task:' +
485
+ NL +
486
+ task);
487
+ }
488
+ // Parse a `/goal` invocation. Subcommands: 'set' (with text), 'show', 'clear'.
489
+ // Returns null when the text is not a /goal command. Kept pure so the main loop
490
+ // and any future interception share one parser.
491
+ export function parseGoalCommand(text) {
492
+ const t = String(text ?? '').trim();
493
+ const m = /^\/goal(?:\s+([\s\S]*))?$/.exec(t);
494
+ if (!m)
495
+ return null;
496
+ const rest = (m[1] ?? '').trim();
497
+ if (rest === '')
498
+ return { sub: 'show' };
499
+ if (rest.toLowerCase() === 'clear' || rest.toLowerCase() === 'off')
500
+ return { sub: 'clear' };
501
+ return { sub: 'set', goal: rest };
502
+ }
503
+ // True when a `/config ...` line can be handled LIVE (while the agent is busy):
504
+ // the text subcommands only touch config values, never the chat. The bare
505
+ // `/config` and `/config menu` (which pause the editor and read keys) are NOT
506
+ // live — they must go through the main loop, so a mid-run menu never fights
507
+ // the running task. Pure, so the interception is unit-tested.
508
+ export function isLiveConfigCommand(text) {
509
+ return /^\/config\s+(?!menu\b|ui\b)\S+/i.test(String(text ?? '').trim());
510
+ }
511
+ // Parse a `/queue` invocation. Returns the subcommand ('list' | 'clear') or
512
+ // null when the text is not a /queue command at all. This runs WHILE the agent
513
+ // is busy (intercepted in the input handler), because the main command loop is
514
+ // blocked on runTask() and a queued /queue would only run after the task —
515
+ // when inspecting/clearing the queue is pointless.
516
+ export function parseQueueCommand(text) {
517
+ const t = String(text ?? '').trim();
518
+ const m = /^\/queue(?:\s+(\S+))?\s*$/.exec(t);
519
+ if (!m)
520
+ return null;
521
+ const arg = (m[1] || '').toLowerCase();
522
+ if (arg === '' || arg === 'list' || arg === 'ls')
523
+ return { sub: 'list' };
524
+ if (arg === 'clear' || arg === 'c' || arg === 'clean')
525
+ return { sub: 'clear' };
526
+ // An unrecognized subcommand falls through to the normal main-loop handler,
527
+ // which prints the usage hint.
528
+ return null;
529
+ }
530
+ // Render the queued messages for the operator (one line per message, text
531
+ // truncated). Pure, so the /queue interception and the main-loop handler share
532
+ // the EXACT same output instead of duplicating the formatting.
533
+ export function formatQueueList(msgs) {
534
+ return (msgs || []).map((m, i) => {
535
+ const one = String(m.text || '')
536
+ .replace(/\s+/g, ' ')
537
+ .trim();
538
+ const shown = one.length > 80 ? one.slice(0, 80) + ' …' : one;
539
+ const att = m.attachments?.length ? ' [+' + m.attachments.length + ']' : '';
540
+ return ' ' + (i + 1) + '. ' + shown + att;
541
+ });
542
+ }
433
543
  function queuedIsImage(a) {
434
544
  return (String(a.mime || '').startsWith('image/') ||
435
545
  isImageName(String(a.name || '')));
package/dist/i18n.js CHANGED
@@ -205,6 +205,78 @@ const CATALOG = {
205
205
  ru: '/queue [clear] показать/очистить очередь сообщений',
206
206
  en: '/queue [clear] list/clear the pending message queue',
207
207
  },
208
+ 'help.cmd.thinking': {
209
+ ru: '/thinking [on|off] включить/выключить режим размышления (можно во время работы)',
210
+ en: '/thinking [on|off] toggle Deep thinking (works while the agent is busy)',
211
+ },
212
+ 'help.cmd.web': {
213
+ ru: '/web [on|off] включить/выключить поиск в интернете (можно во время работы)',
214
+ en: '/web [on|off] toggle Smart search (works while the agent is busy)',
215
+ },
216
+ 'help.cmd.goal': {
217
+ ru: '/goal [текст|clear] задать долгоживущую цель сессии',
218
+ en: '/goal [text|clear] set a long-lived session goal',
219
+ },
220
+ 'help.cmd.loop': {
221
+ ru: '/loop <интервал> <задача> периодически повторять задачу (напр. 10m)',
222
+ en: '/loop <interval> <task> repeat a task periodically (e.g. 10m)',
223
+ },
224
+ 'help.cmd.cron': {
225
+ ru: '/cron <выражение> <задача> запускать по расписанию (5 полей)',
226
+ en: '/cron <expr> <task> schedule a task (5-field cron)',
227
+ },
228
+ 'help.cmd.jobs': {
229
+ ru: '/jobs [rm <id>|clear] список/удаление запланированных задач',
230
+ en: '/jobs [rm <id>|clear] list/remove scheduled tasks',
231
+ },
232
+ 'sched.loop_usage': {
233
+ ru: 'Использование: /loop <интервал> <задача>, например /loop 10m проверь тесты',
234
+ en: 'Usage: /loop <interval> <task>, e.g. /loop 10m check the tests',
235
+ },
236
+ 'sched.cron_usage': {
237
+ ru: 'Использование: /cron "<мин> <час> <день> <мес> <день недели>" <задача>',
238
+ en: 'Usage: /cron "<min> <hour> <dom> <month> <dow>" <task>',
239
+ },
240
+ 'sched.added': {
241
+ ru: '⏰ Задача #{id} добавлена ({when}): {task}',
242
+ en: '⏰ Job #{id} added ({when}): {task}',
243
+ },
244
+ 'sched.title': { ru: 'Запланированные задачи:', en: 'Scheduled jobs:' },
245
+ 'sched.none': { ru: 'Запланированных задач нет.', en: 'No scheduled jobs.' },
246
+ 'sched.fired': {
247
+ ru: '⏰ Задача #{id} сработала: {task}',
248
+ en: '⏰ Job #{id} fired: {task}',
249
+ },
250
+ 'sched.cleared': { ru: 'Задачи очищены: {n}', en: 'Jobs cleared: {n}' },
251
+ 'sched.removed': { ru: 'Задача #{id} удалена.', en: 'Job #{id} removed.' },
252
+ 'sched.rm_usage': {
253
+ ru: 'Использование: /jobs rm <id>',
254
+ en: 'Usage: /jobs rm <id>',
255
+ },
256
+ 'sched.remove_hint': {
257
+ ru: 'Удалить: /jobs rm <id> · очистить всё: /jobs clear',
258
+ en: 'Remove: /jobs rm <id> · clear all: /jobs clear',
259
+ },
260
+ 'goal.set': { ru: '🎯 Цель сессии: {goal}', en: '🎯 Session goal: {goal}' },
261
+ 'goal.loaded': {
262
+ ru: '🎯 Восстановлена цель сессии: {goal}',
263
+ en: '🎯 Restored session goal: {goal}',
264
+ },
265
+ 'goal.current': {
266
+ ru: '🎯 Цель сессии: {goal}',
267
+ en: '🎯 Session goal: {goal}',
268
+ },
269
+ 'goal.none': {
270
+ ru: 'Цель сессии не задана. /goal <текст>, /goal clear.',
271
+ en: 'No session goal. /goal <text>, /goal clear.',
272
+ },
273
+ 'goal.cleared': {
274
+ ru: '🎯 Цель сессии очищена.',
275
+ en: '🎯 Session goal cleared.',
276
+ },
277
+ 'toggle.thinking': { ru: 'Размышление', en: 'Deep thinking' },
278
+ 'toggle.search': { ru: 'Поиск в интернете', en: 'Smart search' },
279
+ 'toggle.set': { ru: '{name}: {state}', en: '{name}: {state}' },
208
280
  'compact.auto_trigger': {
209
281
  ru: '🗜️ Контекст заполнен на {pct}% ({tokens} токенов) — сжимаю историю и открываю новый чат...',
210
282
  en: '🗜️ Context at {pct}% ({tokens} tokens) — compacting the history and opening a fresh chat...',
@@ -374,6 +446,10 @@ const CATALOG = {
374
446
  ru: '⚙ выполняю {name}',
375
447
  en: '⚙ running {name}',
376
448
  },
449
+ // Send/generation lifecycle indicator in the status line. Unlike the
450
+ // animated thinking phrase, it says explicitly that a generation is in
451
+ // flight. 'paused'/'settled' have no label (see LineEditor._stateLabel).
452
+ 'state.generating': { ru: '▶ генерация', en: '▶ generating' },
377
453
  'editor.more': { ru: '…ещё {n}', en: '…{n} more' },
378
454
  'editor.answer': { ru: '● Ответ', en: '● Answer' },
379
455
  // ---------- status ----------
@@ -462,6 +538,13 @@ const CATALOG = {
462
538
  en: '⏹ Ctrl+C — aborting generation...',
463
539
  },
464
540
  'msg.queued': { ru: '📨 В очередь ({n}): ', en: '📨 Queued ({n}): ' },
541
+ // Shown ONCE per session, the first time a message is queued while the
542
+ // agent is working. It explains what happens (sent after the current task)
543
+ // and how to inspect/clear the queue, so the operator knows nothing is lost.
544
+ 'msg.queued_hint': {
545
+ ru: 'Сообщение в очереди — уйдёт после текущей задачи; /queue покажет и очистит.',
546
+ en: 'Message queued — it will be sent after the current task; /queue lists or clears it.',
547
+ },
465
548
  'msg.attached_image': {
466
549
  ru: '🖼 Вложено изображение {marker} ({size}) — {path}',
467
550
  en: '🖼 Attached image {marker} ({size}) — {path}',
@@ -482,10 +565,15 @@ const CATALOG = {
482
565
  ru: ' Похоже, вы в контейнере/Dev Container: клипборд Windows отсюда недоступен. Вставьте путь к файлу (например tmp/pic.png) — он приложится к сообщению.',
483
566
  en: ' Looks like a container/Dev Container: the Windows clipboard is not reachable from here. Paste a path to a file (e.g. tmp/pic.png) — it will be attached to the message.',
484
567
  },
485
- 'msg.task_summary': {
486
- ru: '· задача: {sec}с · инструментов: {tools}',
487
- en: '· task: {sec}s · tools: {tools}',
488
- },
568
+ // Per-task summary parts (joined with "·" by index.ts). Split into separate
569
+ // keys so the tokens part can be omitted when the counter is unknown.
570
+ 'task_sum.dur': { ru: 'длительность: {dur}', en: 'duration: {dur}' },
571
+ 'task_sum.tools': { ru: 'инструментов: {n}', en: 'tools: {n}' },
572
+ 'task_sum.tokens': { ru: 'токенов: {n}', en: 'tokens: {n}' },
573
+ // Unit labels for the localized task duration (see formatDuration).
574
+ 'dur.h': { ru: 'ч', en: 'h' },
575
+ 'dur.m': { ru: 'м', en: 'm' },
576
+ 'dur.s': { ru: 'с', en: 's' },
489
577
  'msg.from_queue': { ru: '▶ Из очереди: ', en: '▶ From queue: ' },
490
578
  'msg.from_queue_batch': {
491
579
  ru: '▶ Из очереди ({n} сообщ.): ',
@@ -498,7 +586,6 @@ const CATALOG = {
498
586
  'msg.queue_title': { ru: 'Очередь сообщений:', en: 'Pending messages:' },
499
587
  'msg.queue_empty': { ru: 'Очередь пуста.', en: 'The queue is empty.' },
500
588
  'msg.queue_cleared': { ru: 'Очередь очищена.', en: 'Queue cleared.' },
501
- 'msg.queue_item': { ru: '{n}. {text}', en: '{n}. {text}' },
502
589
  'msg.queue_usage': {
503
590
  ru: 'Использование: /queue [clear]',
504
591
  en: 'Usage: /queue [clear]',
@@ -876,6 +963,10 @@ const CATALOG = {
876
963
  },
877
964
  'mcp.status_error': { ru: '(ошибка: {v})', en: '(error: {v})' },
878
965
  'status.mcp': { ru: 'MCP-инструменты: {v}', en: 'MCP tools: {v}' },
966
+ 'status.toggles': {
967
+ ru: 'Размышление: {think} · Поиск: {search}',
968
+ en: 'Deep thinking: {think} · Search: {search}',
969
+ },
879
970
  'status.tokens': {
880
971
  ru: 'Контекст (токенов): {v}',
881
972
  en: 'Context (tokens): {v}',