Skip to content

Extract variable resolution structure#1947

Closed
alexmojaki wants to merge 19 commits into
mainfrom
codex/variable-structure-refactor
Closed

Extract variable resolution structure#1947
alexmojaki wants to merge 19 commits into
mainfrom
codex/variable-structure-refactor

Conversation

@alexmojaki
Copy link
Copy Markdown
Collaborator

Stacked on #1945.

This extracts the non-template/non-composition variable structure from the larger composition branch:

  • introduce _BaseVariable with the existing shared variable resolution implementation
  • make Variable a thin subclass whose get() delegates to _get_result_and_record_span()
  • add _record_exception() for variable span exception recording and tests for the CPython traceback edge case

Intentionally excludes template variables, composition expansion, composed metadata, serialized-default composition, and template schema changes.

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 potential issue.

View 2 additional findings in Devin Review.

Open in Devin Review

Comment thread logfire/variables/variable.py
cubic-dev-ai[bot]

This comment was marked as resolved.

cubic-dev-ai[bot]

This comment was marked as resolved.

@alexmojaki alexmojaki marked this pull request as draft May 21, 2026 13:20
Comment thread logfire/variables/variable.py
@alexmojaki alexmojaki marked this pull request as ready for review May 21, 2026 15:27
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 5 additional findings in Devin Review.

Open in Devin Review

Comment on lines +245 to +246
if default_result is not None:
return default_result
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Provider exception lost when _resolve_serialized_default returns a result via render_fn path

When render_fn is not None and the provider returns value=None with a non-None exception (e.g., reason='unrecognized_variable'), the _resolve_serialized_default method at line 245-246 returns a result that does not carry the provider's exception from serialized_result.exception. This is because _resolve_serialized_default creates a fresh ResolvedVariable at line 350 without the provider exception, and the code at line 245-246 returns it directly, bypassing line 248-255 which preserves serialized_result.exception.

The test test_get_preserves_provider_exception_when_using_code_default (tests/test_variables.py:1766) demonstrates the intent that provider exceptions should be preserved when falling back to code default. The plain Variable.get() path (render_fn=None) is unaffected since _resolve_serialized_default returns None in that case. But the future TemplateVariable path (render_fn != None) will lose this exception information.

Prompt for agents
In _resolve method (logfire/variables/variable.py), when _resolve_serialized_default returns a non-None result at line 245-246, the provider exception from serialized_result.exception is lost. The _resolve_serialized_default method (lines 334-361) creates a fresh ResolvedVariable at line 350 that doesn't carry the provider's exception.

To fix this, either:
1. Pass serialized_result.exception into _resolve_serialized_default and have it attach the exception to the returned result, OR
2. After getting default_result back (line 245), set default_result.exception = serialized_result.exception before returning it (or merge exceptions if both exist).

The fix should ensure that when a provider returns value=None with an exception (like RuntimeError('missing') for unrecognized variables), that exception is preserved in the final ResolvedVariable even when render_fn processes the code default.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

(AI) Addressed in stacked PR #1949.

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

3 issues found across 2 files

Confidence score: 3/5

  • There is concrete behavioral risk in logfire/variables/variable.py: Variable.get() can pass None into render_fn, causing resolve-function defaults to run twice on missing-config lookups, which may produce side effects or inconsistent defaults.
  • Error-handling paths look fragile in the same file: _deserialize not catching TypeError can let validator errors escape and lose label/version metadata, and _resolve_serialized_default can mask a provider exception when render_fn returns a non-None value.
  • Given one medium-high issue (6/10, high confidence) plus additional medium issues, this looks mergeable with caution rather than a low-risk change.
  • Pay close attention to logfire/variables/variable.py - default resolution and exception propagation paths may alter runtime behavior and debugging signals.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="logfire/variables/variable.py">

<violation number="1" location="logfire/variables/variable.py:174">
P2: Catch `TypeError` in `_deserialize`. Pydantic v2 lets validator `TypeError`s bubble out, and this path then drops the label/version metadata for the bad value.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

"""
return None

def _deserialize(self, serialized_value: str) -> T_co | ValidationError | ValueError:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: Catch TypeError in _deserialize. Pydantic v2 lets validator TypeErrors bubble out, and this path then drops the label/version metadata for the bad value.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At logfire/variables/variable.py, line 174:

<comment>Catch `TypeError` in `_deserialize`. Pydantic v2 lets validator `TypeError`s bubble out, and this path then drops the label/version metadata for the bad value.</comment>

<file context>
@@ -157,11 +163,19 @@ def __init__(
+        """
+        return None
+
+    def _deserialize(self, serialized_value: str) -> T_co | ValidationError | ValueError:
         """Deserialize a JSON string to the variable's type, returning an Exception on failure."""
         try:
</file context>

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

(AI) Addressed in stacked PR #1949.

Comment on lines +343 to +347
serialized_default = self._get_serialized_default(targeting_key, attributes)
if serialized_default is None:
return None
if render_fn is None:
return None
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot May 21, 2026

Choose a reason for hiding this comment

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

P2: Check render_fn before _get_serialized_default(). Plain Variable.get() passes None, so missing-config lookups call resolve-function defaults twice.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At logfire/variables/variable.py, line 343:

<comment>Check `render_fn` before `_get_serialized_default()`. Plain `Variable.get()` passes `None`, so missing-config lookups call resolve-function defaults twice.</comment>

<file context>
@@ -356,6 +321,45 @@ def _get_default(
+        render_fn: Callable[[str], str] | None = None,
+    ) -> ResolvedVariable[T_co] | None:
+        """Resolve the code default through composition/rendering when needed."""
+        serialized_default = self._get_serialized_default(targeting_key, attributes)
+        if serialized_default is None:
+            return None
</file context>
Suggested change
serialized_default = self._get_serialized_default(targeting_key, attributes)
if serialized_default is None:
return None
if render_fn is None:
return None
if render_fn is None:
return None
serialized_default = self._get_serialized_default(targeting_key, attributes)
if serialized_default is None:
return None

✅ Addressed in d19c2d7

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

(AI) Addressed in d19c2d7

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

reverted

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

(AI) Addressed in stacked PR #1949.

Comment on lines +246 to 247
return default_result
# Provider had no value; surface that the code default was used.
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot May 21, 2026

Choose a reason for hiding this comment

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

P2: Provider exception from serialized_result.exception is lost when _resolve_serialized_default returns a non-None result (i.e., when render_fn is provided). The _resolve_serialized_default method creates a fresh ResolvedVariable without propagating the provider's exception. This means if a provider returns value=None with a meaningful exception (e.g., reason='unrecognized_variable'), that diagnostic information is silently dropped in the render_fn code path, whereas the non-render_fn path at lines 248-255 correctly preserves it via exception=serialized_result.exception.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At logfire/variables/variable.py, line 246:

<comment>Provider exception from `serialized_result.exception` is lost when `_resolve_serialized_default` returns a non-None result (i.e., when `render_fn` is provided). The `_resolve_serialized_default` method creates a fresh `ResolvedVariable` without propagating the provider's exception. This means if a provider returns `value=None` with a meaningful exception (e.g., `reason='unrecognized_variable'`), that diagnostic information is silently dropped in the render_fn code path, whereas the non-render_fn path at lines 248-255 correctly preserves it via `exception=serialized_result.exception`.</comment>

<file context>
@@ -278,34 +227,23 @@ def _resolve(
+                    render_fn=render_fn,
+                )
+                if default_result is not None:
+                    return default_result
                 # Provider had no value; surface that the code default was used.
                 return ResolvedVariable(
</file context>
Suggested change
return default_result
# Provider had no value; surface that the code default was used.
if default_result is not None:
default_result.exception = default_result.exception or serialized_result.exception
return default_result

✅ Addressed in 1147a16

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

(AI) Addressed in stacked PR #1949.

Base automatically changed from codex/resolved-variable-reason to main May 21, 2026 16:05
@dmontagu
Copy link
Copy Markdown
Contributor

Superseded by #1731 — the structural extract from this branch was merged into the variable-composition branch in b2f4c76, and the architectural follow-up that David was working on (extract a shared lookup helper so composition's resolve_ref and _resolve stop drifting) landed there as 85dcbad. Closing in favor of #1731.

@dmontagu dmontagu closed this May 21, 2026
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