penguiflow 2.2.2__tar.gz → 2.2.3__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.

Potentially problematic release.


This version of penguiflow might be problematic. Click here for more details.

Files changed (58) hide show
  1. {penguiflow-2.2.2 → penguiflow-2.2.3}/PKG-INFO +1 -1
  2. {penguiflow-2.2.2 → penguiflow-2.2.3}/penguiflow/__init__.py +1 -1
  3. {penguiflow-2.2.2 → penguiflow-2.2.3}/penguiflow/core.py +24 -1
  4. {penguiflow-2.2.2 → penguiflow-2.2.3}/penguiflow.egg-info/PKG-INFO +1 -1
  5. {penguiflow-2.2.2 → penguiflow-2.2.3}/pyproject.toml +1 -1
  6. {penguiflow-2.2.2 → penguiflow-2.2.3}/tests/test_errors.py +37 -0
  7. {penguiflow-2.2.2 → penguiflow-2.2.3}/LICENSE +0 -0
  8. {penguiflow-2.2.2 → penguiflow-2.2.3}/README.md +0 -0
  9. {penguiflow-2.2.2 → penguiflow-2.2.3}/penguiflow/admin.py +0 -0
  10. {penguiflow-2.2.2 → penguiflow-2.2.3}/penguiflow/bus.py +0 -0
  11. {penguiflow-2.2.2 → penguiflow-2.2.3}/penguiflow/catalog.py +0 -0
  12. {penguiflow-2.2.2 → penguiflow-2.2.3}/penguiflow/debug.py +0 -0
  13. {penguiflow-2.2.2 → penguiflow-2.2.3}/penguiflow/errors.py +0 -0
  14. {penguiflow-2.2.2 → penguiflow-2.2.3}/penguiflow/metrics.py +0 -0
  15. {penguiflow-2.2.2 → penguiflow-2.2.3}/penguiflow/middlewares.py +0 -0
  16. {penguiflow-2.2.2 → penguiflow-2.2.3}/penguiflow/node.py +0 -0
  17. {penguiflow-2.2.2 → penguiflow-2.2.3}/penguiflow/patterns.py +0 -0
  18. {penguiflow-2.2.2 → penguiflow-2.2.3}/penguiflow/planner/__init__.py +0 -0
  19. {penguiflow-2.2.2 → penguiflow-2.2.3}/penguiflow/planner/prompts.py +0 -0
  20. {penguiflow-2.2.2 → penguiflow-2.2.3}/penguiflow/planner/react.py +0 -0
  21. {penguiflow-2.2.2 → penguiflow-2.2.3}/penguiflow/policies.py +0 -0
  22. {penguiflow-2.2.2 → penguiflow-2.2.3}/penguiflow/registry.py +0 -0
  23. {penguiflow-2.2.2 → penguiflow-2.2.3}/penguiflow/remote.py +0 -0
  24. {penguiflow-2.2.2 → penguiflow-2.2.3}/penguiflow/state.py +0 -0
  25. {penguiflow-2.2.2 → penguiflow-2.2.3}/penguiflow/streaming.py +0 -0
  26. {penguiflow-2.2.2 → penguiflow-2.2.3}/penguiflow/testkit.py +0 -0
  27. {penguiflow-2.2.2 → penguiflow-2.2.3}/penguiflow/types.py +0 -0
  28. {penguiflow-2.2.2 → penguiflow-2.2.3}/penguiflow/viz.py +0 -0
  29. {penguiflow-2.2.2 → penguiflow-2.2.3}/penguiflow.egg-info/SOURCES.txt +0 -0
  30. {penguiflow-2.2.2 → penguiflow-2.2.3}/penguiflow.egg-info/dependency_links.txt +0 -0
  31. {penguiflow-2.2.2 → penguiflow-2.2.3}/penguiflow.egg-info/entry_points.txt +0 -0
  32. {penguiflow-2.2.2 → penguiflow-2.2.3}/penguiflow.egg-info/requires.txt +0 -0
  33. {penguiflow-2.2.2 → penguiflow-2.2.3}/penguiflow.egg-info/top_level.txt +0 -0
  34. {penguiflow-2.2.2 → penguiflow-2.2.3}/penguiflow_a2a/__init__.py +0 -0
  35. {penguiflow-2.2.2 → penguiflow-2.2.3}/penguiflow_a2a/server.py +0 -0
  36. {penguiflow-2.2.2 → penguiflow-2.2.3}/setup.cfg +0 -0
  37. {penguiflow-2.2.2 → penguiflow-2.2.3}/tests/test_a2a_server.py +0 -0
  38. {penguiflow-2.2.2 → penguiflow-2.2.3}/tests/test_budgets.py +0 -0
  39. {penguiflow-2.2.2 → penguiflow-2.2.3}/tests/test_cancel.py +0 -0
  40. {penguiflow-2.2.2 → penguiflow-2.2.3}/tests/test_catalog.py +0 -0
  41. {penguiflow-2.2.2 → penguiflow-2.2.3}/tests/test_controller.py +0 -0
  42. {penguiflow-2.2.2 → penguiflow-2.2.3}/tests/test_core.py +0 -0
  43. {penguiflow-2.2.2 → penguiflow-2.2.3}/tests/test_distribution_hooks.py +0 -0
  44. {penguiflow-2.2.2 → penguiflow-2.2.3}/tests/test_metadata.py +0 -0
  45. {penguiflow-2.2.2 → penguiflow-2.2.3}/tests/test_metrics.py +0 -0
  46. {penguiflow-2.2.2 → penguiflow-2.2.3}/tests/test_middlewares.py +0 -0
  47. {penguiflow-2.2.2 → penguiflow-2.2.3}/tests/test_node.py +0 -0
  48. {penguiflow-2.2.2 → penguiflow-2.2.3}/tests/test_patterns.py +0 -0
  49. {penguiflow-2.2.2 → penguiflow-2.2.3}/tests/test_planner_prompts.py +0 -0
  50. {penguiflow-2.2.2 → penguiflow-2.2.3}/tests/test_property_based.py +0 -0
  51. {penguiflow-2.2.2 → penguiflow-2.2.3}/tests/test_react_planner.py +0 -0
  52. {penguiflow-2.2.2 → penguiflow-2.2.3}/tests/test_registry.py +0 -0
  53. {penguiflow-2.2.2 → penguiflow-2.2.3}/tests/test_remote.py +0 -0
  54. {penguiflow-2.2.2 → penguiflow-2.2.3}/tests/test_routing_policy.py +0 -0
  55. {penguiflow-2.2.2 → penguiflow-2.2.3}/tests/test_streaming.py +0 -0
  56. {penguiflow-2.2.2 → penguiflow-2.2.3}/tests/test_testkit.py +0 -0
  57. {penguiflow-2.2.2 → penguiflow-2.2.3}/tests/test_types.py +0 -0
  58. {penguiflow-2.2.2 → penguiflow-2.2.3}/tests/test_viz.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: penguiflow
3
- Version: 2.2.2
3
+ Version: 2.2.3
4
4
  Summary: Async agent orchestration primitives.
5
5
  Author: PenguiFlow Team
6
6
  License: MIT License
@@ -105,4 +105,4 @@ __all__ = [
105
105
  "TrajectoryStep",
106
106
  ]
107
107
 
108
- __version__ = "2.2.2"
108
+ __version__ = "2.2.3"
@@ -14,6 +14,7 @@ from collections import deque
14
14
  from collections.abc import Awaitable, Callable, Mapping, Sequence
15
15
  from contextlib import suppress
16
16
  from dataclasses import dataclass
17
+ from types import TracebackType
17
18
  from typing import Any, cast
18
19
 
19
20
  from .bus import BusEnvelope, MessageBus
@@ -27,6 +28,14 @@ from .types import WM, FinalAnswer, Message, StreamChunk
27
28
 
28
29
  logger = logging.getLogger("penguiflow.core")
29
30
 
31
+ ExcInfo = tuple[type[BaseException], BaseException, TracebackType | None]
32
+
33
+
34
+ def _capture_exc_info(exc: BaseException | None) -> ExcInfo | None:
35
+ if exc is None:
36
+ return None
37
+ return (type(exc), exc, exc.__traceback__)
38
+
30
39
  BUDGET_EXCEEDED_TEXT = "Hop budget exhausted"
31
40
  DEADLINE_EXCEEDED_TEXT = "Deadline exceeded"
32
41
  TOKEN_BUDGET_EXCEEDED_TEXT = "Token budget exhausted"
@@ -750,6 +759,7 @@ class PenguiFlow:
750
759
  raise
751
760
  except TimeoutError as exc:
752
761
  latency = (time.perf_counter() - start) * 1000
762
+ exc_info = _capture_exc_info(exc)
753
763
  await self._emit_event(
754
764
  event="node_timeout",
755
765
  node=node,
@@ -759,6 +769,7 @@ class PenguiFlow:
759
769
  latency_ms=latency,
760
770
  level=logging.WARNING,
761
771
  extra={"exception": repr(exc)},
772
+ exc_info=exc_info,
762
773
  )
763
774
  if attempt >= node.policy.max_retries:
764
775
  timeout_message: str | None = None
@@ -783,6 +794,7 @@ class PenguiFlow:
783
794
  flow_error=flow_error,
784
795
  latency=latency,
785
796
  attempt=attempt,
797
+ exc_info=exc_info,
786
798
  )
787
799
  return
788
800
  attempt += 1
@@ -801,6 +813,7 @@ class PenguiFlow:
801
813
  continue
802
814
  except Exception as exc: # noqa: BLE001
803
815
  latency = (time.perf_counter() - start) * 1000
816
+ exc_info = _capture_exc_info(exc)
804
817
  await self._emit_event(
805
818
  event="node_error",
806
819
  node=node,
@@ -810,6 +823,7 @@ class PenguiFlow:
810
823
  latency_ms=latency,
811
824
  level=logging.ERROR,
812
825
  extra={"exception": repr(exc)},
826
+ exc_info=exc_info,
813
827
  )
814
828
  if attempt >= node.policy.max_retries:
815
829
  flow_error = self._create_flow_error(
@@ -830,6 +844,7 @@ class PenguiFlow:
830
844
  flow_error=flow_error,
831
845
  latency=latency,
832
846
  attempt=attempt,
847
+ exc_info=exc_info,
833
848
  )
834
849
  return
835
850
  attempt += 1
@@ -890,6 +905,7 @@ class PenguiFlow:
890
905
  flow_error: FlowError,
891
906
  latency: float | None,
892
907
  attempt: int,
908
+ exc_info: ExcInfo | None,
893
909
  ) -> None:
894
910
  original = flow_error.unwrap()
895
911
  exception_repr = repr(original) if original is not None else flow_error.message
@@ -906,6 +922,7 @@ class PenguiFlow:
906
922
  latency_ms=latency,
907
923
  level=logging.ERROR,
908
924
  extra=extra,
925
+ exc_info=exc_info,
909
926
  )
910
927
  if self._emit_errors_to_rookery and flow_error.trace_id is not None:
911
928
  await self._emit_to_rookery(flow_error, source=context.owner)
@@ -1365,6 +1382,7 @@ class PenguiFlow:
1365
1382
  latency_ms: float | None,
1366
1383
  level: int,
1367
1384
  extra: dict[str, Any] | None = None,
1385
+ exc_info: ExcInfo | None = None,
1368
1386
  ) -> None:
1369
1387
  node_name = getattr(node, "name", None)
1370
1388
  node_id = getattr(node, "node_id", node_name)
@@ -1398,7 +1416,12 @@ class PenguiFlow:
1398
1416
  extra=extra or {},
1399
1417
  )
1400
1418
 
1401
- logger.log(level, event, extra=event_obj.to_payload())
1419
+ payload = event_obj.to_payload()
1420
+ log_kwargs: dict[str, Any] = {"extra": payload}
1421
+ if exc_info is not None:
1422
+ log_kwargs["exc_info"] = exc_info
1423
+
1424
+ logger.log(level, event, **log_kwargs)
1402
1425
 
1403
1426
  if self._state_store is not None:
1404
1427
  stored_event = StoredEvent.from_flow_event(event_obj)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: penguiflow
3
- Version: 2.2.2
3
+ Version: 2.2.3
4
4
  Summary: Async agent orchestration primitives.
5
5
  Author: PenguiFlow Team
6
6
  License: MIT License
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "penguiflow"
7
- version = "2.2.2"
7
+ version = "2.2.3"
8
8
  description = "Async agent orchestration primitives."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import asyncio
4
+ import logging
4
5
 
5
6
  import pytest
6
7
 
@@ -104,3 +105,39 @@ async def test_flow_error_metadata_for_timeouts() -> None:
104
105
  assert result.metadata["attempt"] == 0
105
106
 
106
107
  await flow.stop()
108
+
109
+
110
+ @pytest.mark.asyncio
111
+ async def test_node_error_logs_exc_info(caplog: pytest.LogCaptureFixture) -> None:
112
+ async def broken(_message: str, _ctx) -> None:
113
+ raise RuntimeError("dependency missing")
114
+
115
+ node = Node(
116
+ broken,
117
+ name="broken",
118
+ policy=NodePolicy(validate="none", max_retries=0),
119
+ )
120
+ flow = create(node.to(), emit_errors_to_rookery=True)
121
+ flow.run()
122
+
123
+ msg = Message(payload="payload", headers=Headers(tenant="demo"))
124
+
125
+ with caplog.at_level(logging.ERROR, logger="penguiflow.core"):
126
+ await flow.emit(msg)
127
+ await asyncio.wait_for(flow.fetch(), timeout=0.5)
128
+
129
+ node_error_records = [
130
+ record for record in caplog.records if record.message == "node_error"
131
+ ]
132
+ assert node_error_records, "expected node_error log entry"
133
+ assert node_error_records[0].exc_info is not None
134
+ assert isinstance(node_error_records[0].exc_info[1], RuntimeError)
135
+
136
+ node_failed_records = [
137
+ record for record in caplog.records if record.message == "node_failed"
138
+ ]
139
+ assert node_failed_records, "expected node_failed log entry"
140
+ assert node_failed_records[0].exc_info is not None
141
+ assert isinstance(node_failed_records[0].exc_info[1], RuntimeError)
142
+
143
+ await flow.stop()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes