Skip to content

feat: flattened tool call schema#756

Open
mykhailobuleshnyi wants to merge 3 commits into
mainfrom
feat/flatten-tool-calling-schema
Open

feat: flattened tool call schema#756
mykhailobuleshnyi wants to merge 3 commits into
mainfrom
feat/flatten-tool-calling-schema

Conversation

@mykhailobuleshnyi
Copy link
Copy Markdown
Contributor

@mykhailobuleshnyi mykhailobuleshnyi commented May 28, 2026

Note

Medium Risk
Changes the FC wire/schema contract and streaming parser for all tool calls; legacy action_input is shimmed but models and clients must align with flat args.

Overview
Function-calling tool arguments are flattened: thought is a top-level field alongside each tool’s real parameters, replacing the nested { thought, action_input } wrapper. Schemas from generate_function_calling_schemas put thought first, drop action_input, and update ReAct FC instructions (including top-level delegate_final).

Parsing and execution use ToolCallArguments with extra="allow" and to_action_input(); parse_as_tool_call still normalizes the legacy wrapper shape for replay/back-compat. Tool-only calls with no other params yield an empty input dict instead of a hard error.

Streaming in FC mode adds JSONInnerThoughtsExtractor to split streaming argument JSON into REASONING (thought) vs TOOL_INPUT (params without thought), with held-buffer flush when thought is missing or late. provide_final_answer still uses the existing JSON field parser.

Tests cover the extractor, flat schemas, streaming behavior, and updated FC recovery (malformed JSON vs missing action_input).

Reviewed by Cursor Bugbot for commit 4382ecd. Bugbot is set up for automated code reviews on this repo. Configure here.

@mykhailobuleshnyi mykhailobuleshnyi requested a review from a team as a code owner May 28, 2026 17:57
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit dfb36c2. Configure here.

# Inside nested value — passthrough.
if self.is_inner_thoughts_value and self.depth == 1:
return "", self._emit_thought('"')
return self._emit_main('"'), ""
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unreachable condition in _handle_quote nested thought routing

Medium Severity

The condition if self.is_inner_thoughts_value and self.depth == 1 on line 149 is inside a block guarded by if self.depth >= 2 on line 147. Since depth >= 2 and depth == 1 are mutually exclusive, the inner branch can never execute — it's dead code. This means quotes inside nested thought values (if thought were ever an object or array) would be incorrectly routed to _emit_main instead of _emit_thought. Other methods like _handle_open_object, _handle_close_object, and _consume_value_char correctly handle nested thought routing, but _handle_quote does not — both the opening quote path (line 149) and the closing quote path (line 170-171) unconditionally route to main at depth >= 2.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit dfb36c2. Configure here.

self.main_buffer = stripped[:-1]
self.main_buffer += "}"
self.state = "end"
return "}", ""
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Streaming deltas contain irrevocable dangling comma when thought is last

Medium Severity

When wait_for_first_key=False (the default) and thought is the last field, _handle_close_object retroactively strips a trailing comma from main_buffer but returns "}" as the delta. The comma was already returned as a delta by _handle_comma in a previous call and cannot be revoked. Concatenating all deltas from process_fragment produces invalid JSON like {"a":"x",}, even though main_buffer is correctly patched to {"a":"x"}. The cumulative buffer and the sum of streaming deltas are inconsistent.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit dfb36c2. Configure here.

inner = json.loads(inner, strict=False)
except json.JSONDecodeError:
inner = {}
args = {"thought": args.get("thought", ""), **(inner if isinstance(inner, dict) else {})}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Legacy shim silently discards non-dict action_input values

Low Severity

The backward-compatibility shim in parse_as_tool_call silently converts non-dict action_input values to empty params. When action_input is a JSON string that decodes to a non-dict (e.g., a list or scalar), or is invalid JSON, inner becomes {} and the original parameters are lost. The old code would have raised a recoverable ActionParsingException, prompting the LLM to retry, but the new shim silently proceeds with an empty tool call — a change from "error + retry" to "silent data loss."

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit dfb36c2. Configure here.

@github-actions
Copy link
Copy Markdown

Coverage

Coverage Report •
FileStmtsMissCoverMissing
dynamiq/callbacks
   inner_thoughts_extractor.py1681292%41, 71–72, 94, 150, 167, 185, 194, 200, 208, 215, 230
   streaming.py6768088%23, 190, 203–204, 212–213, 229–230, 268–269, 277, 304–310, 314, 322–327, 335, 343–344, 399, 403, 407, 450, 453, 461–462, 528, 592, 596, 649, 689–692, 720–721, 804–807, 920, 922, 948, 974–975, 1061, 1088, 1101, 1127–1128, 1130, 1163, 1187–1192, 1228, 1232, 1248, 1250–1258, 1263
dynamiq/nodes/agents
   agent.py88414383%105–106, 110–111, 357–360, 365, 381–382, 400–401, 423, 430–431, 438–439, 445–446, 448, 507–509, 719–721, 736, 745, 771, 787, 806, 818–821, 823, 843–846, 848, 863, 875, 880, 897–898, 908, 911, 921, 961–963, 969–971, 1080, 1085, 1136, 1170, 1172, 1187–1188, 1195, 1198, 1225–1226, 1242, 1281, 1308–1312, 1379, 1385, 1389, 1392, 1426, 1454, 1457, 1462, 1478, 1540–1541, 1576, 1581, 1605, 1661, 1683–1685, 1688–1689, 1699, 1702, 1712, 1719, 1769–1772, 1792–1793, 1797, 1799–1801, 1803, 1805, 1838, 1846–1847, 1849, 1852, 1855, 1906–1909, 1925, 1934, 1999, 2005–2011, 2017, 2019–2028, 2030–2031
dynamiq/nodes/agents/components
   schema_generator.py1884178%61, 64, 142, 252, 256, 261–266, 269–271, 276, 280–281, 283–291, 293, 308, 310, 321–322, 331, 336, 338, 341, 345, 379–380, 382, 387–388
TOTAL32728950870% 

Tests Skipped Failures Errors Time
2411 2 💤 0 ❌ 0 🔥 2m 48s ⏱️

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant