zames_pro 2.92.0 → 2.94.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.94.0] - 2026-11-02
11
+
12
+ ### Added
13
+
14
+ - **dev:** dump unrecognized tool calls and hot-reload the parser
15
+
16
+ ## [2.93.0] - 2026-11-02
17
+
18
+ ### Fixed
19
+
20
+ - **deepseek:** recognize every tool-call shape the chat returns
21
+
10
22
  ## [2.92.0] - 2026-11-01
11
23
 
12
24
  ### Changed
@@ -13,7 +13,7 @@ import { shouldRunDiagnostics, runDiagnostics, } from './diagnostics.js';
13
13
  // The tools that WRITE/EDIT a file (and take a `path`). The completion nudge
14
14
  // only cares about these — `Bash`/`Git*` in MUTATING_TOOLS are not edits.
15
15
  const EDIT_TOOLS = new Set(['Write', 'Edit', 'MultiEdit', 'ApplyPatch']);
16
- 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 = DEFAULT_LOCALE, askDeadlineMs = 240_000, maxAfterToolRetries = 6, infiniteRetries = true, maxRetryWaitMs = 30 * 60_000, onAutoCompact = null, onContextLimit = null, onProviderExhausted = null, autoCompactPct = 95, contextLimit = 1_000_000, getTokenUsage = null, hooks = undefined, permissions = undefined, diagnostics = null, onAskPermission = undefined, selfImprovement = false, onSubagent = null, onAskUser = null, }) {
16
+ 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, devDump = false, locale = DEFAULT_LOCALE, askDeadlineMs = 240_000, maxAfterToolRetries = 6, infiniteRetries = true, maxRetryWaitMs = 30 * 60_000, onAutoCompact = null, onContextLimit = null, onProviderExhausted = null, autoCompactPct = 95, contextLimit = 1_000_000, getTokenUsage = null, hooks = undefined, permissions = undefined, diagnostics = null, onAskPermission = undefined, selfImprovement = false, onSubagent = null, onAskUser = null, }) {
17
17
  // Resolve the hook config ONCE per task: a read per tool call would be
18
18
  // wasteful, and a mid-task edit of hooks.json is not something to chase.
19
19
  // `undefined` means "read .zames/hooks.json"; an explicit null disables
@@ -513,6 +513,14 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
513
513
  // agent stalls even though the model tried to call a tool.
514
514
  const looksLikeToolCall = responseLooksLikeToolCall(rawResponse);
515
515
  if (looksLikeToolCall && malformedRetries < MAX_MALFORMED_RETRIES) {
516
+ if (devDump) {
517
+ const dumped = await dumpUnparsedCall(rawResponse, {
518
+ provider: providerLabel,
519
+ where: 'loop:unparsed',
520
+ });
521
+ if (dumped)
522
+ safeWarning('[dev] unparsed call saved: ' + dumped);
523
+ }
516
524
  malformedRetries++;
517
525
  unparsedRetries++;
518
526
  transcript?.log('malformed_toolcall', {
@@ -1344,5 +1352,62 @@ ${diag}`;
1344
1352
  }
1345
1353
  // The pure helpers (tool-call parser + answer heuristics) live in
1346
1354
  // answer-parse.ts and are imported here for runAgentLoop. They are
1347
- // NOT re-exported: importers get them from answer-parse.js directly .
1348
- import { parseToolCall, responseLooksLikeToolCall, truncateToolResult, normForStale, looksLikeUnfinishedWork, isMeaningfulRespond, extractPreToolText, } from './answer-parse.js';
1355
+ // NOT re-exported: importers get them from answer-parse.js directly.
1356
+ // parseToolCall and responseLooksLikeToolCall live in let bindings:
1357
+ // their modules (answer-parse.ts, xml-toolcall.ts) are NOT hot-reloadable,
1358
+ // so an edit to the parser was reported by the dev watcher and then
1359
+ // silently ignored for the whole run. /reload swaps them through
1360
+ // _setParserBindings().
1361
+ import { truncateToolResult, normForStale, looksLikeUnfinishedWork, isMeaningfulRespond, extractPreToolText, } from './answer-parse.js';
1362
+ import * as answerParse from './answer-parse.js';
1363
+ let parseToolCall = answerParse.parseToolCall;
1364
+ let responseLooksLikeToolCall = answerParse.responseLooksLikeToolCall;
1365
+ export function _setParserBindings(m) {
1366
+ parseToolCall = m.parseToolCall;
1367
+ responseLooksLikeToolCall = m.responseLooksLikeToolCall;
1368
+ }
1369
+ let unparsedDumps = 0;
1370
+ const UNPARSED_DUMP_MAX = 20;
1371
+ const UNPARSED_DUMP_BYTES = 64 * 1024;
1372
+ /** Dev mode: save an unrecognized (but call-looking) answer for analysis. */
1373
+ export async function dumpUnparsedCall(raw, meta) {
1374
+ if (unparsedDumps >= UNPARSED_DUMP_MAX)
1375
+ return '';
1376
+ const text = String(raw || '');
1377
+ if (!text.trim())
1378
+ return '';
1379
+ const fs = await import('node:fs/promises');
1380
+ const p = await import('node:path');
1381
+ const dir = p.join(meta.workdir || process.cwd(), '.zames', 'unparsed');
1382
+ try {
1383
+ await fs.mkdir(dir, { recursive: true });
1384
+ unparsedDumps++;
1385
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
1386
+ const name = stamp + '-' + String(meta.where || 'x').replace(/[^a-z0-9_-]+/gi, '_');
1387
+ const file = p.join(dir, name + '.txt');
1388
+ const NL = String.fromCharCode(10);
1389
+ const head = '# unparsed tool-call candidate' +
1390
+ NL +
1391
+ 'when: ' +
1392
+ new Date().toISOString() +
1393
+ NL +
1394
+ 'provider: ' +
1395
+ (meta.provider || '-') +
1396
+ NL +
1397
+ 'where: ' +
1398
+ meta.where +
1399
+ NL +
1400
+ 'length: ' +
1401
+ text.length +
1402
+ NL +
1403
+ NL +
1404
+ '--- raw answer ---' +
1405
+ NL +
1406
+ NL;
1407
+ await fs.writeFile(file, head + text.slice(0, UNPARSED_DUMP_BYTES), 'utf-8');
1408
+ return file;
1409
+ }
1410
+ catch {
1411
+ return '';
1412
+ }
1413
+ }
@@ -71,7 +71,12 @@ export function responseLooksLikeToolCall(rawResponse) {
71
71
  // colons like "path: ...".
72
72
  (/^\s*[\[\{]/.test(raw) &&
73
73
  /["']?(?:tool|args|command|path|old_string|content|content_base64)["']?\s*:/.test(raw)) ||
74
- /<\s*\|?\s*(DSML|invoke|parameter)/i.test(raw) ||
74
+ /<\s*\|?\s*(DSML|invoke|parameter|tool_use|tool_call|function_call|antml)/i.test(raw) ||
75
+ // OpenAI-style call wrappers: {"function_call": {...}},
76
+ // {"tool_calls": [...]}, {"name": "Bash", "arguments": "..."}. A real
77
+ // shape from the live chat — without these markers the answer was taken
78
+ // as prose and the agent silently stopped instead of calling the tool.
79
+ /["'](?:function_call|tool_calls?|arguments|parameters)["']\s*:/.test(raw) ||
75
80
  /^\s*\[?\s*\{[^}]*$/.test(raw.trim()));
76
81
  }
77
82
  // A respond message is only a real FINAL answer when it carries some meaning.
package/dist/index.js CHANGED
@@ -369,6 +369,19 @@ async function reloadModules() {
369
369
  mod.attach = loaded.get('attach-refs');
370
370
  if (pick('run-task', 'runTask'))
371
371
  mod.runTask = pick('run-task', 'runTask');
372
+ // answer-parse.ts and xml-toolcall.ts are NOT hot-reloadable: the loop
373
+ // keeps the parser functions in LIVE bindings, so /reload must push the
374
+ // freshly imported copy through it (a parser fix must not need a restart).
375
+ try {
376
+ const ap = (await import('./answer-parse.js'));
377
+ const loopMod = loaded.get('agent-loop');
378
+ const setter = loopMod ? loopMod['_setParserBindings'] : undefined;
379
+ if (typeof setter === 'function')
380
+ setter(ap);
381
+ }
382
+ catch (e) {
383
+ errors.push('answer-parse: ' + e.message);
384
+ }
372
385
  return { count: loaded.size, errors };
373
386
  }
374
387
  // Initial load so that mod.buildSystemPrompt and the rest are populated.
@@ -1016,6 +1029,7 @@ function runTaskDeps() {
1016
1029
  config,
1017
1030
  runAgentLoop: mod.runAgentLoop,
1018
1031
  createSpinner: mod.createSpinner,
1032
+ devDump: devMode,
1019
1033
  onAssistantMessage: (msg) => {
1020
1034
  lastAssistantMessage = msg;
1021
1035
  },
package/dist/run-task.js CHANGED
@@ -403,6 +403,7 @@ export async function runTask(deps, browser, tools, taskText, workdir, opts, att
403
403
  },
404
404
  onChatReady,
405
405
  debugLog: debug,
406
+ devDump: deps.devDump === true,
406
407
  locale,
407
408
  askDeadlineMs: providerAskDeadlineMs(config),
408
409
  maxAfterToolRetries: config.browser.maxAfterToolRetries,
@@ -252,5 +252,46 @@ export function parseXmlToolCalls(text) {
252
252
  // name in such an answer, so the call can't be restored — but it also must
253
253
  // not be silently taken as the final answer: agent-loop will ask to retry
254
254
  // (looksLikeToolCall triggers on the word parameter/DSML).
255
- return null;
255
+ //
256
+ // ONE exception is restorable: the tool name itself arrives as a parameter
257
+ // (a `name="tool"` parameter next to `name="args"`). A live answer used
258
+ // exactly this shape; before, it was dropped and the agent stalled after a
259
+ // call that never ran.
260
+ return parseBareDsmlCall(text);
261
+ }
262
+ // A call that arrived as a bare list of parameter tags with NO invoke wrapper:
263
+ // the tool name is the `tool` (or `name`) parameter and the arguments are the
264
+ // `args` parameter (JSON) or the remaining ones.
265
+ function parseBareDsmlCall(text) {
266
+ if (!/<[^>]*?parameter/i.test(text))
267
+ return null;
268
+ const args = parseParameters(text);
269
+ const toolRaw = args['tool'] ?? args['tool_name'] ?? args['name'];
270
+ if (typeof toolRaw !== 'string' || !toolRaw.trim())
271
+ return null;
272
+ const tool = toolRaw.trim();
273
+ let finalArgs = {};
274
+ if ('args' in args) {
275
+ const a = args['args'];
276
+ if (a && typeof a === 'object' && !Array.isArray(a)) {
277
+ finalArgs = a;
278
+ }
279
+ else if (typeof a === 'string') {
280
+ try {
281
+ const parsed = JSON.parse(a);
282
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
283
+ finalArgs = parsed;
284
+ }
285
+ }
286
+ catch { }
287
+ }
288
+ }
289
+ for (const k of Object.keys(args)) {
290
+ if (k === 'tool' || k === 'tool_name' || k === 'name' || k === 'args') {
291
+ continue;
292
+ }
293
+ if (!(k in finalArgs))
294
+ finalArgs[k] = args[k];
295
+ }
296
+ return { tool, args: finalArgs };
256
297
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.92.0",
3
+ "version": "2.94.0",
4
4
  "description": "Terminal coding agent that drives a real browser to chat with DeepSeek, Qwen or GLM — no API key required. Reads and edits files, runs commands, commits to git, like Claude Code / Codex CLI.",
5
5
  "type": "module",
6
6
  "bin": {