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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/reference/api/python-library-reference/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ SPDX-License-Identifier: Apache-2.0 */}
- [`types.EffectiveConfig`](./nemo_fabric.types.md#class-effectiveconfig): Immutable result of config loading and ordered profile application.
- [`types.ErrorInfo`](./nemo_fabric.types.md#class-errorinfo): Structured failure returned inside a normalized ``RunResult``.
- [`types.FabricEvent`](./nemo_fabric.types.md#class-fabricevent): One normalized lifecycle or invocation event.
- [`types.RunOutput`](./nemo_fabric.types.md#class-runoutput): Normalized adapter output.
- [`types.RunPlan`](./nemo_fabric.types.md#class-runplan): Immutable execution plan produced before a runtime is started.
- [`types.RunResult`](./nemo_fabric.types.md#class-runresult): Normalized terminal result from one Fabric invocation.
- [`types.RuntimeCapabilities`](./nemo_fabric.types.md#class-runtimecapabilities): Operations declared by the resolved runtime and adapter.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -818,6 +818,75 @@ Return an immutable view of preserved extension fields.



---


### <kbd>classmethod</kbd> `from_mapping`

```python
from_mapping(mapping: 'Mapping[str, Any]') → 'FabricMapping'
```

Validate and copy a mapping into the requested typed model.

---


### <kbd>method</kbd> `to_dict`

```python
to_dict() → dict[str, Any]
```

Return the same detached representation as ``to_mapping()``.

---


### <kbd>method</kbd> `to_mapping`

```python
to_mapping() → dict[str, Any]
```

Return a detached, JSON-compatible mapping for serialization.


---


## <kbd>class</kbd> `RunOutput`

Normalized adapter output.

``response`` is a known adapter response field whose value follows the core Fabric JSON contract. Other keys are adapter-specific extensions.


### <kbd>method</kbd> `__init__`

```python
__init__(mapping: 'Mapping[str, Any]') → None
```






---

### <kbd>property</kbd> extra_fields

Return an immutable view of preserved extension fields.

---

### <kbd>property</kbd> response

Return the raw ``response`` JSON value, or ``None`` when absent.



---


Expand Down Expand Up @@ -873,7 +942,7 @@ The model is both attribute-accessible and mapping-compatible. A harness failure
- <b>`invocation_id`</b>: Identifier for this invocation.
- <b>`request_id`</b>: Correlated request identifier.
- <b>`status`</b>: Terminal invocation status.
- <b>`output`</b>: JSON-compatible harness output.
- <b>`output`</b>: Object-shaped adapter output as ``RunOutput``; non-object values are preserved as-is.
- <b>`error`</b>: Structured failure, or ``None`` on success.
- <b>`artifacts`</b>: Normalized artifact manifest.
- <b>`telemetry`</b>: Ordered telemetry references.
Expand Down
6 changes: 4 additions & 2 deletions examples/code_review_agent/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,11 @@ async def main() -> None:
else:
output = await fabric.run(config, base_dir=BASE_DIR, input=args.input)
print(json.dumps(output.to_mapping(), indent=2))

if args.show_output and not args.plan:
if isinstance(output.output, dict) and "response" in output.output:
print(f"\n{output.output['response']}")
response = getattr(output.output, "response", None)
if response is not None:
print(f"\n{response}")
elif output.error is not None:
print(f"\n{output.error.message}")
else:
Expand Down
2 changes: 2 additions & 0 deletions python/src/nemo_fabric/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
EffectiveConfig,
ErrorInfo,
FabricEvent,
RunOutput,
RunPlan,
RunResult,
RuntimeCapabilities,
Expand Down Expand Up @@ -71,6 +72,7 @@
"FabricNativeUnavailableError",
"FabricRuntimeError",
"FabricStateError",
"RunOutput",
"RunPlan",
"RunRequest",
"RunResult",
Expand Down
30 changes: 27 additions & 3 deletions python/src/nemo_fabric/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -1205,6 +1205,24 @@ def _normalize(cls, data: dict[str, Any]) -> dict[str, Any]:
return data


class RunOutput(FabricMapping):
"""Normalized adapter output.

``response`` is a known adapter response field whose value follows the
core Fabric JSON contract. Other keys are adapter-specific extensions.
"""

response: JSONValue | None
_fields = frozenset({"response"})
_json_fields = frozenset({"response"})

@property
def response(self) -> JSONValue | None:
"""Return the raw ``response`` JSON value, or ``None`` when absent."""

return _snapshot_value(self._data.get("response"), json_value=True)


class RunResult(FabricMapping):
"""Normalized terminal result from one Fabric invocation.

Expand All @@ -1222,7 +1240,8 @@ class RunResult(FabricMapping):
invocation_id: Identifier for this invocation.
request_id: Correlated request identifier.
status: Terminal invocation status.
output: JSON-compatible harness output.
output: Object-shaped adapter output as ``RunOutput``; non-object values
are preserved as-is.
error: Structured failure, or ``None`` on success.
artifacts: Normalized artifact manifest.
telemetry: Ordered telemetry references.
Expand All @@ -1239,7 +1258,7 @@ class RunResult(FabricMapping):
invocation_id: str
request_id: str
status: str
output: Any
output: RunOutput | JSONValue
error: ErrorInfo | None
artifacts: ArtifactManifest
telemetry: Sequence[TelemetryRef]
Expand All @@ -1264,7 +1283,7 @@ class RunResult(FabricMapping):
"metadata",
}
)
_json_fields = frozenset({"output", "metadata"})
_json_fields = frozenset({"metadata"})

@classmethod
def _normalize(cls, data: dict[str, Any]) -> dict[str, Any]:
Expand Down Expand Up @@ -1298,4 +1317,9 @@ def _normalize(cls, data: dict[str, Any]) -> dict[str, Any]:
FabricEvent.from_mapping(event) for event in data.get("events", [])
)
data["metadata"] = _mapping(data.get("metadata", {}), "result metadata")
raw_output = data.get("output")
if isinstance(raw_output, RunOutput):
data["output"] = raw_output
elif isinstance(raw_output, Mapping):
data["output"] = RunOutput.from_mapping(raw_output)
return data
6 changes: 3 additions & 3 deletions tests/python/test_code_review_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
with_relay_openinference,
with_relay_otel,
)
from nemo_fabric import Fabric, FabricConfig
from nemo_fabric import Fabric, FabricConfig, RunOutput


def test_variant_builders_return_independent_complete_configs():
Expand Down Expand Up @@ -123,8 +123,8 @@ async def test_example_entrypoint_shows_response_after_normalized_output(
capsys,
):
result = MagicMock()
result.output = {"response": "visible response"}
result.to_mapping.return_value = {"output": result.output}
result.output = RunOutput.from_mapping({"response": "visible response"})
result.to_mapping.return_value = {"output": result.output.to_mapping()}
mock_fabric = MagicMock()
mock_fabric.run = AsyncMock(return_value=result)
monkeypatch.setattr(main_module, "Fabric", lambda: mock_fabric)
Expand Down
79 changes: 78 additions & 1 deletion tests/python/test_sdk_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
HarnessConfig,
McpConfig,
MetadataConfig,
RunOutput,
RunPlan,
RunRequest,
RunResult,
Expand Down Expand Up @@ -714,11 +715,87 @@ def test_run_result_exposes_detached_json_values():
metadata["labels"].append("mutated")
future["values"].append(2)

assert result.output == {"plugins": ["observability/nemo_relay"]}
assert result.output.to_mapping() == {"plugins": ["observability/nemo_relay"]}
assert result.metadata == {"labels": ["sdk"]}
assert result.extra_fields["future"] == {"values": [1]}


def test_run_output_exposes_response_and_preserves_extensions():
output = RunOutput.from_mapping(
{
"response": "hello",
"thread_id": "abc",
}
)

assert output.response == "hello"
assert output["response"] == "hello"
assert output["thread_id"] == "abc"
assert output.to_mapping() == {
"response": "hello",
"thread_id": "abc",
}


def test_run_result_wraps_object_output_as_run_output():
result = RunResult.from_mapping(
_run_result(output={"response": "hello", "usage": {"tokens": 1}})
)

assert isinstance(result.output, RunOutput)
assert result.output.response == "hello"
assert result.output["response"] == "hello"
assert result.output["usage"] == {"tokens": 1}
assert result.to_mapping()["output"] == {
"response": "hello",
"usage": {"tokens": 1},
}


def test_run_output_omits_missing_response_from_mapping():
output = RunOutput.from_mapping({"thread_id": "abc"})

assert output.response is None
assert "response" not in output.to_mapping()
assert output.to_mapping() == {"thread_id": "abc"}


def test_run_output_preserves_explicit_null_response():
output = RunOutput.from_mapping({"response": None})

assert output.response is None
assert output.to_mapping() == {"response": None}
Comment thread
coderabbitai[bot] marked this conversation as resolved.


def test_run_output_preserves_non_string_response_without_raising():
output = RunOutput.from_mapping({"response": {"text": "hello"}})

assert output.response == {"text": "hello"}
assert output["response"] == {"text": "hello"}
assert output.to_mapping() == {"response": {"text": "hello"}}


def test_run_result_preserves_structured_response_from_core_valid_output():
result = RunResult.from_mapping(
_run_result(output={"response": {"text": "hello"}, "usage": {"tokens": 1}})
)

assert isinstance(result.output, RunOutput)
assert result.output.response == {"text": "hello"}
assert result.output["response"] == {"text": "hello"}
assert result.to_mapping()["output"] == {
"response": {"text": "hello"},
"usage": {"tokens": 1},
}


def test_run_result_preserves_non_object_output():
result = RunResult.from_mapping(_run_result(output="hello"))

assert result.output == "hello"
assert result.to_mapping()["output"] == "hello"


def test_run_result_normalizes_core_telemetry_reference():
result = RunResult.from_mapping(
_run_result(
Expand Down
Loading