fix(shell-hooks): return bare string from transform_* stdout so response shaping fires (#67890) - #67939
Conversation
|
Reviewed against #67890. The shell-hooks portion is correct and complete for the reported bug:
Two things worth flagging before this lands:
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. |
|
Thanks for identifying a real shell-hook contract mismatch. On current main, Problems
Suggested changes
This is an automated hermes-sweeper review. |
SummaryTwenty-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 consolidationAuthor action on #67939: split Complex graphflowchart 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"
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. |
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 forpre_llm_call, and the consumer inturn_finalizer.pyreads the hook result viaisinstance(_hook_result, str)for replacement — so no shell hook could ever satisfy that check, regardless of what the script printed.Repro: a
transform_llm_outputshell hook that prints a JSON string literal leaves the user-visible response unchanged.Changes
agent/shell_hooks.py:_parse_response()now returnsOptional[Union[str, Dict[str, Any]]]. Atransform_*event short-circuits to a bare-string path: a JSON string literal (printf '"replaced text"\n') comes back as a Pythonstr; a dict-shaped payload is unwrapped from a singletextorcontentfield as a documented alternative for scripts that want to ship metadata next to the body. Empty body still returnsNoneso 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 doesisinstance(result, str).tests/agent/test_shell_hooks.py: 4 newTestParseResponsetests — JSON-string idiom (the repro), dict-with-text, dict-with-content, empty body still no-op. All 21TestParseResponsetests 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 runos.execveon a freshly-written shell-script path. That is a pre-existing environment limitation, unrelated to this PR.Checklist
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 returnedNonestill 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