hermes - ✅(Solved) Fix -z/oneshot 模式下 final_response 丢弃带工具调用的消息正文 [3 pull requests, 1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
NousResearch/hermes-agent#28326Fetched 2026-05-20 04:03:56
View on GitHub
Comments
1
Participants
2
Timeline
10
Reactions
0
Author
Participants
Timeline (top)
cross-referenced ×5labeled ×4commented ×1

Fix Action

Fixed

PR fix notes

PR #28408: fix: preserve substantive content in oneshot/chat() when model appends short follow-up after tool calls

Description (problem / solution / changelog)

Problem

When the model returns substantive content alongside a tool call (e.g. a detailed analysis + a todo housekeeping call), then the follow-up turn after tool execution produces a short epilogue ("还需要深挖吗?", "Anything else?"), the final_response is overwritten with just that tail — losing the substantive answer entirely.

This affects the oneshot/-z mode (chat()) most severely, since the caller only receives final_response and has no way to access the intermediate content.

Fixes #28326

Root Cause

In conversation_loop.py, when the model returns a response with no tool calls (the else branch at L3419), final_response is unconditionally set to assistant_message.content. The prior turn's content (stored in _last_content_with_tools) is only used in the empty-content recovery path, not when the follow-up is non-empty but short.

Fix

When _last_content_with_tools exists and the follow-up is less than 30% of the prior content's length, prepend the prior content before the short follow-up. This preserves the full answer while keeping the model's closing remark.

Heuristic rationale: A follow-up that is <30% of the substantive content is almost always a conversational tail ("还需要深挖吗?", "Let me know if you need more", etc.), not a substantive addition. This threshold is conservative enough to avoid false positives while catching the reported bug pattern.

Changes

  • agent/conversation_loop.py (+24 lines): Add short-followup detection and content prepending logic in the no-tool-call branch
  • tests/run_agent/test_run_agent.py (+40 lines): New test test_oneshot_final_response_not_overwritten_by_short_followup

Testing

337 tests passed, 0 failed (including the new test)

The new test simulates the exact scenario: Turn 1 returns 240-char Chinese content + web_search tool call, Turn 2 returns 7-char follow-up. Asserts both are present in final_response.

Changed files

  • agent/conversation_loop.py (modified, +24/-0)
  • tests/run_agent/test_run_agent.py (modified, +40/-0)

PR #28363: feat(optional-skills): add daily-diary skill

Description (problem / solution / changelog)

Summary

Add a configurable markdown diary system as an optional skill under the productivity category.

Features

  • #diary-start / #diary-end commands for recording entries
  • ISO-week directory organisation with single-file-per-day
  • Configurable storage directory (local, iCloud, Obsidian)
  • First-run setup wizard with interactive prompts
  • Smart cron reminder (skips if diary already written that day)
  • Reconfiguration support (change path, time, remove)
  • Image support with relative paths for Obsidian compatibility
  • Weather annotation (configurable default location)

Related

  • related_skills: [obsidian]

Changed files

  • .dockerignore (removed, +0/-31)
  • .env.example (removed, +0/-469)
  • .envrc (removed, +0/-5)
  • .gitattributes (removed, +0/-2)
  • .github/ISSUE_TEMPLATE/bug_report.yml (removed, +0/-162)
  • .github/ISSUE_TEMPLATE/config.yml (removed, +0/-11)
  • .github/ISSUE_TEMPLATE/feature_request.yml (removed, +0/-85)
  • .github/ISSUE_TEMPLATE/setup_help.yml (removed, +0/-112)
  • .github/PULL_REQUEST_TEMPLATE.md (removed, +0/-75)
  • .github/actions/hermes-smoke-test/action.yml (removed, +0/-47)
  • .github/actions/nix-setup/action.yml (removed, +0/-18)
  • .github/dependabot.yml (removed, +0/-44)
  • .github/workflows/contributor-check.yml (removed, +0/-73)
  • .github/workflows/deploy-site.yml (removed, +0/-97)
  • .github/workflows/docker-publish.yml (removed, +0/-534)
  • .github/workflows/docs-site-checks.yml (removed, +0/-48)
  • .github/workflows/history-check.yml (removed, +0/-58)
  • .github/workflows/lint.yml (removed, +0/-202)
  • .github/workflows/nix-lockfile-fix.yml (removed, +0/-254)
  • .github/workflows/nix.yml (removed, +0/-117)
  • .github/workflows/osv-scanner.yml (removed, +0/-67)
  • .github/workflows/skills-index.yml (removed, +0/-101)
  • .github/workflows/supply-chain-audit.yml (removed, +0/-205)
  • .github/workflows/tests.yml (removed, +0/-85)
  • .github/workflows/upload_to_pypi.yml (removed, +0/-164)
  • .github/workflows/uv-lockfile-check.yml (removed, +0/-119)
  • .gitignore (removed, +0/-75)
  • .mailmap (removed, +0/-108)
  • .plans/openai-api-server.md (removed, +0/-291)
  • .plans/streaming-support.md (removed, +0/-705)
  • AGENTS.md (removed, +0/-1102)
  • CONTRIBUTING.md (removed, +0/-922)
  • Dockerfile (removed, +0/-120)
  • LICENSE (removed, +0/-21)
  • MANIFEST.in (removed, +0/-4)
  • README.md (removed, +0/-194)
  • README.zh-CN.md (removed, +0/-180)
  • RELEASE_v0.10.0.md (removed, +0/-27)
  • RELEASE_v0.11.0.md (removed, +0/-453)
  • RELEASE_v0.12.0.md (removed, +0/-505)
  • RELEASE_v0.13.0.md (removed, +0/-641)
  • RELEASE_v0.14.0.md (removed, +0/-479)
  • RELEASE_v0.2.0.md (removed, +0/-383)
  • RELEASE_v0.3.0.md (removed, +0/-377)
  • RELEASE_v0.4.0.md (removed, +0/-400)
  • RELEASE_v0.5.0.md (removed, +0/-348)
  • RELEASE_v0.6.0.md (removed, +0/-249)
  • RELEASE_v0.7.0.md (removed, +0/-290)
  • RELEASE_v0.8.0.md (removed, +0/-346)
  • RELEASE_v0.9.0.md (removed, +0/-329)
  • SECURITY.md (removed, +0/-331)
  • acp_adapter/__init__.py (removed, +0/-1)
  • acp_adapter/__main__.py (removed, +0/-5)
  • acp_adapter/auth.py (removed, +0/-79)
  • acp_adapter/edit_approval.py (removed, +0/-282)
  • acp_adapter/entry.py (removed, +0/-266)
  • acp_adapter/events.py (removed, +0/-279)
  • acp_adapter/permissions.py (removed, +0/-168)
  • acp_adapter/server.py (removed, +0/-1935)
  • acp_adapter/session.py (removed, +0/-628)
  • acp_adapter/tools.py (removed, +0/-1357)
  • acp_registry/agent.json (removed, +0/-16)
  • acp_registry/icon.svg (removed, +0/-8)
  • agent/__init__.py (removed, +0/-6)
  • agent/account_usage.py (removed, +0/-326)
  • agent/agent_init.py (removed, +0/-0)
  • agent/agent_runtime_helpers.py (removed, +0/-0)
  • agent/anthropic_adapter.py (removed, +0/-0)
  • agent/async_utils.py (removed, +0/-0)
  • agent/auxiliary_client.py (removed, +0/-0)
  • agent/azure_identity_adapter.py (removed, +0/-0)
  • agent/background_review.py (removed, +0/-0)
  • agent/bedrock_adapter.py (removed, +0/-0)
  • agent/browser_provider.py (removed, +0/-0)
  • agent/browser_registry.py (removed, +0/-0)
  • agent/chat_completion_helpers.py (removed, +0/-0)
  • agent/codex_responses_adapter.py (removed, +0/-0)
  • agent/codex_runtime.py (removed, +0/-0)
  • agent/context_compressor.py (removed, +0/-0)
  • agent/context_engine.py (removed, +0/-0)
  • agent/context_references.py (removed, +0/-0)
  • agent/conversation_compression.py (removed, +0/-0)
  • agent/conversation_loop.py (removed, +0/-0)
  • agent/copilot_acp_client.py (removed, +0/-0)
  • agent/credential_pool.py (removed, +0/-0)
  • agent/credential_sources.py (removed, +0/-0)
  • agent/curator.py (removed, +0/-0)
  • agent/curator_backup.py (removed, +0/-0)
  • agent/display.py (removed, +0/-0)
  • agent/error_classifier.py (removed, +0/-0)
  • agent/file_safety.py (removed, +0/-0)
  • agent/gemini_cloudcode_adapter.py (removed, +0/-0)
  • agent/gemini_native_adapter.py (removed, +0/-0)
  • agent/gemini_schema.py (removed, +0/-0)
  • agent/google_code_assist.py (removed, +0/-0)
  • agent/google_oauth.py (removed, +0/-0)
  • agent/i18n.py (removed, +0/-0)
  • agent/image_gen_provider.py (removed, +0/-0)
  • agent/image_gen_registry.py (removed, +0/-0)
  • agent/image_routing.py (removed, +0/-0)

PR #28453: feat: expose structured per-turn content metadata from run_conversation()

Description (problem / solution / changelog)

Problem

run_conversation() returns a flat dict where final_response is overwritten by the last non-tool-call turn. When the model emits substantive content alongside tool calls, that content is stored internally in _last_content_with_tools but never exposed to downstream consumers. This is the root cause of at least 4 open bugs:

  • #28326: oneshot/chat() loses content when model appends short follow-up after tool calls
  • #14894: post_llm_call response overrides cause persistence/history mismatch
  • #7968: _last_content_with_tools fallback bypasses empty-response retries
  • #6067: Telegram does not show text content before tool calls

Closes #28431

Solution

Add a ContentSegment dataclass to agent/conversation_loop.py:

@dataclass
class ContentSegment:
    content: str           # Text content (stripped of think blocks)
    had_tool_calls: bool   # Whether this turn also had tool calls
    tool_call_count: int   # Number of tool calls
    tool_names: list[str]  # Names of tools called

The result dict from run_conversation() now includes:

"content_segments": [  # NEW — list of ContentSegment
    ContentSegment(content="Report...", had_tool_calls=True, tool_call_count=1, tool_names=["web_search"]),
    ContentSegment(content="Done!", had_tool_calls=False),
]

How this fixes the 4 bugs

  1. #28326: chat() can construct final_response from content_segments instead of relying on a single overwritten string.
  2. #14894: post_llm_call hooks can inspect content_segments for informed decisions.
  3. #7968: Empty-response retry logic can check content_segments[-1] instead of _last_content_with_tools.
  4. #6067: Platform adapters can iterate content_segments to deliver each piece of content.

Changes

FileChange
agent/conversation_loop.py+47: ContentSegment dataclass, collection in 2 loop branches, result dict field
tests/run_agent/test_run_agent.py+74: 5 new tests in TestContentSegments

Testing

343 passed, 0 failed (including 5 new tests)

Backward Compatibility

Fully backward compatible. All existing dict keys (final_response, messages, etc.) are unchanged. New content_segments key is additive — consumers that don't read it are unaffected.

Follow-up Opportunities

With content_segments available, these internal mechanisms can be simplified in future PRs:

  • Remove _last_content_with_tools hack
  • Simplify empty-response retry logic
  • Add platform-level content delivery from segments

Changed files

  • agent/conversation_loop.py (modified, +46/-1)
  • tests/run_agent/test_run_agent.py (modified, +74/-0)
RAW_BUFFERClick to expand / collapse

run_conversation() 循环里 final_response 被每次 llm_chat 返回覆盖。模型输出报告正文 + 调用 todo → 正文存在第1次 final_response → 循环继续 → 第2次 final_response 覆盖为"还需要深挖吗?" → chat() 只返回最后一次。

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING