Skip to content

Add harness-level max tool-call and duplicate-suppression controls #61

Description

@justrach

Summary

When embedding graff --json in a product UI, prompt-only tool budgets are not enough. The harness should provide hard tool-call policy controls so applications can prevent duplicate searches/reads, cap runaway loops, and surface a deterministic reason when a tool call is rejected.

This came up while integrating CodeGraff into Lawplain's sandboxed /ask flow.

Related Lawplain issue:

Related broader CodeGraff JSON/embedded UI issue:

Why this is a harness issue

Different models behave very differently with the same system prompt:

Model Behavior observed
mimo-v2.5-pro relatively good, but still repeated one search and one judgment read
glm-5.2 manageable, but repeated some searches/reads
deepseek-v4-pro over-verified and repeated detail reads
minimax-m3 repeatedly read the same judgment and leaked <think> reasoning
kimi-k2.6 very slow and expensive, repeated searches/details

The system prompt included strict instructions:

  • hard limit of 6 tool calls
  • fast path: one search + one detail fetch for broad doctrine questions
  • never repeat the same URL/citation
  • stop once a usable result is found

But those are soft constraints. The model can still emit repeated tool calls, and the harness currently executes them.

For a CLI, this is annoying. For an embedded web app, it becomes:

  • duplicated UI chips
  • high latency
  • higher cost
  • unpredictable completion time
  • worse user trust

The model is responsible for proposing bad calls, but the harness is the only layer that can reliably enforce execution policy.

Concrete example

Question:

What must a plaintiff prove in a defamation claim?

Observed duplicate calls in Lawplain testing:

search: defamation ×2 or ×3
/v1/judgments/2019_SGHC_27 ×3
/v1/judgments/2020_SGHC_278 ×9 in one MiniMax run

Each repeat caused another model turn and grew context/cost.

Requested feature

Add harness-level tool call controls, ideally available in both CLI flags and SDK options.

Possible API:

runAgent({
  model: "mimo-v2.5-pro",
  maxToolCalls: 6,
  dedupeToolCalls: true,
  toolCallKey: "normalized", // or callback in SDK
  onDuplicateToolCall: "reject", // "reject" | "warn" | "allow"
})

CLI equivalent:

graff --json --model mimo-v2.5-pro --max-tool-calls 6 --dedupe-tool-calls

Desired behavior

Max tool calls

When the limit is reached, the harness should not execute further tools. It should return a structured result to the model, e.g.:

{
  "type": "tool_result",
  "name": "bash",
  "error": true,
  "content": "Tool-call budget exhausted: 6/6. Write the best answer from gathered sources."
}

or emit an explicit policy event:

{
  "type": "tool_rejected",
  "reason": "max_tool_calls",
  "limit": 6,
  "tool_call_count": 6
}

Duplicate suppression

For repeated normalized tool inputs, the harness should either:

  1. reject the duplicate with a structured message, or
  2. return the cached previous tool result.

For example:

{
  "type": "tool_rejected",
  "reason": "duplicate_tool_call",
  "duplicate_of": 3,
  "message": "Duplicate tool call suppressed; use the previous result."
}

Normalization suggestions

For the built-in bash tool, exact command matching is too brittle. A useful first pass could normalize common URL/curl calls:

  • strip shell quoting noise
  • extract URL path + query
  • sort query params
  • normalize repeated whitespace
  • ignore harmless output formatting differences like | jq ...

Even a simpler initial implementation — exact JSON input hash — would still be useful.

Acceptance criteria

  • CLI supports a hard max tool-call count.
  • SDK supports a hard max tool-call count.
  • Optional duplicate suppression is available.
  • Rejected calls are visible in JSON mode as structured events/results.
  • Model receives enough information to stop and answer from existing context.
  • The final turn still emits normally after budget exhaustion if the model answers.

Why not solve this only in the app?

Lawplain can visually group duplicate chips, but it cannot reliably prevent the cost/latency because graff owns tool execution internally. Application-level UI grouping is a presentation workaround, not a control plane.

A harness-level tool policy would benefit any embedded/product use of CodeGraff, especially remote/sandboxed deployments.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions