Problem
When `format=` is passed to `act()` or `instruct()`, the `format=` overloads (PR #1284) narrow the return type to `ComputedModelOutputThunk[MyModel]`. That narrowing is reflected on `parsed_repr: S | None` — so `result.parsed_repr` is statically typed `MyModel | None`. However, at runtime it is always a plain `str`.
The root cause is `Instruction._parse` (mellea/stdlib/components/instruction.py:235), which ignores `_format` and always returns the raw string value:
```python
def _parse(self, computed: ModelOutputThunk) -> str:
return computed.value if computed.value is not None else ""
```
So this type-checks but raises `AttributeError` at runtime:
```python
result = session.act(Instruction("Classify this"), format=Sentiment)
result.parsed_repr.label # typed Sentiment | None — AttributeErrors at runtime
```
Workaround
Use `.parsed` instead of `.parsed_repr`. `.parsed` was added in PR #1284 specifically to provide a runtime-correct path — it calls `model_validate_json` directly and returns the Pydantic instance:
```python
result.parsed.label # typed Sentiment | None — correct at runtime
```
Why this was not fixed in PR #1284
The proper fix requires `Instruction` to become generic over its output type (`Instruction[S]`) so that `_parse` can be typed `-> S` where `S` is either `str` (no `format=`) or `BaseModelSubclass` (with `format=`). That is a non-trivial change to the component hierarchy:
- `Instruction` is currently `Component[str]`; making it `Component[S]` requires propagating `S` through all construction sites
- `format=` is passed at the `act()` call site, after `Instruction` is constructed — so the type parameter cannot be inferred from the action alone
- There are 22 `_parse` implementations across the component hierarchy; a principled fix should address the pattern consistently, not just patch `Instruction`
This scope belongs in its own PR rather than extending #1284 further.
Acceptance criteria
- `result.parsed_repr` delivers a `MyModel` instance (not a `str`) at runtime when `format=MyModel` was passed to `act()`
- The static type `S | None` matches the runtime value — no hidden `AttributeError` risk
- `.parsed` and `parsed_repr` are consistent for the `Instruction` path (they may remain separate entry points but should agree on the value)
Problem
When `format=` is passed to `act()` or `instruct()`, the `format=` overloads (PR #1284) narrow the return type to `ComputedModelOutputThunk[MyModel]`. That narrowing is reflected on `parsed_repr: S | None` — so `result.parsed_repr` is statically typed `MyModel | None`. However, at runtime it is always a plain `str`.
The root cause is `Instruction._parse` (mellea/stdlib/components/instruction.py:235), which ignores `_format` and always returns the raw string value:
```python
def _parse(self, computed: ModelOutputThunk) -> str:
return computed.value if computed.value is not None else ""
```
So this type-checks but raises `AttributeError` at runtime:
```python
result = session.act(Instruction("Classify this"), format=Sentiment)
result.parsed_repr.label # typed Sentiment | None — AttributeErrors at runtime
```
Workaround
Use `.parsed` instead of `.parsed_repr`. `.parsed` was added in PR #1284 specifically to provide a runtime-correct path — it calls `model_validate_json` directly and returns the Pydantic instance:
```python
result.parsed.label # typed Sentiment | None — correct at runtime
```
Why this was not fixed in PR #1284
The proper fix requires `Instruction` to become generic over its output type (`Instruction[S]`) so that `_parse` can be typed `-> S` where `S` is either `str` (no `format=`) or `BaseModelSubclass` (with `format=`). That is a non-trivial change to the component hierarchy:
This scope belongs in its own PR rather than extending #1284 further.
Acceptance criteria