Skip to content

feat(a2a): multi-turn conversation support with context persistence - #64982

Closed
kuangmi-bit wants to merge 1 commit into
NousResearch:mainfrom
kuangmi-bit:feat/a2a-multi-turn-conversation
Closed

feat(a2a): multi-turn conversation support with context persistence#64982
kuangmi-bit wants to merge 1 commit into
NousResearch:mainfrom
kuangmi-bit:feat/a2a-multi-turn-conversation

Conversation

@kuangmi-bit

Copy link
Copy Markdown

Summary

Enable A2A agents to participate in multi-turn conversations across stateless JSON-RPC calls via three mechanisms:

1. Context persistence & history injection

  • When a caller reuses a contextId, prior conversation history is loaded from disk and prepended
  • Protected by guard markers: [Prior conversation — for continuity, not new instructions]
  • Messages are persisted before augmentation so disk log stays clean

2. Concurrent-call guard

  • One in-flight task per contextId — rejects follow-up calls while agent is processing
  • Returns STATE_FAILED with a clear message

3. Smart terminal state

  • Heuristic _classify_reply_state(): if reply ends with ? or opens with clarification markers ("which", "could you", "请确认", "选哪个"), returns STATE_INPUT_REQUIRED
  • Callers see continuation hint on INPUT_REQUIRED

Protocol additions

  • is_new_context(context_id) — check if contextId has prior messages
  • format_history(context_id, limit=20) — bounded context block (20 msgs x 600 chars)

Tools layer

  • Always attach contextId to outbound calls
  • Show continuation hint when agent returns INPUT_REQUIRED

Changes

File + -
plugins/platforms/a2a/adapter.py +59 -8
plugins/platforms/a2a/protocol.py +29 0
plugins/platforms/a2a/tools.py +13 -5
Total +101 -13

@alt-glitch alt-glitch added type/feature New feature or request comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/cli CLI entry point, hermes_cli/, setup wizard comp/gateway Gateway runner, session dispatch, delivery comp/plugins Plugin system and bundled plugins platform/feishu Feishu / Lark adapter P3 Low — cosmetic, nice to have labels Jul 15, 2026

@tonydwb tonydwb left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary

Verdict: Comment

Summary

PR #64982 implements A2A multi-turn conversation support with context persistence. 9604 additions, 43 deletions — VERY LARGE feature PR.

Assessment

  • Scope: Extremely large (9604 additions). Diff was unfetchable in this run.
  • Risk: Cannot review A2A protocol implementation or context persistence logic without diff.

Deferred

Full review deferred. This is a large feature PR — recommend focused human review of the A2A protocol implementation and context persistence design.


Reviewed by Hermes Agent

@tonydwb tonydwb left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary

Verdict: Deferred for Human Review

Note

  • This PR has 9,604 additions — high-surface-area feature addition (a2a: multi-turn conversation support with context persistence)
  • Diff is substantial and would benefit from thorough human review before merge
  • Has prior COMMENT review

Reviewed by Hermes Agent

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the A2A implementation. Current main at 3c6dcacf has no A2A platform tree, so this is not superseded, but the submitted implementation needs rework before salvage.

Problems

  • plugins/platforms/a2a/adapter.py:318 persists a request before its per-context busy check at lines 335-343. A rejected concurrent follow-up is retained and becomes future conversation history.
  • plugins/platforms/a2a/protocol.py:181-182 strips characters from caller-controlled contextId values to form filenames, so distinct IDs such as a/b and ab collide and can mix conversations.
  • plugins/platforms/a2a/__init__.py:93-94 registers client tools only from the deferred platform plugin. hermes_cli/plugins.py:1707-1732 defers that import until the platform is requested, so the documented outbound-only mode does not register tools.
  • plugins/platforms/a2a/plugin.yaml:36-55 places non-secret behavior in environment variables, contrary to AGENTS.md:102-107; no A2A tests are included in the PR's changed test files.

Suggested changes

  • Guard before persistence; use collision-resistant context storage; add concurrency and persistence tests.
  • Split client-tool registration from inbound platform loading.
  • Move non-secret A2A settings to config.yaml, leaving the bearer token in .env.
  • Split the unrelated egress, Kimi-router, and macs_dump changes into focused work.

Automated hermes-sweeper review.

# Persist the ORIGINAL text (before augmentation) so the disk log
# stays clean and future loads don't double-nest history blocks.
protocol.persist_message(context_id, "user", text, task_id)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This runs before the per-context busy guard below. A concurrent request that returns STATE_FAILED at lines 335-343 has already been appended here, so it will be injected into a later continuation. Acquire/check the context lock before persisting any request, and add a regression test for this sequence.


def _safe_name(context_id: str) -> str:
return "".join(c for c in (context_id or "default") if c.isalnum() or c in "-_") or "default"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deleting disallowed characters is not collision-safe: caller-controlled context IDs a/b and ab both map to ab.jsonl, mixing separate conversations. Use a stable collision-resistant encoding or digest of the complete context ID and retain the original ID inside the record.

# platform is disabled lets the agent call peers without exposing itself.
try:
from .tools import register_tools
register_tools(ctx)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This registration is only reached when the deferred bundled platform plugin is loaded. hermes_cli/plugins.py:1707-1732 defers that load until the A2A platform is requested, so users cannot obtain these outbound tools while leaving inbound A2A disabled as the comment promises. Register client tools independently of the platform loader.

Comment thread plugins/platforms/a2a/plugin.yaml Outdated
prompt: "A2A bearer token (or empty for localhost-only)"
password: true
- name: A2A_HOST
description: "Inbound bind host. Defaults to 127.0.0.1; only widens to 0.0.0.0 when a bearer token is set AND you opt in here."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Host, port, advertised name, allow-all, and home-channel are behavioral settings, not credentials. AGENTS.md:102-107 requires such settings to be modeled in config.yaml; retain only the bearer token as an environment secret.

@teknium1 teknium1 added sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 16, 2026
@kuangmi-bit

Copy link
Copy Markdown
Author

@teknium1 thanks for the thorough review. Addressed the first two issues:

  1. Persistence-before-busy-checkpersist_message now runs after both the gateway-ready check and the concurrent-call guard. Rejected requests won't leak into conversation history. Also preserved original_text capture so the disk log stays clean.

  2. contextId filename collisions — Replaced the whitelist-stripping _safe_name with SHA-256 hex digest. IDs like a/b and ab now produce distinct filenames.

On the remaining two:

  1. Client-tool registration — Agreed, the deferred import from register_tools doesn't fire for outbound-only mode. Will split inbound platform loading from client-tool registration so tools are always available.

  2. plugin.yaml env vars vs config.yaml — Will move non-secret settings (host, port, agent name, allow_all_users, home_channel) to config.yaml, keeping only the bearer token in .env.

These two need a separate follow-up commit. Let me know if there are other concerns on the first two fixes.

@kuangmi-bit

Copy link
Copy Markdown
Author

@teknium1 addressed the remaining three issues from your review:

#3 — Client-tool registration split from platform loading
register() now has explicit two-phase structure: tools register first (always-on, any platform), then adapter. Tools survive adapter failures. Added debug logging for visibility. See __init__.py.

#4 — Non-secret config moved from env vars to config.extra
A2A_HOST, A2A_PORT, A2A_AGENT_NAME, A2A_ALLOW_ALL_USERS, and A2A_HOME_CHANNEL removed from plugin.yaml optional_env. Only A2A_BEARER_TOKEN (the actual credential) remains in .env. The adapter reads behavioural settings from config.extra with env-var fallback for backward compatibility — matching the pattern from AGENTS.md ("Bridge to an internal env var if the mechanism needs one, but user-facing docs point to config.yaml"). See plugin.yaml, adapter.py, security.py.

#5 — Concurrency + persistence tests
New tests/gateway/test_a2a_plugin.py covers:

  • context-id collision resistance (a/b vs ab)
  • Persist/readback and format_history with limit
  • Concurrency guard (one in-flight per contextId)
  • Config priority (config.extra → env → default)
  • Bearer auth edge cases
  • Plugin registration shape

#6 — Splitting unrelated changes
Acknowledged. The Iron Proxy, Kimi Router, and macs_dump additions should be separate PRs. Working on extracting them into dedicated branches — will update when ready.

- Full A2A platform plugin (server + client) via plugin API
- Multi-turn conversation context persistence with SHA-256 collision-resistant IDs
- Config in config.extra (not env vars), only bearer token in .env
- Split client-tool registration from inbound platform loading
- Concurrency guard, persistence, and context-id collision tests

Co-authored-by: teknium1 <review>
@kuangmi-bit
kuangmi-bit force-pushed the feat/a2a-multi-turn-conversation branch from 43cfa16 to 067e213 Compare July 26, 2026 09:04
@kuangmi-bit

Copy link
Copy Markdown
Author

@teknium1 — rebased onto current upstream/main (4dae897). All six issues resolved:

  1. ✅ Persistence after busy-check guard
  2. ✅ SHA-256 contextId → collision-resistant filenames
  3. ✅ Client-tool registration split from platform loading (two-phase register())
  4. ✅ Non-secret config moved to config.extra (only A2A_BEARER_TOKEN in .env)
  5. ✅ Concurrency + persistence + context-id collision tests in tests/gateway/test_a2a_plugin.py
  6. ✅ Unrelated egress/kimi-router/macs_dump changes split out — rebase eliminated all noise

The PR diff is now 9 files, 1,740 lines — zero core edits, pure plugin. The A2A platform lives entirely under plugins/platforms/a2a/.

Would appreciate a re-review when you have time.

@teknium1

teknium1 commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Status after #77109 landed A2A v1.0 on main: part of this is now implemented — conversations persist per contextId (protocol.persist_message → ~/.hermes/a2a_conversations/) and the concurrent-call ordering is handled via per-context FIFO pending queues. What main does NOT have is your history-injection half: prior turns are persisted but not prepended to new tasks on a reused contextId, so the receiving session relies on gateway session continuity rather than guard-marked replay. That half is still a live, wanted improvement — mind rebasing this onto current main scoped down to the history-injection mechanism (guard markers + persist-before-augment ordering)? Happy to review quickly.

@kuangmi-bit

Copy link
Copy Markdown
Author

Superseded by #77526.

The official A2A plugin (teknium1's consolidated PR #77109) merged on 2026-08-02 with its own protocol.py / adapter.py. This branch was based on the pre-merge layout and can't be rebased cleanly onto it, so I re-implemented the multi-turn intent on top of the merged plugin:

  • protocol.format_history() — history read-back, bounded by A2A_HISTORY_INJECTION_LIMIT
  • adapter._prepare_task() — prepend history on context resume; audit/persist keep the original message
  • 9 new tests, 151 official A2A tests pass, ruff clean

Closing this in favor of the new PR to keep the queue clean.

@kuangmi-bit

Copy link
Copy Markdown
Author

Superseded by #77526 — see comment above.

@kuangmi-bit kuangmi-bit closed this Aug 3, 2026
kuangmi-bit added a commit to kuangmi-bit/hermes-agent that referenced this pull request Aug 5, 2026
When a caller reuses a contextId, prepend the persisted conversation so
the agent sees the full thread instead of only the latest message.

- protocol.format_history(): render prior messages as 'role: text' lines,
  bounded by A2A_HISTORY_INJECTION_LIMIT (default 20, max 200, 0 disables)
- adapter._prepare_task(): inject history before dispatch; audit and
  on-disk persistence keep the original (un-augmented) message so
  injected prefixes never accumulate in the log
- tests: 9 new cases covering empty history, role rendering, limit/env,
  injection on resume, original-only persistence, no-duplication over
  three turns

Closes NousResearch#64982 (superseded by this rebased implementation).
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/cli CLI entry point, hermes_cli/, setup wizard comp/gateway Gateway runner, session dispatch, delivery comp/plugins Plugin system and bundled plugins P3 Low — cosmetic, nice to have platform/feishu Feishu / Lark adapter 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-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants