Skip to content

feat: add tool_display config to control tool call visibility in Discord#263

Closed
white1033 wants to merge 9 commits intoopenabdev:mainfrom
white1033:feat/tool-display-config
Closed

feat: add tool_display config to control tool call visibility in Discord#263
white1033 wants to merge 9 commits intoopenabdev:mainfrom
white1033:feat/tool-display-config

Conversation

@white1033
Copy link
Copy Markdown

@white1033 white1033 commented Apr 13, 2026

What problem does this solve?

Tool call lines in Discord messages contain the full ACP title string, which often includes complete shell commands, file paths, or URL arguments. In public or shared channels this is noisy and can inadvertently expose sensitive paths, tokens embedded in arguments, or internal infrastructure details.

Closes #216

At a Glance

ACP Agent event (ToolCallStart)
        │
        │  title = "Running: curl -s https://internal.corp/api?token=..."
        ▼
┌─────────────────────────────────────────────────┐
│            compose_display(tool_display)         │
│                                                  │
│  full    →  ✅ `Running: curl -s https://...`   │
│  compact →  ✅ `Running`                         │
│  none    →  (tool lines hidden entirely)         │
└────────────────────┬────────────────────────────┘
                     │
                     ▼
            Discord channel message

Prior Art & Industry Research

Hermes Agent:

acp_adapter/tools.py → build_tool_title() uses a "verb: primary_arg" format and truncates long commands at 80 characters (e.g. "terminal: curl -s ...""terminal: curl -s…"). This is essentially the same compact label pattern — Hermes chose brevity by default too. Separately, agent/display.py exposes set_tool_preview_max_len() / get_tool_preview_max_len() as a runtime-configurable integer cap (0 = unlimited). Hermes does not expose a "none" (hide entirely) mode.

OpenClaw:

OpenClaw separates tool event transport from display via ACP ToolCallStart / ToolCallProgress events streamed through src/channels/draft-stream-controls.ts. Display decisions are made at the channel layer, not inside the event emitter — which is the same decoupling this PR achieves by threading tool_display through stream_prompt() rather than hard-coding it at the call site. OpenClaw does not expose a user-facing config option for tool verbosity.

Other references:

  • Discord's own message length cap (2000 chars) makes verbose tool lines a practical problem, not just aesthetic — long tool outputs can crowd out agent text.

Proposed Solution

Add a tool_display field to [reactions] in config.toml:

[reactions]
tool_display = "compact"   # full | compact | none

Three modes:

Mode Behavior Example
full Complete ACP title (previous behavior) Running: curl -s "https://..."
compact Short label or executable name only Running or ✅ curl
none Tool lines hidden; only agent text shown (no tool lines)

Implementation:

  • ToolDisplay enum in config.rs with serde rename_all = "lowercase"
  • compact_title() helper with URL-safe heuristic (detects :// to skip URL colons)
  • ToolEntry::render() and compose_display() accept ToolDisplay; None skips the tool block
  • tool_display plumbed through stream_prompt() to all four compose_display() call sites
  • Helm chart configmap.yaml and values.yaml updated; default is "compact"

Files changed

File Change
src/config.rs Add ToolDisplay enum; add tool_display field to ReactionsConfig; add deserialization tests
src/discord.rs Import ToolDisplay; add compact_title() with URL-safe heuristic; update ToolEntry::render, compose_display, stream_prompt; add 7 unit tests
config.toml.example Document tool_display = "compact" under [reactions]
charts/openab/values.yaml Add toolDisplay: "compact" to reactions block
charts/openab/templates/configmap.yaml Render tool_display from Helm values
k8s/configmap.yaml Add tool_display = "compact" to static manifest

Why this approach?

Enum over boolean: A boolean hide_tools: true only handles two states. The third state (compact) turns out to be the most practically useful — users want to know a tool ran without reading its full arguments. Hermes Agent's experience confirms this: their build_tool_title() truncates by default because full titles are rarely useful to end users.

compact as default (not full): Changing the default is a mild breaking change in appearance, but leaving full as default would mean most users never benefit from the fix. The compact output is still informative — it signals that a tool ran — without exposing arguments.

URL-safe heuristic in compact_title(): The two ACP title formats ("Verb: command" from Kiro and raw-command from claude-agent-acp) collide on colon as separator vs URL scheme. A simple split(':')[0] would garble "curl https://..." into "curl https". The heuristic checks whether the character after : is / to skip URL colons.

Alternatives Considered

  1. Boolean hide_tool_lines: true — Only two states. Loses the compact middle ground that is most useful in practice. Rejected.

  2. Keep full as default, opt-in to compact — Avoids the appearance change but means existing deployments stay noisy. The whole point of the PR is to fix the default experience. Rejected.

  3. Server-side truncation at a character limit (like Hermes' tool_preview_max_len) — An integer cap is less expressive than named modes and harder to reason about (tool_preview_max_len = 10 vs tool_display = "compact"). Named modes also let us add new behaviors later without a config format change. Rejected.

  4. Strip tool lines in the Discord message formatter rather than in compose_display() — Would require post-processing rendered markdown, which is fragile. Keeping the decision in compose_display() keeps it close to where tool entries are assembled. Rejected.

Validation

Automated (36 tests, all passing)

  • cargo test — 36 tests, 0 failures
  • tool_display = "full" shows complete title — compose_display_full_shows_complete_title
  • tool_display = "compact" shows short label only — compose_display_compact_shows_only_prefix
  • tool_display = "none" hides all tool lines — compose_display_none_hides_all_tool_lines
  • Default (no tool_display in config) = compacttool_display_defaults_to_compact
  • All three TOML values parse correctly — tool_display_deserializes_all_modes
  • Invalid values rejected at parse time — tool_display_rejects_invalid_value
  • URL-containing commands not garbled — compact_title_url_in_command_not_truncated
  • "Verb: command" format extracts label — compact_title_verb_colon_format
  • Raw command format extracts executable — compact_title_raw_command_format
  • Empty tool list produces no blank line — compose_display_empty_tools_no_blank_line
  • Helm chart renders tool_display = "compact" — verified via helm template

… ToolDisplay

- render() now takes a ToolDisplay arg and applies compact_title() in Compact mode
- compose_display() now takes a ToolDisplay arg and skips tool lines in None mode
- Added TDD tests for all three modes (Full, Compact, None) and the empty-tools edge case
- Call sites in stream_prompt still pass old 2-arg signature (will be fixed in Task 4)
@white1033 white1033 requested a review from thepagent as a code owner April 13, 2026 01:39
@thepagent
Copy link
Copy Markdown
Collaborator

Closing in favor of #253 (merged), which addresses the core truncation issue by collapsing tool lines during streaming when count exceeds 3. The none display mode from this PR is now partially covered by the collapse behavior.

If you'd like to add additional display modes (full/compact/none) as a configurable option, feel free to open a new PR building on top of #253's compose_display() changes. Thanks for the contribution! 🙏

@thepagent
Copy link
Copy Markdown
Collaborator

Superseded by #253

@thepagent thepagent closed this Apr 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: add tool_display config to control tool call visibility in Discord

2 participants