Add code-style tool-call dialect (Python / TypeScript) - #16
Conversation
Render a tool catalogue as function signatures and read function calls back, for small code-trained models that have never seen P-Format. One grammar reads both spellings; calls live in the existing <tool_call> markers, are registry-gated, accept literals only, and refuse whole bodies that are not entirely calls to known tools. Co-authored-by: Medulla <medulla@tinyhumans.ai>
|
Warning Review limit reached
This review includes 22 billable files and costs up to $5.50. Or wait 28 minutes for your next included review. View limit detailsLimit details: You’ve used the included review currently available. Review configuration: ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (22)
Comment |
Tiny Sweeper reviewTiny Sweeper reviewed this change across 6 lane(s) and found 10 active actionable finding(s). Detailed lane evidence and any incomplete work are listed below. State: Ready for maintainer review Review snapshot
Completeness: Complete What changedThe review could not produce a supported behavioral summary; inspect the cited changed surface and lane details below. FeaturesNone identified with supported citations. TestsNo supported feature-to-test mapping was produced. Test execution is not inferred. Findings
Resolved this pass
Before mergeNone. How this fits togetherflowchart LR
n0["ToolCallFormat<br/>changed"]:::changed
n1["render_json_catalogue<br/>changed"]:::changed
n2["ToolDialect"]:::impacted
n3["...s_one_signature_per_line_in_binding_order"]:::impacted
n4["render_code_catalogue"]:::impacted
n5["...alogue_is_smaller_than_the_json_catalogue"]:::impacted
n6["ToolSpec"]:::impacted
n7["weather_schema"]:::impacted
n1 -->|uses| n6
n2 -->|uses| n0
n2 -->|uses| n6
n3 -->|calls| n4
n3 -->|tests| n4
n3 -->|calls| n7
n3 -->|tests| n7
n4 -->|uses| n6
n5 -->|calls| n1
n5 -->|tests| n1
n5 -->|calls| n4
n5 -->|tests| n4
n5 -->|calls| n7
n5 -->|tests| n7
n7 -->|uses| n6
classDef changed fill:#0d4429,stroke:#238636,color:#e6edf3
classDef impacted fill:#161b22,stroke:#6e7681,color:#c9d1d9
classDef flagged fill:#5a1e02,stroke:#d93f0b,color:#ffffff
classDef blocking fill:#67060c,stroke:#f85149,color:#ffffff
Agent review detailscritique
security
tests
commits
description
e2e
Evidence and run details
|
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 705677c820
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Requesting changes: 2 lane(s) blocking, worst finding is high.
Fix or reply to the findings below and push. The next review clears this automatically once they are gone — you should not need to dismiss anything by hand.
$0.1024 · 2,078,856 in / 52,262 out · 260,688 cached (13%) · ladder/vectors, gpt-5.6-luna, deepseek/deepseek-v4-flash, deepseek-v4-flash · 1,210 embedded
critique: $0.0575 · 1,140,810 in / 31,693 out · 117,157 cached (10%) · gpt-5.6-luna, deepseek/deepseek-v4-flash, deepseek-v4-flash
security: $0.0425 · 826,613 in / 18,717 out · 43,999 cached (5%) · gpt-5.6-luna
tests: $0.0007 · 43,414 in / 72 out · 39,447 cached (91%) · deepseek/deepseek-v4-flash
description: $0.0007 · 34,785 in / 268 out · 30,817 cached (89%) · deepseek/deepseek-v4-flash
The literal parser now enforces a maximum nesting depth of 64, rejects malformed numbers with leading zeros or trailing separators, and refuses f-strings containing interpolation braces. Tool names that are not valid identifiers now render as an explicit unsupported-tool comment, and invalid parameter names fall back to a generic `args: dict` signature. Empty argument objects are no longer treated as a single unpacked object, and the `$` character is accepted in bare words. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
The `one` helper in the codecall tests now binds the result of `parse_calls` to a mutable variable, allowing the test to potentially modify the parsed calls before assertions. This change aligns the helper with the mutability expectations of the test suite. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
The condition for unpacking a single object argument now also requires that the function has more than one parameter, preventing premature unpacking when only one parameter is declared. This fixes the case where a single-parameter function with a dict argument was incorrectly treated as an object to unpack. The test expectation is updated to reflect the corrected ordering of properties in the rendered signature. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
# Conflicts: # crates/tinytools-agent/src/dialect/test.rs
The identifier validation now respects the target language's syntax rules, using ASCII-only checks for both Python and TypeScript, and rejecting reserved words in TypeScript. This prevents generating invalid code signatures for properties like "$value" in Python or "class" in TypeScript, with tests covering these cases. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 369cd3dde2
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| | `PFormatDialect` | `name[0\|<a>\|1\|<b>]` signatures in the prompt's tool section | `<tool_call>name[0\|value]</tool_call>` | yes | | ||
| | `CodeDialect` | `def name(a: str, b: int = None) -> str` or `function name(a: string, b?: number): string;` signatures in the prompt's tool section | `<tool_call>name(a="value")</tool_call>` or `name({a: "value"})` | yes | | ||
|
|
||
| The code dialect exists for small, code-trained models: a function signature |
There was a problem hiding this comment.
Add the required implementation plan
This adds the accepted behavior and full implementation for the new dialect, but a repository-wide search of docs/plans/ finds no linked, implementation-ordered code-call plan. Add the required plan and link it to this specification so the design and implementation sequence remain reviewable.
AGENTS.md reference: AGENTS.md:L206-L208
Useful? React with 👍 / 👎.
| //! dialect — sees it, and a top-level ```` ```python ```` fence stays what it | ||
| //! is everywhere else in this crate: an example, not a call. | ||
|
|
||
| mod literal; |
There was a problem hiding this comment.
Document the complex codecall module
The new codecall module contains a parser, literal scanner, signature renderer, types, and more than a thousand lines of implementation and tests, but its directory has no module-level README.md. Add one covering the design, public surface, and operational constraints required for complex modules.
AGENTS.md reference: AGENTS.md:L202-L203
Useful? React with 👍 / 👎.
| .any(|param| !is_identifier(param, style)); | ||
| if has_invalid_param { | ||
| return match style { | ||
| CodeStyle::Python => format!("def {name}(args: dict) -> str"), |
There was a problem hiding this comment.
Make the synthetic args keyword bind
Fresh evidence after the earlier non-identifier fix is that the Python protocol explicitly tells models to prefer keyword arguments, so this advertised signature naturally produces read_file(args={"file-path": "x"}); bind rejects args because it is not a real schema property, and single-object unpacking is disabled when keyword arguments are present. Either make this synthetic parameter an accepted wrapper or render/instruct a positional dictionary call.
Useful? React with 👍 / 👎.
| } | ||
| self.skip_trivia(); | ||
| match self.peek().ok_or(Refuse)? { | ||
| '"' | '\'' | '`' => self.string(false).map(Literal::Str), |
There was a problem hiding this comment.
Reject interpolation in backtick templates
When a TypeScript model emits read_file(path=${root}/data), this direct backtick branch decodes the template as the literal string ${root}/data and dispatches it without evaluating interpolation. The f-string guard only runs through word(), so backtick templates containing ${...} must also be refused to avoid silently corrupted arguments.
Useful? React with 👍 / 👎.
| fn literal(value: &Value) -> String { | ||
| match value { | ||
| Value::String(s) => Value::String(s.clone()).to_string(), | ||
| other => other.to_string(), |
There was a problem hiding this comment.
Render Python enum literals with Python spellings
For a valid schema such as {"enum":[true,null]} or a boolean/null const, this shared JSON serialization makes the Python catalogue emit Literal[true, null]; Python requires True and None, so the advertised signature is invalid and encourages the model to use the wrong language spelling. Render these scalar literals according to CodeStyle.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
The previously-blocking findings are resolved. Clearing the changes request.
$0.0478 · 961,346 in / 43,983 out · 183,312 cached (19%) · ladder/vectors, gpt-5.6-luna, deepseek-v4-flash, deepseek/deepseek-v4-flash · 1,223 embedded
critique: $0.0240 · 443,760 in / 25,768 out · 48,384 cached (11%) · gpt-5.6-luna, deepseek-v4-flash, deepseek/deepseek-v4-flash
security: $0.0209 · 395,374 in / 15,648 out · 28,572 cached (7%) · gpt-5.6-luna
tests: $0.0012 · 46,875 in / 1,230 out · 40,924 cached (87%) · deepseek/deepseek-v4-flash
description: $0.0007 · 38,358 in / 110 out · 32,418 cached (85%) · deepseek/deepseek-v4-flash
| let mut end = usize::from(matches!(bytes.first(), Some(b'+' | b'-'))); | ||
| let integer_start = end; | ||
| consume_digits(bytes, &mut end)?; | ||
| if bytes.get(integer_start) == Some(&b'0') |
There was a problem hiding this comment.
Reject leading-zero numbers with separators
The leading-zero check only rejects a 0 followed directly by another digit. Inputs such as 0_1 and 0_1.5 therefore pass consume_digits, have their underscores removed, and are parsed as 1 and 1.5. These are malformed numeric spellings in the supported Python/JavaScript literal grammar and silently changing them to different values can invoke a tool with unintended arguments. Reject a zero followed by either a digit or a numeric separator before continuing the parse.
[RULE] reject-malformed-numeric-spellings ·
| } | ||
|
|
||
| /// A JSON value spelled as a type-level literal (`"a"`, `1`, `true`). | ||
| fn literal(value: &Value) -> String { |
There was a problem hiding this comment.
Render Python literals with Python spellings
This helper is used for both Python and TypeScript signatures, but other.to_string() uses JSON spellings. A Python schema such as {"const": true} or {"enum": [null, false]} therefore renders Literal[true], Literal[null], or Literal[false], which is not valid Python syntax. Render booleans and null as True/False/None for CodeStyle::Python while retaining JSON spellings for TypeScript.
[RULE] source-language-literal ·
| && !variants.is_empty() | ||
| { | ||
| let mut seen = Vec::new(); | ||
| for variant in variants { |
There was a problem hiding this comment.
Bound recursion through schema combinators
The advertised MAX_DEPTH limit is not applied to nested anyOf or oneOf branches: each recursive call receives the unchanged depth. A schema containing deeply nested combinators can consequently recurse until stack overflow instead of collapsing to the fallback type. Increment or otherwise check the depth before descending through these branches, as is done for arrays and objects.
[RULE] bounded-recursion ·
| self.bump(); | ||
| return Ok(out); | ||
| } | ||
| let c = self.bump().ok_or(Refuse)?; |
There was a problem hiding this comment.
Handle escaped quotes in raw strings
In a raw string, a backslash before the matching quote escapes that quote in Python syntax and the quote must remain part of the string. This branch appends the backslash but then the next loop iteration treats the quote as the terminator, so an input such as r"a\\\"b" is truncated and the remaining text makes the literal fail. Consume and append the escaped quote when raw is true, while still rejecting a raw string whose final backslash escapes its closing quote.
[RULE] raw-string-escaping ·
| let rest = self.rest(); | ||
| let mut end = 0; | ||
| for (idx, c) in rest.char_indices() { | ||
| let ok = if idx == 0 { |
There was a problem hiding this comment.
Handle dollar-prefixed identifiers consistently
This scanner accepts $ at the start and inside every identifier, including Python calls and bare dictionary keys. For example, tool($value=1) or tool({$value: 1}) is accepted even though $value is not a valid Python identifier; if a tool exposes such a property, malformed Python text can be bound and dispatched. Since Cursor does not receive the CodeStyle, either pass the style through and apply language-specific identifier rules or reject $ in the shared grammar if Python compatibility is the required contract.
[RULE] language-specific-identifier-validation ·
| self.bump(); | ||
| return Ok(out); | ||
| } | ||
| let c = self.bump().ok_or(Refuse)?; |
There was a problem hiding this comment.
Reject unescaped newlines in quoted strings
For non-triple single- or double-quoted Python/JavaScript strings, an unescaped newline is not valid syntax. string currently appends \n like any other character, so malformed input such as a quoted value split across two physical lines is accepted as a string literal instead of being refused. Reject c == '\n' (and the relevant carriage-return case) here unless the string is triple-quoted or the newline was consumed by escape as a line continuation.
[RULE] reject-invalid-string-literals ·
| } else { | ||
| (self.rest().get(..4).ok_or(Refuse)?, 4) | ||
| }; | ||
| if let Some(decoded) = u32::from_str_radix(hex, 16).ok().and_then(char::from_u32) { |
There was a problem hiding this comment.
Reject malformed Unicode escapes
The invalid-escape branch preserves only \\u and leaves the malformed digits unread, so inputs such as "\\u12" or "\\u{not-hex}" are accepted as literal text. These are malformed Unicode escapes in the supported source languages, not unknown escapes; refusing them avoids silently changing invalid source into a different argument value. Apply the same strictness to malformed \\x escapes if that is the intended grammar contract.
[RULE] reject-malformed-string-escapes ·
| | "with" | ||
| | "yield" | ||
| ), | ||
| CodeStyle::TypeScript => !matches!( |
There was a problem hiding this comment.
Reject all TypeScript reserved tool names
The TypeScript validation list omits reserved words such as interface, implements, let, package, private, protected, public, and static. A tool named interface therefore passes is_identifier and renders as function interface(...): ...;, which is not a valid TypeScript declaration and makes the generated catalogue unusable for that tool. Extend the reserved-word check (or use a complete language-appropriate identifier validator) before emitting the signature.
Additional critique observation
Reject all reserved TypeScript tool names
[RULE] source-identifier-validation
The TypeScript name check omits reserved words such as interface, implements, private, and public. A tool named interface therefore passes validation and produces function interface(): string;, which is not a valid TypeScript declaration. Expand the reserved-word check (including contextual TypeScript keywords that cannot be used here) or quote/reject such names before emitting the signature.
[RULE] invalid-identifier ·
Summary
Adds a fourth text dialect,
CodeDialect, for small code-trained models: the catalogue is rendered as function signatures and the model calls tools as code.One grammar (
src/codecall/) reads both the Python and the TypeScript spelling (read_file({path: "src/main.rs"})). It hooks intotagged.rs::decode_bodyafter P-Format and before the JSON paths, so streaming and every dialect inherit it. It reusesPFormatRegistryfor positional binding and type coercion, so the catalogue order and the binder order come from the samefrom_schemacall.Bounds kept: a call still needs a
<tool_call>/```tool_callmarker (a top-level```pythonfence stays an example); literals only (a bare identifier is a variable → refused); unknown tool / unknown keyword / duplicate / positional overflow → refused, never guessed; all-or-nothing per tag body, so prose mentioningread_file(...)never binds arguments.Measured against
qwen3:8bvia Ollama with a 10-tool / 20-task fixture (OpenHumantool-dialect-bench): python 517 prompt tokens vs xml 703 vs pformat 795, with exact-argument accuracy 90% vs 80% (xml) vs 60% (pformat) and identical call recovery (95%).Draft until the consuming PRs are up: tinyhumansai/tinyagents (
ToolDispatcher::{Python, Typescript}) and tinyhumansai/openhuman (agent.tool_dispatcher = "python" | "typescript"). Merge order: this → tinyagents → openhuman.Related issue
None.
API or behavior changes
codecall(CodeStyle,parse_calls,render_code_signature,render_code_type),dialect::CodeDialect,render::{render_code_catalogue, code_instructions}; re-exportedparse_code_calls.CallSource::Code(enum is#[non_exhaustive], additive).dialect::ToolCallFormatgainsPythonandTypeScript(it is not#[non_exhaustive]).pformat::coerce_valueis nowpub(crate).<tool_call>body that is a code call to a registered tool now parses (previouslyMalformed). Bodies that are P-Format / JSON / Kimi / GLM are unchanged (they never start withNAME().Validation
cargo fmt --all -- --checkcargo clippy --all-targets --all-features -- -D warningscargo build --all-targets --all-featurescargo test --all-features— 289 passed intinytools-agentRUSTDOCFLAGS="-D warnings" cargo doc --no-deps --all-featuresTests
src/codecall/test.rs(45): keyword / positional / mixed binding, every literal kind incl. delimiters inside strings, escapes, triple quotes, raw and template strings, nested lists/dicts, trailing commas, comments,;and newline separators, prefix stripping, the three branches of the single-object rule, prose-in-body refusal, unknown-name / unknown-keyword / duplicate / overflow refusal, coercion, both signature renderers, enum/array/union/nested/depth-capped types.parse/test/tagged.rs(+9): hook ordering — Code / P-Format / JSON / GLM siblings each keep their ownCallSource; fence-in-tag unwrapped; top-level```pythonis not a call; registry required.stream/test.rs(+1): a call split mid-string across three deltas is released once and never shown.dialect/test.rs(+7): both spellings parse, fallback per tag, protocol block pinned, catalogue snapshots for both styles (asserting optional params are alphabetical), signature-order == binding-order, code catalogue < JSON catalogue and code block < half of the P-Format block.Deliberately untested: a string literal containing
</tool_call>truncates the body — the same inherited limit JSON bodies have.Documentation
crates/tinytools-agent/README.md(module and grammar tables),docs/specs/agent-tool-protocols.md(new "Text dialects" section, compatibility note), module docs on every new file.Checklist
#[allow(...)],#[ignore], or relaxed lints.envcontents in the diff or the descriptionCo-authored-by: Medulla medulla@tinyhumans.ai