Skip to content

fix(cursor): map taskToolCall so subagent dispatches are named - #13

Merged
dorrfrog merged 2 commits into
jfrog:mainfrom
talma:fix/cursor-task-tool-call
Jul 30, 2026
Merged

dorrfrog merged 2 commits into
jfrog:mainfrom
talma:fix/cursor-task-tool-call

Conversation

@talma

@talma talma commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Problem

Cursor reports a subagent dispatch as:

{"tool_call": {"taskToolCall": {"args": {"description": "...", "prompt": "..."}}}}

taskToolCall is absent from _CURSOR_TOOL_TYPE_MAP, so _extract_cursor_tool() falls through every branch and returns ("", {}) — even though the payload carries both description and prompt. The data was always there; only the name lookup was missing.

Impact

The empty ToolCall.name breaks three things:

1. Deterministic rule scorers cannot observe the dispatch. tools_invoked, tools_invoked_in_order, tool_args_contain and tool_result_contains all key on tc.name in scorer/rules/trajectory.py. A scenario asserting delegation fails as though the agent never delegated — a false negative, the worst direction for a test. tool_name_in_cli() does not rescue it, since the raw stream contains taskToolCall, not the tool name.

Measured against a real recorded payload:

Assertion Before After
tools_invoked: [Task] ✗ False ✓ True
tool_args_contain: {Task: …} ✗ "tool Task not invoked" ✓ matches
skill_invoked(implementation-planner) (False, '') (True, 'via Task tool args')

2. LLM judges are shown [TOOL CALL: ] with Input: {} (scorer/llm/scorer.py:145) and are left to guess, which makes any delegation criterion nondeterministic. Observed downstream: identical agent behaviour, opposite verdicts across runs — "the tool sequence ends with an empty call consistent with a Task dispatch" (pass) vs "the tool calls are all Read/Glob/Grep/Shell … no Task dispatch is present" (fail). The dispatch had in fact occurred; the planner sub-transcript was present under tool_calls[N].result.success.conversationSteps.

3. Live progress silently drops it. parse_stream_event() does if not name: return None, so the single most significant event in a subagent flow renders as nothing.

The practical cost downstream is that delegation can only be asserted through prose the agent happens to emit, never through what it actually did.

Fix

One map entry. Named Task to match Claude Code's name for the same primitive, so scenarios can assert on it uniformly across agents.

Tests

  • test_parses_cursor_task_tool_call — the dispatch surfaces as Task with args intact, mirroring the existing readToolCall / shellToolCall / grepToolCall cases.
  • test_unknown_tool_call_subtype_stays_unnamed — regression guard that unmapped subtype keys still return ("", {}).

Verification

  • uv run make check3212 passed, 45 skipped, lint clean.
  • uv run belt agent info cursor — CLI healthy (per CONTRIBUTING's "unit tests can pass while the CLI is broken").
  • No lockfile or doc-parity churn; diff is +37 lines across 2 files.

Cursor reports a subagent dispatch as:

    {"tool_call": {"taskToolCall": {"args": {"description": ..., "prompt": ...}}}}

`taskToolCall` was absent from `_CURSOR_TOOL_TYPE_MAP`, so
`_extract_cursor_tool()` fell through every branch and returned `("", {})`
even though the payload carries both `description` and `prompt`.

Three consequences of the empty name:

1. Deterministic rule scorers cannot observe the dispatch. `tools_invoked`,
   `tools_invoked_in_order`, `tool_args_contain` and `tool_result_contains`
   all key on `tc.name`, so a scenario asserting a dispatch fails as though
   the agent never delegated -- a false negative. `tool_name_in_cli()` does
   not rescue it either, since the raw stream contains `taskToolCall` rather
   than the tool name.

2. LLM judges are shown `[TOOL CALL: ]` with `Input: {}` and are left to
   guess. Observed in a downstream repo: identical agent behaviour, opposite
   verdicts across runs -- "the tool sequence ends with an empty call
   consistent with a Task dispatch" (pass) versus "the tool calls are all
   Read/Glob/Grep/Shell ... no Task dispatch is present" (fail). The dispatch
   had in fact occurred; the planner sub-transcript was present under
   `tool_calls[N].result.success.conversationSteps`.

3. `parse_stream_event()` returns None on an empty name, so the single most
   significant event in a subagent flow is dropped from live progress output.

Name it `Task` to match Claude Code's name for the same primitive, so
scenarios can assert on it uniformly across agents.

Tests: assert the dispatch surfaces as `Task` with args intact, plus a
regression test that unmapped subtype keys still return `("", {})`.
@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@talma

talma commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator Author

recheck

@talma

talma commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator Author

I have read the CLA Document and I hereby sign the CLA

1 similar comment
@talma

talma commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator Author

I have read the CLA Document and I hereby sign the CLA

@talma

talma commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator Author

recheck

@dorrfrog dorrfrog added the improvement Automatically generated release notes label Jul 30, 2026
@github-actions

Copy link
Copy Markdown

👍 Frogbot scanned this pull request and did not find any new security issues.


@dorrfrog
dorrfrog merged commit 90bd105 into jfrog:main Jul 30, 2026
10 checks passed

This branch was successfully deployed

1 active deployment
frogbot 6f13218a Deployed Jul 30, 2026 by dorrfrog via scan-pull-request #33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

improvement Automatically generated release notes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants