by-framework 0.2.2.dev3__py3-none-any.whl → 0.2.2.dev5__py3-none-any.whl

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 (60) hide show
  1. by_framework/client/client.py +117 -9
  2. by_framework/common/config.py +2 -0
  3. by_framework/common/constants.py +5 -1
  4. by_framework/common/logger.py +13 -1
  5. by_framework/core/__init__.py +95 -0
  6. by_framework/core/availability.py +495 -0
  7. by_framework/core/delivery_gate.py +60 -0
  8. by_framework/core/discovery.py +359 -0
  9. by_framework/core/extensions/__init__.py +35 -0
  10. by_framework/core/extensions/agent_config.py +64 -0
  11. by_framework/core/extensions/plugin.py +314 -0
  12. by_framework/core/extensions/registry.py +711 -0
  13. by_framework/core/extensions/trace_provider.py +20 -0
  14. by_framework/core/protocol/__init__.py +133 -0
  15. by_framework/core/protocol/action_type.py +33 -0
  16. by_framework/core/protocol/agent_state.py +78 -0
  17. by_framework/core/protocol/byai_codec.py +101 -0
  18. by_framework/core/protocol/byai_command.py +53 -0
  19. by_framework/core/protocol/byai_types.py +7 -0
  20. by_framework/core/protocol/commands.py +285 -0
  21. by_framework/core/protocol/content_codec.py +17 -0
  22. by_framework/core/protocol/content_type.py +38 -0
  23. by_framework/core/protocol/data_message.py +45 -0
  24. by_framework/core/protocol/data_shapes.py +83 -0
  25. by_framework/core/protocol/event_type.py +34 -0
  26. by_framework/core/protocol/events.py +69 -0
  27. by_framework/core/protocol/message.py +99 -0
  28. by_framework/core/protocol/message_header.py +78 -0
  29. by_framework/core/protocol/responses.py +94 -0
  30. by_framework/core/protocol/results.py +149 -0
  31. by_framework/core/registry.py +1254 -0
  32. by_framework/core/runtime/__init__.py +29 -0
  33. by_framework/core/runtime/agent_config_manager.py +283 -0
  34. by_framework/core/runtime/agent_runtime_state.py +75 -0
  35. by_framework/core/runtime/file_manager.py +437 -0
  36. by_framework/core/runtime/file_paths.py +76 -0
  37. by_framework/core/runtime/file_permissions.py +71 -0
  38. by_framework/core/runtime/filestore/__init__.py +15 -0
  39. by_framework/core/runtime/filestore/base.py +140 -0
  40. by_framework/core/runtime/filestore/local.py +321 -0
  41. by_framework/core/runtime/history/__init__.py +10 -0
  42. by_framework/core/runtime/history/base.py +57 -0
  43. by_framework/core/runtime/history/history_manager.py +55 -0
  44. by_framework/core/runtime/history/in_memory.py +58 -0
  45. by_framework/core/runtime/session_manager.py +118 -0
  46. by_framework/core/wakeup_controller.py +151 -0
  47. by_framework/core/workspace.py +126 -0
  48. by_framework/metrics/__init__.py +2 -0
  49. by_framework/metrics/collector.py +161 -0
  50. by_framework/trace/__init__.py +2 -0
  51. by_framework/trace/span_recorder.py +30 -0
  52. by_framework/worker/context.py +1 -65
  53. by_framework/worker/heartbeat.py +204 -18
  54. by_framework/worker/runner.py +123 -6
  55. by_framework/worker/worker.py +7 -0
  56. {by_framework-0.2.2.dev3.dist-info → by_framework-0.2.2.dev5.dist-info}/METADATA +1 -1
  57. by_framework-0.2.2.dev5.dist-info/RECORD +93 -0
  58. by_framework-0.2.2.dev3.dist-info/RECORD +0 -49
  59. {by_framework-0.2.2.dev3.dist-info → by_framework-0.2.2.dev5.dist-info}/WHEEL +0 -0
  60. {by_framework-0.2.2.dev3.dist-info → by_framework-0.2.2.dev5.dist-info}/licenses/LICENSE +0 -0
@@ -43,7 +43,16 @@ from by_framework.core.protocol.responses import (
43
43
  )
44
44
  from by_framework.core.registry import WorkerRegistry
45
45
  from by_framework.errors import WorkerRegistryNotSetError
46
- from by_framework.trace.span_recorder import (SpanRecorder, TraceSpan, str_to_uint64)
46
+ from by_framework.trace.span_recorder import (
47
+ ObservabilityConfig,
48
+ SpanRecorder,
49
+ TraceSpan,
50
+ build_observability_config,
51
+ sanitize_io_value,
52
+ str_to_uint64,
53
+ )
54
+ from by_framework.trace.trace_schema import TraceRecord
55
+ from by_framework.trace.trace_writer import TraceWriteClient
47
56
 
48
57
  if TYPE_CHECKING:
49
58
  pass
@@ -89,13 +98,30 @@ class GatewayClient:
89
98
  redis_client: Optional[Redis] = None,
90
99
  interceptors: Optional[List[GatewayInterceptor]] = None,
91
100
  span_recorder: Optional[SpanRecorder] = None,
101
+ obs_config: Optional[ObservabilityConfig] = None,
92
102
  ):
93
103
  self.registry = registry
94
104
  self.redis = (
95
105
  redis_client or (registry.redis if registry else None) or get_redis()
96
106
  )
97
107
  self.interceptors = interceptors or []
98
- self.span_recorder = span_recorder or SpanRecorder(self.redis)
108
+ self._obs_config = obs_config or build_observability_config()
109
+ self.span_recorder = span_recorder or SpanRecorder(
110
+ self.redis, config=self._obs_config
111
+ )
112
+ self._trace_writer = TraceWriteClient(
113
+ self.redis, ttl_seconds=self._obs_config.ttl_seconds
114
+ )
115
+ self._langfuse_dispatch_fn = self._resolve_langfuse_dispatch_fn()
116
+
117
+ @staticmethod
118
+ def _resolve_langfuse_dispatch_fn() -> Any:
119
+ try:
120
+ from by_framework_trace_langfuse import start_client_dispatch_observation
121
+
122
+ return start_client_dispatch_observation
123
+ except ImportError:
124
+ return None
99
125
 
100
126
  def add_interceptor(self, interceptor: GatewayInterceptor):
101
127
  self.interceptors.append(interceptor)
@@ -607,6 +633,20 @@ class GatewayClient:
607
633
  message_id = f"{MESSAGE_ID_PREFIX}{uuid.uuid4().hex[:8]}"
608
634
  if not trace_id:
609
635
  trace_id = uuid.uuid4().hex
636
+ trace_start_ts = int(time.time() * 1000)
637
+ is_root_dispatch = not params.get("parent_message_id", "")
638
+
639
+ # Write trace root start so the trace is visible even before the worker
640
+ # picks up the task. Only written for root dispatches (no parent).
641
+ if is_root_dispatch:
642
+ await self._write_trace_root_start(
643
+ trace_id=trace_id,
644
+ message_id=message_id,
645
+ session_id=str(params.get("session_id", "")),
646
+ target_agent_type=str(params.get("target_agent_type", "")),
647
+ content=params.get("content"),
648
+ start_ts=trace_start_ts,
649
+ )
610
650
 
611
651
  metadata = dict(params.get("metadata", {}) or {})
612
652
  trace_parent_span_id = metadata.pop("trace_parent_span_id", "")
@@ -619,7 +659,7 @@ class GatewayClient:
619
659
  )
620
660
 
621
661
  langfuse_client_dispatch = None
622
- if not params["parent_message_id"]:
662
+ if not langfuse_parent_observation_id:
623
663
  langfuse_client_dispatch = self._start_langfuse_client_dispatch_observation(
624
664
  trace_id=trace_id,
625
665
  message_id=message_id,
@@ -631,8 +671,7 @@ class GatewayClient:
631
671
  metadata=metadata,
632
672
  )
633
673
  observation_id = getattr(langfuse_client_dispatch, "id", "")
634
- if observation_id:
635
- langfuse_parent_observation_id = observation_id
674
+ langfuse_parent_observation_id = observation_id or trace_parent_span_id
636
675
 
637
676
  header = MessageHeader(
638
677
  message_id=message_id,
@@ -734,6 +773,13 @@ class GatewayClient:
734
773
  output={"success": False, "error": availability.error},
735
774
  error=availability.error,
736
775
  )
776
+ if is_root_dispatch:
777
+ await self._write_trace_root_end(
778
+ trace_id=trace_id,
779
+ status="FAILED",
780
+ end_ts=int(time.time() * 1000),
781
+ error=availability.error or "",
782
+ )
737
783
  return response
738
784
  if availability.status == AvailabilityStatus.QUEUE_PENDING:
739
785
  should_dispatch_control = False
@@ -750,6 +796,13 @@ class GatewayClient:
750
796
  output={"success": False, "error": str(err)},
751
797
  error=str(err),
752
798
  )
799
+ if is_root_dispatch:
800
+ await self._write_trace_root_end(
801
+ trace_id=trace_id,
802
+ status="FAILED",
803
+ end_ts=int(time.time() * 1000),
804
+ error=str(err),
805
+ )
753
806
  return SendMessageResponse(
754
807
  success=False,
755
808
  status=ExecutionStatus.FAILED,
@@ -766,6 +819,13 @@ class GatewayClient:
766
819
  output={"success": False, "error": str(err)},
767
820
  error=str(err),
768
821
  )
822
+ if is_root_dispatch:
823
+ await self._write_trace_root_end(
824
+ trace_id=trace_id,
825
+ status="FAILED",
826
+ end_ts=int(time.time() * 1000),
827
+ error=str(err),
828
+ )
769
829
  return SendMessageResponse(
770
830
  success=False,
771
831
  status=ExecutionStatus.FAILED,
@@ -860,10 +920,10 @@ class GatewayClient:
860
920
  content: Any,
861
921
  metadata: Dict[str, Any],
862
922
  ) -> Any:
923
+ if self._langfuse_dispatch_fn is None:
924
+ return None
863
925
  try:
864
- from by_framework_trace_langfuse import start_client_dispatch_observation
865
-
866
- return start_client_dispatch_observation(
926
+ return self._langfuse_dispatch_fn(
867
927
  trace_id=trace_id,
868
928
  message_id=message_id,
869
929
  target_agent_type=target_agent_type,
@@ -877,7 +937,6 @@ class GatewayClient:
877
937
  logger.warning(
878
938
  "Langfuse client.dispatch observation skipped: %s",
879
939
  err,
880
- exc_info=True,
881
940
  )
882
941
  return None
883
942
 
@@ -951,3 +1010,52 @@ class GatewayClient:
951
1010
  logger.warning(
952
1011
  "Failed to record client dispatch span: %s", err, exc_info=True
953
1012
  )
1013
+
1014
+ async def _write_trace_root_start(
1015
+ self,
1016
+ *,
1017
+ trace_id: str,
1018
+ message_id: str,
1019
+ session_id: str,
1020
+ target_agent_type: str,
1021
+ content: Any,
1022
+ start_ts: int,
1023
+ ) -> None:
1024
+ """Write trace root start — best-effort, never propagates errors."""
1025
+ try:
1026
+ await self._trace_writer.record_trace(
1027
+ TraceRecord(
1028
+ trace_id=trace_id,
1029
+ name=target_agent_type,
1030
+ session_id=session_id,
1031
+ root_message_id=message_id,
1032
+ root_agent_type=target_agent_type,
1033
+ input=sanitize_io_value(content, self._obs_config),
1034
+ status="QUEUED",
1035
+ start_ts=start_ts,
1036
+ )
1037
+ )
1038
+ except Exception: # pylint: disable=broad-exception-caught
1039
+ pass
1040
+
1041
+ async def _write_trace_root_end(
1042
+ self,
1043
+ *,
1044
+ trace_id: str,
1045
+ status: str,
1046
+ end_ts: int,
1047
+ error: str = "",
1048
+ ) -> None:
1049
+ """Write trace root end on routing failure — best-effort."""
1050
+ try:
1051
+ meta: dict = {"error": error} if error else {}
1052
+ await self._trace_writer.record_trace(
1053
+ TraceRecord(
1054
+ trace_id=trace_id,
1055
+ status=status,
1056
+ end_ts=end_ts,
1057
+ metadata=meta,
1058
+ )
1059
+ )
1060
+ except Exception: # pylint: disable=broad-exception-caught
1061
+ pass
@@ -50,6 +50,8 @@ class WorkerConfig:
50
50
  )
51
51
  heartbeat_lease_ttl_seconds: int = RedisKeys.WORKER_DEFAULT_LEASE_TTL_SECONDS
52
52
  lock_ttl_seconds: int = 60
53
+ worker_id_claim_max_wait_seconds: int = 90
54
+ worker_id_claim_retry_interval_seconds: float = 3.0
53
55
  stream_block_ms: int = 2000
54
56
 
55
57
 
@@ -136,7 +136,11 @@ class RedisKeys:
136
136
  # Set of known workers used for registry enumeration.
137
137
  KNOWN_WORKERS = "byai_gateway:registry:workers"
138
138
  WORKER_DEFAULT_HEARTBEAT_INTERVAL_SECONDS = 5
139
- WORKER_DEFAULT_LEASE_TTL_SECONDS = 15
139
+ # the heartbeat interval gives enough margin even when the main event
140
+ # loop is briefly occupied by an LLM call. The dedicated heartbeat thread
141
+ # makes starvation practically impossible, but the larger TTL is a second
142
+ # line of defence and also gives Worker 2 time to claim after a crash.
143
+ WORKER_DEFAULT_LEASE_TTL_SECONDS = 30
140
144
 
141
145
  # Default TTL (7 days) for cleaning up session-related aggregate Keys
142
146
  DEFAULT_SESSION_TTL = 7 * 24 * 3600
@@ -167,7 +167,19 @@ def get_logger(name: str) -> logging.Logger:
167
167
  Returns:
168
168
  Logger instance
169
169
  """
170
- return logging.getLogger(name)
170
+ base_logger = setup_logging()
171
+ if name == base_logger.name:
172
+ return base_logger
173
+
174
+ child_logger = logging.getLogger(name)
175
+ child_logger.setLevel(base_logger.level)
176
+ child_logger.propagate = False
177
+
178
+ if not child_logger.handlers:
179
+ for handler in base_logger.handlers:
180
+ child_logger.addHandler(handler)
181
+
182
+ return child_logger
171
183
 
172
184
 
173
185
  # Expose default logger
@@ -0,0 +1,95 @@
1
+ """
2
+ Core module of the Gateway SDK.
3
+
4
+ Provides core components including protocol definitions, worker registry,
5
+ and workspace management.
6
+ """
7
+
8
+ from .availability import (
9
+ AvailabilityResult,
10
+ AvailabilityRouter,
11
+ AvailabilityStatus,
12
+ DeliveryIntent,
13
+ PendingDelivery,
14
+ RoutePolicy,
15
+ WakeupDecision,
16
+ WakeupDecisionStatus,
17
+ WakeupRequest,
18
+ )
19
+ from .delivery_gate import DeliveryGate
20
+ from .protocol import (
21
+ ActionType,
22
+ AgentState,
23
+ ArtifactEvent,
24
+ AskAgentCommand,
25
+ AskUserEvent,
26
+ BaiYingMessage,
27
+ BaiYingMessageRole,
28
+ BaseCommand,
29
+ CancelTaskCommand,
30
+ CancelTaskResponse,
31
+ DataMessage,
32
+ EventType,
33
+ GatewayCommand,
34
+ MessageContent,
35
+ MessageFile,
36
+ MessageHeader,
37
+ Resource,
38
+ ResumeCommand,
39
+ SendMessageResponse,
40
+ SseMessageType,
41
+ SseReasonMessageType,
42
+ StateChangeEvent,
43
+ StreamChunkEvent,
44
+ command_from_dict,
45
+ get_registered_command,
46
+ register_command,
47
+ unregister_command,
48
+ )
49
+ from .registry import WorkerRegistry
50
+ from .wakeup_controller import WakeupController, WakeupProvider
51
+ from .workspace import WorkspaceManager
52
+
53
+ __all__ = [
54
+ "SendMessageResponse",
55
+ "CancelTaskResponse",
56
+ "BaiYingMessage",
57
+ "BaiYingMessageRole",
58
+ "MessageContent",
59
+ "MessageFile",
60
+ "Resource",
61
+ "DataMessage",
62
+ "ActionType",
63
+ "AgentState",
64
+ "EventType",
65
+ "StateChangeEvent",
66
+ "StreamChunkEvent",
67
+ "ArtifactEvent",
68
+ "AskUserEvent",
69
+ "SseMessageType",
70
+ "SseReasonMessageType",
71
+ "MessageHeader",
72
+ "BaseCommand",
73
+ "AskAgentCommand",
74
+ "ResumeCommand",
75
+ "CancelTaskCommand",
76
+ "GatewayCommand",
77
+ "command_from_dict",
78
+ "register_command",
79
+ "unregister_command",
80
+ "get_registered_command",
81
+ "WorkerRegistry",
82
+ "WorkspaceManager",
83
+ "AvailabilityRouter",
84
+ "AvailabilityStatus",
85
+ "AvailabilityResult",
86
+ "DeliveryIntent",
87
+ "RoutePolicy",
88
+ "PendingDelivery",
89
+ "WakeupRequest",
90
+ "WakeupDecision",
91
+ "WakeupDecisionStatus",
92
+ "WakeupController",
93
+ "WakeupProvider",
94
+ "DeliveryGate",
95
+ ]