Skip to content

70 high single argument arity

Part VII: Ecosystem, Runtime & Agent-Specific | Challenge §70

70. Single-Argument Arity Forcing Agent Loop Overhead

Source: FP

Severity: High | Frequency: Common | Detectability: Easy | Token Spend: Medium | Time: Medium | Context: Low

The Problem

Agents assume that commands operating on resources follow UNIX convention: rm, cp, mv, and similar tools accept one or more positional arguments. When a CLI command like delete, move, or tag only accepts a single positional argument, the agent passes a list and receives unrecognized arguments — a parser-level rejection before any work is done.

# Agent constructs a natural bulk invocation:
$ ws delete /notes/a.md /notes/b.md /notes/c.md
usage: ws [-h] {tree,find,...,delete,...} ...
ws: error: unrecognized arguments: /notes/b.md /notes/c.md

# Agent must now loop — three separate calls instead of one:
$ ws delete /notes/a.md   # exit 0
$ ws delete /notes/b.md   # exit 0
$ ws delete /notes/c.md   # exit 0

The problem compounds when the command has side effects: each loop iteration is a separate process launch, authentication check, and network round-trip. Partial failure mid-loop (§13) leaves the set in an inconsistent state with no built-in rollback.

Additionally, the schema never declares whether a command accepts one or many positional arguments (nargs), so the agent cannot pre-determine arity without probing — forcing either a trial invocation or a fallback to single-call loops as a defensive default.

Impact

  • N process launches, auth checks, and round trips instead of one
  • Partial failure risk proportional to N: early errors leave remaining items unprocessed with no atomic rollback
  • Token and time cost grows linearly with batch size — the larger the agent's intended batch, the worse the overhead
  • Agent cannot distinguish "single-arg by design" from "single-arg by oversight" without reading source code

Solutions

Accept variadic positional arguments for any command whose logic is item-by-item:

# argparse
parser.add_argument("paths", nargs="+", help="One or more paths to delete")

# Click
@click.argument("paths", nargs=-1, required=True)

# Clap (Rust)
#[arg(num_args = 1..)]
paths: Vec<PathBuf>,

# Cobra (Go)
Args: cobra.MinimumNArgs(1),

Report per-item results so the agent can detect partial failure:

{
  "ok": true,
  "results": [
    {"path": "/notes/a.md", "ok": true},
    {"path": "/notes/b.md", "ok": false, "error": {"code": "NOT_FOUND", "message": "Path does not exist"}}
  ]
}

Declare arity in the schema manifest so agents can pre-determine call structure:

{
  "name": "delete",
  "args": [
    {"name": "paths", "nargs": "+", "description": "Paths to delete"}
  ]
}

For framework design: - Commands that perform the same stateless operation per item MUST accept nargs="+" (one or more) positional arguments - Per-item results MUST be returned as an array even when a single path is passed, so the agent can parse the response uniformly - The manifest's args array MUST include nargs ("1", "?", "*", "+"); absence of nargs MUST be treated as "1" by agents - Partial success MUST be reported per-item with a top-level ok: false when any item fails; the agent must not have to infer failure count from missing output

Evaluation

Score Condition
0 Single positional only; variadic invocation rejected with unrecognized arguments; no nargs in schema
1 Multiple paths accepted via --file flag repeated (--file a --file b); positional variadic still not supported
2 Variadic positional accepted (nargs="+"); nargs absent from schema; per-item result array returned
3 Variadic positional with nargs declared in schema manifest; per-item result array with ok per entry; top-level ok: false on any item failure

Check: Invoke tool delete path1 path2 path3 — verify all three are processed in a single call and the response contains a results array with one entry per path.


Agent Workaround

Detect arity from schema before constructing the invocation; loop as a fallback when nargs is "1" or absent:

import subprocess, json

def get_command_nargs(tool: str, subcommand: str, arg_name: str) -> str:
    """Return nargs for a positional arg; default '1' if undeclared."""
    result = subprocess.run(
        [tool, subcommand, "--schema"],
        capture_output=True, text=True,
    )
    try:
        schema = json.loads(result.stdout)
    except (json.JSONDecodeError, ValueError):
        return "1"  # conservative default

    for arg in schema.get("args", []):
        if arg.get("name") == arg_name:
            return arg.get("nargs", "1")
    return "1"

def delete_items(tool: str, paths: list[str]) -> list[dict]:
    """Use variadic call when supported; loop when not."""
    nargs = get_command_nargs(tool, "delete", "paths")

    if nargs in ("+", "*"):
        result = subprocess.run(
            [tool, "delete", *paths],
            capture_output=True, text=True,
        )
        parsed = json.loads(result.stdout)
        return parsed.get("results", [parsed])

    # Fallback: one call per item
    results = []
    for path in paths:
        r = subprocess.run([tool, "delete", path], capture_output=True, text=True)
        try:
            results.append(json.loads(r.stdout))
        except json.JSONDecodeError:
            results.append({"path": path, "ok": r.returncode == 0})
    return results

Limitation: When looping over single-arg calls, partial failure mid-batch leaves already-processed items changed with no rollback — the agent must record which items succeeded before the failure and report the incomplete state rather than retrying the full batch