zames_pro 2.9.3 → 2.10.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.
@@ -72,6 +72,16 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
72
72
  const MAX_MALFORMED_RETRIES = 3;
73
73
  let stallRetries = 0;
74
74
  const MAX_STALL_RETRIES = 5;
75
+ // SINGLE shared budget for "the answer is not a recognized tool call".
76
+ // Before, every guard had its own counter (3+5+3+5+6 = 22 re-asks), so a
77
+ // stuck answer hung the loop for ~20 iterations and then returned an
78
+ // "iteration limit" stub — the exact "agent stopped" symptom. All the
79
+ // guards below now also bump this counter, and once it is exhausted the
80
+ // loop asks for respond exactly once and then finishes with the model's
81
+ // own text (never a stub).
82
+ let unparsedRetries = 0;
83
+ const MAX_UNPARSED_RETRIES = 4;
84
+ let finalRespondAsked = false;
75
85
  // Guard against "the agent stalled": DeepSeek sometimes sends a final text
76
86
  // that merely DESCRIBES the next tool call (or cuts the answer off
77
87
  // mid-word), and the agent silently finishes the task even though the work
@@ -80,8 +90,6 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
80
90
  // chatty model.
81
91
  let looksDoneRetries = 0;
82
92
  const MAX_LOOKSDONE_RETRIES = 3;
83
- let plainTextRetries = 0;
84
- const MAX_PLAINTEXT_RETRIES = 5;
85
93
  // Watchdog against the agent emitting a tool call and then going silent.
86
94
  // After a tool result the expected next answer is a fresh tool call; if we
87
95
  // instead get an EMPTY answer or the EXACT same answer as the previous turn
@@ -90,16 +98,62 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
90
98
  let justRanTool = false;
91
99
  let watchdogRetries = 0;
92
100
  const MAX_WATCHDOG_RETRIES = 3;
101
+ // Retry budget for a browser.ask() TIMEOUT (not for content): the send did
102
+ // not come back in time. This is separate from unparsedRetries because a
103
+ // timeout is an infrastructure failure, not a model protocol violation.
104
+ let afterToolRetries = 0;
105
+ const MAX_AFTER_TOOL_RETRIES = 6;
93
106
  for (let i = 0; i < maxIterations; i++) {
94
107
  onThinking();
95
108
  // The first message (task) is user input: no throttle.
96
109
  // Subsequent ones (tool-result and resend requests) are agent sends:
97
110
  // throttled so we don't hit the rate limit.
98
111
  const isFirst = i === 0;
99
- const rawResponse = await browser.ask(message, {
100
- agent: !isFirst,
101
- attachments: isFirst ? attachments : [],
102
- });
112
+ // Safety net: browser.ask() has its own timeout, but a stuck send used to
113
+ // block the whole loop and look like a silent stop. We race it against a
114
+ // hard deadline and treat a timeout as a nudge (re-ask), never as a
115
+ // final answer. The deadline is generous enough for real long answers.
116
+ const askDeadlineMs = 240_000;
117
+ let rawResponse;
118
+ let askTimer = null;
119
+ try {
120
+ rawResponse = await Promise.race([
121
+ browser.ask(message, {
122
+ agent: !isFirst,
123
+ attachments: isFirst ? attachments : [],
124
+ }),
125
+ new Promise((_, reject) => {
126
+ askTimer = setTimeout(() => reject(new Error('ask() watchdog timeout')), askDeadlineMs);
127
+ if (askTimer && typeof askTimer.unref === 'function') {
128
+ askTimer.unref();
129
+ }
130
+ }),
131
+ ]);
132
+ if (askTimer)
133
+ clearTimeout(askTimer);
134
+ }
135
+ catch (e) {
136
+ if (askTimer)
137
+ clearTimeout(askTimer);
138
+ transcript?.log('ask_timeout', {
139
+ attempt: afterToolRetries,
140
+ error: e.message,
141
+ });
142
+ onWarning('browser.ask() не вернул ответ за ' +
143
+ Math.round(askDeadlineMs / 1000) +
144
+ 'с — повторяю запрос.');
145
+ if (afterToolRetries < MAX_AFTER_TOOL_RETRIES) {
146
+ afterToolRetries++;
147
+ await new Promise((r) => setTimeout(r, 1500));
148
+ continue;
149
+ }
150
+ transcript?.log('ask_timeout_exhausted', {
151
+ message: 'ask() не вернул ответ и лимит повторов исчерпан',
152
+ });
153
+ onWarning('browser.ask() перестал отвечать; лимит повторов исчерпан, ' +
154
+ 'останавливаюсь. Ответа модели нет — проверьте чат DeepSeek вручную.');
155
+ return 'ask() watchdog: ответ модели не получен';
156
+ }
103
157
  await reportChat();
104
158
  transcript?.log('assistant_raw', { response: rawResponse });
105
159
  // The user aborted generation (Esc/Ctrl+C).
@@ -107,24 +161,47 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
107
161
  transcript?.log('user_aborted');
108
162
  return rawResponse;
109
163
  }
110
- // Watchdog: after a tool result we expect a FRESH tool call. If the answer
111
- // is empty or identical to the previous turn (the new message was not
112
- // sent), nudge instead of stopping.
164
+ // Watchdog: after a tool result we expect a FRESH tool call. DeepSeek
165
+ // regularly stops right here; the answer may be (a) empty, (b) an exact
166
+ // copy of the previous turn (the new message was not sent), or (c) a
167
+ // non-empty fragment that parses to nothing and is not a tool call (a
168
+ // cut-off "Stale. Let me ..." or a truncated JSON). All three mean the
169
+ // turn is unfinished: nudge instead of stopping.
113
170
  const wdEmpty = !String(rawResponse || '').trim();
114
- const wdStale = justRanTool && lastRaw.trim() !== '' && rawResponse.trim() === lastRaw.trim();
115
- if (!isFirst && (wdEmpty || wdStale) && watchdogRetries < MAX_WATCHDOG_RETRIES) {
171
+ const wdStale = justRanTool &&
172
+ lastRaw.trim() !== '' &&
173
+ rawResponse.trim() === lastRaw.trim();
174
+ const wdNoCall = justRanTool &&
175
+ !wdEmpty &&
176
+ !wdStale &&
177
+ parseToolCall(rawResponse) === null &&
178
+ !responseLooksLikeToolCall(rawResponse);
179
+ if (!isFirst && (wdEmpty || wdStale || wdNoCall) && watchdogRetries < MAX_WATCHDOG_RETRIES) {
116
180
  watchdogRetries++;
117
181
  transcript?.log('watchdog_nudge', {
118
182
  attempt: watchdogRetries,
119
183
  empty: wdEmpty,
120
184
  stale: wdStale,
185
+ no_call: wdNoCall,
121
186
  response: String(rawResponse || '').slice(0, 200),
122
187
  });
188
+ message =
189
+ 'Ты остановился после результата инструмента. Продолжи работу: ' +
190
+ 'ответь РОВНО одним JSON-объектом вызова инструмента, без текста до и после, ' +
191
+ 'например: {"tool": "Bash", "args": {"command": "..."}}. ' +
192
+ 'Если задача действительно выполнена — вызови respond с итоговым сообщением.';
123
193
  await new Promise((r) => setTimeout(r, 1500));
124
194
  continue;
125
195
  }
126
196
  lastRaw = rawResponse;
127
197
  const parsed = parseToolCall(rawResponse);
198
+ // SILENT CONTRACT: pre-tool text around a valid call is NEVER shown to the
199
+ // operator. It is only handed to onAssistantThought, which is a no-op by
200
+ // default and is not wired to the UI in src/index.ts (so ui.assistant /
201
+ // the terminal never receives "Let me check..." / "Сейчас посмотрю").
202
+ // We cannot strip this text from DeepSeek's own chat output with code —
203
+ // that text is generated by the model. We can only (a) forbid it via the
204
+ // system-prompt ("ONLY TOOL CALLS") and (b) not print it here.
128
205
  if (parsed) {
129
206
  const thought = extractPreToolText(rawResponse);
130
207
  if (thought)
@@ -145,6 +222,7 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
145
222
  const looksLikeToolCall = responseLooksLikeToolCall(rawResponse);
146
223
  if (looksLikeToolCall && malformedRetries < MAX_MALFORMED_RETRIES) {
147
224
  malformedRetries++;
225
+ unparsedRetries++;
148
226
  transcript?.log('malformed_toolcall', {
149
227
  attempt: malformedRetries,
150
228
  response: rawResponse,
@@ -176,6 +254,7 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
176
254
  /(messages? too frequent|too many requests|rate limit|server (is )?busy|service (is )?unavailable|слишком часто|try again later)/i.test(trimmed));
177
255
  if (looksService && stallRetries < MAX_STALL_RETRIES) {
178
256
  stallRetries++;
257
+ unparsedRetries++;
179
258
  transcript?.log('stall_retry', {
180
259
  attempt: stallRetries,
181
260
  response: rawResponse,
@@ -200,6 +279,7 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
200
279
  // continue and to actually call a tool this time (or respond if truly done).
201
280
  if (looksLikeUnfinishedWork(trimmed) && looksDoneRetries < MAX_LOOKSDONE_RETRIES) {
202
281
  looksDoneRetries++;
282
+ unparsedRetries++;
203
283
  transcript?.log('unfinished_retry', {
204
284
  attempt: looksDoneRetries,
205
285
  response: rawResponse,
@@ -220,36 +300,75 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
220
300
  continue;
221
301
  }
222
302
  // STRICT: only tool calls and respond reach the operator. Plain text is
223
- // a protocol violation: re-ask for a tool call instead of printing it.
224
- if (plainTextRetries < MAX_PLAINTEXT_RETRIES) {
225
- plainTextRetries++;
303
+ // a protocol violation. The retries are bounded by the SINGLE
304
+ // unparsedRetries budget, so a chatty model cannot hang the loop for
305
+ // 20+ iterations (the old plainTextRetries + afterToolRetries combo).
306
+ if (unparsedRetries < MAX_UNPARSED_RETRIES) {
307
+ unparsedRetries++;
226
308
  transcript?.log('plaintext_retry', {
227
- attempt: plainTextRetries,
309
+ attempt: unparsedRetries,
228
310
  response: rawResponse.slice(0, 500),
229
311
  });
230
312
  message =
231
- 'Only call tools. Do not write plain text. ' +
232
- 'If the task is done - call respond with the final message. ' +
233
- 'Otherwise reply with EXACTLY one JSON tool-call object, no text around it.';
313
+ (justRanTool
314
+ ? 'Ты остановился после вызова инструмента и написал обычный текст. '
315
+ : 'Ты написал обычный текст без вызова инструмента. ') +
316
+ 'Задача ещё не завершена. Ответь РОВНО одним JSON-объектом вызова ' +
317
+ 'инструмента, без текста до и после, например: ' +
318
+ '{\"tool\": \"Bash\", \"args\": {\"command\": \"...\"}}. ' +
319
+ 'Если задача действительно выполнена — вызови respond с итоговым сообщением.';
234
320
  continue;
235
321
  }
236
- if (responseLooksLikeToolCall(rawResponse)) {
322
+ // Budget exhausted. Ask for respond EXACTLY once more; if the model
323
+ // still does not call it, surface its own text as the final answer
324
+ // (with a single warning) instead of looping to the iteration limit.
325
+ if (!finalRespondAsked) {
326
+ finalRespondAsked = true;
327
+ transcript?.log('final_respond_request', {
328
+ response: rawResponse.slice(0, 500),
329
+ });
330
+ message =
331
+ 'Последний шаг: вызови инструмент respond с итоговым сообщением ' +
332
+ 'оператору. Не пиши обычный текст — только вызов respond, например: ' +
333
+ '{\"tool\": \"respond\", \"args\": {\"message\": \"...\"}}';
334
+ continue;
335
+ }
336
+ const suspiciousFinal = responseLooksLikeToolCall(rawResponse) ||
337
+ !(rawResponse || '').trim();
338
+ if (suspiciousFinal) {
339
+ // The answer LOOKS like a call (or is empty) but could not be parsed
340
+ // even after all retries: warn the operator.
237
341
  transcript?.log('suspicious_final', { response: rawResponse });
342
+ onWarning(translate(locale)('msg.suspicious_stop'));
238
343
  }
344
+ // A meaningful plain-text answer (e.g. a final report the model forgot to
345
+ // wrap in respond) is surfaced as-is WITHOUT a warning: after the bounded
346
+ // re-asks it is the best available result, and warning here only
347
+ // confused the operator in earlier sessions.
239
348
  transcript?.log('plaintext_final', { message: rawResponse });
240
- onWarning(translate(locale)('msg.suspicious_stop'));
241
349
  return rawResponse;
242
350
  }
243
351
  const calls = Array.isArray(parsed) ? parsed : [parsed];
352
+ // A respond mixed with real tool calls must NOT short-circuit the tools.
353
+ // DeepSeek sometimes returns [{"tool":"Edit",...},{"tool":"respond",...}]
354
+ // in ONE answer; handling respond first would silently DROP the other
355
+ // call and the agent would look "stopped after a tool call". We only
356
+ // finish on respond when it is the SOLE call in the answer.
357
+ const realCalls = calls.filter((c) => c.tool !== 'respond');
244
358
  const respondCall = calls.find((c) => c.tool === 'respond');
245
- if (respondCall) {
359
+ if (respondCall && realCalls.length > 0) {
360
+ transcript?.log('respond_mixed_with_tools', {
361
+ tools: realCalls.map((c) => c.tool),
362
+ });
363
+ }
364
+ if (respondCall && realCalls.length === 0) {
246
365
  const msg = typeof respondCall.args.message === 'string'
247
366
  ? respondCall.args.message
248
367
  : String(respondCall.args.message ?? '');
249
368
  // An empty respond is not final: the model called respond but wrote no
250
369
  // summary. Finishing like this would show the operator nothing and the
251
370
  // task would "hang". We ask it to continue (within stallRetries).
252
- if (!msg.trim() && stallRetries < MAX_STALL_RETRIES) {
371
+ if (!isMeaningfulRespond(msg) && stallRetries < MAX_STALL_RETRIES) {
253
372
  stallRetries++;
254
373
  transcript?.log('empty_respond', { attempt: stallRetries });
255
374
  if (debugLog) {
@@ -265,12 +384,27 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
265
384
  'продолжи работу вызовом инструмента.';
266
385
  continue;
267
386
  }
387
+ // An EMPTY respond must NEVER be a silent final: the operator would see
388
+ // nothing and the task would look "stopped after a tool call". If the
389
+ // retry budget is exhausted, warn the operator and keep the run open
390
+ // instead of returning an empty string. Only a NON-empty respond ends
391
+ // the task normally.
392
+ if (!isMeaningfulRespond(msg)) {
393
+ transcript?.log('empty_respond_exhausted', { response: rawResponse });
394
+ onWarning('Модель вызвала respond без текста, и лимит повторов исчерпан. ' +
395
+ 'Проверьте чат DeepSeek вручную.');
396
+ return 'Модель не сформировала итоговое сообщение (пустой respond).';
397
+ }
268
398
  onAssistantMessage(msg);
269
399
  transcript?.log('assistant_final', { message: msg });
270
400
  return msg;
271
401
  }
272
402
  const results = [];
273
- for (const call of calls) {
403
+ // When respond came together with real tools, skip respond here: its
404
+ // message must NOT be delivered before the tools' results are known.
405
+ // The model will get the tool results and can call respond again.
406
+ const callsToRun = realCalls.length > 0 ? realCalls : calls;
407
+ for (const call of callsToRun) {
274
408
  const tool = tools.find((t) => t.name === call.tool);
275
409
  if (!tool) {
276
410
  const err = `Неизвестный инструмент: ${call.tool}`;
@@ -299,6 +433,13 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
299
433
  // Reset the watchdog so the next empty/repeated answer is nudged.
300
434
  justRanTool = true;
301
435
  watchdogRetries = 0;
436
+ // A fresh tool call just ran: reset the per-tool-result nudge budget so
437
+ // a long chain of tools is not cut off by an earlier bad turn.
438
+ afterToolRetries = 0;
439
+ // Real progress was made, so the "unparsed answer" budget is replenished:
440
+ // a long chain of tools must not run out of it because of earlier hiccups.
441
+ unparsedRetries = 0;
442
+ finalRespondAsked = false;
302
443
  if (results.length === 1) {
303
444
  const r = results[0];
304
445
  const resultStr = typeof r.result === 'string' ? r.result : JSON.stringify(r.result);
@@ -340,6 +481,24 @@ export function responseLooksLikeToolCall(rawResponse) {
340
481
  /<\s*\|?\s*(DSML|invoke|parameter)/i.test(raw) ||
341
482
  /^\s*\[?\s*\{[^}]*$/.test(raw.trim()));
342
483
  }
484
+ // A respond message is only a real FINAL answer when it carries some meaning.
485
+ // DeepSeek sometimes finishes with a placeholder — "...", "-", "ok", "done",
486
+ // "готово" — which looks like a stop with no report. Such a respond must not
487
+ // end the task silently: it is treated like an empty one (re-ask).
488
+ function isMeaningfulRespond(msg) {
489
+ const t = (msg || '').trim();
490
+ if (!t)
491
+ return false;
492
+ // Only punctuation/dots/ellipses: "...", "---", "…", "?" — not a report.
493
+ if (/^[.…–—_*?!/\s-]+$/.test(t))
494
+ return false;
495
+ // A bare acknowledgement with no content at all. NOTE: "ok"/"done"/
496
+ // "готово" are NOT in this list: a short "готово" is a legitimate final
497
+ // answer for a small task, and dropping it re-opened the loop.
498
+ if (/^(na|null|undefined)[.!]*$/i.test(t))
499
+ return false;
500
+ return true;
501
+ }
343
502
  // Text that promises a tool call in the future tense but contains no call
344
503
  // itself. DeepSeek regularly "hangs" like this: it writes
345
504
  // "Now update README to mention …", "Let me run the tests", "I'll check now"
package/dist/browser.js CHANGED
@@ -668,7 +668,13 @@ export class DeepSeekBrowser {
668
668
  // make us think the new answer had started when in fact nothing was sent
669
669
  // — and then the agent silently "stopped".
670
670
  const changed = cur && normText(cur) !== normText(beforeText);
671
- if (changed || bodyLen > startBodyLen) {
671
+ // Network capture with a fresh timestamp is the STRONGEST proof that a
672
+ // new answer started: it is the raw SSE body for the CURRENT send. Right
673
+ // after a tool result the DOM may still show the previous answer, so
674
+ // 'changed' can stay false for a while — without this check ask() used
675
+ // to hang until the full timeout and the agent appeared to "stop".
676
+ const netStarted = !!this._netCapture && this._netCaptureAt >= this._lastSentAt;
677
+ if (changed || netStarted || bodyLen > startBodyLen) {
672
678
  started = true;
673
679
  break;
674
680
  }
@@ -703,12 +709,15 @@ export class DeepSeekBrowser {
703
709
  }
704
710
  }
705
711
  }
712
+ const netFresh = !!this._netCapture && this._netCaptureAt >= this._lastSentAt;
706
713
  const cur = await this._readLastAnswerTextClean().catch(() => '');
707
714
  // Ignore an "answer" that is identical to what was on the page BEFORE we
708
715
  // sent the message: that is the previous answer, not a new one. Returning
709
716
  // it would make the agent re-process the old tool call (or silently
710
- // stop). We keep waiting instead.
711
- const isNew = cur && normText(cur) !== normText(beforeText);
717
+ // stop). We keep waiting instead. A fresh network capture is exempt: it
718
+ // belongs to the CURRENT send even if the DOM still shows the old text.
719
+ const isNew = !!cur &&
720
+ (netFresh || normText(cur) !== normText(beforeText));
712
721
  if (isNew && cur === last) {
713
722
  stable++;
714
723
  if (stable >= 2)
@@ -721,8 +730,12 @@ export class DeepSeekBrowser {
721
730
  last = cur;
722
731
  await this.page.waitForTimeout(800);
723
732
  }
724
- if (last && normText(last) !== normText(beforeText))
733
+ if (last && (normText(last) !== normText(beforeText) || this._netCapture)) {
725
734
  return last;
735
+ }
736
+ if (this._netCapture && this._netCaptureAt >= this._lastSentAt) {
737
+ return this._netCapture;
738
+ }
726
739
  throw new Error('Новый ответ не получен (на странице остался прежний текст). ' +
727
740
  'Возможно, сообщение не отправилось.');
728
741
  }
package/dist/i18n.js CHANGED
@@ -283,6 +283,15 @@ const CATALOG = {
283
283
  en: 'IMPORTANT: reply to the operator in English. All text in the respond tool message field, and any explanations, must be in English.',
284
284
  },
285
285
  'prompt.tools_header': { ru: 'You have access to the following tools:', en: 'You have access to the following tools:' },
286
+ // The hard "ONLY TOOL CALLS" block. All prose around a tool call is a
287
+ // protocol violation: the operator never sees it (only tool calls and the
288
+ // final respond reach the terminal), so it is pure pollution. We cannot
289
+ // stop DeepSeek from generating it INSIDE its chat with code (that is the
290
+ // model's output); we can only forbid it by prompt and hide it here.
291
+ 'prompt.only_tool_calls': {
292
+ ru: '## ТОЛЬКО ВЫЗОВЫ ИНСТРУМЕНТОВ (жёсткое правило)\n\nОбщайся с оператором ТОЛЬКО через вызовы инструментов. Любой обычный текст — объяснения, планы, рассуждения, комментарии, извинения, приветствия, заголовки, списки, markdown, эмодзи — ЗАПРЕЩЁН. Он не читается и считается ошибкой.\n\nТВОЙ ЕДИНСТВЕННЫЙ ВЫВОД — вызов инструмента. В каждом ответе ровно один JSON-объект вызова (или массив независимых вызовов), без единого слова до и после.\n\nНЕЛЬЗЯ писать: «сейчас сделаю», «давай посмотрим», «проверю», планы, объяснения, итоги между шагами.\n\nМОЖНО только вызов инструмента и, в самом конце, когда задача выполнена, respond с итогом.\n\nЕдинственное место, где допускается текст, — поле message внутри respond, и только в самом конце.',
293
+ en: '## ONLY TOOL CALLS (hard rule)\n\nTalk to the operator ONLY through tool calls. Any plain text — explanations, plans, reasoning, comments, apologies, greetings, headings, lists, markdown, emoji — is FORBIDDEN. It is not read and counts as an error.\n\nYOUR ONLY OUTPUT is a tool call. Each turn contains exactly one JSON tool-call object (or an array of independent calls), with not a single word before or after.\n\nYou MUST NOT write: "I will now...", "let us look...", "let me check", plans, explanations, progress notes between steps.\n\nALLOWED: only a tool call and, at the very end, when the task is done, respond with the summary.\n\nThe only place where text is allowed is the message field inside respond, and only at the very end.',
294
+ },
286
295
  };
287
296
  export function translate(locale) {
288
297
  const loc = isLocale(locale) ? locale : DEFAULT_LOCALE;
@@ -45,7 +45,20 @@ stop. If you are still working, emit a tool call instead.
45
45
 
46
46
  So the pattern is: tool call, tool call, tool call, ..., then a single final
47
47
  respond. A bare text message without a tool call ends the task and the
48
- operator will not read it, so never use plain text.\n\nNO PROSE AROUND TOOL CALLS. Each turn must contain ONLY the JSON of the tool\ncall(s) — not a single word before or after, not even a short lead-in like\n"Let me check..." or "Now I'll fix it.". The JSON must be the entire response.\n\nWRONG: "Let me read the file first." then a Read call.\nWRONG: a Read call then "I'll analyze the result next."\nRIGHT: only the JSON of the tool call, nothing else.\n\nIf you feel the urge to explain, do not: put it in the final respond when the\ntask is done (or when you must ask the operator), not between tool calls.
48
+ operator will not read it, so never use plain text.
49
+
50
+ ${t('prompt.only_tool_calls')}
51
+
52
+ NO PROSE AROUND TOOL CALLS. Each turn must contain ONLY the JSON of the tool
53
+ call(s) — not a single word before or after, not even a short lead-in like
54
+ "Let me check..." or "Now I'll fix it.". The JSON must be the entire response.
55
+
56
+ WRONG: "Let me read the file first." then a Read call.
57
+ WRONG: a Read call then "I'll analyze the result next."
58
+ RIGHT: only the JSON of the tool call, nothing else.
59
+
60
+ If you feel the urge to explain, do not: put it in the final respond when the
61
+ task is done (or when you must ask the operator), not between tool calls.
49
62
 
50
63
  You have access to the following tools:
51
64
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.9.3",
3
+ "version": "2.10.0",
4
4
  "description": "Terminal coding agent over chat.deepseek.com via Playwright",
5
5
  "type": "module",
6
6
  "bin": {