autoglm-gui 1.3.1__py3-none-any.whl → 1.4.1__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 (61) hide show
  1. AutoGLM_GUI/__main__.py +0 -4
  2. AutoGLM_GUI/adb_plus/qr_pair.py +8 -8
  3. AutoGLM_GUI/agents/__init__.py +20 -0
  4. AutoGLM_GUI/agents/factory.py +160 -0
  5. AutoGLM_GUI/agents/mai_adapter.py +627 -0
  6. AutoGLM_GUI/agents/protocols.py +23 -0
  7. AutoGLM_GUI/api/__init__.py +50 -7
  8. AutoGLM_GUI/api/agents.py +61 -19
  9. AutoGLM_GUI/api/devices.py +12 -18
  10. AutoGLM_GUI/api/dual_model.py +24 -17
  11. AutoGLM_GUI/api/health.py +13 -0
  12. AutoGLM_GUI/api/layered_agent.py +659 -0
  13. AutoGLM_GUI/api/mcp.py +11 -10
  14. AutoGLM_GUI/api/version.py +23 -10
  15. AutoGLM_GUI/api/workflows.py +2 -1
  16. AutoGLM_GUI/config_manager.py +56 -24
  17. AutoGLM_GUI/device_adapter.py +263 -0
  18. AutoGLM_GUI/device_protocol.py +266 -0
  19. AutoGLM_GUI/devices/__init__.py +49 -0
  20. AutoGLM_GUI/devices/adb_device.py +205 -0
  21. AutoGLM_GUI/devices/mock_device.py +183 -0
  22. AutoGLM_GUI/devices/remote_device.py +172 -0
  23. AutoGLM_GUI/dual_model/decision_model.py +4 -4
  24. AutoGLM_GUI/dual_model/protocols.py +3 -3
  25. AutoGLM_GUI/exceptions.py +3 -3
  26. AutoGLM_GUI/mai_ui_adapter/agent_wrapper.py +291 -0
  27. AutoGLM_GUI/metrics.py +13 -20
  28. AutoGLM_GUI/phone_agent_manager.py +219 -134
  29. AutoGLM_GUI/phone_agent_patches.py +2 -1
  30. AutoGLM_GUI/platform_utils.py +5 -2
  31. AutoGLM_GUI/prompts.py +6 -1
  32. AutoGLM_GUI/schemas.py +45 -14
  33. AutoGLM_GUI/scrcpy_stream.py +17 -13
  34. AutoGLM_GUI/server.py +3 -1
  35. AutoGLM_GUI/socketio_server.py +16 -4
  36. AutoGLM_GUI/state.py +10 -30
  37. AutoGLM_GUI/static/assets/{about-Cj6QXqMf.js → about-_XNhzQZX.js} +1 -1
  38. AutoGLM_GUI/static/assets/chat-DwJpiAWf.js +126 -0
  39. AutoGLM_GUI/static/assets/{dialog-CxJlnjzH.js → dialog-B3uW4T8V.js} +3 -3
  40. AutoGLM_GUI/static/assets/index-Cpv2gSF1.css +1 -0
  41. AutoGLM_GUI/static/assets/{index-C_B-Arvf.js → index-Cy8TmmHV.js} +1 -1
  42. AutoGLM_GUI/static/assets/{index-CxJQuE4y.js → index-UYYauTly.js} +6 -6
  43. AutoGLM_GUI/static/assets/{workflows-BTiGCNI0.js → workflows-Du_de-dt.js} +1 -1
  44. AutoGLM_GUI/static/index.html +2 -2
  45. AutoGLM_GUI/types.py +125 -0
  46. {autoglm_gui-1.3.1.dist-info → autoglm_gui-1.4.1.dist-info}/METADATA +147 -65
  47. {autoglm_gui-1.3.1.dist-info → autoglm_gui-1.4.1.dist-info}/RECORD +58 -39
  48. mai_agent/base.py +137 -0
  49. mai_agent/mai_grounding_agent.py +263 -0
  50. mai_agent/mai_naivigation_agent.py +526 -0
  51. mai_agent/prompt.py +148 -0
  52. mai_agent/unified_memory.py +67 -0
  53. mai_agent/utils.py +73 -0
  54. phone_agent/config/prompts.py +6 -1
  55. phone_agent/config/prompts_zh.py +6 -1
  56. AutoGLM_GUI/config.py +0 -23
  57. AutoGLM_GUI/static/assets/chat-BJeomZgh.js +0 -124
  58. AutoGLM_GUI/static/assets/index-Z0uYCPOO.css +0 -1
  59. {autoglm_gui-1.3.1.dist-info → autoglm_gui-1.4.1.dist-info}/WHEEL +0 -0
  60. {autoglm_gui-1.3.1.dist-info → autoglm_gui-1.4.1.dist-info}/entry_points.txt +0 -0
  61. {autoglm_gui-1.3.1.dist-info → autoglm_gui-1.4.1.dist-info}/licenses/LICENSE +0 -0
@@ -9,6 +9,8 @@ from dataclasses import dataclass
9
9
  from enum import Enum
10
10
  from typing import TYPE_CHECKING, Callable, Optional
11
11
 
12
+ from AutoGLM_GUI.agents.protocols import BaseAgent
13
+ from AutoGLM_GUI.types import AgentSpecificConfig
12
14
  from AutoGLM_GUI.exceptions import (
13
15
  AgentInitializationError,
14
16
  AgentNotInitializedError,
@@ -17,7 +19,6 @@ from AutoGLM_GUI.exceptions import (
17
19
  from AutoGLM_GUI.logger import logger
18
20
 
19
21
  if TYPE_CHECKING:
20
- from phone_agent import PhoneAgent
21
22
  from phone_agent.agent import AgentConfig
22
23
  from phone_agent.model import ModelConfig
23
24
 
@@ -46,10 +47,8 @@ class AgentMetadata:
46
47
 
47
48
  @dataclass
48
49
  class StreamingAgentContext:
49
- """Streaming agent 会话上下文."""
50
-
51
- streaming_agent: "PhoneAgent"
52
- original_agent: "PhoneAgent"
50
+ streaming_agent: BaseAgent
51
+ original_agent: BaseAgent
53
52
  stop_event: threading.Event
54
53
 
55
54
 
@@ -95,11 +94,9 @@ class PhoneAgentManager:
95
94
  self._device_locks_lock = threading.Lock()
96
95
 
97
96
  # Agent metadata (indexed by device_id)
97
+ # State is stored in AgentMetadata.state (single source of truth)
98
98
  self._metadata: dict[str, AgentMetadata] = {}
99
99
 
100
- # State tracking
101
- self._states: dict[str, AgentState] = {}
102
-
103
100
  # Streaming agent state (device_id -> StreamingAgentContext)
104
101
  self._streaming_contexts: dict[str, StreamingAgentContext] = {}
105
102
  self._streaming_contexts_lock = threading.Lock()
@@ -107,6 +104,10 @@ class PhoneAgentManager:
107
104
  # Abort events (device_id -> threading.Event)
108
105
  self._abort_events: dict[str, threading.Event] = {}
109
106
 
107
+ # Agent storage (transition from global state to instance state)
108
+ self._agents: dict[str, BaseAgent] = {}
109
+ self._agent_configs: dict[str, tuple["ModelConfig", "AgentConfig"]] = {}
110
+
110
111
  @classmethod
111
112
  def get_instance(cls) -> PhoneAgentManager:
112
113
  """Get singleton instance (thread-safe, double-checked locking)."""
@@ -122,11 +123,11 @@ class PhoneAgentManager:
122
123
  def initialize_agent(
123
124
  self,
124
125
  device_id: str,
125
- model_config: ModelConfig,
126
- agent_config: AgentConfig,
126
+ model_config: "ModelConfig",
127
+ agent_config: "AgentConfig",
127
128
  takeover_callback: Optional[Callable] = None,
128
129
  force: bool = False,
129
- ) -> PhoneAgent:
130
+ ) -> BaseAgent:
130
131
  """
131
132
  Initialize PhoneAgent for a device (thread-safe, idempotent).
132
133
 
@@ -148,15 +149,14 @@ class PhoneAgentManager:
148
149
  - On failure, state is rolled back
149
150
  - state.agents and state.agent_configs remain consistent
150
151
  """
152
+ from AutoGLM_GUI.state import non_blocking_takeover
151
153
  from phone_agent import PhoneAgent
152
154
 
153
- from AutoGLM_GUI.state import agent_configs, agents, non_blocking_takeover
154
-
155
155
  with self._manager_lock:
156
156
  # Check if already initialized
157
- if device_id in agents and not force:
157
+ if device_id in self._agents and not force:
158
158
  logger.debug(f"Agent already initialized for {device_id}")
159
- return agents[device_id]
159
+ return self._agents[device_id]
160
160
 
161
161
  # Check device availability (non-blocking check)
162
162
  device_lock = self._get_device_lock(device_id)
@@ -165,8 +165,15 @@ class PhoneAgentManager:
165
165
  f"Device {device_id} is currently processing a request"
166
166
  )
167
167
 
168
- # Set initializing state
169
- self._states[device_id] = AgentState.INITIALIZING
168
+ # Create metadata first with INITIALIZING state
169
+ self._metadata[device_id] = AgentMetadata(
170
+ device_id=device_id,
171
+ state=AgentState.INITIALIZING,
172
+ model_config=model_config,
173
+ agent_config=agent_config,
174
+ created_at=time.time(),
175
+ last_used=time.time(),
176
+ )
170
177
 
171
178
  try:
172
179
  # Create agent
@@ -177,29 +184,119 @@ class PhoneAgentManager:
177
184
  )
178
185
 
179
186
  # Store in state (transactional)
180
- agents[device_id] = agent
181
- agent_configs[device_id] = (model_config, agent_config)
187
+ self._agents[device_id] = agent
188
+ self._agent_configs[device_id] = (model_config, agent_config)
189
+
190
+ # Update state to IDLE on success
191
+ self._metadata[device_id].state = AgentState.IDLE
192
+
193
+ logger.info(f"Agent initialized for device {device_id}")
194
+ return agent
195
+
196
+ except Exception as e:
197
+ # Rollback on error
198
+ self._agents.pop(device_id, None)
199
+ self._agent_configs.pop(device_id, None)
200
+ self._metadata[device_id].state = AgentState.ERROR
201
+ self._metadata[device_id].error_message = str(e)
202
+
203
+ logger.error(f"Failed to initialize agent for {device_id}: {e}")
204
+ raise AgentInitializationError(
205
+ f"Failed to initialize agent: {str(e)}"
206
+ ) from e
207
+
208
+ def initialize_agent_with_factory(
209
+ self,
210
+ device_id: str,
211
+ agent_type: str,
212
+ model_config: ModelConfig,
213
+ agent_config: AgentConfig,
214
+ agent_specific_config: AgentSpecificConfig,
215
+ takeover_callback: Optional[Callable] = None,
216
+ confirmation_callback: Optional[Callable] = None,
217
+ force: bool = False,
218
+ ) -> "BaseAgent":
219
+ """
220
+ Initialize agent using factory pattern (thread-safe, idempotent).
221
+
222
+ This method uses the agent factory to create agents dynamically based on agent_type.
223
+ New agent types can be added without modifying this method by registering them.
224
+
225
+ Args:
226
+ device_id: Device identifier (USB serial / IP:port)
227
+ agent_type: Type of agent to create (e.g., "phone", "mai")
228
+ model_config: Model configuration
229
+ agent_config: Agent configuration
230
+ agent_specific_config: Agent-specific configuration dict
231
+ takeover_callback: Optional takeover callback
232
+ confirmation_callback: Optional confirmation callback
233
+ force: Force re-initialization even if agent exists
234
+
235
+ Returns:
236
+ BaseAgent: Initialized agent instance
237
+
238
+ Raises:
239
+ AgentInitializationError: If initialization fails
240
+ DeviceBusyError: If device is currently processing
241
+
242
+ Transactional Guarantee:
243
+ - On failure, state is rolled back
244
+ - state.agents and state.agent_configs remain consistent
245
+ """
246
+ from AutoGLM_GUI.agents import create_agent
247
+
248
+ with self._manager_lock:
249
+ # Check if already initialized
250
+ if device_id in self._agents and not force:
251
+ logger.debug(f"Agent already initialized for {device_id}")
252
+ return self._agents[device_id]
253
+
254
+ # Check device availability (non-blocking check)
255
+ device_lock = self._get_device_lock(device_id)
256
+ if device_lock.locked():
257
+ raise DeviceBusyError(
258
+ f"Device {device_id} is currently processing a request"
259
+ )
260
+
261
+ # Create metadata first with INITIALIZING state
262
+ self._metadata[device_id] = AgentMetadata(
263
+ device_id=device_id,
264
+ state=AgentState.INITIALIZING,
265
+ model_config=model_config,
266
+ agent_config=agent_config,
267
+ created_at=time.time(),
268
+ last_used=time.time(),
269
+ )
182
270
 
183
- # Update metadata
184
- self._metadata[device_id] = AgentMetadata(
185
- device_id=device_id,
186
- state=AgentState.IDLE,
271
+ try:
272
+ # Create agent using factory
273
+ agent = create_agent(
274
+ agent_type=agent_type,
187
275
  model_config=model_config,
188
276
  agent_config=agent_config,
189
- created_at=time.time(),
190
- last_used=time.time(),
277
+ agent_specific_config=agent_specific_config,
278
+ takeover_callback=takeover_callback,
279
+ confirmation_callback=confirmation_callback,
191
280
  )
192
- self._states[device_id] = AgentState.IDLE
193
281
 
194
- logger.info(f"Agent initialized for device {device_id}")
282
+ # Store in state (transactional)
283
+ self._agents[device_id] = agent
284
+ self._agent_configs[device_id] = (model_config, agent_config)
285
+
286
+ # Update state to IDLE on success
287
+ self._metadata[device_id].state = AgentState.IDLE
288
+
289
+ logger.info(
290
+ f"Agent of type '{agent_type}' initialized for device {device_id}"
291
+ )
195
292
  return agent
196
293
 
197
294
  except Exception as e:
198
295
  # Rollback on error
199
- agents.pop(device_id, None)
200
- agent_configs.pop(device_id, None)
201
- self._metadata.pop(device_id, None)
202
- self._states[device_id] = AgentState.ERROR
296
+ self._agents.pop(device_id, None)
297
+ self._agent_configs.pop(device_id, None)
298
+ self._metadata[device_id].state = AgentState.ERROR
299
+ self._metadata[device_id].error_message = str(e)
203
300
 
204
301
  logger.error(f"Failed to initialize agent for {device_id}: {e}")
205
302
  raise AgentInitializationError(
@@ -208,41 +305,61 @@ class PhoneAgentManager:
208
305
 
209
306
  def _create_streaming_agent(
210
307
  self,
211
- model_config: "ModelConfig",
212
- agent_config: "AgentConfig",
308
+ device_id: str,
309
+ original_agent: "BaseAgent",
213
310
  on_thinking_chunk: Callable[[str], None],
214
- ) -> "PhoneAgent":
311
+ ) -> "BaseAgent":
215
312
  """
216
- 创建支持流式输出的 PhoneAgent(monkey-patched model_client).
313
+ 创建支持流式输出的 agent(复制原始 agent).
217
314
 
218
315
  Args:
219
- model_config: 模型配置
220
- agent_config: Agent 配置
316
+ device_id: 设备标识符
317
+ original_agent: 原始 agent 实例
221
318
  on_thinking_chunk: 思考块回调函数
222
319
 
223
320
  Returns:
224
- 已 patch 的 PhoneAgent 实例
321
+ 已 patch 的 agent 实例
225
322
  """
323
+ from AutoGLM_GUI.agents.mai_adapter import MAIAgentAdapter
226
324
  from phone_agent import PhoneAgent
227
325
 
228
- from AutoGLM_GUI.state import non_blocking_takeover
326
+ model_config, agent_config = self.get_config(device_id)
229
327
 
230
- # 创建 agent
231
- agent = PhoneAgent(
232
- model_config=model_config,
233
- agent_config=agent_config,
234
- takeover_callback=non_blocking_takeover,
235
- )
328
+ streaming_agent: BaseAgent
329
+ if isinstance(original_agent, MAIAgentAdapter):
330
+ streaming_agent = MAIAgentAdapter(
331
+ model_config=model_config,
332
+ agent_config=agent_config,
333
+ mai_config=original_agent.mai_config,
334
+ takeover_callback=original_agent.action_handler.takeover_callback,
335
+ confirmation_callback=original_agent.action_handler.confirmation_callback,
336
+ on_thinking_chunk=on_thinking_chunk,
337
+ )
338
+ streaming_agent.mai_agent.traj_memory = original_agent.mai_agent.traj_memory
339
+ streaming_agent._step_count = original_agent._step_count
340
+ streaming_agent._current_instruction = original_agent._current_instruction
341
+ elif isinstance(original_agent, PhoneAgent):
342
+ from AutoGLM_GUI.state import non_blocking_takeover
343
+
344
+ phone_streaming_agent = PhoneAgent(
345
+ model_config=model_config,
346
+ agent_config=agent_config,
347
+ takeover_callback=non_blocking_takeover,
348
+ )
236
349
 
237
- # Monkey-patch model_client.request 以支持流式回调
238
- original_request = agent.model_client.request
350
+ original_request = phone_streaming_agent.model_client.request
239
351
 
240
- def patched_request(messages, **kwargs):
241
- return original_request(messages, on_thinking_chunk=on_thinking_chunk)
352
+ def patched_request(messages, **kwargs): # type: ignore[no-untyped-def]
353
+ return original_request(messages, on_thinking_chunk=on_thinking_chunk) # type: ignore[call-arg]
242
354
 
243
- agent.model_client.request = patched_request
355
+ phone_streaming_agent.model_client.request = patched_request # type: ignore[method-assign]
356
+ phone_streaming_agent._context = original_agent._context.copy()
357
+ phone_streaming_agent._step_count = original_agent._step_count
358
+ streaming_agent = phone_streaming_agent
359
+ else:
360
+ raise ValueError(f"Unknown agent type: {type(original_agent)}")
244
361
 
245
- return agent
362
+ return streaming_agent
246
363
 
247
364
  @contextmanager
248
365
  def use_streaming_agent(
@@ -297,21 +414,16 @@ class PhoneAgentManager:
297
414
  auto_initialize=auto_initialize,
298
415
  )
299
416
 
300
- # 获取原始 agent 和配置
417
+ # 获取原始 agent
301
418
  original_agent = self.get_agent(device_id)
302
- model_config, agent_config = self.get_config(device_id)
303
419
 
304
- # 创建 streaming agent
420
+ # 创建 streaming agent(自动检测类型)
305
421
  streaming_agent = self._create_streaming_agent(
306
- model_config=model_config,
307
- agent_config=agent_config,
422
+ device_id=device_id,
423
+ original_agent=original_agent,
308
424
  on_thinking_chunk=on_thinking_chunk,
309
425
  )
310
426
 
311
- # 复制上下文(由于持有设备锁,线程安全)
312
- streaming_agent._context = original_agent._context.copy()
313
- streaming_agent._step_count = original_agent._step_count
314
-
315
427
  # 注册 abort 事件
316
428
  with self._streaming_contexts_lock:
317
429
  self._abort_events[device_id] = stop_event
@@ -326,12 +438,28 @@ class PhoneAgentManager:
326
438
  yield streaming_agent, stop_event
327
439
 
328
440
  finally:
329
- # 同步状态回原始 agent
330
441
  if streaming_agent and not stop_event.is_set():
331
442
  original_agent = self.get_agent_safe(device_id)
332
443
  if original_agent:
333
- original_agent._context = streaming_agent._context
334
- original_agent._step_count = streaming_agent._step_count
444
+ from AutoGLM_GUI.agents.mai_adapter import MAIAgentAdapter
445
+ from phone_agent import PhoneAgent
446
+
447
+ if isinstance(original_agent, MAIAgentAdapter) and isinstance(
448
+ streaming_agent, MAIAgentAdapter
449
+ ):
450
+ original_agent.mai_agent.traj_memory = (
451
+ streaming_agent.mai_agent.traj_memory
452
+ )
453
+ original_agent._step_count = streaming_agent._step_count
454
+ original_agent._current_instruction = (
455
+ streaming_agent._current_instruction
456
+ )
457
+ elif isinstance(original_agent, PhoneAgent) and isinstance(
458
+ streaming_agent, PhoneAgent
459
+ ):
460
+ original_agent._context = streaming_agent._context
461
+ original_agent._step_count = streaming_agent._step_count
462
+
335
463
  logger.debug(
336
464
  f"Synchronized context back to original agent for {device_id}"
337
465
  )
@@ -355,18 +483,15 @@ class PhoneAgentManager:
355
483
  Raises:
356
484
  AgentInitializationError: 如果配置不完整或初始化失败
357
485
  """
486
+ from AutoGLM_GUI.config_manager import config_manager
358
487
  from phone_agent.agent import AgentConfig
359
488
  from phone_agent.model import ModelConfig
360
489
 
361
- from AutoGLM_GUI.config import config
362
- from AutoGLM_GUI.config_manager import config_manager
363
-
364
490
  logger.info(f"Auto-initializing agent for device {device_id}...")
365
491
 
366
492
  # 热重载配置
367
493
  config_manager.load_file_config()
368
494
  config_manager.sync_to_env()
369
- config.refresh_from_env()
370
495
 
371
496
  effective_config = config_manager.get_effective_config()
372
497
 
@@ -388,43 +513,15 @@ class PhoneAgentManager:
388
513
  self.initialize_agent(device_id, model_config, agent_config)
389
514
  logger.info(f"Agent auto-initialized for device {device_id}")
390
515
 
391
- def get_agent(self, device_id: str) -> PhoneAgent:
392
- """
393
- Get initialized agent for a device.
394
-
395
- Auto-initializes the agent using global config if not already initialized.
396
-
397
- Args:
398
- device_id: Device identifier
399
-
400
- Returns:
401
- PhoneAgent: Agent instance
402
-
403
- Raises:
404
- AgentInitializationError: If agent not initialized and auto-init fails
405
- """
406
- from AutoGLM_GUI.state import agents
407
-
516
+ def get_agent(self, device_id: str) -> BaseAgent:
408
517
  with self._manager_lock:
409
- if device_id not in agents:
410
- # 自动初始化:使用全局配置
518
+ if device_id not in self._agents:
411
519
  self._auto_initialize_agent(device_id)
412
- return agents[device_id]
413
-
414
- def get_agent_safe(self, device_id: str) -> Optional[PhoneAgent]:
415
- """
416
- Get initialized agent for a device (safe version, no exception).
417
-
418
- Args:
419
- device_id: Device identifier
420
-
421
- Returns:
422
- PhoneAgent or None: Agent instance or None if not initialized
423
- """
424
- from AutoGLM_GUI.state import agents
520
+ return self._agents[device_id]
425
521
 
522
+ def get_agent_safe(self, device_id: str) -> Optional[BaseAgent]:
426
523
  with self._manager_lock:
427
- return agents.get(device_id)
524
+ return self._agents.get(device_id)
428
525
 
429
526
  def reset_agent(self, device_id: str) -> None:
430
527
  """
@@ -436,28 +533,27 @@ class PhoneAgentManager:
436
533
  Raises:
437
534
  AgentNotInitializedError: If agent not initialized
438
535
  """
536
+ from AutoGLM_GUI.state import non_blocking_takeover
439
537
  from phone_agent import PhoneAgent
440
538
 
441
- from AutoGLM_GUI.state import agent_configs, agents, non_blocking_takeover
442
-
443
539
  with self._manager_lock:
444
- if device_id not in agents:
540
+ if device_id not in self._agents:
445
541
  raise AgentNotInitializedError(
446
542
  f"Agent not initialized for device {device_id}"
447
543
  )
448
544
 
449
545
  # Get cached config
450
- if device_id not in agent_configs:
546
+ if device_id not in self._agent_configs:
451
547
  logger.warning(
452
548
  f"No cached config for {device_id}, only resetting agent state"
453
549
  )
454
- agents[device_id].reset()
550
+ self._agents[device_id].reset()
455
551
  return
456
552
 
457
553
  # Rebuild agent from cached config
458
- model_config, agent_config = agent_configs[device_id]
554
+ model_config, agent_config = self._agent_configs[device_id]
459
555
 
460
- agents[device_id] = PhoneAgent(
556
+ self._agents[device_id] = PhoneAgent(
461
557
  model_config=model_config,
462
558
  agent_config=agent_config,
463
559
  takeover_callback=non_blocking_takeover,
@@ -467,8 +563,7 @@ class PhoneAgentManager:
467
563
  if device_id in self._metadata:
468
564
  self._metadata[device_id].last_used = time.time()
469
565
  self._metadata[device_id].error_message = None
470
-
471
- self._states[device_id] = AgentState.IDLE
566
+ self._metadata[device_id].state = AgentState.IDLE
472
567
 
473
568
  logger.info(f"Agent reset for device {device_id}")
474
569
 
@@ -479,11 +574,9 @@ class PhoneAgentManager:
479
574
  Args:
480
575
  device_id: Device identifier
481
576
  """
482
- from AutoGLM_GUI.state import agent_configs, agents
483
-
484
577
  with self._manager_lock:
485
578
  # Remove agent
486
- agent = agents.pop(device_id, None)
579
+ agent = self._agents.pop(device_id, None)
487
580
  if agent:
488
581
  try:
489
582
  agent.reset() # Clean up agent state
@@ -491,20 +584,17 @@ class PhoneAgentManager:
491
584
  logger.warning(f"Error resetting agent during destroy: {e}")
492
585
 
493
586
  # Remove config
494
- agent_configs.pop(device_id, None)
587
+ self._agent_configs.pop(device_id, None)
495
588
 
496
589
  # Remove metadata
497
590
  self._metadata.pop(device_id, None)
498
- self._states.pop(device_id, None)
499
591
 
500
592
  logger.info(f"Agent destroyed for device {device_id}")
501
593
 
502
594
  def is_initialized(self, device_id: str) -> bool:
503
595
  """Check if agent is initialized for device."""
504
- from AutoGLM_GUI.state import agents
505
-
506
596
  with self._manager_lock:
507
- return device_id in agents
597
+ return device_id in self._agents
508
598
 
509
599
  # ==================== Concurrency Control ====================
510
600
 
@@ -582,8 +672,8 @@ class PhoneAgentManager:
582
672
  if acquired:
583
673
  # Update state
584
674
  with self._manager_lock:
585
- self._states[device_id] = AgentState.BUSY
586
675
  if device_id in self._metadata:
676
+ self._metadata[device_id].state = AgentState.BUSY
587
677
  self._metadata[device_id].last_used = time.time()
588
678
 
589
679
  logger.debug(f"Device lock acquired for {device_id}")
@@ -610,7 +700,8 @@ class PhoneAgentManager:
610
700
 
611
701
  # Update state
612
702
  with self._manager_lock:
613
- self._states[device_id] = AgentState.IDLE
703
+ if device_id in self._metadata:
704
+ self._metadata[device_id].state = AgentState.IDLE
614
705
 
615
706
  logger.debug(f"Device lock released for {device_id}")
616
707
 
@@ -671,13 +762,14 @@ class PhoneAgentManager:
671
762
  def get_state(self, device_id: str) -> AgentState:
672
763
  """Get current agent state."""
673
764
  with self._manager_lock:
674
- return self._states.get(device_id, AgentState.ERROR)
765
+ metadata = self._metadata.get(device_id)
766
+ return metadata.state if metadata else AgentState.ERROR
675
767
 
676
768
  def set_error_state(self, device_id: str, error_message: str) -> None:
677
769
  """Mark agent as errored."""
678
770
  with self._manager_lock:
679
- self._states[device_id] = AgentState.ERROR
680
771
  if device_id in self._metadata:
772
+ self._metadata[device_id].state = AgentState.ERROR
681
773
  self._metadata[device_id].error_message = error_message
682
774
 
683
775
  logger.error(f"Agent error for {device_id}: {error_message}")
@@ -686,14 +778,12 @@ class PhoneAgentManager:
686
778
 
687
779
  def get_config(self, device_id: str) -> tuple[ModelConfig, AgentConfig]:
688
780
  """Get cached configuration for device."""
689
- from AutoGLM_GUI.state import agent_configs
690
-
691
781
  with self._manager_lock:
692
- if device_id not in agent_configs:
782
+ if device_id not in self._agent_configs:
693
783
  raise AgentNotInitializedError(
694
784
  f"No configuration found for device {device_id}"
695
785
  )
696
- return agent_configs[device_id]
786
+ return self._agent_configs[device_id]
697
787
 
698
788
  def update_config(
699
789
  self,
@@ -709,15 +799,13 @@ class PhoneAgentManager:
709
799
  model_config: New model config (None = keep existing)
710
800
  agent_config: New agent config (None = keep existing)
711
801
  """
712
- from AutoGLM_GUI.state import agent_configs
713
-
714
802
  with self._manager_lock:
715
- if device_id not in agent_configs:
803
+ if device_id not in self._agent_configs:
716
804
  raise AgentNotInitializedError(
717
805
  f"No configuration found for device {device_id}"
718
806
  )
719
807
 
720
- old_model_config, old_agent_config = agent_configs[device_id]
808
+ old_model_config, old_agent_config = self._agent_configs[device_id]
721
809
 
722
810
  new_model_config = model_config or old_model_config
723
811
  new_agent_config = agent_config or old_agent_config
@@ -743,7 +831,6 @@ class PhoneAgentManager:
743
831
  Optional[str]: device_id of initialized agent, or None
744
832
  """
745
833
  from AutoGLM_GUI.device_manager import DeviceManager
746
- from AutoGLM_GUI.state import agents
747
834
 
748
835
  with self._manager_lock:
749
836
  # Get device by serial from DeviceManager
@@ -755,7 +842,7 @@ class PhoneAgentManager:
755
842
 
756
843
  # Check all connections for initialized agents
757
844
  for conn in device.connections:
758
- if conn.device_id in agents:
845
+ if conn.device_id in self._agents:
759
846
  return conn.device_id
760
847
 
761
848
  return None
@@ -764,10 +851,8 @@ class PhoneAgentManager:
764
851
 
765
852
  def list_agents(self) -> list[str]:
766
853
  """Get list of all initialized device IDs."""
767
- from AutoGLM_GUI.state import agents
768
-
769
854
  with self._manager_lock:
770
- return list(agents.keys())
855
+ return list(self._agents.keys())
771
856
 
772
857
  def get_metadata(self, device_id: str) -> Optional[AgentMetadata]:
773
858
  """Get agent metadata."""
@@ -7,6 +7,7 @@ This module patches the upstream phone_agent code without modifying the original
7
7
  from typing import Any, Callable
8
8
 
9
9
  from phone_agent.model import ModelClient
10
+ from phone_agent.model.client import ModelResponse
10
11
 
11
12
 
12
13
  # Store original methods
@@ -17,7 +18,7 @@ def _patched_model_request(
17
18
  self,
18
19
  messages: list[dict[str, Any]],
19
20
  on_thinking_chunk: Callable[[str], None] | None = None,
20
- ) -> Any:
21
+ ) -> ModelResponse:
21
22
  """
22
23
  Patched version of ModelClient.request that supports streaming thinking chunks.
23
24
 
@@ -3,7 +3,8 @@
3
3
  import asyncio
4
4
  import platform
5
5
  import subprocess
6
- from typing import Any, Sequence
6
+ from asyncio.subprocess import Process as AsyncProcess
7
+ from typing import Sequence
7
8
 
8
9
 
9
10
  def is_windows() -> bool:
@@ -51,7 +52,9 @@ async def run_cmd_silently(cmd: Sequence[str]) -> subprocess.CompletedProcess:
51
52
  return subprocess.CompletedProcess(cmd, return_code, stdout_str, stderr_str)
52
53
 
53
54
 
54
- async def spawn_process(cmd: Sequence[str], *, capture_output: bool = False) -> Any:
55
+ async def spawn_process(
56
+ cmd: Sequence[str], *, capture_output: bool = False
57
+ ) -> subprocess.Popen[bytes] | AsyncProcess:
55
58
  """Start a long-running process with optional stdio capture."""
56
59
  stdout = subprocess.PIPE if capture_output else None
57
60
  stderr = subprocess.PIPE if capture_output else None
AutoGLM_GUI/prompts.py CHANGED
@@ -11,7 +11,12 @@ from datetime import datetime
11
11
  today = datetime.today()
12
12
  weekday_names = ["星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"]
13
13
  weekday = weekday_names[today.weekday()]
14
- formatted_date = today.strftime("%Y年%m月%d日") + " " + weekday
14
+ # NOTE: Do NOT use strftime with Chinese characters in format string!
15
+ # On some Windows systems with non-UTF-8 locale (e.g., GBK/CP936),
16
+ # strftime("%Y年%m月%d日") raises UnicodeEncodeError because the C library's
17
+ # strftime uses locale encoding, not Python's UTF-8 mode.
18
+ # Use f-string instead to avoid this issue completely.
19
+ formatted_date = f"{today.year}年{today.month:02d}月{today.day:02d}日 {weekday}"
15
20
 
16
21
  MCP_SYSTEM_PROMPT_ZH = f"""
17
22
  # Context