XMLFormatParser coerces numeric XML tag values, silently dropping action inputs

Author: sxh313Created Sep 23, 2026Updated Sep 24, 2026

Description

XMLFormatParser — the format parser that handles the <seed:tool_call> / <answer> XML output style — builds its fast-xml-parser instance with only ignoreAttributes: false:

typescript
// multimodal/gui-agent/action-parser/src/FomatParsers.ts:37-41
const builder = new XMLBuilder();
const parser = new XMLParser({
  ignoreAttributes: false,
});
const object = parser.parse(text);

so fast-xml-parser's default parseTagValue: true still applies, and any tag whose text happens to look like a number comes back as a JS number instead of a string.

Everything downstream is string-typed, so a numeric tag value is either dropped without a trace or reaches a .trim() / .replace() call on a number:

  • FomatParsers.ts:63-70 — content: value as string pushes the raw value straight into finished.inputs. The cast is unsound; this is where the number enters the typed layer.
  • ActionParserHelper.standardizeActionInputsFromXMLObject() (ActionParserHelper.ts:536-571) declares Record<string, string> and branches only on typeof value === 'string' and value && typeof value === 'object', so a numeric parameter tag matches neither and is discarded silently.
  • ActionParserHelper.standardizeAction() (ActionParserHelper.ts:313 and :335, roughInputStrValue.trim()) and parseCoordinates() (:379-380, params.trim()) both declare string parameters.
  • serializeAction() (multimodal/gui-agent/shared/src/utils/actions.ts:14-44) has no numeric branch for finished, so the action serializes as finished(unsupported).
  • multimodal/gui-agent/agent-sdk/src/ToolCallEngine.ts:163,167,185-187 stores the value into finishMessage: string | null and then calls content.replace(...).

Reproduction

Measured on main (c2ad42e3) through the public entry point new DefaultActionParser().parsePrediction(...), with @gui-agent/[email protected] and [email protected] as pinned by multimodal/pnpm-lock.yaml.

Input 1:

<answer>42</answer>
actions:           {"type":"finished","inputs":{"content":42}}
rawActionStrings:  ["finished(unsupported)"]
ToolCallEngine.ts:187  ->  TypeError: content.replace is not a function

Input 2:

<answer>007</answer>
actions:           {"type":"finished","inputs":{"content":7}}   # leading zeros lost
rawActionStrings:  ["finished(unsupported)"]
ToolCallEngine.ts:187  ->  TypeError: content.replace is not a function

Input 3:

<seed:tool_call>
<function=type>
<parameter=content>123456</parameter>
</function>
</seed:tool_call>
actions:           {"type":"type","inputs":{}}   # the text to type is gone
rawActionStrings:  ["type()"]

Input 4:

<think>42</think>
<answer>done</answer>
reasoningContent:  ""        # XMLBuilder().build(42) === "" (FomatParsers.ts:59),
                             # normalized to undefined at DefaultActionParser.ts:96

Expected

A model that answers with a bare number, or emits a numeric-looking parameter such as a verification code, should keep its payload:

actions:           {"type":"finished","inputs":{"content":"42"}}
rawActionStrings:  ["finished(content='42')"]

Root cause / proposed fix

This is a single construction site. Setting parseTagValue: false on FomatParsers.ts:38-40 restores the string contract that the rest of the pipeline already declares, and no conversion is needed anywhere downstream — parseCoordinates() already runs its own Number.parseFloat on the string it receives.

Nothing pins the current coercing behaviour. Searching the whole multimodal/ tree for an XML tag whose text is a bare number (an answer, a parameter=content, a duration, or anything else) matches no existing fixture or test, and none of the 141 currently open PRs modifies anything under multimodal/gui-agent/action-parser/.

Environment

Source: bytedance/UI-TARS-desktop