by-framework 0.2.2.dev4__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.
@@ -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,20 @@ 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
+ )
99
115
  self._langfuse_dispatch_fn = self._resolve_langfuse_dispatch_fn()
100
116
 
101
117
  @staticmethod
@@ -617,6 +633,20 @@ class GatewayClient:
617
633
  message_id = f"{MESSAGE_ID_PREFIX}{uuid.uuid4().hex[:8]}"
618
634
  if not trace_id:
619
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
+ )
620
650
 
621
651
  metadata = dict(params.get("metadata", {}) or {})
622
652
  trace_parent_span_id = metadata.pop("trace_parent_span_id", "")
@@ -743,6 +773,13 @@ class GatewayClient:
743
773
  output={"success": False, "error": availability.error},
744
774
  error=availability.error,
745
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
+ )
746
783
  return response
747
784
  if availability.status == AvailabilityStatus.QUEUE_PENDING:
748
785
  should_dispatch_control = False
@@ -759,6 +796,13 @@ class GatewayClient:
759
796
  output={"success": False, "error": str(err)},
760
797
  error=str(err),
761
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
+ )
762
806
  return SendMessageResponse(
763
807
  success=False,
764
808
  status=ExecutionStatus.FAILED,
@@ -775,6 +819,13 @@ class GatewayClient:
775
819
  output={"success": False, "error": str(err)},
776
820
  error=str(err),
777
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
+ )
778
829
  return SendMessageResponse(
779
830
  success=False,
780
831
  status=ExecutionStatus.FAILED,
@@ -959,3 +1010,52 @@ class GatewayClient:
959
1010
  logger.warning(
960
1011
  "Failed to record client dispatch span: %s", err, exc_info=True
961
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
@@ -5,21 +5,47 @@ Provides core components including protocol definitions, worker registry,
5
5
  and workspace management.
6
6
  """
7
7
 
8
- from .availability import (AvailabilityResult, AvailabilityRouter,
9
- AvailabilityStatus, DeliveryIntent, PendingDelivery,
10
- RoutePolicy, WakeupDecision, WakeupDecisionStatus,
11
- WakeupRequest)
8
+ from .availability import (
9
+ AvailabilityResult,
10
+ AvailabilityRouter,
11
+ AvailabilityStatus,
12
+ DeliveryIntent,
13
+ PendingDelivery,
14
+ RoutePolicy,
15
+ WakeupDecision,
16
+ WakeupDecisionStatus,
17
+ WakeupRequest,
18
+ )
12
19
  from .delivery_gate import DeliveryGate
13
- from .protocol import (ActionType, AgentState, ArtifactEvent, AskAgentCommand,
14
- AskUserEvent, BaiYingMessage, BaiYingMessageRole,
15
- BaseCommand, CancelTaskCommand, CancelTaskResponse,
16
- DataMessage, EventType, GatewayCommand, MessageContent,
17
- MessageFile, MessageHeader, Resource, ResumeCommand,
18
- SendMessageResponse, SseMessageType,
19
- SseReasonMessageType, StateChangeEvent,
20
- StreamChunkEvent, command_from_dict,
21
- get_registered_command, register_command,
22
- unregister_command)
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
+ )
23
49
  from .registry import WorkerRegistry
24
50
  from .wakeup_controller import WakeupController, WakeupProvider
25
51
  from .workspace import WorkspaceManager
@@ -8,9 +8,15 @@ managed through the form of plugins.
8
8
  """
9
9
 
10
10
  from .agent_config import AgentConfig, CallbackType
11
- from .plugin import (AgentConfigsSnapshot, Plugin, PluginBuildContext,
12
- PluginManifest, PluginReloadContext, PluginReloadResult,
13
- PromptTemplate)
11
+ from .plugin import (
12
+ AgentConfigsSnapshot,
13
+ Plugin,
14
+ PluginBuildContext,
15
+ PluginManifest,
16
+ PluginReloadContext,
17
+ PluginReloadResult,
18
+ PromptTemplate,
19
+ )
14
20
  from .registry import PluginRegistry
15
21
  from .trace_provider import TraceProviderFactory
16
22
 
@@ -16,9 +16,11 @@ from typing import TYPE_CHECKING, Any, List, Type
16
16
  from .agent_config import AgentConfig
17
17
 
18
18
  if TYPE_CHECKING:
19
- from by_framework.core.protocol.commands import (AskAgentCommand,
20
- CancelTaskCommand,
21
- ResumeCommand)
19
+ from by_framework.core.protocol.commands import (
20
+ AskAgentCommand,
21
+ CancelTaskCommand,
22
+ ResumeCommand,
23
+ )
22
24
  from by_framework.worker.context import AgentContext
23
25
  from by_framework.worker.worker import GatewayWorker
24
26
 
@@ -19,13 +19,20 @@ import dill
19
19
  from by_framework.common.logger import logger
20
20
 
21
21
  from .agent_config import AgentConfig
22
- from .plugin import (AgentConfigsSnapshot, Plugin, PluginBuildContext,
23
- PluginReloadContext, PluginReloadResult)
22
+ from .plugin import (
23
+ AgentConfigsSnapshot,
24
+ Plugin,
25
+ PluginBuildContext,
26
+ PluginReloadContext,
27
+ PluginReloadResult,
28
+ )
24
29
 
25
30
  if TYPE_CHECKING:
26
- from by_framework.core.protocol.commands import (AskAgentCommand,
27
- CancelTaskCommand,
28
- ResumeCommand)
31
+ from by_framework.core.protocol.commands import (
32
+ AskAgentCommand,
33
+ CancelTaskCommand,
34
+ ResumeCommand,
35
+ )
29
36
  from by_framework.worker.context import AgentContext
30
37
  from by_framework.worker.worker import GatewayWorker
31
38
 
@@ -14,32 +14,66 @@ where possible to ensure protocol stability.
14
14
  """
15
15
 
16
16
  from .action_type import ActionType, ActionTypeLiteral
17
- from .agent_state import (TERMINAL_STATES, AgentState, AgentStateLiteral,
18
- is_terminal_state)
19
- from .byai_codec import (ByaiContentCodec, deserialize_byai_content,
20
- serialize_byai_content)
17
+ from .agent_state import (
18
+ TERMINAL_STATES,
19
+ AgentState,
20
+ AgentStateLiteral,
21
+ is_terminal_state,
22
+ )
23
+ from .byai_codec import (
24
+ ByaiContentCodec,
25
+ deserialize_byai_content,
26
+ serialize_byai_content,
27
+ )
21
28
  from .byai_command import ByaiAskAgentCommand, ByaiResumeCommand
22
29
  from .byai_types import ByaiContent
23
- from .commands import (AskAgentCommand, BaseCommand, CancelTaskCommand,
24
- GatewayCommand, ReloadPluginsCommand, ResumeCommand,
25
- command_from_dict, get_registered_command,
26
- register_command, unregister_command)
30
+ from .commands import (
31
+ AskAgentCommand,
32
+ BaseCommand,
33
+ CancelTaskCommand,
34
+ GatewayCommand,
35
+ ReloadPluginsCommand,
36
+ ResumeCommand,
37
+ command_from_dict,
38
+ get_registered_command,
39
+ register_command,
40
+ unregister_command,
41
+ )
27
42
  from .content_codec import ContentCodec, WireContent
28
43
  from .content_type import SseMessageType, SseReasonMessageType
29
44
  from .data_message import DataMessage
30
- from .data_shapes import (AskAgentBodyDict, CancelTaskBodyDict,
31
- CommandBodyDict, CommandHeaderDict,
32
- ExecutionDataDict, RedisCommandDict, ResumeBodyDict)
45
+ from .data_shapes import (
46
+ AskAgentBodyDict,
47
+ CancelTaskBodyDict,
48
+ CommandBodyDict,
49
+ CommandHeaderDict,
50
+ ExecutionDataDict,
51
+ RedisCommandDict,
52
+ ResumeBodyDict,
53
+ )
33
54
  from .event_type import EventType
34
- from .events import (ArtifactEvent, AskUserEvent, StateChangeEvent,
35
- StreamChunkEvent)
36
- from .message import (BaiYingMessage, BaiYingMessageRole, MessageContent,
37
- MessageFile, Resource)
55
+ from .events import (ArtifactEvent, AskUserEvent, StateChangeEvent, StreamChunkEvent)
56
+ from .message import (
57
+ BaiYingMessage,
58
+ BaiYingMessageRole,
59
+ MessageContent,
60
+ MessageFile,
61
+ Resource,
62
+ )
38
63
  from .message_header import MessageHeader
39
- from .responses import (CancelTaskResponse, CancelTaskResponseDict,
40
- SendMessageResponse, SendMessageResponseDict)
41
- from .results import (AgentTaskResult, JsonValue, ProcessCommandResult,
42
- ensure_json_serializable, normalize_process_result)
64
+ from .responses import (
65
+ CancelTaskResponse,
66
+ CancelTaskResponseDict,
67
+ SendMessageResponse,
68
+ SendMessageResponseDict,
69
+ )
70
+ from .results import (
71
+ AgentTaskResult,
72
+ JsonValue,
73
+ ProcessCommandResult,
74
+ ensure_json_serializable,
75
+ normalize_process_result,
76
+ )
43
77
 
44
78
  __all__ = [
45
79
  "SendMessageResponse",
@@ -5,8 +5,13 @@ from enum import Enum
5
5
  from typing import Any
6
6
 
7
7
  from .content_codec import ContentCodec, WireContent
8
- from .message import (BaiYingMessage, BaiYingMessageRole, MessageContent,
9
- MessageFile, Resource)
8
+ from .message import (
9
+ BaiYingMessage,
10
+ BaiYingMessageRole,
11
+ MessageContent,
12
+ MessageFile,
13
+ Resource,
14
+ )
10
15
 
11
16
 
12
17
  def serialize_byai_content(content: Any) -> Any: