Skip to content

feat(gateway): composable output-guard pipeline + non-blocking suggest_actions tool - #77905

Draft
jeffreyhlin wants to merge 3 commits into
NousResearch:mainfrom
jeffreyhlin:feat/guards-and-actions
Draft

feat(gateway): composable output-guard pipeline + non-blocking suggest_actions tool#77905
jeffreyhlin wants to merge 3 commits into
NousResearch:mainfrom
jeffreyhlin:feat/guards-and-actions

Conversation

@jeffreyhlin

Copy link
Copy Markdown

Summary

Two related extension points at the gateway's outbound seam, both generalizing
patterns that already existed as one-off code paths.

1. Composable output-guard pipeline (gateway/output_guards.py)

Three outbound checks previously lived as separate inline code paths:

  • secret redaction (_redact_gateway_user_facing_secrets)
  • provider-error rewriting (_looks_like_gateway_provider_error)
  • silence-narration drop (_is_silence_narration in delivery.py)

This folds them into one ordered, fault-isolated validator chain. A guard is a
(text, ctx) -> GuardOutcome | None callable that can rewrite or drop a
message; a drop short-circuits the rest. Guards may be sync or async. The
pipeline never raises: a guard that throws is logged and skipped, so a buggy
rule can never block delivery.

Wired into the two existing chokepoints (_sanitize_gateway_final_response
for agent replies, DeliveryRouter._deliver_to_platform for cron deliveries),
both with fallbacks to the legacy inline path.

Two new guards ship off by default, enabled via gateway.guards.* in
config.yaml (no new env vars):

  • strip_em_dashes — style rule as a guard
  • verify_links — async; never emit a URL that doesn't resolve

Adding a rule is now one function plus one registry entry, instead of another
inline branch at a call site.

2. suggest_actions tool — non-blocking tappable follow-up actions

Generalizes the clarify interaction pattern. Where clarify blocks the
agent on an A/B/C question, suggest_actions is fire-and-forget: the agent
attaches up to 6 actions to a message and finishes its turn. Tapping one starts
a new turn seeded with that action's payload.

  • tools/suggest_actions_tool.py — schema + validation/normalization
  • tools/suggested_actions_gateway.py — non-blocking registry
    (register/resolve/eviction/session-cleanup). No blocking Event, no timeout
    thread; a per-session cap bounds memory without a reaper.
  • gateway/platforms/base.py — default numbered-list text fallback, so every
    platform degrades gracefully
  • plugins/platforms/telegram/adapter.py — native inline buttons
    (sa:<set_id>:<index>); a tap resolves the payload server-side (Telegram
    caps callback_data at 64 bytes) and injects a synthetic MessageEvent
    through the normal handle_message entry point

Any turn that would end in "want me to do X or Y?" can become tappable, so
recommendation cards, cron confirmations, and snooze shortcuts all get the
affordance for free.

Registered in the existing clarify toolset rather than a new one, so the
core tool-schema footprint grows by exactly one tool.

Notes for review

Tests

30 new tests (tests/gateway/test_output_guards.py,
tests/tools/test_suggest_actions.py) covering pipeline mechanics
(rewrite/drop/short-circuit/fault-isolation/sync-vs-async), each built-in
guard, tool validation, and registry lifecycle including eviction.

Verified green together with the suites this touches:

tests/gateway/test_output_guards.py tests/tools/test_suggest_actions.py
tests/gateway/test_telegram_noise_filter.py tests/gateway/test_delivery.py
tests/gateway/test_delivery_silence_filter.py tests/test_toolsets.py
tests/tools/test_clarify_tool.py tests/tools/test_clarify_gateway.py
-> 846 passed

Assertions target behavior contracts rather than snapshots (e.g. the secret
test asserts the raw credential does not survive, not a specific mask marker,
since agent.redact masks with *** while the gateway fallback uses
[REDACTED]).

Generalize the three scattered outbound checks (secret redaction,
provider-error rewriting, silence-narration drop) into one ordered,
fault-isolated validator chain in gateway/output_guards.py. Each guard
owns a single concern and can rewrite or drop a message; new rules drop
in as one function plus one registry entry.

Adds two opt-in guards: em-dash stripping (gateway.guards.strip_em_dashes)
and async link verification (gateway.guards.verify_links).

Wired into the two existing chokepoints:
- _sanitize_gateway_final_response (agent final replies)
- DeliveryRouter._deliver_to_platform (cron deliveries)

Legacy behaviour preserved exactly, with fallback paths so a pipeline
error can never block delivery. 15 tests.
Generalize the clarify interaction pattern into a fire-and-forget
affordance primitive. Where clarify blocks the agent on an A/B/C
question, suggest_actions attaches N tappable actions to a message and
ends the turn; tapping one starts a NEW turn seeded with that action's
payload.

- tools/suggest_actions_tool.py: schema + validation/normalization
- tools/suggested_actions_gateway.py: non-blocking action registry
  (register/resolve/eviction/session-cleanup), no blocking Event
- gateway/platforms/base.py: default numbered-list text fallback
- gateway/platforms/telegram.py: native inline-button render +
  sa:<set_id>:<index> callback that injects a synthetic user turn
- gateway/run.py: suggest_actions_callback bridge + session cleanup
- tool_executor + agent_init + toolsets: dispatch and registration

Degrades gracefully on platforms without buttons. 15 tests.
Rebasing onto current main surfaced two real defects in the guard
pipeline commit:

1. _provider_error_guard was still Telegram-gated, a leftover from when
   _sanitize_gateway_final_response only covered Telegram. Upstream
   widened that security invariant to every chat surface (NousResearch#28533 ->
   NousResearch#39293), so WhatsApp/Slack/Signal/Matrix would have leaked raw
   provider error envelopes (which can carry bearer tokens). The guard
   now applies to every surface reaching it; programmatic surfaces are
   already excluded by _GATEWAY_RAW_TEXT_PLATFORMS upstream of the call.

2. The test fixture leaked the opt-in em-dash guard into unrelated
   tests via the module-level pipeline singleton. Env flags are now
   cleared before the reset on both setup and teardown.

Also relaxes the secret-redaction assertion to test the behaviour
contract (raw credential does not survive) instead of a specific mask
marker, since the authoritative agent.redact redactor masks with '***'
while the gateway fallback uses '[REDACTED]'.
@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/gateway Gateway runner, session dispatch, delivery comp/tools Tool registry, model_tools, toolsets sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages needs-decision Awaiting maintainer decision before any implementation labels Aug 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/gateway Gateway runner, session dispatch, delivery comp/tools Tool registry, model_tools, toolsets needs-decision Awaiting maintainer decision before any implementation P3 Low — cosmetic, nice to have sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants