Skip to main content

Hermes Agent Runtime Integration README

This README started life as the reviewer guide for PR #194 (merged to dev 2026-05-05) and now serves as the operational guide for the Hermes-backed agent runtime. It documents the functional integration between the Humanwork agent-service contract and the Hermes runtime, with the Slack dogfood path as the primary slice.

Goal

Paul's requested EOD goal was not full Humanwork MVP completion. The target was a functional integration PR that makes the Hermes-flavor runtime usable by Human.work, with Slack dogfood as the northstar.

The integration problem was:

  • feat/agent-service defines the /v1/* contract surface the Platform calls.
  • feat/hermes-agent contains the real Hermes-backed runtime.
  • The contract branch had stub/fake endpoint behavior.
  • The Hermes runtime existed, but was not fully wired behind the Platform contract surface or the Slack ingestion path.

This PR wires the contract surface to the real runtime and keeps POST-MVP surfaces explicit instead of pretending they are implemented.

Primary Runtime Path

The main customer-facing Slack path is:

  1. Slack sends an Events API payload to POST /channels/slack/events.
  2. ChannelsController verifies or accepts the event according to configured Slack signing settings.
  3. The Slack event is normalized into a Humanwork channel message.
  4. Slack integration credentials are inspected to resolve the owning org from Slack metadata such as team_id, app_id, or channel_id.
  5. ConversationsService.findOrCreateByChannel() creates or reuses the Slack conversation.
  6. ConversationsService.sendMessage() persists the user message and calls AgentClient.chat().
  7. AgentClient calls the Python agent service /v1/chat contract endpoint.
  8. /v1/chat translates the v1 request into the Hermes runtime request shape.
  9. The Python runtime shells out to the real hermes chat CLI, without Hermes gateway.
  10. Hermes returns an assistant reply and, when available, a Hermes session id.
  11. The agent service maps the runtime response back into the v1 contract.
  12. The API persists the agent message and posts a Slack reply when a bot token is configured.

Important Implementation Details

No Fake Hermes On The Critical Path

The production path uses agent/hermes_client.py, which shells out to HERMES_CLI or hermes:

hermes chat -q <prompt> -Q -m <model> --provider <provider> -t <toolsets> --source tool --yolo

The fake Hermes fixture remains test-only. It is used by the fast opt-in API smoke at api/test/agent-runtime-e2e.spec.ts, but the real Slack dogfood smoke uses the actual local Hermes binary.

Session Mapping Fix

Humanwork conversation ids are not Hermes session ids. Passing a fresh Humanwork conversation id directly as hermes --resume <id> fails with real Hermes because the session does not exist yet.

The runtime now resumes Hermes only when a cached Humanwork-to-Hermes mapping exists:

  • First turn: no --resume; Hermes creates a real session.
  • Runtime parses Hermes session_id.
  • Runtime stores Humanwork session key -> Hermes session id.
  • Later turns: runtime passes the cached Hermes session id to --resume.

This is the core difference that fake-Hermes tests did not catch.

Slack Org Resolution

Slack inbound messages now try to resolve orgId from stored Slack integration credentials before calling the conversation service. The resolver checks common metadata/config spellings:

  • team_id / teamId
  • enterprise_id / enterpriseId
  • app_id / appId
  • channel_id / channelId

If no mapping is found, the route remains tolerant and creates the conversation without an org, matching the previous loose webhook behavior.

Conversation Id Propagation

ConversationsService.sendMessage() now passes the actual Platform conversation id into AgentClient.chat(). That means /v1/chat receives the contract-level conversation id instead of falling back to a Slack customer id.

Deferred POST-MVP Surfaces

The task/config/knowledge v1 endpoints are intentionally explicit 501 deferred responses for POST-MVP behavior. This avoids fake success responses on surfaces that are not part of the Slack/Hermes EOD slice.

Files To Review

Agent runtime and contract adapter:

  • agent/routers/chat.py
  • agent/chat_helpers.py
  • agent/hermes_client.py
  • agent/main.py
  • agent/prompts_loader.py
  • agent/routers/deferred.py
  • agent/routers/configure.py
  • agent/routers/knowledge.py
  • agent/routers/tasks.py
  • agent/routers/corrections.py
  • agent/corrections_client.py

API/platform integration:

  • api/src/common/agent.client.ts
  • api/src/conversations/conversations.service.ts
  • api/src/channels/channels.controller.ts
  • api/src/learning/learning.controller.ts
  • api/src/learning/learning.service.ts

Verification:

  • agent/evals/test_contract.py
  • agent/tests/test_chat.py
  • api/test/agent-client.spec.ts
  • api/test/conversations.spec.ts
  • api/test/channels.spec.ts
  • api/test/agent-runtime-e2e.spec.ts
  • api/test/slack-real-hermes-e2e.spec.ts

Contract docs:

  • agent/AGENT_API_SPEC.md
  • docs/api/agent-service-openapi.yaml

Environment Variables

Python agent runtime:

VariablePurpose
AGENT_SERVICE_SECRETShared secret expected as X-Agent-Secret.
HERMES_CLIPath to the real Hermes binary. Defaults to hermes.
HERMES_PROVIDERHermes provider, for example openrouter.
HERMES_MODELProvider model id, for example openai/gpt-4o-mini.
HERMES_TOOLSETSHermes toolsets, for example delegation.
HERMES_TIMEOUT_SECRuntime timeout for Hermes subprocess execution.
HUMANWORK_WORKSPACE_ROOTWorkspace root for runtime sessions/artifacts.

API/platform:

VariablePurpose
AGENT_SERVICE_URLPython agent base URL.
AGENT_SERVICE_SECRETShared secret forwarded to the Python agent.
SLACK_SIGNING_SECRETEnables Slack HMAC verification when configured.
SLACK_BOT_TOKENEnables outbound Slack chat.postMessage replies.

Verification Commands

Run agent tests:

cd agent
.venv/bin/python -m pytest tests evals -q

Run API build and default tests under the repo-compatible Node version:

cd api
source ~/.nvm/nvm.sh
nvm use 22.22.0
npm run build
npm test -- --runInBand

Run the fake-Hermes contract E2E:

cd api
source ~/.nvm/nvm.sh
nvm use 22.22.0
RUN_AGENT_RUNTIME_E2E=true npm test -- agent-runtime-e2e.spec.ts --runInBand

Run the real Slack/Hermes E2E:

cd api
source ~/.nvm/nvm.sh
nvm use 22.22.0
RUN_SLACK_REAL_HERMES_E2E=true npm test -- slack-real-hermes-e2e.spec.ts --runInBand

The real E2E starts a local Python agent service and points it at the real Hermes CLI. It sends a simulated Slack Events API payload through the Nest Slack endpoint and waits for a real Hermes-generated agent reply to be persisted.

Verified In This PR

The latest pushed verification evidence for PR #194 (merged to dev 2026-05-05):

  • Real Hermes CLI smoke passed with openrouter and openai/gpt-4o-mini.
  • Real Python /v1/chat smoke passed with HERMES_CLI=/Users/koristuvac/.local/bin/hermes.
  • Real Slack/Hermes E2E passed: RUN_SLACK_REAL_HERMES_E2E=true npm test -- slack-real-hermes-e2e.spec.ts --runInBand.
  • Agent full suite passed: 491 passed, 14 skipped.
  • API build passed under Node 22.22.0.
  • API Jest passed under Node 22.22.0: 32 suites passed, 2 skipped; 374 passed, 376 total.
  • PR checks are green.

Known Boundaries

  • Live outbound Slack posting requires a real SLACK_BOT_TOKEN or per-org Slack integration credential with bot_token. The code path is wired and tested with the Slack WebClient mocked; the real-Hermes E2E disables outbound Slack posting so it does not need a live workspace token.
  • Full v1 contract coverage is a stretch goal beyond the Slack EOD path. The non-Slack POST-MVP endpoints return explicit deferred 501 envelopes instead of fake success responses.

Post-Merge Notes

PR #194 merged with three known issues flagged by the Greptile security review. They were accepted as non-blocking for the merge but are tracked as follow-ups (GitHub issues to be filed):

  1. HMAC verifyHmac crashes on wrong-length signatures. In api/src/agent-api/agent-api.service.ts, crypto.timingSafeEqual throws a RangeError when the attacker-supplied signature is a different byte length than the expected HMAC. The route should normalize and return 401, not surface a 500. → GitHub issue to be filed.
  2. has_more pagination bug in getConversationMessages. The flag is computed against the unfiltered total instead of the returned page, so has_more is incorrect after filtering. Clients that paginate by following has_more can loop or miss the end. The check should compare msgs.length === limit. → GitHub issue to be filed.
  3. Non-timing-safe PLATFORM_API_TOKEN comparison. assertPlatformToken in api/src/learning/learning.controller.ts uses !== instead of crypto.timingSafeEqual. Low impact (single fixed token, narrow surface) but easy to harden. → GitHub issue to be filed.