Skip to content

feat(tools/wot_engine): add Web-of-Thought multi-agent reasoning - #20158

Closed
Abd0r wants to merge 4 commits into
NousResearch:mainfrom
Abd0r:feat/wot-engine
Closed

feat(tools/wot_engine): add Web-of-Thought multi-agent reasoning#20158
Abd0r wants to merge 4 commits into
NousResearch:mainfrom
Abd0r:feat/wot-engine

Conversation

@Abd0r

@Abd0r Abd0r commented May 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a self-contained multi-agent reasoning engine that coordinates 3-7 LLM agents through a shared message bus. Generic agents (no role taxonomy hardcoded) talk to each other across four communication modes — parallel, streaming, sequential, queue — over any OpenAI-compatible backend. Exposed as a single Hermes tool (wot_chat) under a new wot toolset, plus a methodology skill at skills/coordination/web-of-thought/.

This is a separate concern from PRs #19607 / #19796 (free-tier search backends). They touch different surfaces and are independently reviewable.

Files

  • tools/wot_engine.py — engine + tool registration (1,034 lines)
  • tests/tools/test_wot_engine.py — 36 unit tests, all green
  • skills/coordination/web-of-thought/SKILL.md — methodology guidance for callers (when to invoke, how to design agents, mode selection, cost discipline)

Engine design

  • No role taxonomy. Agents are differentiated only by caller-supplied name + system_prompt. Engine is content-agnostic; the model decides agent personalities dynamically.
  • Four communication modes:
    • parallel — all agents react to the task simultaneously, see peers' completed messages on round boundaries
    • streaming — same as parallel but agents see partial CoT tokens as they're generated
    • sequential — round-robin; each agent gets full prior transcript
    • queue — tag-driven pull (agents declare interests, only act when relevant tag appears)
  • Backend probe at startup. Detects llama.cpp / Ollama / vLLM / OpenAI-compat. Uses id_slot pinning + cache_prompt: true on llama.cpp for KV-cache reuse across agents. Strips trailing /v1 from base_url so callers can pass either form.
  • Reasoning content extraction. Reads delta.reasoning_content separately from delta.content for thinking-mode models (DeepSeek-R1, QwQ, Qwen3.5/3.6 with thinking on). Stored in Message.reasoning so peer messages can choose to propagate raw / strip / summarize CoT (the propagate_reasoning knob; summary is currently stubbed to strip — flagged in the docstring).
  • Cost rails. Per-channel token_budget, per-agent turn_timeout via asyncio.wait_for, monotonic seq per agent on Message envelope.
  • AgentSpec auto-sanitization — real LLM callers emit names like "Critical Thinker" or "Agent A". Whitespace becomes _, disallowed characters dropped. Raises only if the result is empty.
  • Caller-supplied model field is stripped from inner agent specs at wot_chat_tool boundary — outer Hermes tends to hallucinate names like gpt-4o. Engine uses LLM_DEFAULT_MODEL (env-driven) for all inner agents.

Hermes integration

  • Tool registration is at module top-level (not wrapped in try/except) so tools/registry.py:_module_registers_tools AST scanner picks it up.
  • wot toolset is auto-created at module load time via toolsets.create_custom_toolset(...) so -t wot validates without modifying toolsets.py.
  • Skill loads via --skills coordination/web-of-thought and prefixes the system prompt with methodology guidance.

How this fits next to existing Hermes multi-agent surfaces

Hermes already ships several multi-agent / delegation primitives. WoT is additive, not redundant — it fills a specific gap none of them serve.

Capability delegate_task mixture_of_agents Kanban WoT (wot_chat)
Parent sees children's intermediate outputs summary only aggregator-only polled comments full transcript every turn
Children talk to each other no (per #344) no cross-reference polling comments direct @name-mentions
Children see each other's CoT no no no streaming mode pipes partial CoT
Multi-round refinement one-shot per child one-shot per reference model heavyweight (board cycle) native, default 5 rounds
Process model subprocess per child parallel HTTP calls cross-process, durable in-process asyncio
Latency floor process spawn time API round-trip DB persist + claim single API round-trip per agent per round
State persistence none (ephemeral) none (ephemeral) SQLite-backed none (live in-memory)
Best for durable cross-process delegation with isolation Best-of-N synthesis via aggregator long-running multi-profile workflows live multi-perspective reasoning within one task

What WoT specifically adds: in-process live multi-agent reasoning where inner agents can address each other directly and the outer agent sees the full transcript as it forms. That's the niche the existing surfaces don't fill — delegate_task deliberately hides intermediate output, MoA's reference models don't see each other, and Kanban's polling comments aren't real-time. Several long-open feature requests (#412 consensus/voting, #376 adversarial debate, #479 best-of-N + judge, #5876 multi-agent council) all reduce to this missing primitive.

WoT does not replace any of the above. Compose: outer Hermes can call delegate_task for durable cross-process work, dispatch wot_chat for live debate within its own turn, and use Kanban for cross-session orchestration. They're complementary.

Validated end-to-end

Setup: Ubuntu 24.04 + RTX 4050. Isolated Hermes install (separate HERMES_HOME, no overlap with any production setup).

1. Local llama.cpp + Qwen3-4B-Instruct-Q4_K_M (--parallel 4 --jinja --ctx-size 65536):

  • 5/5 sessions completed, 48 WoT messages across runs, 0 inner errors
  • Range of behaviors: deep multi-round debate (18 msgs over 6 rounds), smart short-circuit on triviality (3 msgs in 1 round when all agents emit DONE), self-healing on bad arg shapes (Hermes retried with corrected payload)

2. OpenRouter + DeepSeek-V4-Flash (with skill loaded):

  • 5/5 sessions completed, 23 WoT messages, 1 inner error (token truncation mid-thinking on round 3 of one run; engine surfaced it cleanly via errors[])
  • Skill methodology measurably moved model behavior toward leaner invocations: avg agent name length ~10 chars (vs ~22 unloaded), max_rounds: 3 explicitly set on 5/5, token_budget on 3/5, ~43% latency drop vs no-skill baseline

Coverage — honest framing

Integration-validated end-to-end (with V4 Flash via OpenRouter as inner agents, full session JSONL captured):

  • parallel mode — 5/5 sessions clean, 48 WoT messages, multi-round @-mention emergence
  • streaming mode — 22 streaming chunks + 3 final messages produced, stop_reason=all_done clean
  • sequential mode — agent ordering preserved across 2 rounds with cross-round @-mentions (round-2 alpha addresses round-1 beta)
  • queue mode — interests tags drove tag-prefixed output ([design][code][review]), 3 rounds completed
  • Per-agent turn_timeout — standalone test, 2/2 agents timed out at 2.0s as configured, errors surfaced via errors[]
  • Backend probe (llama.cpp + OpenRouter), slot pinning on llama.cpp
  • Reasoning content extraction (R1 + V4 Flash thinking traces visible in transcripts)
  • AgentSpec auto-sanitization (model-emitted role-y names sanitized cleanly)
  • Caller-supplied model stripping (saved a run when V4 Flash hallucinated gpt-4o)
  • /v1 suffix doubling fix (caught the OpenRouter 404)
  • Hermes tool auto-discovery + custom toolset registration
  • Skill load + methodology effect on model behavior (43% latency drop, lean invocations)

Routing-validated (engine routes correctly; downstream model output quality is upstream's concern):

  • Ollama native /api/chat path for thinking models — backend probe identifies kind='ollama', request hits /api/chat (not /v1/), parses both message.content and message.thinking fields. Tested live with deepseek-r1:1.5b and deepseek-r1:7b. Output quality of small R1 distills + Ollama template handling is broken upstream (well-known) — engine correctly returns whatever Ollama emits.

Unit-test only (no integration run on this PR):

  • vLLM backend branch — code path exists, would need a vLLM-serving instance to validate. Same probe + dispatcher pattern as the validated paths, low risk.

Multi-backend mix — integration-validated (added in second commit b1e8872):

  • AgentSpec now has optional base_url + api_key fields for per-agent backend override
  • _LLMClient caches backend probes per-base_url so each unique target is only probed once
  • Validated live with one session running two agents on different backends simultaneously:
    • alpha on DeepSeek-V4-Flash via OpenRouter (https://openrouter.ai/api/v1)
    • beta on deepseek-r1:1.5b via local Ollama (http://127.0.0.1:11434)
  • Probe cache after run showed both: https://openrouter.ai/api → openai-compat and http://127.0.0.1:11434 → ollama
  • 0 engine errors; both responses assembled into the transcript with correct from-attribution
  • Defensive design: wot_chat_tool boundary strips model + base_url + api_key from outer-Hermes-supplied args (Hermes hallucinates them); direct Python callers using AgentSpec(base_url=..., api_key=...) still work

Stubbed:

  • propagate_reasoning="summary" — currently behaves identically to "strip". A real summary mode would distill peer CoT through a small model; deferred to a follow-up. Docstring is honest about this.

Linked issues

Closes (auto-close on merge):

Refs (does not auto-close — partial coverage):

Test plan

  • pytest -p no:xdist tests/tools/test_wot_engine.py — 36/36 passing on this branch
  • Engine integration tested against llama.cpp + Qwen3-4B-Instruct (5/5 sessions, 0 errors)
  • Engine integration tested against OpenRouter + DeepSeek-V4-Flash (5/5 sessions, 1 truncation surfaced honestly)
  • Skill load + invocation-pattern A/B tested (skill measurably moves model behavior)
  • CI green (will fix anything pytest tests/ flags)

Backwards compatibility

Pure-add. New tool, new toolset, new skill, new test file. Zero changes to existing code paths.

License

MIT (auto per CONTRIBUTING.md).

@Abd0r Abd0r changed the title feat(tools/wot_engine): Web-of-Thought multi-agent reasoning feat(tools/wot_engine): add Web-of-Thought multi-agent reasoning May 5, 2026
@alt-glitch alt-glitch added type/feature New feature or request comp/tools Tool registry, model_tools, toolsets P3 Low — cosmetic, nice to have labels May 5, 2026
@Abd0r Abd0r closed this May 6, 2026
@Abd0r Abd0r reopened this May 6, 2026

@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 substantial implementation and test coverage. The current main checkout does not already contain this capability, but the proposed integration has blocking behavior gaps.

Problems

  • tools/wot_engine.py:662-708 builds each streaming request before streaming begins, while :816-822 only runs those fixed requests concurrently. Published chunks cannot enter another agent's in-flight request, so the advertised same-round streaming exchange is not implemented.
  • tools/wot_engine.py:647-653 and :735-742 produce only DONE tags and never set to_agent; @name addressing and queue interests therefore cannot route ordinary messages.
  • tools/wot_engine.py:918-930 strips tool-call backend fields, then :766-771 uses LLM_*/OLLAMA_URL instead of Hermes's active provider and credential resolution.
  • tools/wot_engine.py:1060-1092 adds an always-available static toolset. Current model_tools.py:389-445 includes all toolsets by default, so this adds the schema to ordinary sessions rather than gating it.

Suggested changes

  • Re-scope through a plugin/MCP or make a deliberately configured, provider-integrated implementation; implement or accurately narrow the routing/streaming contracts and cover them end-to-end.

Automated hermes-sweeper review.

Comment thread tools/wot_engine.py
break

if mode in ("parallel", "streaming"):
turn_fn = (lambda a: a.turn_streaming(round_no)) if mode == "streaming" \

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 only starts independent streams concurrently. Each agent has already drained its inbox and built messages at turn_streaming() entry, so chunks published while another request is in flight cannot affect that request. The advertised same-round streaming exchange needs a different transport/protocol, or this mode should be described as transcript streaming.

Comment thread tools/wot_engine.py Outdated
content=content,
reasoning=resp.reasoning,
round=round_no,
tags=["DONE"] if is_done else [],

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.

There is no parsing of @name or topic syntax before this Message is published: to_agent remains None and normal messages receive no tags. That makes both direct addressing and the queue mode's interests matching unreachable for agent-produced output.

Comment thread tools/wot_engine.py
sanitized: List[Dict[str, Any]] = []
for a in agents:
spec = dict(a)
for k in ("model", "base_url", "api_key"):

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.

The tool schema exposes agents[].model, but this removes it and the engine later uses LLM_DEFAULT_MODEL plus LLM_BASE_URL/LLM_API_KEY. A Hermes tool should use the active agent's resolved provider, credentials, fallbacks, and endpoint rather than unrelated environment variables.

Comment thread tools/wot_engine.py Outdated
"parameters": wot_chat_tool_schema["function"]["parameters"],
},
handler=_wot_handler,
check_fn=lambda: True,

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.

Because this import also adds wot to static TOOLSETS, current model_tools.py includes this tool in the default all-toolsets path. An unconditional check function therefore makes this a default schema addition, not an explicit opt-in capability.

@@ -0,0 +1,101 @@
---
name: web-of-thought
description: When to invoke wot_chat (multi-agent reasoning) and how to design the inner agents — when this beats answering directly.

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 description exceeds the repository's 60-character skill-description limit and does not end with a period. Please also update the skill to the required modern section structure before landing it.

@teknium1 teknium1 added sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-massive Sweeper blast radius: massive — everyone, every turn (invariant surface) labels Jul 12, 2026
@Abd0r

Abd0r commented Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

Partial re-scope for hermes-sweeper review

Salvageability was correctly tagged low. This update does not claim a full rewrite.

Landed (46acd7aa7)

  1. Opt-in tool surfacewot_chat check_fn is no longer always-true. Requires HERMES_ENABLE_WOT=1 (or true/yes/on) so ordinary sessions do not get the multi-agent schema by default.
  2. Honest contracts — module docstring + skill now state:
    • streaming = chunked peer partials around completed peer streams in a round, not mid-generation injection into another in-flight request
    • queue / @name / interests routing is not implemented yet (only DONE early-stop)
    • inner agents use LLM_BASE_URL / LLM_API_KEY / LLM_DEFAULT_MODEL (or OLLAMA_URL), not the parent Hermes session provider

Still open (would be a follow-up / plugin path)

  • True same-round streaming exchange (architecturally hard on OpenAI-compat)
  • @name / interests message routing end-to-end
  • Wire inner agents through Hermes provider + credential resolution
  • Optional: move entirely to plugin/MCP as the review suggested

Happy to close this PR in favor of a narrower plugin design if maintainers prefer that over keeping the experimental engine in-tree.


H.A.M Fixed by H.A.M · status by H.A.M
(opt-in gate + contract narrowing landed; full provider/routing/stream rewrite not claimed)

@Abd0r
Abd0r force-pushed the feat/wot-engine branch from 46acd7a to 11f36ef Compare July 14, 2026 15:24
@Abd0r

Abd0r commented Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

Attribution CI fix

check-attribution was failing on two older tip commits authored as:

syedabdurrehman@Syeds-MacBook-Air.local

That local machine email is not in scripts/release.py AUTHOR_MAP. The mapped identity ra2157218@gmail.comAbd0r already is.

Fix: rewrote author/committer on the PR commits only (filter-branch over the 3 tip commits) to:

Syed Abdur Rehman Ali <ra2157218@gmail.com>

No code change in this push — history identity only. Force-pushed tip: 11f36efa1.

check-attribution is green on the re-run.


Still intentional residual for this PR (not claimed fixed here):

  • full WoT rewrite (Hermes provider wiring, real @name/interests routing, true same-round streaming)
  • branch still sits on an older base — Docker/matrix failures from age are separate from attribution

H.A.M Fixed by H.A.M · attribution identity rewrite

Abd0r added 4 commits July 14, 2026 21:13
Adds a self-contained multi-agent reasoning engine that coordinates
3-7 LLM agents through a shared message bus. Generic agents (no role
taxonomy) talk to each other across four communication modes — parallel,
streaming, sequential, queue — over any OpenAI-compatible backend.

The engine is exposed as a single Hermes tool, `wot_chat`, registered
under a new `wot` toolset. Caller passes agent specs (name +
system_prompt) and a task; the engine orchestrates the conversation and
returns a structured transcript for the outer agent to synthesize.

Files:
- tools/wot_engine.py — engine + tool registration (~1040 lines)
- tests/tools/test_wot_engine.py — 36 unit tests, all green
- skills/coordination/web-of-thought/SKILL.md — methodology guidance
  (when to invoke, how to design agents, mode selection, cost discipline)

Engine specifics:
- Backend probe at startup: detects llama.cpp / Ollama / vLLM /
  OpenAI-compat. Uses id_slot pinning + cache_prompt: true on llama.cpp
  for KV-cache reuse across agents.
- Reasoning content extraction: handles delta.reasoning_content from
  thinking-mode models (DeepSeek-R1, QwQ, etc.) separately from content,
  so peer messages can choose to propagate raw / strip / summarize CoT.
- Per-agent timeout via asyncio.wait_for, per-channel token budget,
  monotonic seq counter on Message envelope for stream debugging.
- AgentSpec auto-sanitizes whitespace in names (real LLMs emit
  "Critical Thinker" / "Agent A"); raises only when sanitized name is
  empty.
- /v1 suffix is stripped from base_url at client init so callers can
  pass either form (http://host:8088 OR http://host:8088/v1) without
  doubling.
- Hermes tool registration is at module top-level (not wrapped in
  try/except) so tools/registry.py:_module_registers_tools picks it
  up via AST scan.

Skill methodology:
- When to invoke wot_chat (multi-perspective questions, tradeoffs,
  decisions with real downside) and when NOT to (lookups, single-fact,
  simple chat).
- Agent design rule: minimal differentiating system_prompt, no
  scripted personas, no role-cargo names. Engine remains role-agnostic.
- Mode selection: parallel (default) / streaming / sequential / queue.
- Cost discipline: max_rounds: 2-3 for most cases, set token_budget
  for hard caps.
- Reading the result: errors first, then agents_done, then transcript.

Validated end-to-end on Ubuntu 24.04 + RTX 4050 with two model
configurations:
1. Local llama.cpp + Qwen3-4B-Instruct-Q4_K_M (--parallel 4 --jinja):
   5/5 sessions completed, 48 WoT messages across runs, 0 inner errors.
2. OpenRouter + DeepSeek-V4-Flash (with skill loaded): 5/5 sessions,
   skill methodology measurably moved model behavior toward leaner
   invocations (avg agent name ~10 chars vs ~22 unloaded; max_rounds
   explicitly set 5/5; 43% latency drop).

License: MIT (auto per CONTRIBUTING.md).
Adds per-agent base_url + api_key fields to AgentSpec, enabling a single
WoT session to mix backends (e.g. one agent on local Ollama, another on
OpenRouter). _LLMClient caches backend probes per-base_url so each unique
target is only probed once across the run.

Engine changes:
- AgentSpec: new optional fields base_url + api_key
- _LLMClient: _probe_cache: Dict[str, BackendInfo], ensure_probed() now
  takes optional base_url_override and caches per-target
- _resolve_target() helper composes the right URL + auth headers per call
- _openai_payload_for(backend, ...) takes backend explicitly (so id_slot +
  cache_prompt only land when the THIS request actually targets llama-server)
- complete() and stream() take base_url_override + api_key_override kwargs
- _stream_openai and _stream_ollama_native take per-call base + headers
- Agent.turn_batch + turn_streaming pass spec.base_url + spec.api_key
- wot_chat_tool boundary strips model + base_url + api_key from outer-Hermes
  args (defensive: outer model hallucinates these); direct Python callers
  using AgentSpec(base_url=..., api_key=...) still work

Tests:
- 39/39 unit tests passing (up from 36)
- New: MultiBackendMixTests verifies per-agent base_url threads to client
- New: WotChatToolStripsCallerControlFields verifies tool boundary strips
  caller-supplied model/base_url/api_key

Validated end-to-end:
- One WoT session with 2 agents on different backends:
  - alpha on DeepSeek-V4-Flash via OpenRouter
  - beta on deepseek-r1:1.5b via local Ollama
- Probe cache shows both targets:
  https://openrouter.ai/api → openai-compat
  http://127.0.0.1:11434 → ollama
- 0 engine errors, both transcripts assembled with correct from-attribution
Partial salvage for @teknium1 hermes-sweeper review on NousResearch#20158
(salvageability=low — full same-round stream injection + Hermes
provider integration remain out of scope for this PR).

- Gate wot_chat behind HERMES_ENABLE_WOT so the tool is not always-on
  in ordinary sessions (check_fn was previously always True).
- Narrow advertised streaming mode: post-completion peer partials, not
  mid-flight injection into another agent's open request.
- Document that queue @name / interests routing is not implemented yet
  (only DONE early-stop).
- Document LLM_BASE_URL credential path vs Hermes session provider.
- Skill.md aligned with the narrowed contract.
- resolve_hermes_endpoint(): Hermes runtime/config first, LLM_BASE_URL
  escape hatch, then OLLAMA/localhost; surface source in result.backend
- parse_message_routing(): @AgentName DMs + #tag interests on publish
- queue mode: history-based eligibility so drained inboxes still wake on
  prior-round DMs/tags; DMs always wake target regardless of interests
- honest mode/schema/toolset docs (streaming ≠ mid-token injection)
- skill: ≤60-char description, Hermes provider, routing table
- tests for routing parse, DM delivery, queue interests, endpoint meta
@Abd0r
Abd0r force-pushed the feat/wot-engine branch from 11f36ef to 9f7a8d9 Compare July 14, 2026 15:49
@Abd0r

Abd0r commented Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

Option B salvage landed — tip 9f7a8d9dd

Rebased onto current main + substantive salvage for teknium1's review blockers:

Provider (Hermes session credentials)

  • New resolve_hermes_endpoint(): Hermes runtime / config / resolve_provider_client / resolve_runtime_provider first
  • Explicit LLM_BASE_URL remains an escape hatch for multi-backend labs
  • Then OLLAMA_URL / localhost
  • Result surfaces backend.endpoint_source, provider, default_model

Routing (@name + #tags)

  • parse_message_routing(): first known @Agent → DM (Message.to_agent); #tag → tags
  • Channel already delivers DMs only to the named peer
  • Queue mode eligibility is history-based (prior rounds):
    • @Name always wakes that agent
    • #tag matching interests wakes matching agents
    • empty interests → any prior broadcast wakes
  • Fixes the old "inbox drain eats DMs before next-round wake" gap

Streaming honesty (unchanged behaviour, clearer contract)

  • Schema + skill + module docs: streaming = round-boundary peer partials, not mid-token injection into an open peer request

Skill rewrite

  • Description under the 60-char bar
  • Hermes provider + routing table + modes

Tests

pytest tests/tools/test_wot_engine.py49 passed (routing parse, DM delivery, queue interests, endpoint meta)

Still intentional residual

  • True same-round mid-generation injection remains impossible on OpenAI-compat (documented)
  • propagate_reasoning=summary still acts as strip
  • Core multi-agent surface is still opt-in (HERMES_ENABLE_WOT=1); CONTRIBUTING may still prefer plugin for heavy multi-agent — happy to follow maintainer call

Attribution rewrite (prior tip) remains: all commits as ra2157218@gmail.com / AUTHOR_MAP → Abd0r.


H.A.M Fixed by H.A.M

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

Labels

comp/tools Tool registry, model_tools, toolsets P3 Low — cosmetic, nice to have sweeper:blast-massive Sweeper blast radius: massive — everyone, every turn (invariant surface) sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades type/feature New feature or request

Projects

None yet

3 participants