bitfab-py 0.76.0__tar.gz → 0.76.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/PKG-INFO +1 -1
  2. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/client.py +92 -29
  3. bitfab_py-0.76.2/bitfab/error_chain.py +101 -0
  4. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/replay.py +10 -12
  5. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/subtree.py +9 -2
  6. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/thread_propagation.py +15 -5
  7. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/pyproject.toml +1 -1
  8. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/README.md +0 -0
  9. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/__init__.py +0 -0
  10. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/agent_middleware.py +0 -0
  11. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/approval.py +0 -0
  12. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/assertion_categories.py +0 -0
  13. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/baml.py +0 -0
  14. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/baml_auto_trace.py +0 -0
  15. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/categories.py +0 -0
  16. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/claude_agent_sdk.py +0 -0
  17. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/client_patching.py +0 -0
  18. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/cloudReplay.py +0 -0
  19. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/cloud_replay_cli.py +0 -0
  20. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/commit_ref.py +0 -0
  21. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/compress.py +0 -0
  22. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/constants.py +0 -0
  23. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/datasets.py +0 -0
  24. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/db_snapshot.py +0 -0
  25. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/experiment_id.py +0 -0
  26. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/experiments.py +0 -0
  27. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/finalizers.py +0 -0
  28. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/git_command.py +0 -0
  29. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/git_state.py +0 -0
  30. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/graders.py +0 -0
  31. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/http.py +0 -0
  32. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/integration_fixture.py +0 -0
  33. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/jev.py +0 -0
  34. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/labels.py +0 -0
  35. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/langgraph.py +0 -0
  36. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/langgraph_integration.py +0 -0
  37. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/langgraph_runs.py +0 -0
  38. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/mock_matching.py +0 -0
  39. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/mock_override.py +0 -0
  40. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/openai_agent_sdk.py +0 -0
  41. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/organization_members.py +0 -0
  42. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/otel.py +0 -0
  43. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/payload_budget.py +0 -0
  44. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/processor_payload.py +0 -0
  45. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/provider_calls.py +0 -0
  46. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/replay_branch.py +0 -0
  47. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/replay_cli.py +0 -0
  48. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/replay_heartbeat.py +0 -0
  49. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/replay_interrupt.py +0 -0
  50. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/replay_invocation.py +0 -0
  51. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/replay_memory.py +0 -0
  52. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/replay_processes.py +0 -0
  53. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/replay_registry.py +0 -0
  54. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/seed_cli.py +0 -0
  55. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/selective_replay.py +0 -0
  56. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/serialize.py +0 -0
  57. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/simulation_plan.py +0 -0
  58. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/span_meta.py +0 -0
  59. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/span_origin.py +0 -0
  60. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/spans.py +0 -0
  61. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/timestamp.py +0 -0
  62. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/trace_completion.py +0 -0
  63. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/trace_metadata.py +0 -0
  64. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/trace_payload.py +0 -0
  65. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/traces.py +0 -0
  66. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/tracing.py +0 -0
  67. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/transport.py +0 -0
  68. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/transport_types.py +0 -0
  69. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/verdicts.py +0 -0
  70. {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/warn_once.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bitfab-py
3
- Version: 0.76.0
3
+ Version: 0.76.2
4
4
  Summary: Bitfab client for provider-based API calls with local BAML execution
5
5
  Author: Harvest Team
6
6
  Requires-Python: >=3.10
@@ -45,6 +45,7 @@ from bitfab.constants import (
45
45
  )
46
46
  from bitfab.datasets import DatasetsClient
47
47
  from bitfab.db_snapshot import DbSnapshotRef, build_snapshot_ref
48
+ from bitfab.error_chain import describe_error
48
49
  from bitfab.experiment_id import resolve_experiment_id
49
50
  from bitfab.experiments import ExperimentsClient
50
51
  from bitfab.graders import GradersClient
@@ -477,6 +478,7 @@ class SpanContext(TypedDict, total=False):
477
478
  enclosingTrace: TraceLink
478
479
  variantChain: VariantChain
479
480
  spanName: str
481
+ traceFunctionKey: str
480
482
  rootTraceFunctionKey: str
481
483
  instrumentation: SpanInstrumentation
482
484
  declaredNode: bool
@@ -547,6 +549,10 @@ _span_stack: ContextVar[list[SpanContext]] = ContextVar(
547
549
  "bitfab_span_stack", default=None
548
550
  )
549
551
 
552
+ _enclosing_trace_function_keys: ContextVar[tuple[str, ...]] = ContextVar(
553
+ "bitfab_enclosing_trace_function_keys", default=()
554
+ )
555
+
550
556
  # Global dict to track active trace states (trace_id -> TraceState)
551
557
  _active_trace_states: dict[str, TraceState] = {}
552
558
 
@@ -680,11 +686,28 @@ def _trace_root_span_context() -> SpanContext | None:
680
686
  )
681
687
 
682
688
 
689
+ def _running_trace_function_keys(stack: list[SpanContext]) -> list[str]:
690
+ keys: list[str] = []
691
+ candidates = list(_enclosing_trace_function_keys.get())
692
+ candidates.extend(context.get("traceFunctionKey", "") for context in stack)
693
+ for key in candidates:
694
+ if key and key not in keys:
695
+ keys.append(key)
696
+ return keys
697
+
698
+
683
699
  class CurrentSpan:
684
700
  """Handle to the current active span, allowing context to be added."""
685
701
 
686
- def __init__(self, context: SpanContext) -> None:
702
+ def __init__(
703
+ self, context: SpanContext, trace_function_keys: list[str] | None = None
704
+ ) -> None:
687
705
  self._context = context
706
+ self._trace_function_keys = (
707
+ trace_function_keys
708
+ if trace_function_keys is not None
709
+ else _running_trace_function_keys([context])
710
+ )
688
711
 
689
712
  @property
690
713
  def id(self) -> str:
@@ -696,6 +719,15 @@ class CurrentSpan:
696
719
  """The trace ID for the current span."""
697
720
  return self._context["traceId"]
698
721
 
722
+ @property
723
+ def trace_function_keys(self) -> list[str]:
724
+ """The keys of every trace function running at this point, outermost
725
+ first, each listed once. A trace function called inside another one
726
+ comes after it, so a cost recorded here can be counted toward each of
727
+ them. Empty outside a trace.
728
+ """
729
+ return list(self._trace_function_keys)
730
+
699
731
  def add_context(self, context: dict[str, Any]) -> None:
700
732
  """Add a context entry to this span.
701
733
 
@@ -744,8 +776,26 @@ def _on_event_loop_thread() -> bool:
744
776
  class CurrentTrace:
745
777
  """Handle to the current active trace, allowing trace-level context to be set."""
746
778
 
747
- def __init__(self, trace_id: str) -> None:
748
- self._trace_id = trace_id
779
+ def __init__(self, context: SpanContext) -> None:
780
+ self._trace_id = context["traceId"]
781
+ self._root_trace_function_key = context.get("rootTraceFunctionKey")
782
+
783
+ @property
784
+ def trace_function_key(self) -> str:
785
+ """The key of the trace function the current trace is stored under,
786
+ which is the one that started it.
787
+
788
+ A ``@span`` or ``get_function`` call inside another trace function
789
+ joins that trace, so it reports the outer key. A ``trace()`` root
790
+ inside another root starts its own trace, so it reports its own key.
791
+ Empty string outside a trace.
792
+ """
793
+ return (
794
+ _resolve_root_trace_function_key(
795
+ self._root_trace_function_key, self._trace_id, None
796
+ )
797
+ or ""
798
+ )
749
799
 
750
800
  def _get_or_create_trace_state(self) -> TraceState:
751
801
  """Get existing trace state or create a new one."""
@@ -961,6 +1011,11 @@ class _NoOpCurrentSpan:
961
1011
  """Return empty string when outside a span context."""
962
1012
  return ""
963
1013
 
1014
+ @property
1015
+ def trace_function_keys(self) -> list[str]:
1016
+ """Return an empty list when outside a span context."""
1017
+ return []
1018
+
964
1019
  def add_context(self, context: dict[str, Any]) -> None:
965
1020
  pass
966
1021
 
@@ -971,6 +1026,11 @@ class _NoOpCurrentSpan:
971
1026
  class _NoOpCurrentTrace:
972
1027
  """No-op trace handle returned when outside a span context."""
973
1028
 
1029
+ @property
1030
+ def trace_function_key(self) -> str:
1031
+ """Return empty string when outside a span context."""
1032
+ return ""
1033
+
974
1034
  def set_session_id(self, session_id: str) -> None:
975
1035
  pass
976
1036
 
@@ -1001,7 +1061,7 @@ def get_current_span() -> CurrentSpan | _NoOpCurrentSpan:
1001
1061
  """
1002
1062
  stack = _get_span_stack()
1003
1063
  if stack:
1004
- return CurrentSpan(stack[-1])
1064
+ return CurrentSpan(stack[-1], _running_trace_function_keys(stack))
1005
1065
  return _NO_OP_SPAN
1006
1066
 
1007
1067
 
@@ -1045,10 +1105,22 @@ def get_current_trace() -> CurrentTrace | _NoOpCurrentTrace:
1045
1105
  """
1046
1106
  stack = _get_span_stack()
1047
1107
  if stack:
1048
- return CurrentTrace(stack[-1]["traceId"])
1108
+ return CurrentTrace(stack[-1])
1049
1109
  return _NO_OP_TRACE
1050
1110
 
1051
1111
 
1112
+ @contextlib.contextmanager
1113
+ def _trace_root_scope(scope: tuple[list[SpanContext], tuple[str, ...]]) -> Any:
1114
+ stack, enclosing_keys = scope
1115
+ stack_token = _span_stack.set(stack)
1116
+ keys_token = _enclosing_trace_function_keys.set(enclosing_keys)
1117
+ try:
1118
+ yield
1119
+ finally:
1120
+ _enclosing_trace_function_keys.reset(keys_token)
1121
+ _span_stack.reset(stack_token)
1122
+
1123
+
1052
1124
  def _run_with_span_stack(stack: list[SpanContext], fn: Callable[[], T]) -> T:
1053
1125
  """Run a function with a new span stack context."""
1054
1126
  token = _span_stack.set(stack)
@@ -2660,6 +2732,7 @@ class Bitfab:
2660
2732
  "spanId": span_id,
2661
2733
  "threadId": executing_thread.ident or 0,
2662
2734
  "spanName": span_name,
2735
+ "traceFunctionKey": trace_function_key,
2663
2736
  "rootTraceFunctionKey": (
2664
2737
  root_state_at_open.get("traceFunctionKey", trace_function_key)
2665
2738
  if root_state_at_open
@@ -3258,7 +3331,7 @@ class Bitfab:
3258
3331
  yielded.append(value)
3259
3332
  yield value
3260
3333
  except Exception as e:
3261
- error_msg = str(e)
3334
+ error_msg = describe_error(e)
3262
3335
  raise
3263
3336
  finally:
3264
3337
  token = _span_stack.set(new_stack)
@@ -3271,7 +3344,7 @@ class Bitfab:
3271
3344
  ):
3272
3345
  iterator.close()
3273
3346
  except Exception as e:
3274
- error_msg = str(e)
3347
+ error_msg = describe_error(e)
3275
3348
  raise
3276
3349
  finally:
3277
3350
  if finalize is not None and error_msg is None:
@@ -3369,7 +3442,7 @@ class Bitfab:
3369
3442
  error_msg = str(e) or e.__class__.__name__
3370
3443
  raise
3371
3444
  except Exception as e:
3372
- error_msg = str(e)
3445
+ error_msg = describe_error(e)
3373
3446
  raise
3374
3447
  finally:
3375
3448
  token = _span_stack.set(new_stack)
@@ -3385,7 +3458,7 @@ class Bitfab:
3385
3458
  error_msg = str(e) or e.__class__.__name__
3386
3459
  raise
3387
3460
  except Exception as e:
3388
- error_msg = str(e)
3461
+ error_msg = describe_error(e)
3389
3462
  raise
3390
3463
  finally:
3391
3464
  if not sent:
@@ -3563,7 +3636,7 @@ class Bitfab:
3563
3636
  span_ctx,
3564
3637
  **base_params,
3565
3638
  result=None,
3566
- error=str(e),
3639
+ error=describe_error(e),
3567
3640
  ended_at=_get_ended_at(),
3568
3641
  )
3569
3642
  raise
@@ -3649,7 +3722,7 @@ class Bitfab:
3649
3722
  span_ctx,
3650
3723
  **base_params,
3651
3724
  result=None,
3652
- error=str(e),
3725
+ error=describe_error(e),
3653
3726
  ended_at=_get_ended_at(),
3654
3727
  )
3655
3728
  raise
@@ -4431,16 +4504,16 @@ class Bitfab:
4431
4504
  surface="opt-out",
4432
4505
  )(inner)
4433
4506
 
4434
- def root_span_stack() -> list[SpanContext] | None:
4507
+ def root_span_stack() -> tuple[list[SpanContext], tuple[str, ...]] | None:
4435
4508
  stack = _get_span_stack()
4436
4509
  enclosing = stack[-1].get("surface") if stack else None
4437
4510
  if enclosing == "opt-in":
4438
4511
  raise _mixed_tracing_error("trace", "span")
4439
4512
  if enclosing != "opt-out":
4440
- return stack
4513
+ return stack, _enclosing_trace_function_keys.get()
4441
4514
  if _in_replay_item() or _in_seed_scope():
4442
4515
  return None
4443
- return []
4516
+ return [], tuple(_running_trace_function_keys(stack))
4444
4517
 
4445
4518
  if inspect.isasyncgenfunction(fn):
4446
4519
 
@@ -4458,21 +4531,15 @@ class Bitfab:
4458
4531
  iterator = spanned(*args, **kwargs)
4459
4532
  try:
4460
4533
  while True:
4461
- token = _span_stack.set(root_stack)
4462
- try:
4534
+ with _trace_root_scope(root_stack):
4463
4535
  try:
4464
4536
  item = await iterator.__anext__()
4465
4537
  except StopAsyncIteration:
4466
4538
  return
4467
- finally:
4468
- _span_stack.reset(token)
4469
4539
  yield item
4470
4540
  finally:
4471
- token = _span_stack.set(root_stack)
4472
- try:
4541
+ with _trace_root_scope(root_stack):
4473
4542
  await iterator.aclose()
4474
- finally:
4475
- _span_stack.reset(token)
4476
4543
 
4477
4544
  elif inspect.iscoroutinefunction(fn):
4478
4545
 
@@ -4481,11 +4548,8 @@ class Bitfab:
4481
4548
  root_stack = root_span_stack()
4482
4549
  if root_stack is None:
4483
4550
  return await body(*args, **kwargs)
4484
- token = _span_stack.set(root_stack)
4485
- try:
4551
+ with _trace_root_scope(root_stack):
4486
4552
  return await spanned(*args, **kwargs)
4487
- finally:
4488
- _span_stack.reset(token)
4489
4553
 
4490
4554
  else:
4491
4555
 
@@ -4494,9 +4558,8 @@ class Bitfab:
4494
4558
  root_stack = root_span_stack()
4495
4559
  if root_stack is None:
4496
4560
  return body(*args, **kwargs)
4497
- return _run_with_span_stack(
4498
- root_stack, lambda: spanned(*args, **kwargs)
4499
- )
4561
+ with _trace_root_scope(root_stack):
4562
+ return spanned(*args, **kwargs)
4500
4563
 
4501
4564
  _trace_root_wrappers.add(detached)
4502
4565
  return detached # type: ignore[return-value]
@@ -0,0 +1,101 @@
1
+ """Read the chain of causes below an exception.
2
+
3
+ A database driver error usually reaches a traced function wrapped one or two
4
+ levels down (``raise QueryError(...) from timeout``). Recording only the top
5
+ message makes a connection timeout read as a broken query, so spans and replay
6
+ results carry the deepest cause too.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Any, Callable
12
+
13
+ MAX_ERROR_CAUSE_DEPTH = 10
14
+ """How many causes below an exception are followed."""
15
+
16
+ _CODE_ATTRIBUTES = ("code", "sqlstate", "pgcode", "errno")
17
+
18
+
19
+ def _cause_of(error: BaseException) -> BaseException | None:
20
+ # The same rule Python's traceback uses: an explicit ``from`` cause wins,
21
+ # and an implicit context counts unless ``from None`` suppressed it. Bitfab's
22
+ # own replay errors also keep their cause on a ``cause`` attribute.
23
+ if error.__cause__ is not None:
24
+ return error.__cause__
25
+ attribute = getattr(error, "cause", None)
26
+ if isinstance(attribute, BaseException):
27
+ return attribute
28
+ if error.__suppress_context__:
29
+ return None
30
+ return error.__context__
31
+
32
+
33
+ def _error_code(error: BaseException) -> str | int | None:
34
+ for attribute in _CODE_ATTRIBUTES:
35
+ value = getattr(error, attribute, None)
36
+ if isinstance(value, (str, int)) and not isinstance(value, bool):
37
+ return value
38
+ return None
39
+
40
+
41
+ def error_causes(error: BaseException) -> list[BaseException]:
42
+ """The causes below ``error``, nearest first, stopping at the depth limit
43
+ or at a cause already seen."""
44
+ causes: list[BaseException] = []
45
+ seen = {id(error)}
46
+ current = error
47
+ while len(causes) < MAX_ERROR_CAUSE_DEPTH:
48
+ cause = _cause_of(current)
49
+ if cause is None or id(cause) in seen:
50
+ break
51
+ causes.append(cause)
52
+ seen.add(id(cause))
53
+ current = cause
54
+ return causes
55
+
56
+
57
+ def describe_error(error: BaseException) -> str:
58
+ """The exception's message, followed by the deepest cause's message when
59
+ it wraps another exception, e.g. ``Failed query: ... (caused by:
60
+ connection timeout)``. The cause is left off when the message already
61
+ contains it."""
62
+ message = str(error)
63
+ causes = error_causes(error)
64
+ if not causes:
65
+ return message
66
+ deepest = str(causes[-1]).strip()
67
+ if not deepest or deepest in message:
68
+ return message
69
+ return f"{message} (caused by: {deepest})"
70
+
71
+
72
+ def serialize_error(
73
+ error: BaseException,
74
+ extra: Callable[[BaseException], dict[str, Any]] | None = None,
75
+ ) -> dict[str, Any]:
76
+ """An exception as a JSON record: type, message, the driver's code when it
77
+ has one, and its causes nested under ``cause`` down to the depth limit.
78
+ ``extra`` adds fields a specific exception type carries."""
79
+ seen: set[int] = set()
80
+
81
+ def serialize(value: BaseException, depth: int) -> dict[str, Any]:
82
+ seen.add(id(value))
83
+ serialized: dict[str, Any] = {
84
+ "type": type(value).__name__,
85
+ "message": str(value),
86
+ }
87
+ code = _error_code(value)
88
+ if code is not None:
89
+ serialized["code"] = code
90
+ if extra is not None:
91
+ serialized.update(extra(value))
92
+ cause = _cause_of(value)
93
+ if (
94
+ cause is not None
95
+ and id(cause) not in seen
96
+ and depth < MAX_ERROR_CAUSE_DEPTH
97
+ ):
98
+ serialized["cause"] = serialize(cause, depth + 1)
99
+ return serialized
100
+
101
+ return serialize(error, 1)
@@ -24,6 +24,7 @@ from bitfab.constants import (
24
24
  _replay_context,
25
25
  _seed_context,
26
26
  )
27
+ from bitfab.error_chain import describe_error, serialize_error
27
28
  from bitfab.git_command import run_git
28
29
  from bitfab.integration_fixture import extract_integration_fixtures
29
30
  from bitfab.mock_matching import MATCHER
@@ -580,19 +581,16 @@ def report_replay_progress(
580
581
 
581
582
  def _replay_json_default(value: Any) -> Any:
582
583
  if isinstance(value, BaseException):
583
- serialized = {
584
- "type": type(value).__name__,
585
- "message": str(value),
586
- }
587
- if isinstance(value, DbBranchReplayError):
588
- serialized["code"] = value.code
589
- serialized["original_trace_id"] = value.original_trace_id
590
- if value.cause is not None:
591
- serialized["cause"] = value.cause
592
- return serialized
584
+ return serialize_error(value, _replay_error_fields)
593
585
  return str(value)
594
586
 
595
587
 
588
+ def _replay_error_fields(error: BaseException) -> dict[str, Any]:
589
+ if isinstance(error, DbBranchReplayError):
590
+ return {"original_trace_id": error.original_trace_id}
591
+ return {}
592
+
593
+
596
594
  def serialize_replay_result(result: ReplayResult) -> str:
597
595
  """Serialize a replay result while retaining structured exception fields."""
598
596
  return json.dumps(result, indent=2, default=_replay_json_default)
@@ -653,7 +651,7 @@ def _replay_item_error_message(error: BaseException) -> str:
653
651
  "replaying it against the live database would produce a result that "
654
652
  "looks valid but did not use the historical data you asked for."
655
653
  )
656
- return str(error)
654
+ return describe_error(error)
657
655
 
658
656
 
659
657
  class AdaptContext(TypedDict):
@@ -3041,7 +3039,7 @@ async def _execute_item_async(
3041
3039
  input=reported_input,
3042
3040
  result=fn_result,
3043
3041
  original_output=item.get("output"),
3044
- error=str(item_error) if item_error is not None else None,
3042
+ error=describe_error(item_error) if item_error is not None else None,
3045
3043
  trace_error=fn_error,
3046
3044
  replay_error=replay_error,
3047
3045
  duration_ms=replay_duration_ms,
@@ -23,6 +23,7 @@ from dataclasses import dataclass, field
23
23
  from typing import Any, Callable
24
24
 
25
25
  from bitfab.constants import _replay_context
26
+ from bitfab.error_chain import describe_error
26
27
  from bitfab.trace_metadata import record_caller_trace_metadata
27
28
  from bitfab.warn_once import warn_once
28
29
 
@@ -795,7 +796,9 @@ def _on_unwind(code: Any, instruction_offset: int, exception: BaseException) ->
795
796
  # the first time an exception passed through library code. Filter instead.
796
797
  if is_library_code(code.co_filename):
797
798
  return None
798
- return _close(code, None, f"{type(exception).__name__}: {exception}")
799
+ return _close(
800
+ code, None, f"{type(exception).__name__}: {describe_error(exception)}"
801
+ )
799
802
 
800
803
 
801
804
  def _is_free_threaded() -> bool:
@@ -1033,7 +1036,11 @@ class BorrowedSpan:
1033
1036
  updated[session] = _without(stack, index)
1034
1037
  _open.set(updated)
1035
1038
  if exc_type is not None:
1036
- self.emit(None, f"{exc_type.__name__}: {exc}")
1039
+ self.emit(
1040
+ None,
1041
+ f"{exc_type.__name__}: "
1042
+ f"{describe_error(exc) if isinstance(exc, BaseException) else exc}",
1043
+ )
1037
1044
 
1038
1045
 
1039
1046
  def borrowed_span(
@@ -17,8 +17,8 @@ the SDK's own span-transport threads are excluded so a long-lived dispatch
17
17
  loop can never pin the context of whichever trace happened to spawn it.
18
18
  Callables that already carry a context (``asyncio.to_thread`` submits
19
19
  ``Context.run``) pass through untouched. Only Bitfab's own variables are
20
- carried (the span stack, the replay context, the seed context, and the submit
21
- origin: the identity of the thread that called submit()/start(), recorded onto
20
+ carried (the span stack, the keys of the trace functions around a nested
21
+ trace root, the replay context, the seed context, and the submit origin: the identity of the thread that called submit()/start(), recorded onto
22
22
  worker spans as ``runtime.submit_thread_id``), so no other library's
23
23
  contextvars change behavior.
24
24
 
@@ -54,7 +54,7 @@ _installed = False
54
54
  _original_submit: Callable[..., Any] | None = None
55
55
  _original_thread_start: Callable[..., Any] | None = None
56
56
 
57
- _CapturedState = tuple[Any, Any, Any, dict[str, Any]]
57
+ _CapturedState = tuple[Any, tuple[str, ...], Any, Any, dict[str, Any]]
58
58
 
59
59
 
60
60
  def _span_stack_var() -> contextvars.ContextVar[Any]:
@@ -63,9 +63,16 @@ def _span_stack_var() -> contextvars.ContextVar[Any]:
63
63
  return _span_stack
64
64
 
65
65
 
66
+ def _enclosing_keys_var() -> contextvars.ContextVar[tuple[str, ...]]:
67
+ from bitfab.client import _enclosing_trace_function_keys
68
+
69
+ return _enclosing_trace_function_keys
70
+
71
+
66
72
  def _capture() -> _CapturedState | None:
67
73
  try:
68
74
  stack = _span_stack_var().get()
75
+ enclosing_keys = _enclosing_keys_var().get()
69
76
  replay_ctx = _replay_context.get()
70
77
  seed_ctx = _seed_context.get()
71
78
  except Exception:
@@ -74,14 +81,16 @@ def _capture() -> _CapturedState | None:
74
81
  return None
75
82
  submitter = threading.current_thread()
76
83
  origin = {"thread_id": submitter.ident, "thread_name": submitter.name}
77
- return (stack, replay_ctx, seed_ctx, origin)
84
+ return (stack, enclosing_keys, replay_ctx, seed_ctx, origin)
78
85
 
79
86
 
80
87
  @contextlib.contextmanager
81
88
  def _entered(state: _CapturedState) -> Any:
82
- stack, replay_ctx, seed_ctx, origin = state
89
+ stack, enclosing_keys, replay_ctx, seed_ctx, origin = state
83
90
  span_var = _span_stack_var()
91
+ keys_var = _enclosing_keys_var()
84
92
  span_token = span_var.set(stack)
93
+ keys_token = keys_var.set(enclosing_keys)
85
94
  replay_token = _replay_context.set(replay_ctx)
86
95
  seed_token = _seed_context.set(seed_ctx)
87
96
  origin_token = _submit_origin.set(origin)
@@ -89,6 +98,7 @@ def _entered(state: _CapturedState) -> Any:
89
98
  yield
90
99
  finally:
91
100
  span_var.reset(span_token)
101
+ keys_var.reset(keys_token)
92
102
  _replay_context.reset(replay_token)
93
103
  _seed_context.reset(seed_token)
94
104
  _submit_origin.reset(origin_token)
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "bitfab-py"
3
- version = "0.76.0"
3
+ version = "0.76.2"
4
4
  description = "Bitfab client for provider-based API calls with local BAML execution"
5
5
  authors = ["Harvest Team"]
6
6
  readme = "README.md"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes