-
Notifications
You must be signed in to change notification settings - Fork 3
Fix/HYBIM-962 review follow ups #209
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -172,7 +172,10 @@ def _mapped_message(source: dict[str, Any], default_role: str) -> dict[str, Any] | |||||||||||||||||||||
| tool_call_id = source.pop("tool_call_id", None) | ||||||||||||||||||||||
| tool_calls = source.pop("tool_calls", None) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if source_parts is not None: | ||||||||||||||||||||||
| # An explicitly supplied parts field is authoritative over legacy content. | ||||||||||||||||||||||
| if isinstance(source_parts, list) and not source_parts: | ||||||||||||||||||||||
| parts = [] | ||||||||||||||||||||||
| elif source_parts is not None: | ||||||||||||||||||||||
| parts = _content_parts(source_parts) | ||||||||||||||||||||||
|
Comment on lines
+175
to
179
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 major (bug): This was raised in the prior review. The response was to add the comment on line 175 and The changelog entry says the fix is so that empty parts are "preserved in OTLP telemetry instead of being serialized as the text The realistic failure mode is an adapter that initializes The narrower rule only honours the empty list when there is genuinely nothing to fall back on, which still fixes the if isinstance(source_parts, list) and not source_parts and content in (None, ""):
parts = []
elif source_parts is not None:
parts = _content_parts(source_parts)Note this preserves the three new tests' intent except If
Suggested change
🤖 Generated by the Astra agent
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The design question here is fair, and I'll answer it directly below. But the suggested patch can't be applied as written — it reintroduces the exact I applied the suggestion verbatim and ran the mapper: The reason is the So for the both-present case the suggestion doesn't preserve For the record, applying it fails only On the actual design question — why
I take the point about silent-vs-loud loss for field debugging. But I'd rather not special-case the canonical field's precedence on the contents of the legacy one — that makes the rule " I don't have a named producer that emits stale Keeping current behavior. Open to the narrower rule if you want to argue for it, but it'd need a correct patch — the posted one regresses the bug the PR is fixing. |
||||||||||||||||||||||
| elif role == "tool": | ||||||||||||||||||||||
| response = {"type": "tool_call_response", "response": _parse_json_value(content)} | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -105,12 +105,7 @@ def commit(self) -> None: | |
| self._root_node = None | ||
|
|
||
| def _conclude_owned_trace(self, trace: Any, output: Any, status_code: int | None) -> None: | ||
| current_parent = self._splunk_ao_logger.current_parent() | ||
| root = current_parent | ||
| while root is not None and root._parent is not None: | ||
| root = root._parent | ||
|
|
||
| if root is trace: | ||
| if self._splunk_ao_logger._is_current_root(trace): | ||
| self._splunk_ao_logger.conclude(output=output, status_code=status_code, conclude_all=True) | ||
|
Comment on lines
107
to
109
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 major (bug): This refactor changes behavior when The old code compared the computed root against root = current_parent
while root is not None and root._parent is not None:
root = root._parent
if root is trace:
self._splunk_ao_logger.conclude(..., conclude_all=True)When The new
The This is reachable today, is not covered by a test ( Suggested fix — make the failure path unconditionally reclaim a chain the handler owns, rather than relying on a possibly- except Exception:
if self._start_new_trace:
self._conclude_owned_trace(owned_trace or self._splunk_ao_logger._current_root(), output="", status_code=500)
_logger.warning("Failed to commit handler telemetry", exc_info=True)Alternatively, keep 🤖 Generated by the Astra agent
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for the detailed trace, but I don't think this one holds up — I tried to reproduce the described state and couldn't reach it. Verified against The premise is that 1. Note the contrast with siblings like 2. No exception of any kind can escape self._record_otel_ids(trace)
self._sync_otel_context(trace)
return trace
3. The logging-disabled path leaves no chain. 4. So Separately: the suggested fix would introduce a real bug. That stamps someone else's trace as a 500 and clears their parent chain — exactly the invariant Leaving as-is. Happy to reopen if you can show a concrete path where |
||
|
|
||
| def log_node_tree(self, node: Node) -> None: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -110,17 +110,7 @@ def _commit_trace(self, trace: Trace) -> None: | |
| self._splunk_ao_logger.conclude(output=self._last_output, status_code=self._last_status_code) | ||
|
|
||
| def _conclude_current_trace_on_failure(self) -> None: | ||
| if self._owned_trace is None: | ||
| return | ||
|
|
||
| current_parent = self._splunk_ao_logger.current_parent() | ||
| if current_parent is None: | ||
| return | ||
|
|
||
| root = current_parent | ||
| while root._parent is not None: | ||
| root = root._parent | ||
| if root is self._owned_trace: | ||
| if self._splunk_ao_logger._is_current_root(self._owned_trace): | ||
| self._splunk_ao_logger.conclude(output="", status_code=500, conclude_all=True) | ||
|
Comment on lines
112
to
114
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 minor (bug): The old code had an explicit
This is the same shape as the 🤖 Generated by the Astra agent |
||
|
|
||
| def _log_node_tree(self, node: Node, first_node: bool = False) -> None: | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -368,7 +368,9 @@ def __init__( | |||||||||||||
| "User must provide project_name or project_id to SplunkAOLogger, or set it as an environment variable." | ||||||||||||||
| ) | ||||||||||||||
| if self.experiment_id is None and self.agent_stream_name is None and self.agent_stream_id is None: | ||||||||||||||
| raise SplunkAOLoggerException("agent_stream or agent_stream_id is required to initialize SplunkAOLogger.") | ||||||||||||||
| raise SplunkAOLoggerException( | ||||||||||||||
| "agent_stream or agent_stream_id is required to initialize SplunkAOLogger." | ||||||||||||||
| ) | ||||||||||||||
|
|
||||||||||||||
| if local_metrics: | ||||||||||||||
| self.local_metrics = local_metrics | ||||||||||||||
|
|
@@ -413,12 +415,20 @@ def _set_current_parent(self, parent: StepWithChildSpans | None) -> None: | |||||||||||||
| super()._set_current_parent(parent) | ||||||||||||||
| self._sync_otel_context(parent) | ||||||||||||||
|
|
||||||||||||||
| def reset_parent_tracking(self) -> None: | ||||||||||||||
| """Clear proprietary and OTel tracking for the current request context.""" | ||||||||||||||
| current_parent = self.current_parent() | ||||||||||||||
| root = current_parent | ||||||||||||||
| def _current_root(self) -> StepWithChildSpans | None: | ||||||||||||||
| """Return the root of the current proprietary parent chain.""" | ||||||||||||||
| root = self.current_parent() | ||||||||||||||
| while root is not None and root._parent is not None: | ||||||||||||||
| root = root._parent | ||||||||||||||
| return root | ||||||||||||||
|
|
||||||||||||||
| def _is_current_root(self, trace: Trace | None) -> bool: | ||||||||||||||
| """Return whether trace owns the current proprietary parent chain.""" | ||||||||||||||
| return trace is not None and self._current_root() is trace | ||||||||||||||
|
Comment on lines
+425
to
+427
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 minor (design): The
So mypy cannot actually verify that callers pass a Consider widening to match what def _is_current_root(self, trace: StepWithChildSpans | None) -> bool:
Suggested change
🤖 Generated by the Astra agent |
||||||||||||||
|
|
||||||||||||||
| def reset_parent_tracking(self) -> None: | ||||||||||||||
| """Clear proprietary and OTel tracking for the current request context.""" | ||||||||||||||
| root = self._current_root() | ||||||||||||||
|
|
||||||||||||||
| self._set_current_parent(None) | ||||||||||||||
| if root is not None: | ||||||||||||||
|
|
||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -86,6 +86,26 @@ def failing_operation() -> None: | |
| logger = splunk_ao_context.get_logger_instance() | ||
| assert logger.current_parent() is None | ||
| assert splunk_ao_context.get_current_trace() is None | ||
| assert (logger._sink.spans[-1].attributes or {})["splunk_ao.status_code"] == 500 | ||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_async_coroutine_exception_is_preserved_and_owned_trace_is_concluded(initialized_context: None) -> None: | ||
| # Given: a decorated async operation that raises an application exception | ||
| @log(span_type="workflow") | ||
| async def failing_operation() -> None: | ||
| await asyncio.sleep(0) | ||
| raise RuntimeError("async application failure") | ||
|
|
||
| # When: the operation is awaited | ||
| with pytest.raises(RuntimeError, match="async application failure"): | ||
| await failing_operation() | ||
|
|
||
| # Then: the original exception is re-raised and both telemetry contexts are released | ||
| logger = splunk_ao_context.get_logger_instance() | ||
| assert logger.current_parent() is None | ||
| assert splunk_ao_context.get_current_trace() is None | ||
|
Comment on lines
+100
to
+107
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 minor (testing): The test name promises
(The sync sibling at line 79 has the same gap; no need to fix it here, but it would be worth strengthening both together.) 🤖 Generated by the Astra agent |
||
| assert (logger._sink.spans[-1].attributes or {})["splunk_ao.status_code"] == 500 | ||
|
|
||
|
|
||
| def test_sync_generator_concludes_on_close_and_preserves_errors(initialized_context: None) -> None: | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 minor (bug): When a message carries an explicitly empty
partsand a non-emptycontent, this now silently drops the content entirely.Input
{"role": "user", "parts": [], "content": "hello"}maps to{"role": "user", "parts": []}—"hello"is gone. The old code was also lossy here (it emitted the bogus{"type": "text", "content": "[]"}and droppedcontenttoo), so this isn't a regression, but the fix is the natural place to decide the precedence. Since the point of the change is to stop losing/garbling content, havingparts: []win over real content seems like the wrong tiebreak — an adapter that initializesparts=[]by default and puts the payload incontentwould produce a message with no content at all.Suggest only honouring the empty list when there is nothing else to fall back on:
If
parts: []is meant to be authoritative regardless ofcontent, that's a defensible call — worth a short comment saying so, plus a test pinning the both-present case so the precedence isn't accidentally flipped later.🤖 Generated by the Astra agent