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
1 change: 1 addition & 0 deletions .agents/skills/contribute-api/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ runtime or bindings.
- Decide whether the CLI, PyO3 binding, Python SDK, type stubs, schemas, or the
Python and TypeScript adapter-contract bindings must expose the new surface
- Keep every affected public surface in parity
- `RunRequest.relay_session_root` is forwarded as the typed `AgentRunRequest.relay_session_root` field. Keep Rust, Python, TypeScript, and schema representations in parity. Context keys do not control Relay propagation; preserve the adapter fallback for unusable UUIDs.
- Update docs and examples in the same branch

## Minimum Acceptance
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ NeMo Fabric separates consumers from execution targets through a stable, version
## Documentation

- [Quickstart](docs/getting-started/quickstart.mdx): Run a first agent through the Python SDK.
- [Conversation telemetry](docs/sdk/python.mdx): Use `RunRequest.relay_session_root` to group Deep Agents and mini-SWE-agent turns under one NeMo Relay session.
- [Installation](docs/getting-started/install.mdx): Install the runtime, adapters, harness dependencies, and telemetry components, and choose a deployment scenario.
- [Adapter catalog](sdk/python/nemo-fabric-adapter-catalog/README.md): Inspect bundled adapter metadata on a host without installing harness SDKs.
- [Host admission](docs/sdk/python.mdx#inspect-metadata-on-a-separate-host): Validate requested capabilities and reject host/task descriptor drift before execution.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,7 @@ class AgentRunRequest(AgentContractBlock):
"""Southbound invocation request passed to an adapter target."""

input: JsonValue = _json_value_field()
relay_session_root: str | None = _optional()
context: dict[str, JsonValue] = _json_dict()


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@
},
"input": {
"description": "Request payload for the adapter target."
},
"relay_session_root": {
"description": "UUID propagation root shared across conversation turns.",
"type": [
"string",
"null"
]
}
},
"required": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,8 @@ export interface AgentRunRequest {
* Request payload for the adapter target.
*/
input: JsonValue;
/**
* UUID propagation root shared across conversation turns.
*/
relay_session_root?: string | null;
}
13 changes: 13 additions & 0 deletions adapters/python/common/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,19 @@ other request IDs remain available as `nemo_fabric_request_id` metadata. The
helper always preserves that metadata so Relay-backed streaming can identify
the active turn.

Pass `request.relay_session_root` as the helper's `session_root` to let a
caller group invocations into one Relay session. SDK callers set
`RunRequest.relay_session_root`; core forwards it as the typed
`AgentRunRequest.relay_session_root` field. Context keys do not control Relay
propagation.
When that value is a UUID string Relay accepts, it becomes the propagated root
and is recorded as `nemo_fabric_session_root` metadata. A UUID request ID stays
the parent; otherwise the session root is also the parent. Relay derives ATIF
`session_id` from the root, so invocations that share a session root export
separate trajectories under one session. A missing value, a non-string, a
string that is not a UUID, the nil UUID, or a UUID whose final eight bytes are
zero falls back to the request root without an error.

This helper does not apply to adapters that send telemetry through an external
Relay gateway or whose upstream integration creates an isolated scope context.
Those adapters retain their native session correlation until their Relay
Expand Down
32 changes: 26 additions & 6 deletions adapters/python/common/src/nemo_fabric_adapters/common/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,20 +259,40 @@ def native_telemetry_config(payload: dict[str, Any]) -> dict[str, Any]:
return config if isinstance(config, dict) else {}


def relay_request_context(request_id: str) -> tuple[Any, dict[str, str]]:
"""Use a UUID request ID as Relay's propagated root and preserve metadata."""

metadata = {"nemo_fabric_request_id": request_id}
def _uuid_or_none(value: object) -> str | None:
if not isinstance(value, str):
return None
try:
request_uuid = str(uuid.UUID(request_id))
parsed = uuid.UUID(value)
except ValueError:
return None
# Relay derives the OTel span id from the low 8 bytes and rejects a zero one.
if not any(parsed.bytes[8:]):
return None
return str(parsed)


def relay_request_context(
request_id: str,
session_root: str | None = None,
) -> tuple[Any, dict[str, str]]:
"""Root Relay at a usable session root, else a UUID request ID; preserve metadata."""

metadata = {"nemo_fabric_request_id": request_id}
request_uuid = _uuid_or_none(request_id)
session_uuid = _uuid_or_none(session_root)

root_uuid = session_uuid or request_uuid
if root_uuid is None:
return nullcontext(), metadata
if session_uuid is not None:
metadata["nemo_fabric_session_root"] = session_uuid

from nemo_relay import PropagationContext
from nemo_relay import create_scope_stack_from_propagation
from nemo_relay import use_scope_stack

propagation = PropagationContext(request_uuid, root_uuid=request_uuid)
propagation = PropagationContext(request_uuid or root_uuid, root_uuid=root_uuid)
stack = create_scope_stack_from_propagation(propagation)
return use_scope_stack(stack), metadata

Expand Down
6 changes: 6 additions & 0 deletions adapters/python/deepagents/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,12 @@ includes the NeMo Relay Python package.
- The top-level invocation runs inside a
`nemo_relay.scope.scope("deepagents-request", nemo_relay.ScopeType.Agent)`
scope, so the whole NeMo Fabric turn is captured under one Agent scope.
A UUID `request_id` roots that scope's propagation. To group turns into one
Relay session, send the same UUID string as
`RunRequest.relay_session_root`
on each invocation: it becomes the propagated root, a UUID `request_id`
stays the parent (otherwise the session root is), and ATIF exports one trajectory per turn under one `session_id`.
Values Relay cannot use fall back to the request root without an error.
- `NemoRelayDeepAgentsCallbackHandler()` is added to the LangGraph run config
(without dropping consumer-provided callbacks) to capture LangGraph scopes
and human-in-the-loop interrupt/resume marks.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -735,6 +735,7 @@ async def invoke(
user_message,
request_id,
runtime_context.invocation_id,
request.relay_session_root,
)

if outcome.error is None:
Expand Down Expand Up @@ -807,6 +808,7 @@ async def _invoke_with_telemetry(
user_message: str,
request_id: str,
invocation_id: str,
session_root: str | None,
) -> TurnOutcome:
"""Run one turn inside the Relay plugin/scope, isolating telemetry faults.

Expand Down Expand Up @@ -836,7 +838,7 @@ async def _invoke_with_telemetry(
# turn, which would lose one of the two.
try:
request_context, metadata = common_utils.relay_request_context(
request_id
request_id, session_root
)
metadata["nemo_fabric_invocation_id"] = invocation_id
with request_context:
Expand Down
4 changes: 3 additions & 1 deletion adapters/python/mini-swe-agent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,12 +153,14 @@ hierarchy without requiring upstream changes.
### Correlation IDs

The top-level `mini-swe-agent.request` Agent scope stores the NeMo Fabric
request and invocation IDs as Relay metadata:
request and invocation IDs, and a caller-supplied session root, as Relay
metadata:

| NeMo Fabric ID | Relay metadata key | Meaning |
| --- | --- | --- |
| `request_id` | `nemo_fabric_request_id` | Correlates the caller's logical request. A caller can provide the same value when it wants to correlate retries or related processing. |
| `invocation_id` | `nemo_fabric_invocation_id` | Identifies one concrete invocation attempt. NeMo Fabric assigns a new value to each invocation. |
| `RunRequest.relay_session_root` | `nemo_fabric_session_root` | Groups invocations into one Relay session. A UUID string Relay accepts becomes the propagated Relay root; a UUID `request_id` stays the parent, otherwise the session root is also the parent. Absent when the value is missing or unusable; the invocation proceeds without an error. |

Nested step, LLM, and `bash` tool events are correlated through the Relay scope
hierarchy; they do not repeat these metadata fields. The `runtime_id`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,9 @@ async def invoke(
inherited_quarantine = self._telemetry_quarantine is not None
telemetry_errors: list[str] = []
if self._relay_enabled:
result, telemetry_errors = await self._run_with_relay(task, context)
result, telemetry_errors = await self._run_with_relay(
task, context, request.relay_session_root
)
else:
result = await self._run_agent(task)
failed = result.get("exit_status") != "Submitted"
Expand Down Expand Up @@ -188,6 +190,7 @@ async def _run_with_relay(
self,
task: str,
context: contract.RuntimeContext,
session_root: str | None,
) -> tuple[dict[str, Any], list[str]]:
if self._telemetry_quarantine is not None:
self._agent.begin_relay_invocation(None)
Expand All @@ -208,7 +211,7 @@ async def _run_with_relay(
) as activation:
common_utils.reject_inherited_relay_plugin_config(activation.report)
request_context, metadata = common_utils.relay_request_context(
context.request_id
context.request_id, session_root
)
metadata["nemo_fabric_invocation_id"] = context.invocation_id
with (
Expand Down
3 changes: 3 additions & 0 deletions crates/fabric-core/src/agent_execution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ use thiserror::Error;
pub struct AgentRunRequest {
/// Request payload for the adapter target.
pub input: Value,
/// UUID propagation root shared across conversation turns.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub relay_session_root: Option<String>,
/// Caller-provided task, rollout, workflow, or application context.
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub context: BTreeMap<String, Value>,
Expand Down
1 change: 1 addition & 0 deletions crates/fabric-core/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6723,6 +6723,7 @@ mod tests {
let mut descriptor = load_adapter_descriptor(&path).expect("Claude descriptor");
let request = AgentRunRequest {
input: serde_json::json!("review"),
relay_session_root: None,
context: BTreeMap::new(),
extensions: BTreeMap::from([("profile".to_string(), serde_json::json!("strict"))]),
};
Expand Down
38 changes: 38 additions & 0 deletions crates/fabric-core/src/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ static LOCAL_SERVICES: LazyLock<Mutex<BTreeMap<String, Arc<Mutex<LocalServiceRec
pub struct RunRequest {
/// Request id.
pub request_id: String,
/// Optional UUID propagation root shared across conversation turns.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub relay_session_root: Option<String>,
/// Request payload for the harness.
#[serde(default)]
pub input: Value,
Expand All @@ -93,6 +96,7 @@ impl RunRequest {
pub fn text(input: impl Into<String>) -> Self {
Self {
request_id: new_id("request"),
relay_session_root: None,
input: Value::String(input.into()),
context: BTreeMap::new(),
overrides: None,
Expand Down Expand Up @@ -2684,6 +2688,7 @@ fn project_agent_run_request(request: &RunRequest) -> Result<AgentRunRequest> {
};
Ok(AgentRunRequest {
input: request.input.clone(),
relay_session_root: request.relay_session_root.clone(),
context: request.context.clone(),
extensions,
})
Expand Down Expand Up @@ -4730,6 +4735,7 @@ for line in sys.stdin:
fn agent_run_request_projection_keeps_only_southbound_fields() {
let request = RunRequest {
request_id: "request-1".to_string(),
relay_session_root: None,
input: serde_json::json!({"task": "review"}),
context: BTreeMap::from([("rollout".to_string(), serde_json::json!(2))]),
overrides: Some(serde_json::json!({"acme": {"mode": "strict"}})),
Expand All @@ -4752,6 +4758,38 @@ for line in sys.stdin:
);
}

#[test]
fn typed_session_root_projects_into_adapter_request() {
let root = "018f47a4-3af7-7d94-8e61-9f0f89b5d312";
let mut request = RunRequest::text("first turn");
request.relay_session_root = Some(root.to_string());
request.context.insert(
"relay_session_root".to_string(),
serde_json::json!("legacy"),
);
let projected = project_agent_run_request(&request).expect("project request");
assert_eq!(projected.relay_session_root.as_deref(), Some(root));
assert_eq!(projected.context["relay_session_root"], "legacy");
let wire = serde_json::to_value(&projected).unwrap();
assert_eq!(wire["relay_session_root"], root);
let decoded_adapter: AgentRunRequest = serde_json::from_value(wire).unwrap();
assert_eq!(decoded_adapter, projected);
assert_eq!(request.context["relay_session_root"], "legacy");
let decoded: RunRequest =
serde_json::from_value(serde_json::to_value(&request).unwrap()).unwrap();
assert_eq!(decoded.relay_session_root.as_deref(), Some(root));
request.relay_session_root = None;
let projected = project_agent_run_request(&request).unwrap();
assert_eq!(projected.relay_session_root, None);
assert_eq!(projected.context["relay_session_root"], "legacy");
assert!(
serde_json::to_value(projected)
.unwrap()
.get("relay_session_root")
.is_none()
);
}

#[test]
fn agent_run_request_projection_rejects_non_object_overrides() {
let mut request = RunRequest::text("review");
Expand Down
9 changes: 9 additions & 0 deletions docs/adapter-contract/tutorials/execution.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,15 @@ for the exact shape. Runtime identity belongs in `RuntimeContext`, not in
`AgentConfig.workflow`. Per-invocation task input belongs in the request, not in
workflow settings.

For in-process Relay telemetry, the optional typed
`AgentRunRequest.relay_session_root` carries the caller's conversation UUID.
Pass it directly to `relay_request_context(context.request_id,
request.relay_session_root)` when opening the invocation Agent scope. A usable
root groups trajectories from separate invocations under one Relay session.
Context keys do not control propagation. Omit the field to retain per-request
behavior. Runtime and adapter-contract versions must support the field when
it is supplied.

**Success Check**: The adapter reads correlation and environment context from
`RuntimeContext` and treats every ID as opaque.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2550,6 +2550,7 @@ The model defines the following fields:
| Field | Type | Required | Default | Constraints | Description |
| --- | --- | --- | --- | --- | --- |
| `input` | `Any` | No | `''` | — | — |
| `relay_session_root` | `str \| None` | No | `None` | — | UUID propagation root shared across conversation turns; unusable values fall back to the request ID. |
| `request_id` | `str` | No | `<generated>` | `MinLen(min_length=1)` | — |
| `context` | `dict[str, Any]` | No | `dict()` | — | — |
| `overrides` | `dict[str, Any] \| None` | No | `None` | — | — |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ SPDX-License-Identifier: Apache-2.0 */}

Generated from `cargo doc --no-deps -p nemo-fabric-core`.

<pre className="rust-signature"><code dangerouslySetInnerHTML={{"__html": "pub struct AgentRunRequest &#123;\n pub input: <a href=\"https://docs.rs/serde_json/1.0.150/serde_json/value/enum.Value.html\">Value</a>,\n pub context: <a href=\"https://doc.rust-lang.org/1.94.0/alloc/collections/btree/map/struct.BTreeMap.html\">BTreeMap</a>&lt;<a href=\"https://doc.rust-lang.org/1.94.0/alloc/string/struct.String.html\">String</a>, <a href=\"https://docs.rs/serde_json/1.0.150/serde_json/value/enum.Value.html\">Value</a>&gt;,\n pub extensions: <a href=\"https://doc.rust-lang.org/1.94.0/alloc/collections/btree/map/struct.BTreeMap.html\">BTreeMap</a>&lt;<a href=\"https://doc.rust-lang.org/1.94.0/alloc/string/struct.String.html\">String</a>, <a href=\"https://docs.rs/serde_json/1.0.150/serde_json/value/enum.Value.html\">Value</a>&gt;,\n&#125;"}} /></pre>
<pre className="rust-signature"><code dangerouslySetInnerHTML={{"__html": "pub struct AgentRunRequest &#123;\n pub input: <a href=\"https://docs.rs/serde_json/1.0.150/serde_json/value/enum.Value.html\">Value</a>,\n pub relay_session_root: <a href=\"https://doc.rust-lang.org/1.94.0/core/option/enum.Option.html\">Option</a>&lt;<a href=\"https://doc.rust-lang.org/1.94.0/alloc/string/struct.String.html\">String</a>&gt;,\n pub context: <a href=\"https://doc.rust-lang.org/1.94.0/alloc/collections/btree/map/struct.BTreeMap.html\">BTreeMap</a>&lt;<a href=\"https://doc.rust-lang.org/1.94.0/alloc/string/struct.String.html\">String</a>, <a href=\"https://docs.rs/serde_json/1.0.150/serde_json/value/enum.Value.html\">Value</a>&gt;,\n pub extensions: <a href=\"https://doc.rust-lang.org/1.94.0/alloc/collections/btree/map/struct.BTreeMap.html\">BTreeMap</a>&lt;<a href=\"https://doc.rust-lang.org/1.94.0/alloc/string/struct.String.html\">String</a>, <a href=\"https://docs.rs/serde_json/1.0.150/serde_json/value/enum.Value.html\">Value</a>&gt;,\n&#125;"}} /></pre>

Southbound invocation request passed to an adapter target.

Expand All @@ -20,6 +20,10 @@ Southbound invocation request passed to an adapter target.

Request payload for the adapter target.

### `relay_session_root: Option<String>`

UUID propagation root shared across conversation turns.

### `context: BTreeMap<String, Value>`

Caller-provided task, rollout, workflow, or application context.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ SPDX-License-Identifier: Apache-2.0 */}

Generated from `cargo doc --no-deps -p nemo-fabric-core`.

<pre className="rust-signature"><code dangerouslySetInnerHTML={{"__html": "pub struct RunRequest &#123;\n pub request_id: <a href=\"https://doc.rust-lang.org/1.94.0/alloc/string/struct.String.html\">String</a>,\n pub input: <a href=\"https://docs.rs/serde_json/1.0.150/serde_json/value/enum.Value.html\">Value</a>,\n pub context: <a href=\"https://doc.rust-lang.org/1.94.0/alloc/collections/btree/map/struct.BTreeMap.html\">BTreeMap</a>&lt;<a href=\"https://doc.rust-lang.org/1.94.0/alloc/string/struct.String.html\">String</a>, <a href=\"https://docs.rs/serde_json/1.0.150/serde_json/value/enum.Value.html\">Value</a>&gt;,\n pub overrides: <a href=\"https://doc.rust-lang.org/1.94.0/core/option/enum.Option.html\">Option</a>&lt;<a href=\"https://docs.rs/serde_json/1.0.150/serde_json/value/enum.Value.html\">Value</a>&gt;,\n&#125;"}} /></pre>
<pre className="rust-signature"><code dangerouslySetInnerHTML={{"__html": "pub struct RunRequest &#123;\n pub request_id: <a href=\"https://doc.rust-lang.org/1.94.0/alloc/string/struct.String.html\">String</a>,\n pub relay_session_root: <a href=\"https://doc.rust-lang.org/1.94.0/core/option/enum.Option.html\">Option</a>&lt;<a href=\"https://doc.rust-lang.org/1.94.0/alloc/string/struct.String.html\">String</a>&gt;,\n pub input: <a href=\"https://docs.rs/serde_json/1.0.150/serde_json/value/enum.Value.html\">Value</a>,\n pub context: <a href=\"https://doc.rust-lang.org/1.94.0/alloc/collections/btree/map/struct.BTreeMap.html\">BTreeMap</a>&lt;<a href=\"https://doc.rust-lang.org/1.94.0/alloc/string/struct.String.html\">String</a>, <a href=\"https://docs.rs/serde_json/1.0.150/serde_json/value/enum.Value.html\">Value</a>&gt;,\n pub overrides: <a href=\"https://doc.rust-lang.org/1.94.0/core/option/enum.Option.html\">Option</a>&lt;<a href=\"https://docs.rs/serde_json/1.0.150/serde_json/value/enum.Value.html\">Value</a>&gt;,\n&#125;"}} /></pre>

A request passed to a NeMo Fabric-managed harness runtime.

Expand All @@ -19,6 +19,10 @@ A request passed to a NeMo Fabric-managed harness runtime.

Request id.

### `relay_session_root: Option<String>`

Optional UUID propagation root shared across conversation turns.

### `input: Value`

Request payload for the harness.
Expand Down
Loading
Loading