Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/integration-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

name: integration-smoke

# End-to-end check that x-dynamo-trajectory-id emitted by this package
# End-to-end check that x-dynamo-session-id emitted by this package
# round-trips through Dynamo's actual frontend + mocker into the request trace
# sink. Builds Dynamo from ai-dynamo/dynamo@main on every run — published
# wheels lag behind features (e.g. the agent_trace sink), so we need source
Expand Down Expand Up @@ -66,6 +66,9 @@ jobs:
with:
python-version: "3.11"

- name: Test Hermes plugin
run: python3 -m unittest discover -s hermes-plugin/tests

- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@master
with:
Expand Down
10 changes: 5 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ SPDX-License-Identifier: Apache-2.0
Repo layout:

- `pi-plugin/` — Pi extension registering a `dynamo` provider for Dynamo's OpenAI-compatible chat-completions endpoint.
- `hermes-plugin/` — Hermes middleware plugin that injects Dynamo trajectory headers from Hermes `session_id`.
- `hermes-plugin/` — Hermes middleware plugin that injects Dynamo session headers from Hermes `session_id`.

The Pi plugin has three source files under `pi-plugin/src/`:

- `index.ts` — thin re-export of the light implementation.
- `src/light/provider.ts` — config + streamSimple wrapper. Reads `DYN_REQUEST_TRACE`, `DYN_AGENT_*`, and `PI_SUBAGENT_*` env vars. When tracing is enabled, stamps `x-dynamo-trajectory-id` / parent headers and leaves Pi `sessionId` untouched.
- `src/light/provider.ts` — config + streamSimple wrapper. Reads `DYN_REQUEST_TRACE`, `DYN_AGENT_*`, and `PI_SUBAGENT_*` env vars. When tracing is enabled, stamps `x-dynamo-session-id` / parent headers and leaves Pi `sessionId` untouched.
- `src/light/tool-relay.ts` — ZMQ PUSH publisher for Pi tool events. Connects to a Dynamo-bound PULL endpoint. Wire format: `[topic, seq_be_u64, msgpack(RequestTraceRecord)]`.

## Build, test, check
Expand All @@ -28,7 +28,7 @@ npm run build # tsc -p tsconfig.build.json → dist/

Pi tests live in `pi-plugin/test/` as siblings of `pi-plugin/src/`. Use vitest's `describe`/`it`/`expect`. Mirror the existing structure: one test file per source file, fixture data inline rather than separate fixture files.

`pi-plugin/test/integration/smoke.mjs` is the out-of-band end-to-end check — driven by `pi-plugin/scripts/integration-smoke.sh`, not vitest. It boots Dynamo's frontend + mocker, sends one real chat completion, and asserts `x-dynamo-trajectory-id` becomes `trajectory_id` in the request trace JSONL. Two cases: top-level trajectory id and the pi-subagents bridge. Mocker output is garbage; assertions only target the trace envelope. CI clones `ai-dynamo/dynamo@main` and builds from source. Cargo cache keeps warm runs ~60-90s, cold ~10 min. `workflow_dispatch` accepts a `dynamo_ref` input for ad-hoc validation against a specific branch, tag, or SHA.
`pi-plugin/test/integration/smoke.mjs` is the out-of-band end-to-end check — driven by `pi-plugin/scripts/integration-smoke.sh`, not vitest. It boots Dynamo's frontend + mocker, sends one real chat completion, and asserts `x-dynamo-session-id` becomes `session_id` in the request trace JSONL. Two cases: top-level session id and the pi-subagents bridge. Mocker output is garbage; assertions only target the trace envelope. CI clones `ai-dynamo/dynamo@main` and builds from source. Cargo cache keeps warm runs ~60-90s, cold ~10 min. `workflow_dispatch` accepts a `dynamo_ref` input for ad-hoc validation against a specific branch, tag, or SHA.

For real Pi CLI lifecycle validation against a Dynamo endpoint, read `pi-plugin/skills/pi-headless-dynamo/SKILL.md` first and drive the actual interactive Pi TUI instead of faking provider requests or pi-subagents env.

Expand Down Expand Up @@ -61,7 +61,7 @@ python3 -m unittest discover -s hermes-plugin/tests
| Prefix | Direction | Examples |
|---|---|---|
| `DYNAMO_*` | client config (we read) | `DYNAMO_BASE_URL`, `DYNAMO_API_KEY` |
| `DYN_AGENT_*` | optional trajectory override / subagent parent link | `DYN_AGENT_TRAJECTORY_ID`, `DYN_AGENT_PARENT_TRAJECTORY_ID` |
| `DYN_AGENT_*` | optional session override / subagent parent link | `DYN_AGENT_SESSION_ID`, `DYN_AGENT_PARENT_SESSION_ID` |
| `DYN_REQUEST_TRACE*` | request trace switch and tool bridge | `DYN_REQUEST_TRACE`, `DYN_REQUEST_TRACE_TOOL_EVENTS_ZMQ_ENDPOINT` |
| `PI_SUBAGENT_*` | pi-subagents bookkeeping (we read only) | `PI_SUBAGENT_CHILD`, `PI_SUBAGENT_RUN_ID`, `PI_SUBAGENT_CHILD_AGENT`, `PI_SUBAGENT_CHILD_INDEX` |
| `OPENAI_BASE_URL` | OpenAI-compatibility fallback (we read) | only consulted when `DYNAMO_BASE_URL` is unset |
Expand All @@ -83,6 +83,6 @@ External contributions are not currently accepted. This is an NVIDIA-internal co

## What to leave alone

- Dynamo owns the request trace schema. The Pi provider stamps trajectory headers for LLM requests and keeps explicit tool calls on the ZMQ trace path. The Hermes plugin only stamps request headers.
- Dynamo owns the request trace schema. The Pi provider stamps session headers for LLM requests and keeps explicit tool calls on the ZMQ trace path. The Hermes plugin only stamps request headers.
- The `request.trace.v1` schema is owned upstream by Dynamo (`dynamo/lib/llm/src/request_trace/`). Don't change record shapes here without an upstream PR landing first.
- `pi-plugin/package-lock.json` churn from npm version differences should be reverted before committing (`git checkout -- pi-plugin/package-lock.json` if a no-op edit appears).
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ Small agent integrations for Dynamo request tracing.
## Layout

- `pi-plugin/` - Pi provider plugin for Dynamo's OpenAI-compatible endpoint.
- `hermes-plugin/` - Hermes middleware plugin that maps Hermes `session_id` to `x-dynamo-trajectory-id`.
- `hermes-plugin/` - Hermes middleware plugin that maps Hermes `session_id` to `x-dynamo-session-id`.

Each plugin owns its own tests and install instructions.
8 changes: 4 additions & 4 deletions hermes-plugin/README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
# Hermes Dynamo Trajectory Plugin
# Hermes Dynamo Session Plugin

Hermes plugin that copies the current Hermes `session_id` into Dynamo's `x-dynamo-trajectory-id` request header.
Hermes plugin that copies the current Hermes `session_id` into Dynamo's `x-dynamo-session-id` request header.

## Install

```bash
git clone https://github.com/ai-dynamo/agent-plugins.git ~/agent-plugins
mkdir -p ~/.hermes/plugins
ln -sfnT ~/agent-plugins/hermes-plugin ~/.hermes/plugins/dynamo_trajectory
hermes plugins enable dynamo_trajectory
ln -sfnT ~/agent-plugins/hermes-plugin ~/.hermes/plugins/dynamo_session
hermes plugins enable dynamo_session
```

## Validate
Expand Down
6 changes: 3 additions & 3 deletions hermes-plugin/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

"""Inject Hermes session IDs as Dynamo trajectory headers."""
"""Inject Hermes session IDs as Dynamo session headers."""

HEADER = "x-dynamo-trajectory-id"
_PATCHED_ATTR = "_dynamo_trajectory_headers_patched"
HEADER = "x-dynamo-session-id"
_PATCHED_ATTR = "_dynamo_session_headers_patched"


def register(ctx) -> None:
Expand Down
4 changes: 2 additions & 2 deletions hermes-plugin/plugin.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: dynamo_trajectory
name: dynamo_session
version: "0.1.0"
description: "Optional Dynamo trajectory header injection for Hermes."
description: "Optional Dynamo session header injection for Hermes."
author: NVIDIA
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@


def load_plugin():
spec = importlib.util.spec_from_file_location("dynamo_trajectory_plugin", PLUGIN_PATH)
spec = importlib.util.spec_from_file_location("dynamo_session_plugin", PLUGIN_PATH)
module = importlib.util.module_from_spec(spec)
assert spec.loader is not None
spec.loader.exec_module(module)
return module


class DynamoTrajectoryPluginTest(unittest.TestCase):
class DynamoSessionPluginTest(unittest.TestCase):
def test_pre_api_request_patches_openai_client_creation(self):
plugin = load_plugin()
calls = []
Expand Down Expand Up @@ -52,7 +52,7 @@ def _create_openai_client(self, client_kwargs, *, reason, shared):
self.assertEqual(calls[0][0], "pre_api_request")
self.assertEqual(result["default_headers"]["x-test"], "1")
self.assertEqual(
result["default_headers"]["x-dynamo-trajectory-id"],
result["default_headers"]["x-dynamo-session-id"],
"hermes-session",
)

Expand Down
43 changes: 22 additions & 21 deletions pi-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,17 @@ A Pi extension that registers a `dynamo` provider backed by [Dynamo](https://git
pi --model dynamo/<model-id>
```

With one switch (`DYN_REQUEST_TRACE=1`) it also stamps Dynamo trajectory headers, gives each pi-subagent its own trajectory id, and can relay Pi tool events into the trace — all without patching `pi-mono`.
With one switch (`DYN_REQUEST_TRACE=1`) it also stamps Dynamo session headers, gives each pi-subagent its own session id, and can relay Pi tool events into the trace — all without patching `pi-mono`.

## What it does

- **Model provider** — registers `dynamo`, discovers models from `/v1/models` (falls back to `dynamo/default`), and streams via Pi's OpenAI-compatible path.
- **Trajectory headers** — adds `x-dynamo-trajectory-id` and optional parent headers so Dynamo can attribute each LLM request as a trajectory in its trace.
- **Subagent trajectory ids** — gives each [pi-subagents](https://github.com/nicobailon/pi-subagents) child its own trajectory id. See [Subagent trajectory ids](#subagent-trajectory-ids).
- **Session headers** — adds `x-dynamo-session-id` and optional parent headers so Dynamo can attribute each LLM request as a session in its trace.
- **Subagent session ids** — gives each [pi-subagents](https://github.com/nicobailon/pi-subagents) child its own session id. See [Subagent session ids](#subagent-session-ids).
- **Tool-event relay** — optionally pushes Pi `tool_start` / `tool_end` / `tool_error` events to Dynamo over ZMQ so one trace shows LLM spans and tool spans together.

Everything but the bare model provider is gated by the `DYN_REQUEST_TRACE` master switch and is off by default.
Session headers carry identity only; they do not activate sticky or session-aware routing.

## Install

Expand All @@ -35,31 +36,31 @@ Point Pi at a running Dynamo endpoint:
```bash
export DYNAMO_BASE_URL=http://127.0.0.1:8000/v1
export DYNAMO_API_KEY=dummy # local Dynamo usually ignores this; defaults to dynamo-local
export DYN_REQUEST_TRACE=1 # opt into trajectory tracing + optional tool relay
export DYN_REQUEST_TRACE=1 # opt into session tracing + optional tool relay

pi --model dynamo/<model-id> -p "Reply exactly ok."
```

That's the whole required setup. Everything else is only set when you want to override it — see [Configuration](#configuration).

## Subagent trajectory ids
## Subagent session ids

When `DYN_REQUEST_TRACE=1`, the provider preserves Pi's normal `sessionId` and adds explicit Dynamo trajectory headers.
When `DYN_REQUEST_TRACE=1`, the provider preserves Pi's normal `sessionId` and adds explicit Dynamo session headers.

```mermaid
sequenceDiagram
participant Root as Root pi process
participant Child as Subagent pi process
participant Dynamo
Root->>Dynamo: x-dynamo-trajectory-id = S_root
Child->>Dynamo: x-dynamo-trajectory-id = T_child, parent = S_root
Root->>Dynamo: x-dynamo-session-id = S_root
Child->>Dynamo: x-dynamo-session-id = S_child, parent = S_root
```

- The root `trajectory_id` is Pi's own `sessionId`.
- The child `trajectory_id` is the subagent's own identity (`PI_SUBAGENT_RUN_ID:PI_SUBAGENT_CHILD_AGENT:PI_SUBAGENT_CHILD_INDEX`), so it needs no extra operator setup.
- The provider sends those values as `x-dynamo-trajectory-id` and `x-dynamo-parent-trajectory-id`.
- The root `session_id` is Pi's own `sessionId`.
- The child `session_id` is the subagent's own identity (`PI_SUBAGENT_RUN_ID:PI_SUBAGENT_CHILD_AGENT:PI_SUBAGENT_CHILD_INDEX`), so it needs no extra operator setup.
- The provider sends those values as `x-dynamo-session-id` and `x-dynamo-parent-session-id`.

> ZMQ tool records can include parent/child **trajectory ids** when `DYN_AGENT_TRAJECTORY_ID` is set on the root. See [Trajectory linking](#trajectory-linking).
> ZMQ tool records can include parent/child **session ids** when `DYN_AGENT_SESSION_ID` is set on the root. See [Session linking](#session-linking).

## Configuration

Expand All @@ -69,14 +70,14 @@ The only thing you must set is the connection (`DYNAMO_BASE_URL`) and, to enable
| --- | --- | --- |
| `DYNAMO_BASE_URL` | `http://127.0.0.1:8000/v1` | Dynamo endpoint root (falls back to `OPENAI_BASE_URL`). |
| `DYNAMO_API_KEY` | `dynamo-local` | Bearer token. |
| `DYN_REQUEST_TRACE` | off | **Master switch.** When truthy (`1`/`true`/`yes`/`on`), enables Dynamo trajectory headers and the tool relay. |
| `DYN_AGENT_TRAJECTORY_ID` | unset | Optional parent trajectory seed for [trajectory linking](#trajectory-linking) in subagents. |
| `DYN_AGENT_PARENT_TRAJECTORY_ID` | unset | Parent trajectory; set manually to override the bridge. |
| `DYN_REQUEST_TRACE` | off | **Master switch.** When truthy (`1`/`true`/`yes`/`on`), enables Dynamo session headers and the tool relay. |
| `DYN_AGENT_SESSION_ID` | unset | Optional parent session seed for [session linking](#session-linking) in subagents. |
| `DYN_AGENT_PARENT_SESSION_ID` | unset | Parent session; set manually to override the bridge. |
| `DYN_REQUEST_TRACE_TOOL_EVENTS_ZMQ_ENDPOINT` | unset | Dynamo-bound ZMQ PULL endpoint for the tool relay. |

`PI_SUBAGENT_CHILD` / `PI_SUBAGENT_RUN_ID` / `PI_SUBAGENT_CHILD_AGENT` / `PI_SUBAGENT_CHILD_INDEX` are **read, never set** — pi-subagents populates them and the provider uses them to derive the child `trajectory_id` and parent link.
`PI_SUBAGENT_CHILD` / `PI_SUBAGENT_RUN_ID` / `PI_SUBAGENT_CHILD_AGENT` / `PI_SUBAGENT_CHILD_INDEX` are **read, never set** — pi-subagents populates them and the provider uses them to derive the child `session_id` and parent link.

With `DYN_REQUEST_TRACE` on, the provider does not mutate request payloads. It adds Dynamo trajectory headers and `x-request-id` when absent.
With `DYN_REQUEST_TRACE` on, the provider does not mutate request payloads. It adds Dynamo session headers and `x-request-id` when absent.

<details>
<summary>Tool-event wire format</summary>
Expand All @@ -90,9 +91,9 @@ When a tool-event endpoint is set, Pi connects a ZMQ PUSH socket and sends one m
The record uses Dynamo's `dynamo.request.trace.v1` schema (`event_type`, `event_source`, `agent_context`, and a `tool` object with timing/status). Dynamo owns the PULL bind side, so multiple Pi processes and subagents can all connect as producers. Terminal `tool_end` / `tool_error` records are self-contained.
</details>

## Trajectory linking
## Session linking

The provider keeps parent and child trajectory ids distinct for ZMQ tool records. When a pi-subagents child inherits the parent's `DYN_AGENT_TRAJECTORY_ID`, the provider reinterprets it as the child's `parent_trajectory_id` and synthesizes a fresh child `trajectory_id` (`runId:childAgent:childIndex`), mutating `process.env` so nested chains stay attributable. Setting `DYN_AGENT_PARENT_TRAJECTORY_ID` manually overrides the parent link. If you don't set `DYN_AGENT_TRAJECTORY_ID` at all, every subagent still gets its own child trajectory id — only the explicit parent-to-child link is absent.
The provider keeps parent and child session ids distinct for ZMQ tool records. When a pi-subagents child inherits the parent's `DYN_AGENT_SESSION_ID`, the provider reinterprets it as the child's `parent_session_id` and synthesizes a fresh child `session_id` (`runId:childAgent:childIndex`), mutating `process.env` so nested chains stay attributable. Setting `DYN_AGENT_PARENT_SESSION_ID` manually overrides the parent link. If you don't set `DYN_AGENT_SESSION_ID` at all, every subagent still gets its own child session id — only the explicit parent-to-child link is absent.

## Local Dynamo

Expand Down Expand Up @@ -122,13 +123,13 @@ npm run test # vitest
npm run build # -> dist/
```

`scripts/integration-smoke.sh` boots Dynamo's frontend + mocker and asserts `x-dynamo-trajectory-id` becomes `trajectory_id` in the trace; it is the out-of-band end-to-end check.
`scripts/integration-smoke.sh` boots Dynamo's frontend + mocker and asserts `x-dynamo-session-id` becomes `session_id` in the trace; it is the out-of-band end-to-end check.

## Troubleshooting

- **`/v1/models` empty** — wait for the backend to load; confirm frontend and worker share the same discovery/request/event planes and `DYN_FILE_KV`.
- **Model unknown** — `curl "$DYNAMO_BASE_URL/models"` and use the returned id as `dynamo/<id>`; restart Pi if discovery failed before Dynamo was ready.
- **No agent_context in trace rows** — make sure `DYN_REQUEST_TRACE` is set and Dynamo is new enough to map `x-dynamo-trajectory-id`.
- **No agent_context in trace rows** — make sure `DYN_REQUEST_TRACE` is set and Dynamo is new enough to map `x-dynamo-session-id`.
- **Tool spans missing** — set a tool-event endpoint on both sides and confirm the run actually used tools.

## Scope
Expand Down
2 changes: 1 addition & 1 deletion pi-plugin/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "pi-dynamo-provider",
"version": "0.1.0",
"description": "Pi extension package that registers a Dynamo OpenAI-compatible provider with trajectory headers.",
"description": "Pi extension package that registers a Dynamo OpenAI-compatible provider with session headers.",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
Expand Down
4 changes: 2 additions & 2 deletions pi-plugin/scripts/integration-smoke.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
# SPDX-License-Identifier: Apache-2.0
#
# Start a Dynamo frontend + mocker worker against a known-good Dynamo version,
# then run test/integration/smoke.mjs which asserts that x-dynamo-trajectory-id
# becomes trajectory identity in the trace sink. Tears down processes on exit.
# then run test/integration/smoke.mjs which asserts that x-dynamo-session-id
# becomes session identity in the trace sink. Tears down processes on exit.
#
# Required env:
# DYNAMO_TEST_MODEL_ID HuggingFace model id for the mocker tokenizer
Expand Down
Loading
Loading