Skip to content

Agent help

Machine-readable, agent-oriented --help: the AGENT_HELP trigger, --help-agents, and print_agent_help/describe.

duho.agenthelp

Agent-oriented help: a detailed, machine-readable description of a CLI.

Where the normal --help renders a compact, human-facing usage block, this module emits a complete, structured (JSON) description of a duho CLI -- enough for an AI agent (or any tool) to understand the whole command surface in one shot: every subcommand, each option with its type/default/required/ repeatable flags, positionals, per-field environment-variable bindings, mutually-exclusive conflict groups, examples, and exit codes.

Two triggers, both wired up by duho.args:

  • The AGENT_HELP/AGENTS_HELP environment variables (always on, zero-config; either truthy triggers). When one is set to a truthy value, the ordinary -h/--help action emits this agent description instead of the human help. Nothing changes for a normal --help unless one of these env vars is deliberately set, so default human behavior is byte-identical. Both defaults are replaced (no aliasing) by an explicit _agent_help_env_ class attribute naming a single variable.
  • An opt-in --help-agents flag. Set _agent_help_ = True on the CLI root to add a discoverable flag that always emits the agent description, regardless of the environment.

Built on duho's existing introspection. The description is produced by walking the built argparse parser tree (so injected flags -- --version, --print-completion -- and the whole subcommand tree are included) and enriching each command with duho's own field metadata (:func:get_clsargs / :class:ClsArgDeclaration and the per-field :class:ArgumentBuilder) looked up via the _duho_cls_ handle _initparser_ stashes on every parser. A subparser with no duho class behind it (e.g. a duho.app module command) is still described from its argparse actions alone -- just without the duho-only metadata (env bindings, conflicts) that has no argparse equivalent.

This mirrors :mod:duho.completion: one parser-tree walk that produces plain data, kept out of the import duho hot path (duho.args imports it lazily, only when an agent-help trigger actually fires).

DEFAULT_ENV = 'AGENT_HELP' module-attribute

DEFAULT_ENVS = (DEFAULT_ENV, 'AGENTS_HELP') module-attribute

SCHEMA = 'duho/agent-help@1' module-attribute

__all__ = ['SCHEMA', 'DEFAULT_ENV', 'DEFAULT_ENVS', 'agent_help_requested', 'describe', 'describe_parser', 'render', 'print_agent_help'] module-attribute

agent_help_requested(env_name=None, environ=None)

True when a trigger env var is set truthy.

env_name explicit (e.g. an app's _agent_help_env_ override) checks only that one variable -- no aliasing once a caller names their own. env_name left None checks every name in :data:DEFAULT_ENVS (AGENT_HELP and AGENTS_HELP); either truthy triggers agent help. environ defaults to os.environ (injectable for tests). An unset variable is False; a set variable is True unless its stripped/lowercased value is one of :data:duho.text.BOOL_FALSE.

Source code in src/duho/agenthelp.py
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
def agent_help_requested(
    env_name: _ty.Optional[str] = None,
    environ: _ty.Optional[_ty.Mapping[str, str]] = None,
) -> bool:
    """True when a trigger env var is set truthy.

    ``env_name`` explicit (e.g. an app's ``_agent_help_env_`` override) checks
    only that one variable -- no aliasing once a caller names their own.
    ``env_name`` left ``None`` checks every name in :data:`DEFAULT_ENVS`
    (``AGENT_HELP`` and ``AGENTS_HELP``); either truthy triggers agent help.
    ``environ`` defaults to ``os.environ`` (injectable for tests). An unset
    variable is False; a set variable is True unless its stripped/lowercased
    value is one of :data:`duho.text.BOOL_FALSE`.
    """
    environ = _os.environ if environ is None else environ
    if env_name is not None:
        return _truthy(environ.get(env_name))
    return any(_truthy(environ.get(name)) for name in DEFAULT_ENVS)

describe(cls, argv=None)

Build cls's parser and return its agent-help document (a dict).

Standalone counterpart to the --help-agents flag / AGENT_HELP trigger: builds the parser tree fresh and describes it, independent of whether either trigger is wired up. argv is accepted for signature symmetry with the other entry points and currently unused (the description is static).

Source code in src/duho/agenthelp.py
664
665
666
667
668
669
670
671
672
673
def describe(cls: type[_Args], argv: _ty.Optional[_ty.Sequence[str]] = None) -> dict:
    """Build ``cls``'s parser and return its agent-help document (a dict).

    Standalone counterpart to the ``--help-agents`` flag / ``AGENT_HELP`` trigger:
    builds the parser tree fresh and describes it, independent of whether either
    trigger is wired up. ``argv`` is accepted for signature symmetry with the
    other entry points and currently unused (the description is static).
    """
    parser = cls._parser_()
    return describe_parser(parser, root=True, root_cls=cls)

describe_parser(parser, *, root=False, root_cls=None, name=None, aliases=None)

Describe one built ArgumentParser (and its subtree) as plain data.

root adds the document-level keys (schema tag, version, exit codes, examples). name/aliases label a subcommand within its parent.

Source code in src/duho/agenthelp.py
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
def describe_parser(
    parser: _argparse.ArgumentParser,
    *,
    root: bool = False,
    root_cls: _ty.Optional[type[_Args]] = None,
    name: _ty.Optional[str] = None,
    aliases: _ty.Optional[_ty.Sequence[str]] = None,
) -> dict:
    """Describe one built ``ArgumentParser`` (and its subtree) as plain data.

    ``root`` adds the document-level keys (schema tag, version, exit codes,
    examples). ``name``/``aliases`` label a subcommand within its parent.
    """
    return _describe_parser(
        parser, root=root, root_cls=root_cls, name=name, aliases=aliases, seen=set()
    )

print_agent_help(cls, file=None)

Print cls's agent-help JSON document to file (default stdout).

Written via :func:duho._compat.write_machine: raw UTF-8 bytes with LF-only newlines, bypassing file's text-mode encoding and newline translation, the same writer duho.args's agent-help actions use -- so this standalone entry point and the -h/--help-agents triggers behave identically on any host encoding.

Source code in src/duho/agenthelp.py
697
698
699
700
701
702
703
704
705
706
def print_agent_help(cls: type[_Args], file: _ty.Optional[_ty.TextIO] = None) -> None:
    """Print ``cls``'s agent-help JSON document to ``file`` (default stdout).

    Written via :func:`duho._compat.write_machine`: raw UTF-8
    bytes with LF-only newlines, bypassing ``file``'s text-mode encoding and
    newline translation, the same writer ``duho.args``'s agent-help actions use
    -- so this standalone entry point and the ``-h``/``--help-agents``
    triggers behave identically on any host encoding.
    """
    _compat.write_machine(render(describe(cls)), file)

render(spec)

Serialise an agent-help document to a JSON string (trailing newline).

json is imported lazily (not at module top) so import duho never pays its import cost -- only actually rendering an agent-help document does. This keeps the framework's zero-eager-json contract (see tests/test_config_json.py::test_json_import_is_lazy).

ensure_ascii=True: a non-ASCII character (an arrow in a docstring, a Latin-1 accent) written through a piped Windows stdout's text layer raises UnicodeEncodeError (empty output, exit 1) or comes out console-code-page-encoded instead of UTF-8. ASCII-escaping every non-ASCII character survives any stream encoding and decodes back to the identical string, at the cost of a few extra bytes and less readable raw JSON -- irrelevant for a document meant to be parsed, not read.

Source code in src/duho/agenthelp.py
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
def render(spec: dict) -> str:
    """Serialise an agent-help document to a JSON string (trailing newline).

    ``json`` is imported lazily (not at module top) so ``import duho`` never pays
    its import cost -- only actually rendering an agent-help document does. This
    keeps the framework's zero-eager-``json`` contract (see
    ``tests/test_config_json.py::test_json_import_is_lazy``).

    ``ensure_ascii=True``: a non-ASCII character (an arrow in a
    docstring, a Latin-1 accent) written through a piped Windows stdout's
    text layer raises ``UnicodeEncodeError`` (empty output, exit 1) or comes
    out console-code-page-encoded instead of UTF-8. ASCII-escaping every
    non-ASCII character survives any stream encoding and decodes back to the
    identical string, at the cost of a few extra bytes and less readable raw
    JSON -- irrelevant for a document meant to be parsed, not read.
    """
    import json as _json

    return _json.dumps(spec, indent=2, ensure_ascii=True) + "\n"