Skip to content

Add async session boundary to the Python SDK (FABRIC-10) - #12

Merged
AjayThorve merged 10 commits into
NVIDIA:mainfrom
AjayThorve:ajay/fabric-10-support-session-management-and-cancellations
Jun 23, 2026
Merged

Add async session boundary to the Python SDK (FABRIC-10)#12
AjayThorve merged 10 commits into
NVIDIA:mainfrom
AjayThorve:ajay/fabric-10-support-session-management-and-cancellations

Conversation

@AjayThorve

@AjayThorve AjayThorve commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

Implements the async SDK session boundary (FABRIC-10) — the remaining runtime-mode work beyond the stable oneshot run.

What

A client-side multi-turn Session layered over the existing stateless adapter call. No new wire schemas — turns reuse RunRequest / RunResult.

Verb Behavior
start / start_config resolve a plan → Session; raise FabricSessionUnsupportedError if the adapter is not session-capable
invoke one turn; replays the transcript as request.context.history; merges per-turn overrides
stream yields normalized events, then the final RunResult (buffered; async-iterator shape is token-stream-ready)
cancel cooperatively aborts the in-flight turn (idle or running); marks the session cancelled
stop finalize; idempotent; async context manager auto-stops

Session exposes id, status, messages (transcript), and info. The hermes-sdk adapter gains resolve_history so a turn's request.context.history reaches run_conversation (request context wins over static settings; oneshot behavior unchanged).

Scope / bound

Session state is client-side — the transcript is replayed as conversation history each turn. A persistent, harness-stateful session is a later phase, bounded by Hermes support (FABRIC-20). This cut ships the function boundary with minimal semantics: buffered stream (no live token streaming), and cooperative cancel (the inline adapter runs in a worker thread that cannot be hard-killed; the process path can terminate). Session-support gating is by adapter kind today; an explicit adapter-descriptor capability flag is a follow-up.

Tests

  • python/tests/smoke_sdk_sessions.py — dependency-free (no native ext, no Hermes); fakes the inline adapter to assert multi-turn history threading, buffered stream, cooperative cancel (idle + in-flight), idempotent stop, and gating. Passing.
  • tests/smoke_hermes_session.py — opt-in real-Hermes multi-turn memory check, gated by RUN_FABRIC_HERMES_INTEGRATION (needs Hermes + NVIDIA_API_KEY + the native ext).
  • examples/session_quickstart.py — start → invoke → stream → stop.

Draft pending the related in-flight PRs (SDK guardrails, SDK API doc); will rebase as they land.

Summary by CodeRabbit

  • New Features
    • Added multi-turn, stateful conversational Session support with persistent transcript history, including SessionStatus, Session, and FabricClient.start() / start_config() lifecycle controls.
    • Expanded public SDK exports for session types and added a session quickstart example + a Hermes session profile.
  • Bug Fixes
    • Improved Hermes conversation history handling by prioritizing request-provided history with a safe fallback.
  • Documentation
    • Updated the README “Use Fabric” section with multi-turn session guidance and code examples.
  • Tests
    • Added dependency-light SDK session smoke tests, a dependency-free unit test suite, and an opt-in Hermes in-process integration smoke test.
  • Chores
    • Extended CI smoke testing with an additional session smoke script.

@linear

linear Bot commented Jun 23, 2026

Copy link
Copy Markdown

@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds multi-turn session support to the NeMo-Fabric Python SDK via Session, SessionStatus, and FabricSessionUnsupportedError types in client.py; FabricClient.start() and start_config() as native-extension-only entry points; Hermes adapter request-scoped history resolution; full pytest and smoke test coverage; quickstart example; Hermes session profile; README documentation; and CI registration.

Changes

Multi-turn Session SDK

Layer / File(s) Summary
Session exception, status enum, Session class, and public exports
python/src/nemo_fabric/client.py, python/src/nemo_fabric/__init__.py
FabricSessionUnsupportedError exception; SessionStatus enum (ACTIVE/STOPPED/CANCELLED); Session class with invoke(), stream(), cancel(), stop() async methods and async context manager support; _make_session() validates inline adapter entrypoint and constructs Session; _merge_overrides() merges session-level, per-turn, and request-level overrides; all new symbols re-exported from __init__.py.
FabricClient session entry points
python/src/nemo_fabric/client.py
FabricClient.start(path, profile, overrides) and start_config(config, profile_configs, base_dir, overrides) resolve a run plan via native extension and delegate to _make_session(); both require native extension and session-capable adapter, raising FabricNativeUnavailableError or FabricSessionUnsupportedError on failure.
Hermes adapter request-scoped history resolution
adapters/hermes-sdk/src/nemo_fabric_adapters/hermes_sdk/adapter.py
resolve_history(payload) reads history from request.context.history when present, falling back to harness settings.history; wired into agent.run_conversation call to enable per-turn transcript replay and multi-turn state continuity.
Example agent profile cleanup and session profile
examples/code-review-agent/agent.yaml, examples/code-review-agent/profiles/hermes-*.yaml
Removes unused agent_profile: code_reviewer field from base agent config and all four Hermes profiles (cli, relay, sdk). Adds new hermes-session.yaml profile configuring Hermes adapter for session mode with library transport, chat-to-message I/O, session parameters (max_turns, max_tokens, temperature), environment/workspace/artifacts bindings, and smoke test system prompt.
pytest unit tests
tests/test_session.py
Covers session initialization state, history accumulation and replay across turns, per-turn correlation handle recording, session id adoption, override merging at three levels, streaming event/result ordering, stop/cancel idempotency, in-flight cancellation abort via CancelledError, unsupported adapter gating, native unavailability error, concurrency blocking, empty transcript handling, and immutability of returned transcript/invocation lists.
Dependency-free SDK smoke tests
python/tests/smoke_sdk_sessions.py
Exercises full session lifecycle with fake inline adapter: multi-turn history propagation, streaming event/result ordering, stop idempotency and invoke-blocking, cancel idempotency when idle, in-flight cancel abort, and rejection of non-session-capable adapters; registered in CI dependency-free smoke loop.
Hermes integration smoke test
tests/smoke_hermes_session.py
End-to-end two-turn session using Hermes adapter, gated on RUN_FABRIC_HERMES_INTEGRATION=1 and NVIDIA_API_KEY; validates conversation memory persistence and correct multi-turn response generation across turns.
README docs, quickstart example, and CI registration
README.md, examples/session_quickstart.py, .github/workflows/ci_python.yml
README documents multi-turn session usage, start_config(), stream(), and cancel() semantics; session_quickstart.py provides runnable example with invoke/stream/stop patterns and session state inspection; CI registers smoke_sdk_sessions.py in dependency-free smoke loop.

Sequence Diagram(s)

sequenceDiagram
    participant Caller
    participant FabricClient
    participant _make_session
    participant Session
    participant _run_inline_adapter

    Caller->>FabricClient: start(path, profile, overrides)
    FabricClient->>FabricClient: resolve plan via native extension
    FabricClient->>_make_session: plan, client, overrides
    _make_session->>_make_session: validate inline Python entrypoint
    _make_session-->>Caller: Session(status=ACTIVE)

    Caller->>Session: invoke(input, overrides)
    Session->>Session: merge all override levels
    Session->>Session: build context with accumulated history
    Session->>_run_inline_adapter: plan, request, entrypoint
    _run_inline_adapter-->>Session: result (output, session_id, handles)
    Session->>Session: absorb messages/invocations, update id
    Session-->>Caller: result

    Caller->>Session: stream(input, overrides)
    Session->>Session: invoke internally
    Session->>Caller: yield events sequentially
    Session->>Caller: yield terminal succeeded result

    Caller->>Session: cancel()
    Session->>Session: cancel in-flight task (CancelledError)
    Session->>Session: status = CANCELLED
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • dagardner-nv
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.73% 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 accurately summarizes the primary change: adding async session boundary support to the Python SDK, directly matching the main objectives and file changes.
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 unit tests (beta)
  • Create PR with unit tests

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

@AjayThorve
AjayThorve force-pushed the ajay/fabric-10-support-session-management-and-cancellations branch from e45fb60 to 1bcdf23 Compare June 23, 2026 16:45
…cel/stop)

Finish the async SDK boundary beyond the stable oneshot `run`: a client-side
multi-turn Session over the existing stateless adapter call. No new wire
schemas -- turns reuse RunRequest / RunResult.

- FabricClient.start / start_config open a Session (FabricSessionUnsupportedError
  when the resolved adapter is not session-capable).
- Session.invoke runs one turn, replaying the accumulated transcript as
  request.context.history and merging per-turn overrides.
- Session.stream yields normalized events then the final RunResult (buffered;
  the async-iterator shape is forward-compatible with token streaming).
- Session.cancel cooperatively aborts the in-flight turn (idle or running) and
  marks the session cancelled; Session.stop finalizes (idempotent, async cm).
- hermes-sdk adapter: resolve_history threads request.context.history into
  run_conversation (request context wins over static settings; oneshot unchanged).

Session state is client-side (transcript replayed as history); a persistent,
harness-stateful session is a later phase, bounded by Hermes support.

Tests: python/tests/smoke_sdk_sessions.py (dependency-free; fakes the inline
adapter to assert multi-turn threading, buffered stream, cooperative cancel,
idempotent stop, gating). tests/smoke_hermes_session.py (opt-in real-Hermes
multi-turn, gated by RUN_FABRIC_HERMES_INTEGRATION). examples/session_quickstart.py.

Signed-off-by: Ajay Thorve <athorve@nvidia.com>
Add the dependency-free session smoke (smoke_sdk_sessions) to the explicit CI
smoke list so the Session boundary is enforced on every PR. The real-Hermes
session smoke stays gated (RUN_FABRIC_HERMES_INTEGRATION) and out of CI.

Signed-off-by: Ajay Thorve <athorve@nvidia.com>
Track {request_id, runtime_id, invocation_id} per turn and expose them via
Session.invocations, so a session is correlatable to its runtimes, telemetry,
and artifacts even when it spans multiple runtimes (one per turn) -- the case
when the harness has no resumable runtime (e.g. Hermes). Aligns the session
surface toward the runtime-mode contract without faking a persistent runtime.

Signed-off-by: Ajay Thorve <athorve@nvidia.com>
@AjayThorve
AjayThorve force-pushed the ajay/fabric-10-support-session-management-and-cancellations branch from 74aa461 to 37755fb Compare June 23, 2026 21:05
Add Args/Returns/Raises (and Yields for stream) to the session surface so the
generated API reference documents the full contract: start, start_config,
Session.invoke, Session.stream, and the previously-undocumented Session.info.
Records the raised errors per method (FabricNativeUnavailableError,
FabricSessionUnsupportedError, RuntimeError on a non-active session). The
full-SDK docstring pass + Fern regeneration is FABRIC-2 / PR #7.

Signed-off-by: Ajay Thorve <athorve@nvidia.com>
@AjayThorve
AjayThorve marked this pull request as ready for review June 23, 2026 21:21
pytest unit tests for the Session boundary (start/invoke/stream/cancel/stop),
matching the suite added in #13: dependency-free via a monkeypatched inline
adapter. Covers history replay, per-turn override merge, invocation-handle
correlation, output.session_id adoption, buffered stream, idempotent stop +
context manager, cooperative cancel (idle and in-flight), the info summary,
defensive-copy semantics of messages/invocations, an empty-output turn, adapter
gating, and the native-required error. Auto-discovered by the CI `uv run pytest`.

Signed-off-by: Ajay Thorve <athorve@nvidia.com>
@AjayThorve
AjayThorve force-pushed the ajay/fabric-10-support-session-management-and-cancellations branch from 2f591fe to 3a4ae87 Compare June 23, 2026 21:23

@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: 4

🧹 Nitpick comments (2)
README.md (1)

190-190: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Inconsistent session.status formatting with quickstart example.

The README code accesses session.status directly (which prints the enum object), but examples/session_quickstart.py:32 uses session.status.value to show the enum string value. For consistency and cleaner output, use .value:

print(session.id, session.status.value, len(session.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 `@README.md` at line 190, The print statement accessing session.status should
use session.status.value instead to access the enum string value rather than the
enum object representation. This ensures consistency with the
examples/session_quickstart.py file and produces cleaner output. Update the
print statement that displays session.id, session.status, and
len(session.messages) by changing session.status to session.status.value.
tests/test_session.py (1)

78-197: 📐 Maintainability & Code Quality | 🔵 Trivial

Async tests are properly configured via asyncio_mode = "auto".

The repository intentionally enables async test discovery in pyproject.toml:40. All 15 unmarked async def test_* functions in this file will run correctly without explicit decorators. Adding @pytest.mark.asyncio decorators is optional but would improve clarity and self-documentation of test intent.

🤖 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 78 - 197, Add the `@pytest.mark.asyncio`
decorator to all async test functions in the file to improve clarity and
self-document test intent. Place the decorator immediately above each async def
test_* function signature for the following functions:
test_new_session_is_active_and_empty, test_invoke_threads_accumulated_history,
test_invoke_records_per_turn_handles, test_invoke_adopts_session_id_from_output,
test_invoke_merges_session_and_turn_overrides,
test_stream_yields_events_then_result,
test_stop_is_idempotent_and_blocks_invoke, test_context_manager_auto_stops,
test_cancel_when_idle_marks_cancelled, test_cancel_aborts_in_flight_turn,
test_make_session_rejects_non_session_adapter, and
test_start_requires_native_extension. While these tests will run correctly with
the current asyncio_mode configuration, explicit decorators improve readability
and make async test intent clearer to other developers.
🤖 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 78-79: The issue is that using the `or` operator in the return
statement treats empty lists as falsy, so when a request explicitly sets history
to an empty list to clear it, the code incorrectly falls back to the static
harness history. Instead of using `context.get("history") or ...`, check for key
presence explicitly by verifying if "history" exists in the context dictionary
before falling back to settings_payload. Replace the `or` logic with a
conditional that returns context["history"] if the key exists (regardless of its
value being empty or not), and only falls back to
settings_payload(payload).get("history") when the key is actually absent from
context.

In `@python/src/nemo_fabric/client.py`:
- Around line 412-413: The session class has a race condition where concurrent
invoke() or stream() calls can simultaneously replay the same transcript,
overwrite _current_task, and compete in _absorb(), causing turns to be lost or
reordered. Add an asyncio.Lock as an instance attribute in the session
initialization (alongside _current_task and id) to serialize access to critical
sections. Use this lock to guard all turn replay and mutation operations in
invoke(), stream(), and _absorb() methods to ensure only one call processes
turns at a time.
- Line 24: The issue is that stale context.history from incoming requests can
bypass the authoritative accumulated session transcript (around line 482),
existing request overrides can bypass the documented session/per-turn merge
logic (around line 486), and message objects are stored and exposed by reference
which allows callers or adapters to mutate them and corrupt future turns. To fix
this, ensure the session's accumulated transcript is always authoritative by not
allowing incoming request context.history to override it, enforce the documented
merge logic for overrides rather than letting request overrides bypass it, and
create deep copies of all message objects before storing them (in methods around
lines 420-423, 482-486, 570-572) so mutations to caller-held references do not
affect the session state.

In `@README.md`:
- Around line 181-194: The code example in the README is missing the `import
asyncio` statement at the top. The example defines an async function `chat()`
and then calls it with `asyncio.run(chat())` at the end, but the asyncio module
is never imported. Add `import asyncio` as the first import statement before the
`from nemo_fabric import FabricClient` line to resolve the missing dependency.

---

Nitpick comments:
In `@README.md`:
- Line 190: The print statement accessing session.status should use
session.status.value instead to access the enum string value rather than the
enum object representation. This ensures consistency with the
examples/session_quickstart.py file and produces cleaner output. Update the
print statement that displays session.id, session.status, and
len(session.messages) by changing session.status to session.status.value.

In `@tests/test_session.py`:
- Around line 78-197: Add the `@pytest.mark.asyncio` decorator to all async test
functions in the file to improve clarity and self-document test intent. Place
the decorator immediately above each async def test_* function signature for the
following functions: test_new_session_is_active_and_empty,
test_invoke_threads_accumulated_history, test_invoke_records_per_turn_handles,
test_invoke_adopts_session_id_from_output,
test_invoke_merges_session_and_turn_overrides,
test_stream_yields_events_then_result,
test_stop_is_idempotent_and_blocks_invoke, test_context_manager_auto_stops,
test_cancel_when_idle_marks_cancelled, test_cancel_aborts_in_flight_turn,
test_make_session_rejects_non_session_adapter, and
test_start_requires_native_extension. While these tests will run correctly with
the current asyncio_mode configuration, explicit decorators improve readability
and make async test intent clearer to other developers.
🪄 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: f731c5b1-f57f-4338-91d7-759948d8475c

📥 Commits

Reviewing files that changed from the base of the PR and between 62c5aeb and 2f591fe.

📒 Files selected for processing (9)
  • .github/workflows/ci_python.yml
  • README.md
  • adapters/hermes-sdk/src/nemo_fabric_adapters/hermes_sdk/adapter.py
  • examples/session_quickstart.py
  • python/src/nemo_fabric/__init__.py
  • python/src/nemo_fabric/client.py
  • python/tests/smoke_sdk_sessions.py
  • tests/smoke_hermes_session.py
  • tests/test_session.py

Comment thread adapters/hermes-sdk/src/nemo_fabric_adapters/hermes_sdk/adapter.py Outdated
Comment thread python/src/nemo_fabric/client.py
Comment thread python/src/nemo_fabric/client.py
Comment thread README.md
The example's hermes_sdk/cli/relay profiles override the base agent to
runtime.mode: oneshot, so the session quickstart/smoke previously ran over a
oneshot-declared config. Add a hermes_session profile (hermes-sdk adapter,
runtime.mode: session) and point the quickstart, the gated real-Hermes smoke,
and the README session snippet at it, so the example demonstrates session mode
honestly. Document that sessions are SDK-only (no fabric CLI equivalent).

Signed-off-by: Ajay Thorve <athorve@nvidia.com>
@AjayThorve
AjayThorve requested a review from dagardner-nv June 23, 2026 22:08
- adapter resolve_history: check `history` key presence so an explicit empty
  history ([]) clears the conversation instead of falling back to static
  settings.
- Session: the accumulated transcript is authoritative (thread it over any
  caller-supplied request.context.history); request-level overrides are merged
  (session < request < per-turn) rather than bypassing the merge; messages are
  deep-copied on store, expose, and thread so caller/adapter mutation cannot
  corrupt the session.
- README: use session.status.value for consistency with the quickstart.
- tests: cover authoritative history, request-override merge, and deep-copy
  isolation. (Skipped the @pytest.mark.asyncio nitpick: asyncio_mode=auto is the
  repo convention and the #13 suite omits the decorator.)

Signed-off-by: Ajay Thorve <athorve@nvidia.com>
agent_profile was set in agent.yaml and every hermes profile but is read by
neither the adapters nor the core, so it was inert. Drop it.

Signed-off-by: Ajay Thorve <athorve@nvidia.com>
- Session.invoke claims an in-flight guard before any await, so concurrent
  invoke()/stream() calls on one session are rejected (turns are ordered)
  rather than racing the transcript and _absorb().
- README session example: add the missing `import asyncio`.
- test: a second concurrent invoke is rejected while a turn is in flight.

Signed-off-by: Ajay Thorve <athorve@nvidia.com>
Comment thread examples/code-review-agent/profiles/hermes-session.yaml
Comment thread README.md Outdated
Signed-off-by: Ajay Thorve <athorve@nvidia.com>
@AjayThorve
AjayThorve force-pushed the ajay/fabric-10-support-session-management-and-cancellations branch from 33ebaa3 to 0b02612 Compare June 23, 2026 23:10
@AjayThorve
AjayThorve merged commit 62e2577 into NVIDIA:main Jun 23, 2026
4 checks passed
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