Phoenix LLM spans now show with real prompts and completions - #17
Merged
Conversation
- Updated the Dockerfile to build and install the `nemo-flow` CLI binary, enabling observability features. - Introduced a new `nemo-flow-finalize-shim` script to ensure proper session finalization and ATIF file generation for each conversation turn. - Enhanced the `generate-config.ts` to include NeMo-Flow shell hooks for event handling. - Updated the `start.sh` script to launch Hermes with the `nemo-flow` wrapper, facilitating telemetry and observability. - Added configuration files for NeMo-Flow, including `nemo-flow-plugins.toml.in` for observability settings. Signed-off-by: Matt Penn <mpenn@nvidia.com>
Hermes shell hooks ship sanitized metadata only, so Phoenix LLM spans carried counters instead of the actual prompt/completion. Hermes v0.14.0 passes the unsanitized request_messages and assistant_message to in-process plugin hooks, which a small plugin can forward to NeMo-Flow's gateway in the shape its adapter marks as exact-payload. - Upgrade Hermes from v0.11.0 (NemoClaw base image pin) to v0.14.0 via the same uv-tarball install pattern NemoClaw uses, with SHA256 verify. - Add the nemo-flow-bridge plugin (pre/post_api_request handlers, SDK response serializer, POSTs to NEMO_FLOW_GATEWAY_URL/hooks/hermes, fails open). - Drop pre/post_api_request from the shell-hook event list and add plugins.enabled so the plugin owns those events exclusively. - Set OTEL_BSP_MAX_EXPORT_BATCH_SIZE=1 / SCHEDULE_DELAY=100 on the gateway launch. The default 5s/512-span BatchSpanProcessor groups a turn's spans into one multi-span POST that the OpenShell L7 proxy ACKs with 200 but does not forward; single-span POSTs land reliably. Signed-off-by: Matt Penn <mpenn@nvidia.com>
- Updated the Hermes plugin to include pre/post_tool_call hooks, allowing for stable tool_call_ids to be synthesized and paired with their respective events, ensuring accurate telemetry in Phoenix spans. - Modified the generate-config.ts and SOUL.md files to reflect changes in event handling and credential usage. - Improved the start.sh script to export OpenTelemetry BatchSpanProcessor settings for immediate span flushing, preventing silent span loss during multi-scope turns. - Adjusted the plugin.yaml description to clarify the inclusion of tool call hooks. Signed-off-by: Matt Penn <mpenn@nvidia.com>
Signed-off-by: Matt Penn <mpenn@nvidia.com>
Signed-off-by: Matt Penn <mpenn@nvidia.com>
Signed-off-by: Matt Penn <mpenn@nvidia.com>
Collaborator
Signed-off-by: Matt Penn <mpenn@nvidia.com>
Signed-off-by: Matt Penn <mpenn@nvidia.com>
…ration - Simplified comments and improved clarity regarding the NeMo-Relay sidecar gateway and its interaction with Hermes processes. - Introduced a new `hermes-cli-shim` script to manage interactive shell invocations of the `hermes` CLI, ensuring proper telemetry flow and configuration handling. - Removed obsolete functions from `start.sh` related to rc-file management, streamlining the script for better maintainability. Signed-off-by: Matt Penn <mpenn@nvidia.com>
…rity - Updated the Dockerfile to replace the `finalize-shim` with a new `finalize-hook` script, enhancing the handling of session finalization events. - Improved comments in the Dockerfile and related scripts for better understanding of NeMo-Relay's interaction with Hermes. - Adjusted the `generate-config.ts` to reflect the new finalize hook and ensure proper event handling. - Streamlined the `start.sh` script by removing references to the obsolete config.toml, focusing on the necessary plugins.toml for observability. Signed-off-by: Matt Penn <mpenn@nvidia.com>
3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Summary
End-to-end NeMo-Relay observability for Hermes via a single persistent sidecar gateway. One daemon serves every Hermes surface — PID-1 gateway, Slack/Outlook bridge-driven turns, and interactive
hermes chatTUI — producing per-turn ATIF trajectory files at/tmp/atif/*.jsonplus Phoenix LLM spans carrying the real prompt + completion, paired tool spans (no orphans), and per-turn agent root spans. All four surfaces land in the same correlated Phoenix session.Architecture
start.shlaunches a long-running NeMo-Relay daemon at sandbox startup (start_nemo_relay_sidecarhelper, listens on127.0.0.1:4040). Every Hermes process discovers it viaNEMO_RELAY_GATEWAY_URLin env — exported explicitly on PID-1's launch list and via_PROXY_ENV_FILE(sourced by/sandbox/.bashrc) for interactive shells. The daemon fails hard on/healthztimeout: dumps the last 30 lines of/tmp/nemo-relay.logto stderr andexit 1s. Silent telemetry loss is worse than a noisy startup error.Two event paths feed the daemon:
generate-config.ts):on_session_*,pre/post_llm_call,subagent_stopvianemo-relay hook-forward hermes.plugins/nemo-relay/):pre/post_api_request+pre/post_tool_call— forwards the rich kwargs (full request messages, response SDK object, stable tool_call_ids) that the shell path strips.nemo-relay-finalize-hookruns as a secondon_session_endcommand to synthesize a per-turnon_session_finalize(Hermes's native finalize only fires on idle expiry). Daemon dispatches to ATIF (always-on) and OpenInference (gated byPHOENIX_COLLECTOR_ENDPOINT).Interactive
hermesinvocations from sandbox shells resolve through a PATH-prepended CLI shim at/usr/local/lib/nemoclaw/bin/hermes— warns onsetup/doctorabout the in-sandbox config lifecycle andexecs the upstream binary for everything else. Telemetry flows because the shell'sNEMO_RELAY_GATEWAY_URLpropagates to the exec'd process; the in-process plugin POSTs to the sidecar like any other Hermes surface.Sandbox workarounds
Seven non-obvious places where the OpenShell L7 proxy, OpenShell exec-session allowlist, OpenTelemetry batching, and PTY relay break observability or TUI bootstrap. Diagnosed empirically — keep these in mind if anything regresses.
OTEL_BSP_MAX_EXPORT_BATCH_SIZE=1 / SCHEDULE_DELAY=100 / EXPORT_TIMEOUT=2000(_export_otel_bsp_tunings()helper instart.sh, applied to the sidecar's env).tool_call_idbetween pre and post. Hermes firespre_tool_callwith empty id,post_tool_callwith the real one. NeMo-Relay's adapter synthesizes a fresh UUID per call → 2 unpaired spans per invocation (one carries{"status":"closed_by_agent_end"}). → Plugin synthesizes stable IDs fromSHA1(task_id, tool_name, args)and pairs pre/post via a boundeddeque(maxlen=512)FIFO.openshell:resolve:env:. SOUL.md taught the model that pattern by literal example → it rode in every LLM request's system prompt → OTLP egress carrying it got rejected, silently losing all LLM spans. → SOUL.md "Credential placeholders" section rewritten to teach the rule (recognize structured placeholders, use verbatim, don't refuse/parse/echo) without printing the trigger string.input.valuewas a lossy"Requested tools: …"summary. OpenInference's display fn finds messages viacontent.get("messages"). Sending the body as a bare list missed that path; the fallback picked up tool-role messages'namefield. → Plugin wraps as{"messages":[...], "model":..., "max_tokens":...}per NeMo-Relay's documentedLlmRequest.contentshape. Phoenix now showssystem: … \n\n user: <prompt> \n\n assistant: ….hermes/hermes chatlaunched the upstream binary withoutNEMO_RELAY_GATEWAY_URLin env, so the in-process plugin no-op'd → ATIF and Phoenix never saw TUI turns. Direct-path/usr/local/bin/hermesand non-bash shells bypassed any bash-function fix entirely. → PATH-prepended CLI shim at/usr/local/lib/nemoclaw/bin/hermes(installed via Dockerfile, prepended toPATHfrom_PROXY_ENV_FILE). Works for any shell — bash, zsh, sh, scripts. Shim blockssetup/doctorwith a one-line lifecycle warning, thenexecs/usr/local/bin/hermesfor every subcommand. Telemetry rides onNEMO_RELAY_GATEWAY_URLalready in shell env.hermes chatbanner hung on PyPI fetches. TUI banner enumeration triggers lazy installs (e.g._lazy_ensure("tts.edge")→uv pip install edge-tts), but the sandbox can't reach PyPI by policy. → Pre-install the relevant extras viaHERMES_UV_EXTRAS="messaging web cli edge-tts"; setHERMES_DISABLE_LAZY_INSTALLS=1as belt-and-suspenders for anything not pre-installed (set in both image ENV — for the gateway PID 1 — and_PROXY_ENV_FILE— for interactive shells, since the allowlist strips it from ENV).Plumbing
request_messages,assistant_message, response SDK object). uv-tarball install with SHA256 verification.nemo-relay-cliin the existing builder stage. (Upstream renamednemo-flow-cli→nemo-relay-cliat 0.3.0; this PR includes the rebrand migration.)agents/hermes/plugins/{nemoclaw,nemo-relay}/; non-plugin assets underagents/hermes/nemo-relay/{finalize-hook,hermes-cli-shim,plugins.toml.in}./usr/local/bin/nemo-relay(upstream daemon);/usr/local/lib/nemoclaw/bin/{hermes,nemo-relay-finalize-hook}(example-owned helpers — kept off default PATH so PID-1 hermes resolves the upstream binary directly)./etc/nemo-relay/plugins.tomlcarries the observability config (ATIF + OpenInference). Noconfig.toml— daemon mode doesn't use[agents.hermes].policy.yaml:phoenix_collectoregress for/usr/local/bin/nemo-relay.nemoclaw_status/nemoclaw_info/nemoclaw_reload_skillstools. No startup banner — info is on-demand via the tools.Test plan
bash scripts/tear-down.sh && bash scripts/bring-up.shbuilds cleanpgrep -af nemo-relayshows exactly one long-running daemon process (no per-invocation ephemeral gateways)curl -sf http://127.0.0.1:4040/healthzfrom inside the sandbox returns{"status":"ok"}ls /sandbox/.hermes-data/plugins/showsnemoclaw/andnemo-relay/input.value; one TOOL span per invocation; agent root closes per turn/tmp/atif/nemo-relay-atif-*.jsonlands per turn with fullmessagesarrayPOST /v1/traces 200 OKper spanPHOENIX_COLLECTOR_ENDPOINT, rebuild → ATIF still works, no Phoenix exportopenshell sandbox connect→which hermesreturns/usr/local/lib/nemoclaw/bin/hermes;type hermesshows the file path (not a function)hermes chatfrom the connected shell. Banner renders cleanly (no]11;rgb:…garbage in the input field, no PyPI-fetch hang). Drive a turn that uses a tool. Confirm the Phoenix span and/tmp/atif/*.jsonland for the TUI turn and correlate to the same sidecar PID as the Slack/Outlook turns above./exit) returns the user to the sandbox shell prompt — theopenshell sandbox connectsession stays alivebash -c 'hermes setup': shim prints the in-sandbox config lifecycle note, then runs the upstream wizard/usr/local/bin/hermes setup(full path) bypasses the shim warning (known and intentional — integrity hash at/sandbox/.hermes/.config-hashis the real boundary)Pending upstream work
Two Hermes upstream PRs may obsolete parts of this PR when they land:
When these land, the follow-up cleanup is small and well-scoped:
plugins/nemo-relay/__init__.py+ its directory + the"nemo-relay"entry inplugins.enabledingenerate-config.ts+ the DockerfileCOPYof it). The upstream plugin from #29724 takes overpre/post_api_requestandpre/post_tool_callforwarding.tool_call_id) becomes unnecessary if the middleware refactor in #29722 smooths Hermes's pre/post call sites or if the upstream plugin emits stable IDs.input.valuelossy summary) becomes unnecessary if the upstream plugin already wraps the request body in{messages, model, max_tokens}shape.