Skip to content

Add Hermes Agent and Codex CLI providers (multi-provider runtime support) - #383

Open
josephndex wants to merge 1 commit into
pixel-agents-hq:mainfrom
josephndex:feat/hermes-codex-providers
Open

Add Hermes Agent and Codex CLI providers (multi-provider runtime support)#383
josephndex wants to merge 1 commit into
pixel-agents-hq:mainfrom
josephndex:feat/hermes-codex-providers

Conversation

@josephndex

Copy link
Copy Markdown

Description

Adds two new HookProvider integrations — Codex CLI and Hermes Agent — on top of a small runtime patch that lets AgentRuntime host more than one provider at once (previously hardcoded to exactly one). All three providers now share one AgentStateStore/office; agents from any of them render side by side, independently tracked.

Both new integrations are built against each CLI's actual current capabilities, checked directly rather than assumed:

  • Codex CLI (0.144.1) has no hooks/plugin API. It does write a structured, append-only JSONL rollout per session (~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonlsession_meta, response_item.function_call/function_call_output, event_msg.task_complete). codexTailer.ts polls that directory (byte-offset tracked per file, partial-line-safe) and re-emits parsed events through the existing POST /api/hooks/:providerId ingress — reading Codex's own structured format, not scraping terminal output.
  • Hermes Agent (v0.20.0) turns out to have three separate hook systems, and only one of them actually covers CLI sessions: gateway hooks fire only for messaging-platform sessions (Telegram/Discord/Slack/WhatsApp/Teams), shell hooks cover CLI but only a small fixed event set, and plugin hooks (ctx.register_hook() under ~/.hermes/plugins/) carry the full documented lifecycle catalog this needed (pre_tool_call/post_tool_call/on_session_start/on_session_end/on_session_finalize/subagent_start/subagent_stop). installHooks() drops a small stdlib-only Python plugin there and enables it via the documented hermes plugins enable CLI — no new dependency in Hermes's venv, no hand-edited config.yaml.

The runtime patch

AgentRuntime/HookEventHandler previously took one provider: HookProvider at construction. This PR changes the constructor to providers: HookProvider[] and builds one HookEventHandler per provider (each still scoped to exactly one provider internally — no shared mutable "current provider" field, so concurrent events from different providers can't race). All handlers share the runtime's store/timer Maps/SessionRouter seam. The module-level file-watcher/team singletons (setHookProvider, setFileWatcherHookProvider, setTeamProvider) stay bound to providers[0] (Claude) only, since neither new provider relies on the JSONL-scanning fallback path — this keeps Claude's existing behavior byte-for-byte identical (see Test plan).

Type of change

  • New feature

Test plan

  • npm run build && cd server && npx vitest run419 passed (377 pre-existing, unmodified, confirming Claude support has zero regressions + 42 new).
  • New: server/__tests__/codex.test.ts, server/__tests__/hermes.test.tsnormalizeHookEvent per event, formatToolStatus, reading-vs-writing tool classification.
  • New: server/__tests__/agentRuntime.multiProvider.test.ts — simultaneous sessions from different providers landing in one store tagged by providerId, duplicate-event handling, malformed/wrong-typed events never throwing, isolated tool state between concurrent agents on different providers, unknown-providerId events dropped safely, dispose() tearing down every provider handler.
  • Manual: ran a real codex exec session against a temp dir and confirmed the full parse → normalize → route pipeline fired end-to-end over the actual JSONL rollout Codex wrote (confirmed via server logs); confirmed the untracked-project-dir privacy gate (the same one Claude uses) correctly declined to visually adopt it, proving the gate applies uniformly across providers.
  • npm run lint, npm run check-types, npm run asyncapi:generate (no drift), npm run e2e:inventory (no drift) all pass clean.

E2E coverage

  • If user-visible behavior changed, an e2e test was added or updated under pixel-agents/e2e/tests/.
  • Explain below.

What e2e tests cover this change? No Playwright e2e was added. The existing suite's e2e model (e2e/README.md → "Mocking model & rules") is built around mocking a real claude binary via mock-claude/VS Code Electron fixtures — extending that pattern to two more real CLIs (codex, hermes) that don't have an equivalent lightweight mock harness felt like a separate, larger effort better scoped as its own follow-up rather than folded into this PR. The agentRuntime.multiProvider.test.ts Vitest suite exercises the same runtime code path (handleHookEventHookEventHandlerAgentStateStore) that a Playwright e2e test would ultimately assert against, just without the browser/webview layer — happy to add real e2e coverage as a follow-up if maintainers would rather have it in this PR before merge.

Known limitations (documented inline in the code)

  • Codex's JSONL rollout format has no explicit "session ended" record, so a session is heuristically closed after an idle timeout if the process doesn't clearly exit first.
  • Hermes only loads plugins at process start (HookRegistry.discover_and_load()), so a Hermes session already running before the plugin is installed cannot retroactively receive events — this is a Hermes limitation, not something this PR works around by restarting sessions.

Extends the single-provider AgentRuntime/HookEventHandler to route by
providerId across N concurrently-registered providers (one HookEventHandler
per provider, sharing one AgentStateStore), then adds two new providers on
that seam:

- Codex CLI: no hooks/plugin API exists (checked against codex-cli 0.144.1's
  full --help surface), but Codex does write a structured, append-only JSONL
  rollout per session at ~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl
  (session_meta, response_item.function_call/function_call_output,
  event_msg.task_complete). codexTailer.ts polls that directory (byte-offset
  tracked per file, partial-line-safe), and re-emits parsed events through
  the existing POST /api/hooks/:providerId ingress -- reading Codex's own
  structured format, not scraping terminal output.

- Hermes Agent: has three separate hook systems (checked against a live
  v0.20.0 install + its docs). Gateway hooks only fire for messaging-platform
  sessions, not CLI. Shell hooks cover CLI but only a small fixed event set.
  Plugin hooks (ctx.register_hook() under ~/.hermes/plugins/) are the one
  that covers CLI sessions with the full lifecycle catalog needed here
  (pre_tool_call/post_tool_call/on_session_start/on_session_end/
  on_session_finalize/subagent_start/subagent_stop). installHooks() drops a
  small stdlib-only Python plugin there and enables it via the documented
  `hermes plugins enable` CLI -- no dependency added to Hermes's venv, no
  hand-edited config.yaml.

Claude Code's own provider is untouched; all upstream tests still pass.
Adds 42 new tests (codex.test.ts, hermes.test.ts,
agentRuntime.multiProvider.test.ts) covering per-event normalization,
malformed/duplicate events, simultaneous multi-provider sessions sharing one
store, isolated tool state between concurrent agents, and dispose().

Known limitations documented inline: Codex's rollout format has no explicit
session-end record, so a session is heuristically closed after an idle
timeout; Hermes only loads plugins at process start, so a session already
running before installation won't retroactively receive events.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant