Skip to content

feat: add adapter-selectable runtime strategies - #83

Closed
AjayThorve wants to merge 1 commit into
mainfrom
ajay/fabric-59-execution-strategy-contract
Closed

feat: add adapter-selectable runtime strategies#83
AjayThorve wants to merge 1 commit into
mainfrom
ajay/fabric-59-execution-strategy-contract

Conversation

@AjayThorve

@AjayThorve AjayThorve commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

Overview

Adds the normalized, adapter-selectable runtime execution strategy contract for FABRIC-59 while preserving the existing Fabric.start_runtime() and Runtime.invoke() APIs and the backward-compatible process_per_invocation default.

The shared vocabulary contains process_per_invocation, persistent_local_host, and remote_service. Claude and Codex currently declare the first two. remote_service is validated and fails closed when an adapter does not declare a real remote runtime transport; it does not silently fall back to per-invocation execution.

Details

  • Reads the requested strategy from harness.settings.runtime_strategy and rejects unknown or adapter-unsupported values during planning.
  • Adds adapter descriptor execution capabilities and the versioned fabric.adapter.lifecycle/v1alpha1 start/invoke/stop contract.
  • Implements a supervised persistent local host with ordered invocation, bounded lifecycle timeouts, crash diagnostics, normalized adapter failures, cleanup, and idempotent stop.
  • Adds the shared Python lifecycle host and enables persistent execution for the Claude and Codex adapters.
  • Keeps DeepAgents and Hermes explicit on process_per_invocation; no first-party adapter currently advertises remote_service.
  • Exposes the resolved strategy through plans, runtime handles, invocations, doctor output, Python types, JSON schemas, and generated API documentation.
  • Documents the execution model and verifies process reuse, lifecycle failures, telemetry environment scoping, adapter capability validation, and Claude/Codex planning and startup.

Example usage

Select and verify persistent local hosting without starting either adapter:

from examples.code_review_agent import BASE_DIR, claude_config, codex_config
from nemo_fabric import Fabric

fabric = Fabric()

for build_config in (claude_config, codex_config):
    config = build_config()
    config.harness.settings["runtime_strategy"] = "persistent_local_host"

    plan = fabric.plan(config, base_dir=BASE_DIR)
    assert plan.execution_strategy == "persistent_local_host"

Reuse one persistent adapter host for ordered invocations:

config = codex_config()  # claude_config() uses the same Fabric lifecycle.
config.harness.settings["runtime_strategy"] = "persistent_local_host"

async with await fabric.start_runtime(config, base_dir=BASE_DIR) as runtime:
    assert runtime.handle.execution_strategy == "persistent_local_host"
    first = await runtime.invoke(input="Inspect calculator.py.")
    second = await runtime.invoke(input="Now propose the smallest safe fix.")

assert first.runtime_id == second.runtime_id

Omit runtime_strategy to retain the compatibility default; the resolved plan and runtime handle report process_per_invocation.

Validation

  • cargo fmt --all -- --check
  • just test-rust — 73 passed
  • env PATH="$HOME/.local/bin:$PATH" just test-python — 382 passed, 11 skipped
  • git diff --check origin/main...HEAD
  • Regenerated and reviewed JSON schemas, Python API references, and Rust API references.
  • Ran the Fern documentation validator directly: 0 errors, 1 hidden warning. The aggregate just docs recipe was not used because npm is not on this shell's default PATH; its generators and validator were run separately.

Where should the reviewer start?

Start with ExecutionStrategy and AdapterExecutionSupport in crates/fabric-core/src/config.rs, then review the persistent-host lifecycle and failure semantics in crates/fabric-core/src/runtime.rs. The adapter-side protocol endpoint is isolated in adapters/common/src/nemo_fabric_adapters/common/lifecycle.py.

Related Issues: (use one of the action keywords Closes / Fixes / Resolves / Relates to)

  • Closes FABRIC-59

  • I confirm this contribution is my own work, or I have the right to submit it under this project's license.

  • I searched existing issues and open pull requests, and this does not duplicate existing work.

Signed-off-by: Ajay Thorve <athorve@nvidia.com>
@linear

linear Bot commented Jul 17, 2026

Copy link
Copy Markdown

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Fabric adds selectable adapter execution strategies, including persistent local hosts. It introduces a versioned lifecycle protocol, propagates execution strategy through plans and runtime handles, adds lifecycle schemas and validation, integrates Claude and Codex adapters, and documents the updated contracts.

Changes

Execution strategy and lifecycle hosting

Layer / File(s) Summary
Lifecycle protocol and adapter entrypoints
adapters/common/..., adapters/claude/..., adapters/codex/...
Adds lifecycle-host request processing, runtime ownership checks, telemetry environment scoping, stdout isolation, and adapter lifecycle startup paths.
Execution strategy planning and contracts
crates/fabric-core/src/config.rs, crates/fabric-core/src/error.rs, crates/fabric-core/src/doctor.rs, python/src/nemo_fabric/types.py, schemas/*
Adds execution-strategy descriptors, planning and validation, lifecycle contract checks, errors, doctor metadata, and required Python/schema fields.
Persistent runtime execution
crates/fabric-core/src/runtime.rs
Adds persistent host process management for start, invoke, and stop operations, including protocol exchange, timeouts, crash handling, cleanup, and idempotent stopping.
Schemas and API reference updates
schemas/*, docs/reference/api/*, docs/sdk/python.mdx
Documents and publishes lifecycle request/response schemas, execution strategy types, lifecycle contracts, runtime fields, errors, and generated API entries.
Lifecycle and SDK validation
tests/adapters/*, tests/python/*, crates/fabric-core/src/runtime.rs
Adds coverage for lifecycle ordering, runtime mismatches, environment scoping, strategy propagation, persistent-host failures, crashes, and cleanup behavior.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.07% 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
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.
Title check ✅ Passed The title follows Conventional Commits and accurately summarizes the main change.
Description check ✅ Passed The description includes the required overview, reviewer start point, related issue, validation, and confirmation checkboxes.
✨ 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/fabric-59-execution-strategy-contract

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

@github-actions

Copy link
Copy Markdown

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

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/types.py (1)

992-1015: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Reject unknown execution strategy values in Python models.

_required_text accepts any nonempty string, while Rust and JSON Schema permit only process_per_invocation, persistent_local_host, and remote_service. Use a Literal or enum annotation and validate the exact set in both normalizers; add rejection tests.

As per coding guidelines, “Determine and update every affected public surface ... so they remain in parity.” As per path instructions, “Stubs and runtime implementations should stay aligned.”

Also applies to: 1226-1252

🤖 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/types.py` around lines 992 - 1015, Restrict
execution_strategy to the supported values process_per_invocation,
persistent_local_host, and remote_service instead of accepting any nonempty
string. Update the execution_strategy annotation and validation in both
normalizers, including the corresponding implementation at the additionally
referenced section, keeping runtime and stub/public model surfaces aligned. Add
rejection tests for unknown and invalid values while preserving acceptance of
all three supported strategies.

Sources: Coding guidelines, Path instructions

🤖 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/codex/src/nemo_fabric_adapters/codex/adapter.py`:
- Line 32: Update the lifecycle import in the adapter module to use Ruff’s
lint-compliant import form instead of aliasing the module to the same name.
Preserve all existing references to lifecycle and avoid unrelated changes.

In `@adapters/common/src/nemo_fabric_adapters/common/lifecycle.py`:
- Line 236: Update the response output path around json.dumps in the lifecycle
handler to catch serialization failures, prevent the persistent host from
terminating, and emit a safe normalized lifecycle error response instead.
Preserve the existing successful JSON output behavior and flushing for
serializable adapter results.

In `@crates/fabric-core/src/runtime.rs`:
- Around line 1086-1088: Update the lifecycle invocation flow around
exchange_lifecycle_message and the corresponding persistent-invoke path to use
the configured lifecycle timeout or a cancellable receive instead of waiting
indefinitely while holding the host mutex. On timeout, terminate and unregister
the host, clean up any stale response state, and return the existing
timeout/error result while preserving normal successful responses and allowing
stop_runtime() to proceed.
- Around line 1263-1284: Bound persistent-host protocol output and diagnostics
across the runtime lifecycle, including the command setup and the associated
stdout/stderr consumption paths. Replace unlimited read_line-based message
handling with the existing PERSISTENT_HOST_DIAGNOSTIC_LIMIT (or equivalent
bounded framing), and route stderr through a bounded ring buffer or rotating
file instead of an unbounded host.stderr.log. Apply the same limits at the
additional persistent-host lifecycle sites noted in the review while preserving
supervision and cleanup behavior.
- Around line 974-983: Resolve adapter_effective_config(plan) before calling
spawn_persistent_host in both affected runtime lifecycle paths. Store the result
and reuse it when constructing AdapterLifecycleStart, so configuration errors
return before the child host and runtime directory are created. Preserve
existing error propagation and lifecycle behavior after successful resolution.

In `@docs/sdk/python.mdx`:
- Around line 156-163: Add a complete introductory sentence immediately before
the JSON example, explaining that the block demonstrates an adapter descriptor
declaring versioned lifecycle support; leave the example content unchanged.

In `@schemas/adapter-descriptor.schema.json`:
- Around line 24-44: Update the AdapterExecutionSupport schema to require a
nonempty strategies array, and add a conditional constraint requiring a non-null
lifecycle_contract_version whenever strategies contains persistent_local_host.
Keep the constraint scoped to that strategy while preserving support for other
execution strategies, and synchronize any checked-in JSON Schema snapshots with
the updated contract.

In `@tests/adapters/test_adapters_common_lifecycle.py`:
- Around line 120-149: Update
test_lifecycle_host_scopes_invocation_telemetry_environment to assign the host
value through os.environ[variable] instead of monkeypatch.setenv; retain the
existing lifecycle setup, assertions, and repository fixture-based environment
restoration.

In `@tests/adapters/test_claude_adapter.py`:
- Around line 972-982: Update test_main_serves_lifecycle_protocol_when_requested
to set adapter.lifecycle.CONTRACT_ENV through os.environ with the
CONTRACT_VERSION value, while retaining monkeypatch for replacing
adapter.lifecycle.serve.

In `@tests/adapters/test_codex_adapter.py`:
- Around line 584-594: Update test_main_serves_lifecycle_protocol_when_requested
to set adapter.lifecycle.CONTRACT_ENV to adapter.lifecycle.CONTRACT_VERSION via
os.environ instead of monkeypatch.setenv; retain monkeypatch for replacing
adapter.lifecycle.serve.

---

Outside diff comments:
In `@python/src/nemo_fabric/types.py`:
- Around line 992-1015: Restrict execution_strategy to the supported values
process_per_invocation, persistent_local_host, and remote_service instead of
accepting any nonempty string. Update the execution_strategy annotation and
validation in both normalizers, including the corresponding implementation at
the additionally referenced section, keeping runtime and stub/public model
surfaces aligned. Add rejection tests for unknown and invalid values while
preserving acceptance of all three supported strategies.
🪄 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: ASSERTIVE

Plan: Enterprise

Run ID: 1aff2465-033a-4034-83fd-9881850725e3

📥 Commits

Reviewing files that changed from the base of the PR and between 98794a1 and 49c1c05.

📒 Files selected for processing (144)
  • README.md
  • adapters/claude/README.md
  • adapters/claude/fabric-adapter.json
  • adapters/claude/src/nemo_fabric_adapters/claude/adapter.py
  • adapters/codex/README.md
  • adapters/codex/fabric-adapter.json
  • adapters/codex/src/nemo_fabric_adapters/codex/adapter.py
  • adapters/common/src/nemo_fabric_adapters/common/lifecycle.py
  • adapters/deepagents/fabric-adapter.json
  • adapters/hermes/fabric-adapter.json
  • crates/fabric-core/src/config.rs
  • crates/fabric-core/src/doctor.rs
  • crates/fabric-core/src/error.rs
  • crates/fabric-core/src/lib.rs
  • crates/fabric-core/src/runtime.rs
  • crates/fabric-core/src/schema.rs
  • docs/reference/api/python-library-reference/nemo_fabric.types.md
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/constant-adapter-lifecycle-contract-version.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-adapterdescriptorsource.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-adapterkind.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-capabilitykind.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-capabilitytarget.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-controllocation.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-environmentownership.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-executionstrategy.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-fabricdocument.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-mcpexposure.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-relayatifstorageconfig.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-relayatofendpointfieldnamepolicy.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-relayatofendpointtransport.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-relayatofmode.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-relayotlptransport.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-relayunsupportedbehavior.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-resolutionstrategy.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-telemetryprovider.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/fn-load-adapter-descriptor.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/fn-load-fabric-document.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/fn-resolve-effective-config-from-config.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/fn-resolve-effective-config-with-profiles.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/fn-resolve-effective-config.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/fn-resolve-run-plan-from-config.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/fn-resolve-run-plan-from-effective-config.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/fn-resolve-run-plan-with-profiles.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/fn-resolve-run-plan.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/fn-validate-agent-directory.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/index.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-adapterconfigsupport.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-adapterdescriptor.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-adapterexecutionsupport.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-adapterrequirements.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-adaptertelemetryprovidersupport.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-adaptertelemetrysupport.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-capabilityplan.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-capabilityroute.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-capabilitytargetplan.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-effectiveconfig.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-environmentconfig.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-environmentplan.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-fabricconfig.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-harnessconfig.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-mcpconfig.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-mcpserverconfig.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-mcpserverplan.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-metadataconfig.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-modelconfig.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-profileconfig.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-profileregistryconfig.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-relayatifconfig.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-relayatofconfig.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-relayatofendpointconfig.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-relaycomponentconfig.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-relayconfig.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-relayconfigpolicy.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-relayobservabilityconfig.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-relayotlpconfig.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-resolvecontext.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-resolvedadapterdescriptor.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-runplan.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-runtimecapabilities.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-runtimeconfig.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-skillconfig.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-telemetryconfig.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-telemetryplan.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-telemetryproviderconfig.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-toolsconfig.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-toolsplan.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/doctor/enum-doctorstatus.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/doctor/fn-doctor-plan.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/doctor/index.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/doctor/struct-doctorcheck.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/doctor/struct-doctorreport.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/error/enum-fabricerror.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/error/index.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/error/type-result.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/fn-version.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/index.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/runtime/enum-adapterlifecycleoperation.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/runtime/enum-adapterlifecycleoutcome.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/runtime/enum-adapterlifecyclerequestkind.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/runtime/enum-errorstage.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/runtime/enum-runstatus.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/runtime/fn-invoke-runtime.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/runtime/fn-prepare-environment.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/runtime/fn-run-plan.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/runtime/fn-start-runtime.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/runtime/fn-stop-runtime.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/runtime/index.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/runtime/struct-adapterinvocation.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/runtime/struct-adapterlifecyclerequest.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/runtime/struct-adapterlifecycleresponse.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/runtime/struct-adapterlifecyclestart.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/runtime/struct-adapterlifecyclestop.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/runtime/struct-artifactmanifest.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/runtime/struct-artifactref.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/runtime/struct-environmenthandle.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/runtime/struct-errorinfo.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/runtime/struct-fabricevent.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/runtime/struct-invocationhandle.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/runtime/struct-runrequest.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/runtime/struct-runresult.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/runtime/struct-runtimecontext.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/runtime/struct-runtimehandle.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/runtime/struct-runtimetelemetrycontext.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/runtime/struct-telemetryref.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/schema/enum-schemaname.mdx
  • docs/reference/api/rust-library-reference/nemo-fabric-core/schema/index.mdx
  • docs/sdk/python.mdx
  • examples/harbor/swebench/adapters/claude/fabric-adapter.json
  • examples/harbor/swebench/adapters/hermes/fabric-adapter.json
  • python/src/nemo_fabric/types.py
  • schemas/SCHEMA.md
  • schemas/adapter-descriptor.schema.json
  • schemas/adapter-invocation.schema.json
  • schemas/adapter-lifecycle-request.schema.json
  • schemas/adapter-lifecycle-response.schema.json
  • schemas/run-plan.schema.json
  • schemas/runtime-handle.schema.json
  • tests/adapters/test_adapters_common_lifecycle.py
  • tests/adapters/test_claude_adapter.py
  • tests/adapters/test_codex_adapter.py
  • tests/python/test_code_review_example.py
  • tests/python/test_runtime.py
  • tests/python/test_sdk_contract.py
  • tests/python/test_sdk_runtimes.py


import nemo_fabric_adapters.common.relay_gateway as relay_gateway
import nemo_fabric_adapters.common.relay_hooks as relay_hooks
import nemo_fabric_adapters.common.lifecycle as lifecycle

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use the lint-compliant lifecycle import.

Ruff PLR0402 flags this alias form.

Proposed fix
-import nemo_fabric_adapters.common.lifecycle as lifecycle
+from nemo_fabric_adapters.common import lifecycle
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import nemo_fabric_adapters.common.lifecycle as lifecycle
from nemo_fabric_adapters.common import lifecycle
🧰 Tools
🪛 Ruff (0.15.21)

[warning] 32-32: Use from nemo_fabric_adapters.common import lifecycle in lieu of alias

Replace with from nemo_fabric_adapters.common import lifecycle

(PLR0402)

🤖 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 `@adapters/codex/src/nemo_fabric_adapters/codex/adapter.py` at line 32, Update
the lifecycle import in the adapter module to use Ruff’s lint-compliant import
form instead of aliasing the module to the same name. Preserve all existing
references to lifecycle and avoid unrelated changes.

Source: Linters/SAST tools

),
)
should_stop = False
print(json.dumps(response, sort_keys=True), file=output_stream, flush=True)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Keep response serialization inside the protocol boundary.

A non-JSON-serializable adapter output raises here and terminates the persistent host without a normalized response. Catch serialization failures and emit a safe lifecycle error.

Proposed fix
-        print(json.dumps(response, sort_keys=True), file=output_stream, flush=True)
+        try:
+            encoded = json.dumps(response, sort_keys=True)
+        except (TypeError, ValueError) as error:
+            operation = response.get("operation")
+            if not isinstance(operation, str):
+                operation = "start"
+            print(
+                f"Invalid lifecycle response: {error}",
+                file=sys.stderr,
+                flush=True,
+            )
+            encoded = json.dumps(
+                _response(
+                    operation,
+                    error=_error(
+                        operation,
+                        "lifecycle_invalid_response",
+                        "Adapter returned an invalid lifecycle response",
+                    ),
+                ),
+                sort_keys=True,
+            )
+        print(encoded, file=output_stream, flush=True)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
print(json.dumps(response, sort_keys=True), file=output_stream, flush=True)
try:
encoded = json.dumps(response, sort_keys=True)
except (TypeError, ValueError) as error:
operation = response.get("operation")
if not isinstance(operation, str):
operation = "start"
print(
f"Invalid lifecycle response: {error}",
file=sys.stderr,
flush=True,
)
encoded = json.dumps(
_response(
operation,
error=_error(
operation,
"lifecycle_invalid_response",
"Adapter returned an invalid lifecycle response",
),
),
sort_keys=True,
)
print(encoded, file=output_stream, flush=True)
🤖 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 `@adapters/common/src/nemo_fabric_adapters/common/lifecycle.py` at line 236,
Update the response output path around json.dumps in the lifecycle handler to
catch serialization failures, prevent the persistent host from terminating, and
emit a safe normalized lifecycle error response instead. Preserve the existing
successful JSON output behavior and flushing for serializable adapter results.

Comment on lines +974 to +983
let mut host = spawn_persistent_host(plan, &runtime)?;
let request = AdapterLifecycleRequest::new(AdapterLifecycleRequestKind::Start(
AdapterLifecycleStart {
runtime: runtime.clone(),
effective_config: adapter_effective_config(plan)?,
capability_plan: plan.capability_plan.clone(),
capabilities: plan.capabilities.clone(),
telemetry_plan: plan.telemetry_plan.clone(),
},
));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Resolve the effective configuration before spawning the host.

Line 978 can fail after Line 974 starts the child. That returns without terminating the unregistered process or removing its runtime directory.

Proposed fix
-        let mut host = spawn_persistent_host(plan, &runtime)?;
+        let effective_config = adapter_effective_config(plan)?;
+        let mut host = spawn_persistent_host(plan, &runtime)?;
         let request = AdapterLifecycleRequest::new(AdapterLifecycleRequestKind::Start(
             AdapterLifecycleStart {
                 runtime: runtime.clone(),
-                effective_config: adapter_effective_config(plan)?,
+                effective_config,

As per path instructions, review the Rust core for runtime lifecycle correctness and error semantics.

Also applies to: 2196-2201

🤖 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 `@crates/fabric-core/src/runtime.rs` around lines 974 - 983, Resolve
adapter_effective_config(plan) before calling spawn_persistent_host in both
affected runtime lifecycle paths. Store the result and reuse it when
constructing AdapterLifecycleStart, so configuration errors return before the
child host and runtime directory are created. Preserve existing error
propagation and lifecycle behavior after successful resolution.

Source: Path instructions

Comment on lines +1086 to +1088
let mut host = host.lock().unwrap_or_else(|error| error.into_inner());
let output =
exchange_lifecycle_message(&mut host, &runtime.runtime_id, &lifecycle_request, None)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Bound or cancel persistent invoke waits.

The host mutex is held while recv() waits without a timeout. A live host that never returns a newline blocks the invocation forever, and concurrent stop_runtime() also blocks on that mutex. Use a configurable deadline or cancellable receive, then terminate and unregister the host on timeout to avoid stale responses.

The PR objective explicitly promises lifecycle timeouts and cleanup.

Also applies to: 1456-1497

🤖 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 `@crates/fabric-core/src/runtime.rs` around lines 1086 - 1088, Update the
lifecycle invocation flow around exchange_lifecycle_message and the
corresponding persistent-invoke path to use the configured lifecycle timeout or
a cancellable receive instead of waiting indefinitely while holding the host
mutex. On timeout, terminate and unregister the host, clean up any stale
response state, and return the existing timeout/error result while preserving
normal successful responses and allowing stop_runtime() to proceed.

Comment on lines +1263 to +1284
let stderr_path = runtime_dir.join("host.stderr.log");
let stderr = File::create(&stderr_path).map_err(|source| FabricError::Write {
path: stderr_path.clone(),
source,
})?;
let (mut command, command_display) = match persistent_host_command(plan, runtime) {
Ok(command) => command,
Err(error) => {
let _ = std::fs::remove_dir_all(&runtime_dir);
return Err(error);
}
};
command
.env(
"FABRIC_ADAPTER_LIFECYCLE_CONTRACT",
ADAPTER_LIFECYCLE_CONTRACT_VERSION,
)
.env("FABRIC_RUNTIME_ID", &runtime.runtime_id)
.env("FABRIC_HOME", &runtime_dir)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::from(stderr));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Bound persistent-host stdout and stderr consumption.

read_line() permits an unlimited response line, while stderr writes to an unlimited file for the host’s lifetime. A malformed or noisy adapter can exhaust memory or disk; PERSISTENT_HOST_DIAGNOSTIC_LIMIT only limits later reads. Cap protocol messages and drain stderr into a bounded ring buffer or rotating file.

As per path instructions, review the Rust core for runtime lifecycle correctness and supervision.

Also applies to: 1319-1340, 1594-1600

🤖 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 `@crates/fabric-core/src/runtime.rs` around lines 1263 - 1284, Bound
persistent-host protocol output and diagnostics across the runtime lifecycle,
including the command setup and the associated stdout/stderr consumption paths.
Replace unlimited read_line-based message handling with the existing
PERSISTENT_HOST_DIAGNOSTIC_LIMIT (or equivalent bounded framing), and route
stderr through a bounded ring buffer or rotating file instead of an unbounded
host.stderr.log. Apply the same limits at the additional persistent-host
lifecycle sites noted in the review while preserving supervision and cleanup
behavior.

Source: Path instructions

Comment thread docs/sdk/python.mdx
Comment on lines +156 to +163
```json
{
"execution": {
"lifecycle_contract_version": "fabric.adapter.lifecycle/v1alpha1",
"strategies": ["process_per_invocation", "persistent_local_host"]
}
}
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Nice to have — Introduce the JSON example.

Add a complete lead-in before this block, such as: “An adapter descriptor declares versioned lifecycle support as follows:”

As per coding guidelines, introduce every code block with a complete sentence.

🤖 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 `@docs/sdk/python.mdx` around lines 156 - 163, Add a complete introductory
sentence immediately before the JSON example, explaining that the block
demonstrates an adapter descriptor declaring versioned lifecycle support; leave
the example content unchanged.

Source: Coding guidelines

Comment on lines +24 to +44
"AdapterExecutionSupport": {
"additionalProperties": true,
"description": "Execution strategies implemented by an adapter.",
"properties": {
"lifecycle_contract_version": {
"description": "Version of the external start/invoke/stop contract used by persistent strategies.",
"type": [
"string",
"null"
]
},
"strategies": {
"description": "Execution strategies implemented by this adapter.",
"items": {
"$ref": "#/$defs/ExecutionStrategy"
},
"type": "array",
"uniqueItems": true
}
},
"type": "object"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Must fix — Require a lifecycle version for persistent hosts.

persistent_local_host can validate without lifecycle_contract_version, and execution can contain no strategies. Require a nonempty strategies array and conditionally require a non-null version when it contains persistent_local_host; otherwise schema-valid external descriptors can fail only later during planning.

Proposed schema constraint
     "AdapterExecutionSupport": {
       "additionalProperties": true,
+      "required": ["strategies"],
+      "allOf": [
+        {
+          "if": {
+            "properties": {
+              "strategies": {
+                "contains": { "const": "persistent_local_host" }
+              }
+            }
+          },
+          "then": {
+            "properties": {
+              "lifecycle_contract_version": {
+                "type": "string",
+                "minLength": 1
+              }
+            },
+            "required": ["lifecycle_contract_version"]
+          }
+        }
+      ],
       "properties": {
...
         "strategies": {
+          "minItems": 1,

As per coding guidelines, public contract changes must keep checked-in JSON Schema snapshots synchronized; this snapshot should encode the versioned lifecycle constraint.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"AdapterExecutionSupport": {
"additionalProperties": true,
"description": "Execution strategies implemented by an adapter.",
"properties": {
"lifecycle_contract_version": {
"description": "Version of the external start/invoke/stop contract used by persistent strategies.",
"type": [
"string",
"null"
]
},
"strategies": {
"description": "Execution strategies implemented by this adapter.",
"items": {
"$ref": "#/$defs/ExecutionStrategy"
},
"type": "array",
"uniqueItems": true
}
},
"type": "object"
"AdapterExecutionSupport": {
"additionalProperties": true,
"allOf": [
{
"if": {
"properties": {
"strategies": {
"contains": { "const": "persistent_local_host" }
}
}
},
"then": {
"properties": {
"lifecycle_contract_version": {
"type": "string",
"minLength": 1
}
},
"required": ["lifecycle_contract_version"]
}
}
],
"description": "Execution strategies implemented by an adapter.",
"properties": {
"lifecycle_contract_version": {
"description": "Version of the external start/invoke/stop contract used by persistent strategies.",
"type": [
"string",
"null"
]
},
"strategies": {
"description": "Execution strategies implemented by this adapter.",
"items": {
"$ref": "`#/`$defs/ExecutionStrategy"
},
"minItems": 1,
"type": "array",
"uniqueItems": true
}
},
"required": ["strategies"],
"type": "object"
🤖 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 `@schemas/adapter-descriptor.schema.json` around lines 24 - 44, Update the
AdapterExecutionSupport schema to require a nonempty strategies array, and add a
conditional constraint requiring a non-null lifecycle_contract_version whenever
strategies contains persistent_local_host. Keep the constraint scoped to that
strategy while preserving support for other execution strategies, and
synchronize any checked-in JSON Schema snapshots with the updated contract.

Source: Coding guidelines

Comment on lines +120 to +149
def test_lifecycle_host_scopes_invocation_telemetry_environment(monkeypatch):
runtime_id = "runtime-1"
variable = "FABRIC_TEST_LIFECYCLE_ENV"
monkeypatch.setenv(variable, "host-value")
requests = [
_request("start", {"runtime": {"runtime_id": runtime_id}}),
_request(
"invoke",
{
"runtime_context": {
"runtime_id": runtime_id,
"telemetry": {"env": {variable: "invocation-value"}},
},
"request": {"input": "hello"},
},
),
_request("stop", {"runtime_id": runtime_id}),
]
input_stream = io.StringIO("".join(f"{json.dumps(item)}\n" for item in requests))
output_stream = io.StringIO()

lifecycle.serve(
lambda _payload: {"value": os.environ[variable]},
input_stream=input_stream,
output_stream=output_stream,
)

responses = [json.loads(line) for line in output_stream.getvalue().splitlines()]
assert responses[1]["outcome"]["output"] == {"value": "invocation-value"}
assert os.environ[variable] == "host-value"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Use os.environ for the test environment override.

Replace monkeypatch.setenv(variable, "host-value") with os.environ[variable] = "host-value"; the repository’s autouse fixture restores the environment.

As per coding guidelines, “Use os.environ to modify environment variables in tests; do not use monkeypatch.setenv.”

🧰 Tools
🪛 ast-grep (0.44.1)

[info] 137-137: use jsonify instead of json.dumps for JSON output
Context: json.dumps(item)
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)

🤖 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/adapters/test_adapters_common_lifecycle.py` around lines 120 - 149,
Update test_lifecycle_host_scopes_invocation_telemetry_environment to assign the
host value through os.environ[variable] instead of monkeypatch.setenv; retain
the existing lifecycle setup, assertions, and repository fixture-based
environment restoration.

Source: Coding guidelines

Comment on lines +972 to +982
def test_main_serves_lifecycle_protocol_when_requested(monkeypatch):
serve = MagicMock()
monkeypatch.setenv(
adapter.lifecycle.CONTRACT_ENV,
adapter.lifecycle.CONTRACT_VERSION,
)
monkeypatch.setattr(adapter.lifecycle, "serve", serve)

adapter.main()

serve.assert_called_once_with(adapter.run)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Set the lifecycle contract through os.environ.

Keep monkeypatch for setattr, but assign the environment variable through os.environ.

As per coding guidelines, “Use os.environ to modify environment variables in tests; do not use monkeypatch.setenv.”

🤖 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/adapters/test_claude_adapter.py` around lines 972 - 982, Update
test_main_serves_lifecycle_protocol_when_requested to set
adapter.lifecycle.CONTRACT_ENV through os.environ with the CONTRACT_VERSION
value, while retaining monkeypatch for replacing adapter.lifecycle.serve.

Source: Coding guidelines

Comment on lines +584 to +594
def test_main_serves_lifecycle_protocol_when_requested(monkeypatch):
serve = MagicMock()
monkeypatch.setenv(
adapter.lifecycle.CONTRACT_ENV,
adapter.lifecycle.CONTRACT_VERSION,
)
monkeypatch.setattr(adapter.lifecycle, "serve", serve)

adapter.main()

serve.assert_called_once_with(adapter.run)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Set the lifecycle contract through os.environ.

Keep monkeypatch for replacing serve, but assign the environment variable through os.environ.

As per coding guidelines, “Use os.environ to modify environment variables in tests; do not use monkeypatch.setenv.”

🤖 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/adapters/test_codex_adapter.py` around lines 584 - 594, Update
test_main_serves_lifecycle_protocol_when_requested to set
adapter.lifecycle.CONTRACT_ENV to adapter.lifecycle.CONTRACT_VERSION via
os.environ instead of monkeypatch.setenv; retain monkeypatch for replacing
adapter.lifecycle.serve.

Source: Coding guidelines

@AjayThorve

Copy link
Copy Markdown
Collaborator Author

Closing as superseded by a narrower FABRIC-59 recut. This prototype exposed adapter execution mechanics as a northbound strategy, and its persistence tests prove host-process reuse rather than native SDK-client reuse. The replacement will define a private, versioned adapter runtime lifecycle, retain the per-invocation path as a compatibility driver, and prove stateful execution with a real Claude SDK client. The branch is intentionally retained for design reference.

@AjayThorve AjayThorve closed this Jul 17, 2026
@AjayThorve
AjayThorve deleted the ajay/fabric-59-execution-strategy-contract branch July 17, 2026 18:04
@coderabbitai coderabbitai Bot mentioned this pull request Aug 10, 2026
2 tasks
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.

1 participant