Skip to content

Fix SDK session runtime lifecycle - #16

Merged
AjayThorve merged 3 commits into
mainfrom
ajay/fix-session-runtime-lifecycle
Jun 24, 2026
Merged

Fix SDK session runtime lifecycle#16
AjayThorve merged 3 commits into
mainfrom
ajay/fix-session-runtime-lifecycle

Conversation

@AjayThorve

@AjayThorve AjayThorve commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • SDK execution/session APIs now use the core runtime lifecycle: start/invoke/stop. Stream is a buffered wrapper over invoke; cancel is SDK-side cooperative cleanup over stop.
  • Key Hermes SDK sessions by Fabric runtime_id and resume via Hermes SessionDB.
  • Remove the extra SDK session id surface and update session docs/smokes.

Validation

  • cargo test
  • cargo check -p fabric-python
  • cargo fmt --all --check
  • python3 -m pytest tests/test_hermes_sdk_adapter.py tests/test_session.py -q
  • python3 python/tests/smoke_sdk_sessions.py
  • RUN_FABRIC_HERMES_INTEGRATION=1 "$HERMES_PYTHON" tests/smoke_hermes_session.py

Summary by CodeRabbit

  • New Features
    • Added native runtime lifecycle bindings to start/invoke/stop runtimes from Python.
    • Multi-turn sessions now run on a single persistent native runtime across turns for continuity (stable runtime identity).
  • Bug Fixes
    • Session conversation context is now loaded from the runtime session history store for more reliable carryover.
    • Improved Hermes SDK session behavior to consistently map runtime identity to session history/output.
  • Documentation
    • Updated Fabric and Hermes-SDK guidance for session-based execution and adapter invocation expectations.
  • Tests
    • Updated/expanded smoke and unit tests to validate session lifecycle, streaming events, and Hermes SDK runtime/history mapping.
  • Breaking Changes
    • Removed FabricSessionUnsupportedError from the package’s public re-exports.

@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: e2f405be-9283-43f0-8f81-d1dd5f8d5927

📥 Commits

Reviewing files that changed from the base of the PR and between 1e40146 and 3ea8ae0.

📒 Files selected for processing (2)
  • python/src/nemo_fabric/client.py
  • tests/test_session.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • python/src/nemo_fabric/client.py
  • tests/test_session.py

📝 Walkthrough

Walkthrough

Python session handling now uses native runtime handles for start/invoke/stop flows, and the Hermes SDK adapter now loads prior conversation state from SessionDB using runtime ids. Documentation and tests were updated to match the new session lifecycle.

Changes

Runtime session lifecycle

Layer / File(s) Summary
Native runtime bindings
crates/fabric-python/src/lib.rs, python/src/nemo_fabric/_native.pyi
PyO3 bindings and Python stubs add runtime start, invoke, and stop entrypoints plus JSON parsing helpers.
Client session runtime
python/src/nemo_fabric/client.py, python/src/nemo_fabric/__init__.py
FabricClient and Session switch to native runtime handles for session creation, per-turn invoke, stop, runtime introspection, and lifecycle control; FabricSessionUnsupportedError is no longer re-exported.
Hermes adapter history
adapters/hermes-sdk/README.md, adapters/hermes-sdk/src/nemo_fabric_adapters/hermes_sdk/adapter.py, tests/test_hermes_sdk_adapter.py, tests/fixtures/hermes-shim-agent/adapters/hermes-shim/src/nemo_fabric_test_adapters/hermes_shim/adapter.py
Hermes adapter docs, code, and tests now load conversation history from SessionDB using the runtime session id and wire that state into AIAgent.
Session docs
README.md, examples/session_quickstart.py, python/tests/smoke_typed_config.py, tests/smoke_hermes_session.py
README guidance and related examples/tests now describe runtime-backed multi-turn sessions and native session API requirements.
Session tests
python/tests/*, tests/test_session.py
Session smoke and unit tests now cover native-backed starts, invokes, streams, stops, cancels, runtime stability, typed-config handling, and environment resolution.

Sequence Diagram(s)

Native session lifecycle

sequenceDiagram
  participant FabricClient
  participant _native
  participant Session
  FabricClient->>_native: start_runtime(plan_json)
  _native-->>FabricClient: runtime
  FabricClient->>Session: construct(plan, runtime)
  Session->>_native: invoke_runtime(plan_json, runtime_json, request_json)
  _native-->>Session: result
  Session->>_native: stop_runtime(plan_json, runtime_json)
  _native-->>Session: stop result
Loading

Hermes runtime history

sequenceDiagram
  participant run_hermes_sdk
  participant SessionDB
  participant AIAgent
  run_hermes_sdk->>SessionDB: load_runtime_history(session_id)
  SessionDB-->>run_hermes_sdk: conversation_history
  run_hermes_sdk->>AIAgent: initialize(session_id, session_db, platform="fabric")
  run_hermes_sdk->>AIAgent: run_conversation(conversation_history)
  AIAgent-->>run_hermes_sdk: output
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • NVIDIA/NeMo-Fabric#12: Both PRs modify the Hermes SDK adapter’s per-turn history handling in adapters/hermes-sdk/src/nemo_fabric_adapters/hermes_sdk/adapter.py.
  • NVIDIA/NeMo-Fabric#15: Both PRs touch Hermes adapter history resolution and runtime/session plumbing in the same adapter code path.

Suggested labels

enhancement

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.64% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: aligning SDK session handling with the runtime lifecycle.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ajay/fix-session-runtime-lifecycle

Comment @coderabbitai help to get the list of available commands.

Signed-off-by: Ajay Thorve <athorve@nvidia.com>
@AjayThorve
AjayThorve force-pushed the ajay/fix-session-runtime-lifecycle branch from 0cdae6d to eaf558a Compare June 24, 2026 19:58
@AjayThorve
AjayThorve requested a review from dagardner-nv June 24, 2026 19:59
@AjayThorve
AjayThorve marked this pull request as ready for review June 24, 2026 19:59
@AjayThorve

Copy link
Copy Markdown
Collaborator Author

@coderabbitai can you review

@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown

@AjayThorve Sure, kicking off a full review now.

✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
python/src/nemo_fabric/client.py (1)

560-562: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Accept an explicit empty transcript update.

The doc says messages updates when the adapter returns a messages list, but and messages keeps the previous transcript when the runtime intentionally returns []. That leaves session.messages stale after a transcript reset.

Proposed fix
         messages = output.get("messages")
-        if isinstance(messages, list) and messages:
+        if isinstance(messages, list):
             self._messages = deepcopy(messages)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/src/nemo_fabric/client.py` around lines 560 - 562, The transcript
update in client.py is skipping explicit empty lists because the current guard
in the session update logic rejects falsy values, which leaves stale messages
behind after a reset. Update the messages handling in the relevant session
method that reads output["messages"] so it accepts any list, including [], and
assigns deepcopy(messages) to self._messages whenever the adapter returns a
list. Keep the existing type check around messages, but remove the truthiness
condition so empty transcripts replace the previous state.
🧹 Nitpick comments (3)
python/tests/smoke_sdk_sessions.py (1)

125-128: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Keep the cancel smoke checking native cleanup.

This test no longer retains the FakeNative, so it misses whether cancel() actually calls stop_runtime once. That is the key lifecycle contract for SDK-side cleanup.

Proposed test strengthening
 async def cancel_when_idle_marks_cancelled() -> None:
-    session = _session(FakeNative())
+    native = FakeNative()
+    session = _session(native)
     await session.cancel()
+    await session.cancel()
     assert session.status is SessionStatus.CANCELLED
+    assert native.stopped == 1
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/tests/smoke_sdk_sessions.py` around lines 125 - 128, The cancel smoke
test is no longer verifying SDK cleanup through the native layer, so strengthen
cancel_when_idle_marks_cancelled by keeping a reference to FakeNative and
asserting that Session.cancel() triggers stop_runtime exactly once. Use the
existing _session helper and the Session.cancel method to locate the flow, and
add the native-side assertion alongside the status check so the lifecycle
contract is covered.
tests/test_session.py (2)

348-357: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a start_config(...) session-path test.

This file now covers start(...), run(...), and run_config(...), but the typed-config session entrypoint added alongside them is still untested. A regression in FabricClient.start_config() would miss CI even though the README now documents it as the session equivalent. Mirroring this case with await client.start_config(config) and one session.invoke(...) would close that gap.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_session.py` around lines 348 - 357, Add a session-path test for
the typed-config entrypoint by mirroring the existing NativeClient.run_config
coverage in this test module. Create a new async test around
FabricClient.start_config() using the same FakeNative/config setup, call await
client.start_config(config), then exercise one session.invoke(...) and assert
the expected runtime/session behavior so start_config() is covered alongside
start(), run(), and run_config(). Reference the FabricClient.start_config method
and the session.invoke path to keep the test easy to locate.

126-135: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Make the test double observe start()'s plan inputs.

NativeClient.plan() returns _plan() unconditionally, so the start(...) tests never verify that the production code forwarded the requested path and profile into planning. Delegating to FakeNative.plan(...) or asserting those arguments here would keep this suite from passing on a broken FabricClient.start() call path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_session.py` around lines 126 - 135, The NativeClient test double
currently ignores the inputs passed into plan(), so the start() tests do not
verify that FabricClient.start forwards the requested path and profile into
planning. Update NativeClient.plan to delegate to FakeNative.plan with the
received path and profile, or add assertions on those arguments there, so the
suite exercises the real call path through FabricClient.start and FakeNative.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@adapters/hermes-sdk/src/nemo_fabric_adapters/hermes_sdk/adapter.py`:
- Around line 65-71: Move the resume-session alias resolution in
load_runtime_history() ahead of the session_db.get_session(session_id) early
return so the resolved session ID is checked first. Use
resolve_resume_session_id() to map the raw runtime_id to the actual session ID,
then perform the get_session() guard against that resolved ID before loading
history. Update the existing load_runtime_history() flow so resumed Fabric
sessions can hydrate even when the original session row is missing.

In `@crates/fabric-python/src/lib.rs`:
- Around line 161-190: Release the GIL around the blocking runtime lifecycle
calls in the PyO3 wrappers: `start_runtime`, `invoke_runtime`, and
`stop_runtime` currently call `fabric_core::start_runtime`,
`fabric_core::invoke_runtime`, and `fabric_core::stop_runtime` directly, which
can block Python threads during subprocess work. Update these `#[pyfunction]`
bodies to execute the `fabric_core` calls inside `py.allow_threads(...)` (or the
equivalent current PyO3 API) while keeping the existing parsing and JSON
conversion logic outside that block.

In `@python/src/nemo_fabric/client.py`:
- Around line 520-528: The session cleanup path in the cancel/stop flow makes
failed cancellation unrecoverable because the finally block in the method
handling _stop_runtime() always sets SessionStatus.CANCELLED even when cleanup
fails. Update the logic around _stop_runtime() so the status is only set to
CANCELLED after a successful stop, or move to a retryable cleanup state on
failure, and preserve the ability for stop()/cancel() to retry rather than
returning early.
- Around line 530-535: `Client.stop()` currently allows `_stop_runtime()` to run
while `invoke_runtime()` may still be active, which can let a late result be
absorbed after the session is stopped. Add a guard in `stop()` to detect an
in-flight turn and either reject the stop or delegate to `cancel()` instead of
proceeding. Use the existing `stop()`, `cancel()`, `invoke_runtime()`,
`_stop_runtime()`, and `SessionStatus` symbols to keep the behavior consistent
with the session state machine.
- Around line 664-665: The cleanup call in the `invoke_runtime` flow is
overwriting the original failure because `stop_runtime` is executed in the
`finally` block without protecting an in-flight exception. Update the
`invoke_runtime`/`finally` logic in `client.py` so that `stop_runtime` errors
are suppressed or handled separately when `invoke_runtime` has already raised,
and only surface the cleanup failure when there was no prior invoke error.

---

Outside diff comments:
In `@python/src/nemo_fabric/client.py`:
- Around line 560-562: The transcript update in client.py is skipping explicit
empty lists because the current guard in the session update logic rejects falsy
values, which leaves stale messages behind after a reset. Update the messages
handling in the relevant session method that reads output["messages"] so it
accepts any list, including [], and assigns deepcopy(messages) to self._messages
whenever the adapter returns a list. Keep the existing type check around
messages, but remove the truthiness condition so empty transcripts replace the
previous state.

---

Nitpick comments:
In `@python/tests/smoke_sdk_sessions.py`:
- Around line 125-128: The cancel smoke test is no longer verifying SDK cleanup
through the native layer, so strengthen cancel_when_idle_marks_cancelled by
keeping a reference to FakeNative and asserting that Session.cancel() triggers
stop_runtime exactly once. Use the existing _session helper and the
Session.cancel method to locate the flow, and add the native-side assertion
alongside the status check so the lifecycle contract is covered.

In `@tests/test_session.py`:
- Around line 348-357: Add a session-path test for the typed-config entrypoint
by mirroring the existing NativeClient.run_config coverage in this test module.
Create a new async test around FabricClient.start_config() using the same
FakeNative/config setup, call await client.start_config(config), then exercise
one session.invoke(...) and assert the expected runtime/session behavior so
start_config() is covered alongside start(), run(), and run_config(). Reference
the FabricClient.start_config method and the session.invoke path to keep the
test easy to locate.
- Around line 126-135: The NativeClient test double currently ignores the inputs
passed into plan(), so the start() tests do not verify that FabricClient.start
forwards the requested path and profile into planning. Update NativeClient.plan
to delegate to FakeNative.plan with the received path and profile, or add
assertions on those arguments there, so the suite exercises the real call path
through FabricClient.start and FakeNative.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 6ee54338-5cc7-486b-924e-339cd2e6cadb

📥 Commits

Reviewing files that changed from the base of the PR and between ae37986 and eaf558a.

📒 Files selected for processing (16)
  • README.md
  • adapters/hermes-sdk/README.md
  • adapters/hermes-sdk/src/nemo_fabric_adapters/hermes_sdk/adapter.py
  • crates/fabric-python/src/lib.rs
  • examples/session_quickstart.py
  • python/src/nemo_fabric/__init__.py
  • python/src/nemo_fabric/_native.pyi
  • python/src/nemo_fabric/client.py
  • python/tests/smoke_environment_handle.py
  • python/tests/smoke_native_sdk.py
  • python/tests/smoke_sdk_sessions.py
  • python/tests/smoke_typed_config.py
  • tests/fixtures/hermes-shim-agent/adapters/hermes-shim/src/nemo_fabric_test_adapters/hermes_shim/adapter.py
  • tests/smoke_hermes_session.py
  • tests/test_hermes_sdk_adapter.py
  • tests/test_session.py
💤 Files with no reviewable changes (2)
  • python/src/nemo_fabric/init.py
  • examples/session_quickstart.py

Comment thread adapters/hermes-sdk/src/nemo_fabric_adapters/hermes_sdk/adapter.py Outdated
Comment thread crates/fabric-python/src/lib.rs
Comment thread python/src/nemo_fabric/client.py
Comment thread python/src/nemo_fabric/client.py Outdated
Comment thread python/src/nemo_fabric/client.py Outdated
Signed-off-by: Ajay Thorve <athorve@nvidia.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
python/src/nemo_fabric/client.py (1)

520-536: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Block new turns while cleanup is in progress.

cancel() and stop() await _stop_runtime() while the session still reports ACTIVE. Once a cancelled invoke() clears _current_task—or when stop() starts from idle—another invoke() can enter and call invoke_runtime against a runtime that is already being stopped. Add a closing state/flag checked by invoke(), and clear it if _stop_runtime() fails so cleanup remains retryable.

Suggested direction
 class Session:
     def __init__(
@@
         self._status = SessionStatus.ACTIVE
         self._current_task: asyncio.Task[Any] | None = None
+        self._closing = False
@@
         if self._status is not SessionStatus.ACTIVE:
             raise RuntimeError(f"cannot invoke a {self._status.value} session")
+        if self._closing:
+            raise RuntimeError("cannot invoke while session is closing")
         if self._current_task is not None:
             raise RuntimeError(
                 "session is already running a turn; turns are ordered (one at a time)"
@@
         if self._status is not SessionStatus.ACTIVE:
             return
+        if self._closing:
+            return
+        self._closing = True
         task = self._current_task
-        if task is not None and not task.done() and task is not asyncio.current_task():
-            task.cancel()
-        await self._stop_runtime()
-        self._status = SessionStatus.CANCELLED
+        try:
+            if task is not None and not task.done() and task is not asyncio.current_task():
+                task.cancel()
+            await self._stop_runtime()
+            self._status = SessionStatus.CANCELLED
+        finally:
+            self._closing = False
@@
         if self._status is SessionStatus.ACTIVE:
+            if self._closing:
+                return
             task = self._current_task
             if task is not None and not task.done() and task is not asyncio.current_task():
                 raise RuntimeError("cannot stop while a turn is in flight; use cancel()")
-            await self._stop_runtime()
-            self._status = SessionStatus.STOPPED
+            self._closing = True
+            try:
+                await self._stop_runtime()
+                self._status = SessionStatus.STOPPED
+            finally:
+                self._closing = False
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/src/nemo_fabric/client.py` around lines 520 - 536, Add a
closing/in-progress state guard in the session lifecycle so new turns cannot
start while `cancel()` or `stop()` is awaiting `_stop_runtime()`. Update
`invoke()` to check this flag before calling `invoke_runtime`, and set it at the
start of `cancel()` and `stop()` in `python/src/nemo_fabric/client.py` alongside
`SessionStatus` handling. If `_stop_runtime()` fails, make sure the flag is
cleared so the cleanup path remains retryable, and keep the existing
`SessionStatus.CANCELLED` / `SessionStatus.STOPPED` transitions only after
successful shutdown.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@python/src/nemo_fabric/client.py`:
- Around line 520-536: Add a closing/in-progress state guard in the session
lifecycle so new turns cannot start while `cancel()` or `stop()` is awaiting
`_stop_runtime()`. Update `invoke()` to check this flag before calling
`invoke_runtime`, and set it at the start of `cancel()` and `stop()` in
`python/src/nemo_fabric/client.py` alongside `SessionStatus` handling. If
`_stop_runtime()` fails, make sure the flag is cleared so the cleanup path
remains retryable, and keep the existing `SessionStatus.CANCELLED` /
`SessionStatus.STOPPED` transitions only after successful shutdown.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 86f1669e-9c9a-40e8-b5ba-992dcef6a828

📥 Commits

Reviewing files that changed from the base of the PR and between eaf558a and 1e40146.

📒 Files selected for processing (6)
  • adapters/hermes-sdk/src/nemo_fabric_adapters/hermes_sdk/adapter.py
  • crates/fabric-python/src/lib.rs
  • python/src/nemo_fabric/client.py
  • python/tests/smoke_sdk_sessions.py
  • tests/test_hermes_sdk_adapter.py
  • tests/test_session.py
🚧 Files skipped from review as they are similar to previous changes (4)
  • tests/test_hermes_sdk_adapter.py
  • adapters/hermes-sdk/src/nemo_fabric_adapters/hermes_sdk/adapter.py
  • crates/fabric-python/src/lib.rs
  • python/tests/smoke_sdk_sessions.py

Signed-off-by: Ajay Thorve <athorve@nvidia.com>
@AjayThorve
AjayThorve merged commit 9bc4174 into main Jun 24, 2026
4 checks passed
@AjayThorve
AjayThorve deleted the ajay/fix-session-runtime-lifecycle branch June 27, 2026 03:30
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.

2 participants