lybic-guiagents 0.3.0__py3-none-any.whl → 0.4.0__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.

Potentially problematic release.


This version of lybic-guiagents might be problematic. Click here for more details.

gui_agents/grpc_app.py CHANGED
@@ -29,6 +29,7 @@ import platform
29
29
  from concurrent import futures
30
30
  import grpc
31
31
  import uuid
32
+ import datetime
32
33
 
33
34
  from lybic import LybicClient, LybicAuth, Sandbox
34
35
  import gui_agents.cli_app as app
@@ -44,12 +45,13 @@ class AgentServicer(agent_pb2_grpc.AgentServicer):
44
45
  Implements the Agent gRPC service.
45
46
  """
46
47
 
47
- def __init__(self, max_concurrent_task_num = 1):
48
+ def __init__(self, max_concurrent_task_num: int = 1, log_dir: str = "runtime"):
48
49
  """
49
50
  Initialize the AgentServicer with concurrency and runtime state.
50
51
 
51
52
  Parameters:
52
53
  max_concurrent_task_num (int): Maximum number of agent tasks allowed to run concurrently; defaults to 1.
54
+ log_dir (str): Directory for logging and task-related files.
53
55
  """
54
56
  self.lybic_auth: LybicAuth | None = None
55
57
  self.max_concurrent_task_num = max_concurrent_task_num
@@ -58,6 +60,7 @@ class AgentServicer(agent_pb2_grpc.AgentServicer):
58
60
  self.task_lock = asyncio.Lock()
59
61
  self.lybic_client: LybicClient | None = None
60
62
  self.sandbox: Sandbox | None = None
63
+ self.log_dir = log_dir
61
64
 
62
65
  async def GetAgentTaskStream(self, request, context):
63
66
  """
@@ -110,6 +113,42 @@ class AgentServicer(agent_pb2_grpc.AgentServicer):
110
113
  domain=platform.node(),
111
114
  )
112
115
 
116
+ async def _setup_task_state(self, task_id: str) -> Registry:
117
+ """Setup global state and registry for task execution with task isolation"""
118
+ # Create timestamp-based directory structure like cli_app.py
119
+ datetime_str = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
120
+ timestamp_dir = Path(self.log_dir) / f"{datetime_str}_{task_id[:8]}" # Include task_id prefix
121
+ cache_dir = timestamp_dir / "cache" / "screens"
122
+ state_dir = timestamp_dir / "state"
123
+
124
+ cache_dir.mkdir(parents=True, exist_ok=True)
125
+ state_dir.mkdir(parents=True, exist_ok=True)
126
+
127
+ # Create task-specific registry
128
+ task_registry = Registry()
129
+
130
+ # Register global state for this task in task-specific registry
131
+ global_state = GlobalState(
132
+ screenshot_dir=str(cache_dir),
133
+ tu_path=str(state_dir / "tu.json"),
134
+ search_query_path=str(state_dir / "search_query.json"),
135
+ completed_subtasks_path=str(state_dir / "completed_subtasks.json"),
136
+ failed_subtasks_path=str(state_dir / "failed_subtasks.json"),
137
+ remaining_subtasks_path=str(state_dir / "remaining_subtasks.json"),
138
+ termination_flag_path=str(state_dir / "termination_flag.json"),
139
+ running_state_path=str(state_dir / "running_state.json"),
140
+ display_info_path=str(timestamp_dir / "display.json"),
141
+ agent_log_path=str(timestamp_dir / "agent_log.json")
142
+ )
143
+
144
+ # Register in task-specific registry using instance method
145
+ registry_key = "GlobalStateStore"
146
+ task_registry.register_instance(registry_key, global_state)
147
+
148
+ logger.info(f"Created task-specific registry for task {task_id}")
149
+
150
+ return task_registry
151
+
113
152
  async def _run_task(self, task_id: str, backend_kwargs):
114
153
  """
115
154
  Run the lifecycle of a single agent task: mark it running, execute the agent, record final state, emit stream messages, and unregister the task.
@@ -136,23 +175,29 @@ class AgentServicer(agent_pb2_grpc.AgentServicer):
136
175
  # Send message through stream manager
137
176
  await stream_manager.add_message(task_id, "starting", "Task starting")
138
177
 
139
- # Set task_id for the agent to enable streaming from within the agent
178
+ # Create task-specific registry
179
+ task_registry = await self._setup_task_state(task_id)
180
+
181
+ # Set task_id for the agent. This is needed so that agent.reset() can find the right components.
140
182
  if hasattr(agent, 'set_task_id'):
141
183
  agent.set_task_id(task_id)
142
184
 
143
185
  hwi = HardwareInterface(backend='lybic', **backend_kwargs)
144
186
 
187
+ # We need to set the registry for the main thread context before reset
188
+ Registry.set_task_registry(task_id, task_registry)
145
189
  agent.reset()
190
+ Registry.remove_task_registry(task_id) # Clean up main thread's local
146
191
 
147
- # Run the blocking function in a separate thread to avoid blocking the event loop
148
- mode: InstanceMode | None = backend_kwargs["mode"]
192
+ # Run the blocking function in a separate thread, passing the context
193
+ mode: InstanceMode | None = backend_kwargs.get("mode")
149
194
  if mode and mode == InstanceMode.NORMAL:
150
- await asyncio.to_thread(app.run_agent_normal,agent, query, hwi, steps, False)
195
+ await asyncio.to_thread(app.run_agent_normal, agent, query, hwi, steps, False, task_id=task_id, task_registry=task_registry)
151
196
  else:
152
- await asyncio.to_thread(app.run_agent_fast, agent, query, hwi, steps, False)
197
+ await asyncio.to_thread(app.run_agent_fast, agent, query, hwi, steps, False, task_id=task_id, task_registry=task_registry)
153
198
 
154
- global_state: GlobalState = Registry.get("GlobalStateStore") # type: ignore
155
- final_state = global_state.get_running_state()
199
+ # The final state is now determined inside the thread. We'll assume success if no exception.
200
+ final_state = "completed"
156
201
 
157
202
  async with self.task_lock:
158
203
  self.tasks[task_id]["final_state"] = final_state
@@ -171,6 +216,7 @@ class AgentServicer(agent_pb2_grpc.AgentServicer):
171
216
  await stream_manager.add_message(task_id, "error", f"An error occurred: {e}")
172
217
  finally:
173
218
  logger.info(f"Task {task_id} processing finished.")
219
+ # Registry cleanup is now handled within the worker thread
174
220
  await stream_manager.unregister_task(task_id)
175
221
 
176
222
  async def _make_backend_kwargs(self, request):
@@ -213,23 +259,30 @@ class AgentServicer(agent_pb2_grpc.AgentServicer):
213
259
 
214
260
  platform_str = platform.system()
215
261
  sid = ''
262
+ sandbox_pb = None
263
+
216
264
  if backend == 'lybic':
217
265
  if request.HasField("sandbox"):
218
266
  shape = request.sandbox.shapeName
219
267
  sid = request.sandbox.id
220
268
  if sid:
221
269
  logger.info(f"Using existing sandbox with id: {sid}")
270
+ sandbox_pb = await self._get_sandbox_pb(sid) # if not exist raise NotFound
271
+ platform_str = sandbox_pb.os
222
272
  else:
223
- sid, platform_str = await self._create_sandbox(shape)
273
+ sandbox_pb = await self._create_sandbox(shape)
274
+ sid, platform_str = sandbox_pb.id, sandbox_pb.os
224
275
 
225
276
  if request.sandbox.os != agent_pb2.SandboxOS.OSUNDEFINED:
226
277
  platform_str = platform_map.get(request.sandbox.os, platform.system())
227
278
  else:
228
- sid, platform_str = await self._create_sandbox(shape)
229
-
279
+ sandbox_pb = await self._create_sandbox(shape)
280
+ sid, platform_str = sandbox_pb.id, sandbox_pb.os
230
281
  else:
231
- platform_str = platform_map.get(request.sandbox.os, platform.system())
282
+ if request.HasField("sandbox") and request.sandbox.os != agent_pb2.SandboxOS.OSUNDEFINED:
283
+ platform_str = platform_map.get(request.sandbox.os, platform.system())
232
284
 
285
+ backend_kwargs["sandbox"] = sandbox_pb
233
286
  backend_kwargs["platform"] = platform_str
234
287
  backend_kwargs["precreate_sid"] = sid
235
288
 
@@ -250,12 +303,6 @@ class AgentServicer(agent_pb2_grpc.AgentServicer):
250
303
  return backend_kwargs
251
304
 
252
305
  async def _make_agent(self,request):
253
- # todo: add max_steps support
254
- # max_steps = 50
255
- # if request.HasField("runningConfig") and request.runningConfig.steps:
256
- # max_steps = request.runningConfig.steps
257
-
258
- # Dynamically build tools_dict based on global config
259
306
  """
260
307
  Builds and returns an AgentS2 configured for the incoming request by applying model and provider overrides to the tool configurations.
261
308
 
@@ -345,7 +392,6 @@ class AgentServicer(agent_pb2_grpc.AgentServicer):
345
392
  tools_config=tools_config,
346
393
  )
347
394
 
348
-
349
395
  async def RunAgentInstruction(self, request, context):
350
396
  """
351
397
  Stream task progress for a newly created instruction-run agent while managing task lifecycle and concurrency.
@@ -390,8 +436,13 @@ class AgentServicer(agent_pb2_grpc.AgentServicer):
390
436
  "query": request.instruction,
391
437
  "agent": agent,
392
438
  "max_steps": max_steps,
439
+ "sandbox": backend_kwargs["sandbox"],
393
440
  }
394
441
 
442
+ # This property is used to pass sandbox information.
443
+ # It has now completed its mission and needs to be deleted, otherwise other backends may crash.
444
+ del backend_kwargs["sandbox"]
445
+
395
446
  task_future = asyncio.create_task(self._run_task(task_id, backend_kwargs))
396
447
  self.tasks[task_id]["future"] = task_future
397
448
  try:
@@ -449,6 +500,7 @@ class AgentServicer(agent_pb2_grpc.AgentServicer):
449
500
  "query": request.instruction,
450
501
  "agent": agent,
451
502
  "max_steps": max_steps,
503
+ "sandbox": backend_kwargs["sandbox"],
452
504
  }
453
505
 
454
506
  # Start the task in background
@@ -510,7 +562,7 @@ class AgentServicer(agent_pb2_grpc.AgentServicer):
510
562
  status=task_status,
511
563
  message=message,
512
564
  result=result,
513
- sandbox=task_info["request"].sandbox
565
+ sandbox=task_info["sandbox"]
514
566
  )
515
567
 
516
568
  def _mask_config_secrets(self, config: CommonConfig) -> CommonConfig:
@@ -700,7 +752,7 @@ class AgentServicer(agent_pb2_grpc.AgentServicer):
700
752
  logger.info(f"Global embedding LLM config updated to: {request.llmConfig.modelName}")
701
753
  return request.llmConfig
702
754
 
703
- async def _create_sandbox(self,shape:str):
755
+ async def _create_sandbox(self,shape:str)->agent_pb2.Sandbox:
704
756
  """
705
757
  Create a sandbox with the given shape via the Lybic service and return its identifier and operating system.
706
758
 
@@ -721,7 +773,49 @@ class AgentServicer(agent_pb2_grpc.AgentServicer):
721
773
  self.sandbox = Sandbox(self.lybic_client)
722
774
  result = await self.sandbox.create(shape=shape)
723
775
  sandbox = await self.sandbox.get(result.id)
724
- return sandbox.sandbox.id, sandbox.sandbox.shape.os
776
+
777
+ return agent_pb2.Sandbox(
778
+ id=sandbox.sandbox.id,
779
+ os=sandbox.sandbox.shape.os.upper(),
780
+ shapeName=sandbox.sandbox.shapeName,
781
+ hardwareAcceleratedEncoding=sandbox.sandbox.shape.hardwareAcceleratedEncoding,
782
+ virtualization=sandbox.sandbox.shape.virtualization,
783
+ architecture=sandbox.sandbox.shape.architecture,
784
+ )
785
+
786
+ async def _get_sandbox_pb(self, sid: str) -> agent_pb2.Sandbox:
787
+ """
788
+ Retrieves sandbox details for a given sandbox ID and returns them as a protobuf message.
789
+ """
790
+ if not self.lybic_auth:
791
+ raise ValueError("Lybic client not initialized. Please call SetGlobalCommonConfig before")
792
+
793
+ if not self.lybic_client:
794
+ await self._new_lybic_client()
795
+ if not self.sandbox:
796
+ self.sandbox = Sandbox(self.lybic_client)
797
+
798
+ sandbox_details = await self.sandbox.get(sid)
799
+
800
+ os_raw = getattr(sandbox_details.sandbox.shape, "os", "") or ""
801
+ os_upper = str(os_raw).upper()
802
+ if "WIN" in os_upper:
803
+ os_enum = agent_pb2.SandboxOS.WINDOWS
804
+ elif "LINUX" in os_upper or "UBUNTU" in os_upper:
805
+ os_enum = agent_pb2.SandboxOS.LINUX
806
+ elif "ANDROID" in os_upper:
807
+ os_enum = agent_pb2.SandboxOS.ANDROID
808
+ else:
809
+ os_enum = agent_pb2.SandboxOS.OSUNDEFINED
810
+
811
+ return agent_pb2.Sandbox(
812
+ id=sandbox_details.sandbox.id,
813
+ os=os_enum,
814
+ shapeName=sandbox_details.sandbox.shapeName,
815
+ hardwareAcceleratedEncoding=sandbox_details.sandbox.shape.hardwareAcceleratedEncoding,
816
+ virtualization=sandbox_details.sandbox.shape.virtualization,
817
+ architecture=sandbox_details.sandbox.shape.architecture,
818
+ )
725
819
 
726
820
  async def serve():
727
821
  """
@@ -735,8 +829,8 @@ async def serve():
735
829
  """
736
830
  port = os.environ.get("GRPC_PORT", 50051)
737
831
  max_workers = int(os.environ.get("GRPC_MAX_WORKER_THREADS", 100))
738
- # task_num = int(os.environ.get("TASK_MAX_TASKS", 5))
739
- servicer = AgentServicer(max_concurrent_task_num=1)
832
+ task_num = int(os.environ.get("TASK_MAX_TASKS", 5))
833
+ servicer = AgentServicer(max_concurrent_task_num=task_num, log_dir=app.log_dir)
740
834
  server = grpc.aio.server(futures.ThreadPoolExecutor(max_workers))
741
835
  agent_pb2_grpc.add_AgentServicer_to_server(servicer, server)
742
836
  server.add_insecure_port(f'[::]:{port}')
@@ -752,32 +846,6 @@ def main():
752
846
  has_display, pyautogui_available, _ = app.check_display_environment()
753
847
  compatible_backends, incompatible_backends = app.get_compatible_backends(has_display, pyautogui_available)
754
848
  app.validate_backend_compatibility('lybic', compatible_backends, incompatible_backends)
755
- timestamp_dir = os.path.join(app.log_dir, app.datetime_str)
756
- cache_dir = os.path.join(timestamp_dir, "cache", "screens")
757
- state_dir = os.path.join(timestamp_dir, "state")
758
-
759
- os.makedirs(cache_dir, exist_ok=True)
760
- os.makedirs(state_dir, exist_ok=True)
761
-
762
- Registry.register(
763
- "GlobalStateStore",
764
- GlobalState(
765
- screenshot_dir=cache_dir,
766
- tu_path=os.path.join(state_dir, "tu.json"),
767
- search_query_path=os.path.join(state_dir, "search_query.json"),
768
- completed_subtasks_path=os.path.join(state_dir,
769
- "completed_subtasks.json"),
770
- failed_subtasks_path=os.path.join(state_dir,
771
- "failed_subtasks.json"),
772
- remaining_subtasks_path=os.path.join(state_dir,
773
- "remaining_subtasks.json"),
774
- termination_flag_path=os.path.join(state_dir,
775
- "termination_flag.json"),
776
- running_state_path=os.path.join(state_dir, "running_state.json"),
777
- display_info_path=os.path.join(timestamp_dir, "display.json"),
778
- agent_log_path=os.path.join(timestamp_dir, "agent_log.json")
779
- )
780
- )
781
849
  asyncio.run(serve())
782
850
 
783
851
  if __name__ == '__main__':
@@ -92,29 +92,41 @@ class AgentService:
92
92
 
93
93
  return logger
94
94
 
95
- def _get_or_create_agent(self, mode: str, **kwargs) -> Union[AgentS2, AgentSFast]:
95
+ def _get_or_create_agent(self, mode: str, task_id: Optional[str] = None, **kwargs) -> Union[AgentS2, AgentSFast]:
96
96
  """Get or create agent instance based on mode"""
97
- cache_key = f"{mode}_{hash(str(sorted(kwargs.items())))}"
98
-
97
+ # Include task_id in cache key for task isolation when task_id is provided
98
+ if task_id:
99
+ cache_key = f"{mode}_{task_id}_{hash(str(sorted(kwargs.items())))}"
100
+ else:
101
+ cache_key = f"{mode}_{hash(str(sorted(kwargs.items())))}"
102
+
99
103
  if cache_key not in self._agents:
100
104
  agent_kwargs = {
101
105
  'platform': kwargs.get('platform', self.config.default_platform),
102
106
  'enable_takeover': kwargs.get('enable_takeover', self.config.enable_takeover),
103
107
  'enable_search': kwargs.get('enable_search', self.config.enable_search),
104
108
  }
105
-
109
+
106
110
  if mode == AgentMode.FAST.value:
107
111
  self._agents[cache_key] = AgentSFast(**agent_kwargs)
108
112
  else:
109
113
  self._agents[cache_key] = AgentS2(**agent_kwargs)
110
-
114
+
111
115
  self.logger.debug(f"Created new agent: {mode} with kwargs: {agent_kwargs}")
112
-
113
- return self._agents[cache_key]
116
+
117
+ # Set task_id on the agent for task-specific operations
118
+ agent = self._agents[cache_key]
119
+ if task_id and hasattr(agent, 'set_task_id'):
120
+ agent.set_task_id(task_id)
121
+
122
+ return agent
114
123
 
115
- def _get_or_create_hwi(self, backend: str, **kwargs) -> HardwareInterface:
124
+ def _get_or_create_hwi(self, backend: str, task_id: Optional[str] = None, **kwargs) -> HardwareInterface:
116
125
  """Get or create hardware interface instance"""
117
- cache_key = f"{backend}_{hash(str(sorted(kwargs.items())))}"
126
+ if task_id:
127
+ cache_key = f"{backend}_{task_id}_{hash(str(sorted(kwargs.items())))}"
128
+ else:
129
+ cache_key = f"{backend}_{hash(str(sorted(kwargs.items())))}"
118
130
 
119
131
  if cache_key not in self._hwi_instances:
120
132
  # Get backend-specific config
@@ -134,17 +146,20 @@ class AgentService:
134
146
  return self._hwi_instances[cache_key]
135
147
 
136
148
  def _setup_global_state(self, task_id: str) -> str:
137
- """Setup global state for task execution"""
149
+ """Setup global state for task execution with task isolation"""
138
150
  # Create timestamp-based directory structure like cli_app.py
139
151
  datetime_str = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
140
- timestamp_dir = Path(self.config.log_dir) / datetime_str
152
+ timestamp_dir = Path(self.config.log_dir) / f"{datetime_str}_{task_id[:8]}" # Include task_id prefix
141
153
  cache_dir = timestamp_dir / "cache" / "screens"
142
154
  state_dir = timestamp_dir / "state"
143
-
155
+
144
156
  cache_dir.mkdir(parents=True, exist_ok=True)
145
157
  state_dir.mkdir(parents=True, exist_ok=True)
146
-
147
- # Register global state for this task
158
+
159
+ # Create task-specific registry
160
+ task_registry = Registry()
161
+
162
+ # Register global state for this task in task-specific registry
148
163
  global_state = GlobalState(
149
164
  screenshot_dir=str(cache_dir),
150
165
  tu_path=str(state_dir / "tu.json"),
@@ -157,11 +172,16 @@ class AgentService:
157
172
  display_info_path=str(timestamp_dir / "display.json"),
158
173
  agent_log_path=str(timestamp_dir / "agent_log.json")
159
174
  )
160
-
161
- # Use task-specific registry key to avoid conflicts
175
+
176
+ # Register in task-specific registry using instance method
162
177
  registry_key = "GlobalStateStore"
163
- Registry.register(registry_key, global_state)
164
-
178
+ task_registry.register_instance(registry_key, global_state)
179
+
180
+ # Set task registry in thread-local storage
181
+ Registry.set_task_registry(task_id, task_registry)
182
+
183
+ self.logger.info(f"Setup task-specific registry for task {task_id}")
184
+
165
185
  return str(timestamp_dir)
166
186
 
167
187
  def _execute_task_internal(self, request: TaskRequest, task_result: TaskResult) -> TaskResult:
@@ -173,19 +193,21 @@ class AgentService:
173
193
  # Setup global state
174
194
  task_dir = self._setup_global_state(task_result.task_id)
175
195
 
176
- # Create agent and hardware interface
196
+ # Create agent and hardware interface with task_id
177
197
  agent = self._get_or_create_agent(
178
198
  request.mode,
199
+ task_id=task_result.task_id,
179
200
  platform=self.config.default_platform,
180
201
  enable_takeover=request.enable_takeover,
181
202
  enable_search=request.enable_search
182
203
  )
183
-
204
+
184
205
  hwi = self._get_or_create_hwi(
185
206
  request.backend,
207
+ task_id=task_result.task_id,
186
208
  **(request.config or {})
187
209
  )
188
-
210
+
189
211
  # Reset agent state
190
212
  agent.reset()
191
213
 
@@ -259,22 +281,17 @@ class AgentService:
259
281
  task_result.mark_failed(error_msg)
260
282
 
261
283
  finally:
262
- # Cleanup global state registry
263
- registry_key = f"GlobalStateStore"
264
- try:
265
- # Registry doesn't have unregister method, we'll use clear or manual removal
266
- if hasattr(Registry, '_services') and registry_key in Registry._services:
267
- del Registry._services[registry_key]
268
- except:
269
- pass
284
+ # Cleanup task-specific registry
285
+ Registry.remove_task_registry(task_result.task_id)
286
+ self.logger.info(f"Cleaned up task-specific registry for task {task_result.task_id}")
270
287
 
271
288
  return task_result
272
289
 
273
- def _run_agent_normal_internal(self, agent, instruction: str, hwi, max_steps: int,
290
+ def _run_agent_normal_internal(self, agent, instruction: str, hwi, max_steps: int,
274
291
  enable_takeover: bool, task_id: str):
275
292
  """Run agent in normal mode (adapted from cli_app.py)"""
276
293
  # This is a simplified version - you may want to adapt the full logic from cli_app.py
277
- global_state: GlobalState = Registry.get(f"GlobalStateStore") # type: ignore
294
+ global_state: GlobalState = Registry.get_from_context("GlobalStateStore", task_id) # type: ignore
278
295
  global_state.set_Tu(instruction)
279
296
  global_state.set_running_state("running")
280
297
 
@@ -316,10 +333,10 @@ class AgentService:
316
333
  hwi.dispatchDict(code[0])
317
334
  time.sleep(1.0)
318
335
 
319
- def _run_agent_fast_internal(self, agent, instruction: str, hwi, max_steps: int,
320
- enable_takeover: bool, task_id: str):
336
+ def _run_agent_fast_internal(self, agent, instruction: str, hwi, max_steps: int,
337
+ enable_takeover: bool, task_id: str):
321
338
  """Run agent in fast mode (adapted from cli_app.py)"""
322
- global_state: GlobalState = Registry.get(f"GlobalStateStore") # type: ignore
339
+ global_state: GlobalState = Registry.get_from_context("GlobalStateStore", task_id) # type: ignore
323
340
  global_state.set_Tu(instruction)
324
341
  global_state.set_running_state("running")
325
342
 
@@ -4,19 +4,127 @@
4
4
  # from gui_agents.store.registry import Registry
5
5
  # GlobalStateStore = Registry.get("GlobalStateStore")
6
6
 
7
+ import threading
8
+ from typing import Optional, ClassVar
9
+
10
+
7
11
  class Registry:
8
- _services: dict[str, object] = {}
12
+ """
13
+ Registry class that supports both global singleton and task-specific instances.
14
+ It uses a process-wide dictionary for task registries to ensure visibility
15
+ across threads, making it compatible with asyncio.to_thread.
16
+ """
17
+ # For global singletons (backward compatibility)
18
+ _global_services: ClassVar[dict[str, object]] = {}
19
+ _global_lock: ClassVar[threading.RLock] = threading.RLock()
20
+
21
+ # Process-wide storage for task-specific registries, protected by a lock
22
+ _task_registries: ClassVar[dict[str, 'Registry']] = {}
23
+ _task_registries_lock: ClassVar[threading.RLock] = threading.RLock()
24
+
25
+ # Thread-local storage can be used as a cache for faster access
26
+ _thread_local: ClassVar[threading.local] = threading.local()
27
+
28
+ def __init__(self):
29
+ """Create a new registry instance (for a specific task)."""
30
+ self._services: dict[str, object] = {}
31
+ self._lock = threading.RLock()
9
32
 
33
+ # ========== Instance methods (for a single registry) ==========
34
+ def register_instance(self, name: str, obj: object):
35
+ """Register an object in this registry instance."""
36
+ with self._lock:
37
+ self._services[name] = obj
38
+
39
+ def get_instance(self, name: str) -> object:
40
+ """Get an object from this registry instance."""
41
+ with self._lock:
42
+ if name not in self._services:
43
+ raise KeyError(f"{name!r} not registered in this Registry instance")
44
+ return self._services[name]
45
+
46
+ def clear_instance(self):
47
+ """Clear all objects in this registry instance."""
48
+ with self._lock:
49
+ self._services.clear()
50
+
51
+ # ========== Class methods for global registry (backward compatibility) ==========
10
52
  @classmethod
11
53
  def register(cls, name: str, obj: object):
12
- cls._services[name] = obj
54
+ """Register an object in the global registry."""
55
+ with cls._global_lock:
56
+ cls._global_services[name] = obj
13
57
 
14
58
  @classmethod
15
59
  def get(cls, name: str) -> object:
16
- if name not in cls._services:
17
- raise KeyError(f"{name!r} not registered in Registry")
18
- return cls._services[name]
60
+ """Get an object from the global registry."""
61
+ with cls._global_lock:
62
+ if name not in cls._global_services:
63
+ raise KeyError(f"{name!r} not registered in global Registry")
64
+ return cls._global_services[name]
19
65
 
20
66
  @classmethod
21
67
  def clear(cls):
22
- cls._services.clear()
68
+ """Clear all objects in the global registry."""
69
+ with cls._global_lock:
70
+ cls._global_services.clear()
71
+
72
+ # ========== Task-specific registry management (Process-wide) ==========
73
+ @classmethod
74
+ def set_task_registry(cls, task_id: str, registry: 'Registry'):
75
+ """Set a task-specific registry, making it visible process-wide."""
76
+ with cls._task_registries_lock:
77
+ cls._task_registries[task_id] = registry
78
+
79
+ # Also set it in thread-local for faster access within the current thread
80
+ if not hasattr(cls._thread_local, 'task_cache'):
81
+ cls._thread_local.task_cache = {}
82
+ cls._thread_local.task_cache[task_id] = registry
83
+
84
+ @classmethod
85
+ def get_task_registry(cls, task_id: str) -> Optional['Registry']:
86
+ """Get a task-specific registry, checking thread-local cache first."""
87
+ # Check thread-local cache first for performance
88
+ if hasattr(cls._thread_local, 'task_cache'):
89
+ cached_registry = cls._thread_local.task_cache.get(task_id)
90
+ if cached_registry:
91
+ return cached_registry
92
+
93
+ # If not in cache, check the process-wide dictionary
94
+ with cls._task_registries_lock:
95
+ registry = cls._task_registries.get(task_id)
96
+ if registry:
97
+ # Populate cache for subsequent calls in the same thread
98
+ if not hasattr(cls._thread_local, 'task_cache'):
99
+ cls._thread_local.task_cache = {}
100
+ cls._thread_local.task_cache[task_id] = registry
101
+ return registry
102
+
103
+ @classmethod
104
+ def remove_task_registry(cls, task_id: str):
105
+ """Remove a task-specific registry from process-wide and thread-local storage."""
106
+ # Remove from the main process-wide storage
107
+ with cls._task_registries_lock:
108
+ cls._task_registries.pop(task_id, None)
109
+
110
+ # Remove from the current thread's local cache, if it exists
111
+ if hasattr(cls._thread_local, 'task_cache'):
112
+ cls._thread_local.task_cache.pop(task_id, None)
113
+
114
+ @classmethod
115
+ def get_from_context(cls, name: str, task_id: Optional[str] = None) -> object:
116
+ """
117
+ Get an object, trying task-specific registry first, then global registry.
118
+ This is now thread-safe across different threads for the same task_id.
119
+ """
120
+ # Try task-specific registry first
121
+ if task_id:
122
+ task_registry = cls.get_task_registry(task_id)
123
+ if task_registry:
124
+ try:
125
+ return task_registry.get_instance(name)
126
+ except KeyError:
127
+ pass # Fall back to global registry
128
+
129
+ # Fall back to global registry for CLI mode or if not in task registry
130
+ return cls.get(name)
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lybic-guiagents
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: An open-source agentic framework that enables AI to use computers like humans and can provide a multi-agent runtime environment as an infrastructure capability
5
5
  Author: Lybic Development Team
6
6
  Author-email: Lybic Development Team <lybic@tingyutech.com>
7
7
  License-Expression: Apache-2.0
8
8
  Classifier: Programming Language :: Python :: 3
9
- Requires-Python: >=3.12, <3.14
9
+ Requires-Python: >=3.12, <3.15
10
10
  Description-Content-Type: text/markdown
11
11
  License-File: LICENSE
12
12
  Requires-Dist: dotenv
@@ -281,11 +281,11 @@ powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/0.8.5/install.ps
281
281
  # testing uv installation, version should be 0.8.5
282
282
  uv --version
283
283
 
284
- # 2. Install the python 3.12
285
- uv python install 3.12.11
284
+ # 2. Install the python 3.14
285
+ uv python install 3.14
286
286
 
287
287
  # 3. Create a virtual environment
288
- uv venv -p 3.12.11
288
+ uv venv -p 3.14
289
289
 
290
290
  # 4. Activate the virtual environment
291
291
  # macOS and Linux
@@ -525,7 +525,7 @@ USE_PRECREATE_VM=Ubuntu
525
525
  **Problem**: `ModuleNotFoundError` or package import errors.
526
526
 
527
527
  **Solution**:
528
- - Ensure you're using Python 3.12.11 as specified
528
+ - Ensure you're using Python >= 3.12
529
529
  - Activate the virtual environment:
530
530
  ```bash
531
531
  # macOS/Linux