Skip to content

fix(shell-hooks): return bare string from transform_* stdout so response shaping fires (#67890) - #67939

Open
Ahmett101 wants to merge 3 commits into
NousResearch:mainfrom
Ahmett101:fix/67890-shell-transform-string-output
Open

fix(shell-hooks): return bare string from transform_* stdout so response shaping fires (#67890)#67939
Ahmett101 wants to merge 3 commits into
NousResearch:mainfrom
Ahmett101:fix/67890-shell-transform-string-output

Conversation

@Ahmett101

Copy link
Copy Markdown
Contributor

Summary

Shell hooks registered for the response-shaping technique (transform_llm_output, transform_tool_result, transform_terminal_output) are documented, allowlisted, and visibly run — but their stdout is silently dropped. _parse_response() forced every event through the {"context": ...} extraction designed for pre_llm_call, and the consumer in turn_finalizer.py reads the hook result via isinstance(_hook_result, str) for replacement — so no shell hook could ever satisfy that check, regardless of what the script printed.

Repro: a transform_llm_output shell hook that prints a JSON string literal leaves the user-visible response unchanged.

Changes

  • agent/shell_hooks.py: _parse_response() now returns Optional[Union[str, Dict[str, Any]]]. A transform_* event short-circuits to a bare-string path: a JSON string literal (printf '"replaced text"\n') comes back as a Python str; a dict-shaped payload is unwrapped from a single text or content field as a documented alternative for scripts that want to ship metadata next to the body. Empty body still returns None so the turn keeps the original text.
  • agent/shell_hooks.py: _make_callback() return type widened to match. No new public surface — the consumer side already does isinstance(result, str).
  • tests/agent/test_shell_hooks.py: 4 new TestParseResponse tests — JSON-string idiom (the repro), dict-with-text, dict-with-content, empty body still no-op. All 21 TestParseResponse tests pass.

How to Test

pytest tests/agent/test_shell_hooks.py::TestParseResponse -q

21/21 passed (4 new: test_transform_llm_output_json_string_returns_bare_str, test_transform_tool_result_dict_with_text_field, test_transform_terminal_output_dict_with_content_field, test_transform_event_no_body_returns_none)

Note: TestCallbackSubprocess (6 tests) fails on Termux because the harness cannot run os.execve on a freshly-written shell-script path. That is a pre-existing environment limitation, unrelated to this PR.

Checklist

  • Tests pass — 21/21 (parse); pre-existing subprocess env limit on Termux does not reproduce the fix
  • Follows Conventional Commits
  • Changes scoped to this fix only
  • Cross-platform impact assessed (Linux / macOS / WSL2 / Windows / Termux) — fix is portable; only the verification harness is Termux-limited
  • profile-safe paths used
  • .env not used for non-credential settings

Risk & Impact: Low. The widened return type is a strict superset (Dict is still returned for pre_tool_call / pre_verify / pre_llm_call), and the consumer's isinstance(result, str) already discriminates; scripts that previously returned None still do, scripts that previously returned a dict still do. Only the previously-silent path (string stdout for transform_*) is now wired.

Type: Bug fix
Closes: #67890

@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/acp Agent Communication Protocol adapter comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/cli CLI entry point, hermes_cli/, setup wizard comp/cron Cron scheduler and job management needs-decision Awaiting maintainer decision before any implementation sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades labels Jul 20, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Related to #67890 and #56429 for response-shaping hooks. The hook repair is valid, but this branch also bundles unrelated ACP and cron/dashboard fixes; please split or confirm the combined scope.

@PRATHAMESH75

Copy link
Copy Markdown
Contributor

Reviewed against #67890. The shell-hooks portion is correct and complete for the reported bug:

  • agent/shell_hooks.py::_parse_response() now short-circuits the _TRANSFORM_RESPONSE_EVENTS family before the {"context": ...} extraction and returns a bare str (JSON string literal) or an unwrapped text/content field. The widened Optional[Union[str, Dict[str, Any]]] return type is a clean superset.
  • I traced the consumer chain: the shell-hook callback is registered into the plugin manager's _hooks[event] list, so hermes_cli.plugins.invoke_hook("transform_llm_output", ...) collects its raw return unwrapped, and agent/turn_finalizer.py:423 (isinstance(_hook_result, str) and _hook_result) then fires the replacement. So a printf '"replaced text"\n' hook now actually replaces the response — the exact repro in transform_llm_output shell hook is documented but silently non-functional (stdout is dropped) #67890.
  • tests/agent/test_shell_hooks.py covers the JSON-string idiom, dict-with-text, dict-with-content, and the empty-body no-op. Good invariant-style coverage.

Two things worth flagging before this lands:

  1. Scope — this PR bundles three unrelated changes. The diff also touches cron/jobs.py + hermes_cli/web_server.py (an update-field whitelist that cites Bug: update_job silently persists unknown keys with no validation #67625, a different issue) and acp_adapter/session.py (a list_sessions seen_ids dedup change with no linked issue). That contradicts the PR's own checked box "My PR contains only changes related to this fix" and the repo's one-logical-change-per-PR guideline, and it couples the (clean, ready) transform_llm_output shell hook is documented but silently non-functional (stdout is dropped) #67890 fix to review of two separate concerns. Splitting the cron/Bug: update_job silently persists unknown keys with no validation #67625 work and the ACP change into their own PRs would let the shell-hooks fix merge on its own merits.

  2. transform_tool_result / transform_terminal_output have no consumer yet. _parse_response now handles all three _TRANSFORM_RESPONSE_EVENTS, but a grep shows no invoke_hook("transform_tool_result"/"transform_terminal_output", ...) call site — only transform_llm_output is actually consumed (in turn_finalizer.py). The extra two are harmless and forward-looking (the docs' VALID_HOOKS table lists them), just noting they're inert until a caller exists — the doc-vs-behavior gap transform_llm_output shell hook is documented but silently non-functional (stdout is dropped) #67890 raises is only truly closed for transform_llm_output.

Net: the #67890 fix itself is right and I'd be glad to see it merge — ideally on its own, decoupled from the #67625/ACP changes.

@teknium1

Copy link
Copy Markdown
Contributor

Thanks for identifying a real shell-hook contract mismatch. On current main, agent/shell_hooks.py:596-597 rejects JSON string stdout and agent/shell_hooks.py:617-619 only returns a context dict, while agent/turn_finalizer.py:499-503 only applies a non-empty string replacement. The transform-string approach therefore addresses the reported failure. It also fits the live tool and terminal transform consumers at model_tools.py:1390-1410 and tools/terminal_tool.py:2800-2812.

Problems

Suggested changes

This is an automated hermes-sweeper review.

@teknium1 teknium1 added sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 30, 2026
@GottZ

GottZ commented Aug 3, 2026

Copy link
Copy Markdown

This was generated by AI during triage.

Summary

Twenty-one PRs address or reference seven related cron/dashboard and shell-hook issues: no-agent run visibility, event-loop starvation, immediate triggering, obsolete root-store routing, update-field validation, shell-transform returns, and pending trigger feedback. Their diffs range from focused fixes to superseded references and bundled cross-subsystem changes.

Related pull requests

Duplicates

#53692 duplicates #44087; #50978 and #50979 are twins and narrower duplicates of #45491; #67646 is superseded by #67660, while #67939 bundles the same cron-validation change; #67939's shell-hook portion duplicates the focused #67949; #70833 is superseded by #70840. #62247, #62587, and #66954 overlap the no-agent history work but differ in timing, covered outcomes, and sidebar handling.

Suggested consolidation

Author action on #67939: split agent/shell_hooks.py and tests/agent/test_shell_hooks.py from the unrelated ACP and cron/dashboard changes, as required by its high-salvageability keep_open verdict; because #67949 is the recorded focused best fix for #67890, the extracted hook portion can defer to #67949 rather than creating a second review lane. Keep #62247, #67660, #67949, #70638, #70840, and #76831 open only along their explicit review salvage paths, have #46956 defer to #70638's selected-job provider design, and retain the closed duplicate chains #53692#44087, #50978/#50979#45491, #67646#67660, and #70833#70840.

Complex graph

flowchart LR
    classDef open fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
    classDef merged fill:#dcfce7,stroke:#15803d,color:#14532d
    classDef closed fill:#e5e7eb,stroke:#6b7280,color:#1f2937
    classDef unverified fill:#f3f4f6,stroke:#9ca3af,color:#374151
    classDef best stroke-width:3px,stroke:#b45309
    classDef target stroke-width:3px,stroke:#4338ca
    I67625(["issue #67625 (open)"])
    I67890(["issue #67890 (open)"])
    subgraph Dup67939 ["PRs duplicating each other"]
        P67939["PR #67939 (open)"]
        P67949["PR #67949 (open)"]
    end
    P67939 -.->|partial| I67625
    P67939 -->|fixes| I67890
    class I67625 open
    class I67890 open
    class P67939 open
    class P67949 open
    class P67949 best
    class P67939 target
    click I67625 "https://github.com/NousResearch/hermes-agent/issues/67625"
    click I67890 "https://github.com/NousResearch/hermes-agent/issues/67890"
    click P67939 "https://github.com/NousResearch/hermes-agent/pull/67939"
    click P67949 "https://github.com/NousResearch/hermes-agent/pull/67949"
Loading

Graph: solid arrow = fixes / best fix, dashed arrow = partial or unverified (see edge label); boxed group = PRs duplicating each other; amber border = best fix; indigo border = target; gray node = closed (state tag in the node label).

Cross-PR triage: Reviewed 21 pull requests and 7 issues in this complex. Each diff was read against this issue; Assessment working set: 390 kB of PR diffs, 66 kB of issue/PR text, 38 kB of discussion (46 comments), 34 verify verdicts. verdicts reflect diff content, not PR titles. Part of an automated triage batch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/acp Agent Communication Protocol adapter comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/cli CLI entry point, hermes_cli/, setup wizard comp/cron Cron scheduler and job management needs-decision Awaiting maintainer decision before any implementation P2 Medium — degraded but workaround exists sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

transform_llm_output shell hook is documented but silently non-functional (stdout is dropped)

5 participants