oagi-core 0.10.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 (68) hide show
  1. oagi/__init__.py +148 -0
  2. oagi/agent/__init__.py +33 -0
  3. oagi/agent/default.py +124 -0
  4. oagi/agent/factories.py +74 -0
  5. oagi/agent/observer/__init__.py +38 -0
  6. oagi/agent/observer/agent_observer.py +99 -0
  7. oagi/agent/observer/events.py +28 -0
  8. oagi/agent/observer/exporters.py +445 -0
  9. oagi/agent/observer/protocol.py +12 -0
  10. oagi/agent/protocol.py +55 -0
  11. oagi/agent/registry.py +155 -0
  12. oagi/agent/tasker/__init__.py +33 -0
  13. oagi/agent/tasker/memory.py +160 -0
  14. oagi/agent/tasker/models.py +77 -0
  15. oagi/agent/tasker/planner.py +408 -0
  16. oagi/agent/tasker/taskee_agent.py +512 -0
  17. oagi/agent/tasker/tasker_agent.py +324 -0
  18. oagi/cli/__init__.py +11 -0
  19. oagi/cli/agent.py +281 -0
  20. oagi/cli/display.py +56 -0
  21. oagi/cli/main.py +77 -0
  22. oagi/cli/server.py +94 -0
  23. oagi/cli/tracking.py +55 -0
  24. oagi/cli/utils.py +89 -0
  25. oagi/client/__init__.py +12 -0
  26. oagi/client/async_.py +290 -0
  27. oagi/client/base.py +457 -0
  28. oagi/client/sync.py +293 -0
  29. oagi/exceptions.py +118 -0
  30. oagi/handler/__init__.py +24 -0
  31. oagi/handler/_macos.py +55 -0
  32. oagi/handler/async_pyautogui_action_handler.py +44 -0
  33. oagi/handler/async_screenshot_maker.py +47 -0
  34. oagi/handler/pil_image.py +102 -0
  35. oagi/handler/pyautogui_action_handler.py +291 -0
  36. oagi/handler/screenshot_maker.py +41 -0
  37. oagi/logging.py +55 -0
  38. oagi/server/__init__.py +13 -0
  39. oagi/server/agent_wrappers.py +98 -0
  40. oagi/server/config.py +46 -0
  41. oagi/server/main.py +157 -0
  42. oagi/server/models.py +98 -0
  43. oagi/server/session_store.py +116 -0
  44. oagi/server/socketio_server.py +405 -0
  45. oagi/task/__init__.py +21 -0
  46. oagi/task/async_.py +101 -0
  47. oagi/task/async_short.py +76 -0
  48. oagi/task/base.py +157 -0
  49. oagi/task/short.py +76 -0
  50. oagi/task/sync.py +99 -0
  51. oagi/types/__init__.py +50 -0
  52. oagi/types/action_handler.py +30 -0
  53. oagi/types/async_action_handler.py +30 -0
  54. oagi/types/async_image_provider.py +38 -0
  55. oagi/types/image.py +17 -0
  56. oagi/types/image_provider.py +35 -0
  57. oagi/types/models/__init__.py +32 -0
  58. oagi/types/models/action.py +33 -0
  59. oagi/types/models/client.py +68 -0
  60. oagi/types/models/image_config.py +47 -0
  61. oagi/types/models/step.py +17 -0
  62. oagi/types/step_observer.py +93 -0
  63. oagi/types/url.py +3 -0
  64. oagi_core-0.10.1.dist-info/METADATA +245 -0
  65. oagi_core-0.10.1.dist-info/RECORD +68 -0
  66. oagi_core-0.10.1.dist-info/WHEEL +4 -0
  67. oagi_core-0.10.1.dist-info/entry_points.txt +2 -0
  68. oagi_core-0.10.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,405 @@
1
+ # -----------------------------------------------------------------------------
2
+ # Copyright (c) OpenAGI Foundation
3
+ # All rights reserved.
4
+ #
5
+ # This file is part of the official API project.
6
+ # Licensed under the MIT License.
7
+ # -----------------------------------------------------------------------------
8
+
9
+ import asyncio
10
+ import logging
11
+ from datetime import datetime
12
+ from typing import Any
13
+
14
+ from pydantic import ValidationError
15
+
16
+ from ..agent import AsyncDefaultAgent, create_agent
17
+ from ..client import AsyncClient
18
+ from ..exceptions import check_optional_dependency
19
+ from ..types.models.action import Action, ActionType
20
+ from .agent_wrappers import SocketIOActionHandler, SocketIOImageProvider
21
+ from .config import ServerConfig
22
+ from .models import (
23
+ BaseActionEventData,
24
+ ClickEventData,
25
+ DragEventData,
26
+ ErrorEventData,
27
+ FinishEventData,
28
+ HotkeyEventData,
29
+ InitEventData,
30
+ ScrollEventData,
31
+ TypeEventData,
32
+ WaitEventData,
33
+ )
34
+ from .session_store import Session, session_store
35
+
36
+ check_optional_dependency("socketio", "Server features", "server")
37
+ import socketio # noqa: E402
38
+
39
+ logger = logging.getLogger(__name__)
40
+
41
+ sio = socketio.AsyncServer(
42
+ async_mode="asgi",
43
+ cors_allowed_origins="*",
44
+ logger=False,
45
+ engineio_logger=False,
46
+ )
47
+
48
+
49
+ class SessionNamespace(socketio.AsyncNamespace):
50
+ def __init__(self, namespace: str, config: ServerConfig):
51
+ super().__init__(namespace)
52
+ self.config = config
53
+ self.background_tasks: dict[str, asyncio.Task] = {}
54
+
55
+ async def on_connect(self, sid: str, environ: dict, auth: dict | None) -> bool:
56
+ session_id = self.namespace.split("/")[-1]
57
+ logger.info(f"Client {sid} connected to session {session_id}")
58
+
59
+ session = session_store.get_session(session_id)
60
+ if session:
61
+ session.socket_id = sid
62
+ session.namespace = self.namespace
63
+ session_store.update_activity(session_id)
64
+
65
+ # Create OAGI client if not exists
66
+ if not session.oagi_client:
67
+ session.oagi_client = AsyncClient(
68
+ base_url=self.config.oagi_base_url,
69
+ api_key=self.config.oagi_api_key,
70
+ )
71
+ else:
72
+ logger.warning(f"Connection to non-existent session {session_id}")
73
+ # Create session on connect if it doesn't exist
74
+ session = Session(
75
+ session_id=session_id,
76
+ instruction="",
77
+ mode="actor", # Default mode
78
+ model=self.config.default_model,
79
+ temperature=self.config.default_temperature,
80
+ )
81
+ session.socket_id = sid
82
+ session.namespace = self.namespace
83
+ session.oagi_client = AsyncClient(
84
+ base_url=self.config.oagi_base_url,
85
+ api_key=self.config.oagi_api_key,
86
+ )
87
+ session_store.sessions[session_id] = session
88
+
89
+ return True
90
+
91
+ async def on_disconnect(self, sid: str) -> None:
92
+ session_id = self.namespace.split("/")[-1]
93
+ logger.info(f"Client {sid} disconnected from session {session_id}")
94
+
95
+ # Cancel any background tasks
96
+ if sid in self.background_tasks:
97
+ self.background_tasks[sid].cancel()
98
+ del self.background_tasks[sid]
99
+
100
+ # Start cleanup task
101
+ asyncio.create_task(self._cleanup_after_timeout(session_id))
102
+
103
+ async def _cleanup_after_timeout(self, session_id: str) -> None:
104
+ await asyncio.sleep(self.config.session_timeout_seconds)
105
+
106
+ session = session_store.get_session(session_id)
107
+ if session:
108
+ current_time = datetime.now().timestamp()
109
+ if (
110
+ current_time - session.last_activity
111
+ >= self.config.session_timeout_seconds
112
+ ):
113
+ logger.info(f"Session {session_id} timed out, cleaning up")
114
+
115
+ # Close OAGI client
116
+ if session.oagi_client:
117
+ await session.oagi_client.close()
118
+
119
+ session_store.delete_session(session_id)
120
+
121
+ async def on_init(self, sid: str, data: dict) -> None:
122
+ try:
123
+ session_id = self.namespace.split("/")[-1]
124
+ logger.info(f"Initializing session {session_id}")
125
+
126
+ # Validate input
127
+ event_data = InitEventData(**data)
128
+
129
+ # Get or create session
130
+ session = session_store.get_session(session_id)
131
+ if not session:
132
+ logger.error(f"Session {session_id} not found")
133
+ await self.emit(
134
+ "error",
135
+ ErrorEventData(
136
+ message=f"Session {session_id} not found"
137
+ ).model_dump(),
138
+ room=sid,
139
+ )
140
+ return
141
+
142
+ # Update session with init data
143
+ session.instruction = event_data.instruction
144
+ if event_data.mode:
145
+ session.mode = event_data.mode
146
+ if event_data.model:
147
+ session.model = event_data.model
148
+ if event_data.temperature is not None:
149
+ session.temperature = event_data.temperature
150
+ session.status = "running"
151
+ session_store.update_activity(session_id)
152
+
153
+ logger.info(
154
+ f"Session {session_id} initialized with: {event_data.instruction} "
155
+ f"(mode={event_data.mode}, model={event_data.model})"
156
+ )
157
+
158
+ # Create agent and wrappers
159
+ agent = create_agent(
160
+ mode=session.mode,
161
+ api_key=self.config.oagi_api_key,
162
+ base_url=self.config.oagi_base_url,
163
+ max_steps=self.config.max_steps,
164
+ model=event_data.model,
165
+ temperature=event_data.temperature,
166
+ )
167
+
168
+ action_handler = SocketIOActionHandler(self, session)
169
+ image_provider = SocketIOImageProvider(self, session, session.oagi_client)
170
+
171
+ # Start execution in background using agent
172
+ task = asyncio.create_task(
173
+ self._run_agent_task(
174
+ agent,
175
+ session,
176
+ action_handler,
177
+ image_provider,
178
+ event_data.instruction,
179
+ )
180
+ )
181
+ self.background_tasks[sid] = task
182
+
183
+ except ValidationError as e:
184
+ logger.error(f"Invalid init data: {e}")
185
+ await self.emit(
186
+ "error",
187
+ ErrorEventData(
188
+ message="Invalid init data",
189
+ details={"validation_errors": e.errors()},
190
+ ).model_dump(),
191
+ room=sid,
192
+ )
193
+ except Exception as e:
194
+ logger.error(f"Error in init: {e}", exc_info=True)
195
+ await self.emit(
196
+ "error",
197
+ ErrorEventData(message=str(e)).model_dump(),
198
+ room=sid,
199
+ )
200
+
201
+ async def _run_agent_task(
202
+ self,
203
+ agent: AsyncDefaultAgent,
204
+ session: Session,
205
+ action_handler: SocketIOActionHandler,
206
+ image_provider: SocketIOImageProvider,
207
+ instruction: str,
208
+ ) -> None:
209
+ try:
210
+ # Execute task using agent
211
+ success = await agent.execute(
212
+ instruction=instruction,
213
+ action_handler=action_handler,
214
+ image_provider=image_provider,
215
+ )
216
+
217
+ # Update session status
218
+ if success:
219
+ session.status = "completed"
220
+ logger.info(
221
+ f"Task completed successfully for session {session.session_id}"
222
+ )
223
+
224
+ # Emit finish event
225
+ await self.call(
226
+ "finish",
227
+ FinishEventData(index=0, total=1).model_dump(),
228
+ to=session.socket_id,
229
+ timeout=self.config.socketio_timeout,
230
+ )
231
+ else:
232
+ session.status = "failed"
233
+ logger.warning(f"Task failed for session {session.session_id}")
234
+
235
+ session_store.update_activity(session.session_id)
236
+
237
+ except asyncio.CancelledError:
238
+ logger.info(f"Agent task cancelled for session {session.session_id}")
239
+ session.status = "cancelled"
240
+ except Exception as e:
241
+ logger.error(f"Error in agent task: {e}", exc_info=True)
242
+ session.status = "failed"
243
+ if session.socket_id:
244
+ await self.emit(
245
+ "error",
246
+ ErrorEventData(message=f"Execution failed: {str(e)}").model_dump(),
247
+ room=session.socket_id,
248
+ )
249
+
250
+ async def _emit_actions(self, session: Session, actions: list[Action]) -> None:
251
+ total = len(actions)
252
+
253
+ for i, action in enumerate(actions):
254
+ try:
255
+ ack = await self._emit_single_action(session, action, i, total)
256
+ session.actions_executed += 1
257
+
258
+ if ack and not ack.get("success"):
259
+ logger.warning(f"Action {i} failed: {ack.get('error')}")
260
+
261
+ except Exception as e:
262
+ logger.error(f"Error emitting action {i}: {e}", exc_info=True)
263
+
264
+ async def _emit_single_action(
265
+ self, session: Session, action: Action, index: int, total: int
266
+ ) -> dict | None:
267
+ arg = action.argument.strip("()")
268
+ common = BaseActionEventData(index=index, total=total).model_dump()
269
+
270
+ logger.info(f"Emitting action {index + 1}/{total}: {action.type.value} {arg}")
271
+ match action.type:
272
+ case (
273
+ ActionType.CLICK
274
+ | ActionType.LEFT_DOUBLE
275
+ | ActionType.LEFT_TRIPLE
276
+ | ActionType.RIGHT_SINGLE
277
+ ):
278
+ coords = arg.split(",")
279
+ if len(coords) >= 2:
280
+ x, y = int(coords[0]), int(coords[1])
281
+ else:
282
+ logger.warning(f"Invalid action coordinates: {arg}")
283
+ return None
284
+
285
+ return await self.call(
286
+ action.type.value,
287
+ ClickEventData(**common, x=x, y=y).model_dump(),
288
+ to=session.socket_id,
289
+ timeout=self.config.socketio_timeout,
290
+ )
291
+
292
+ case ActionType.DRAG:
293
+ coords = arg.split(",")
294
+ if len(coords) >= 4:
295
+ x1, y1, x2, y2 = (int(coords[i]) for i in range(4))
296
+ else:
297
+ logger.warning(f"Invalid drag coordinates: {arg}")
298
+ return None
299
+
300
+ return await self.call(
301
+ "drag",
302
+ DragEventData(**common, x1=x1, y1=y1, x2=x2, y2=y2).model_dump(),
303
+ to=session.socket_id,
304
+ timeout=self.config.socketio_timeout,
305
+ )
306
+
307
+ case ActionType.HOTKEY:
308
+ combo = arg.strip()
309
+ count = action.count or 1
310
+
311
+ return await self.call(
312
+ "hotkey",
313
+ HotkeyEventData(**common, combo=combo, count=count).model_dump(),
314
+ to=session.socket_id,
315
+ timeout=self.config.socketio_timeout,
316
+ )
317
+
318
+ case ActionType.TYPE:
319
+ text = arg.strip()
320
+
321
+ return await self.call(
322
+ "type",
323
+ TypeEventData(**common, text=text).model_dump(),
324
+ to=session.socket_id,
325
+ timeout=self.config.socketio_timeout,
326
+ )
327
+
328
+ case ActionType.SCROLL:
329
+ parts = arg.split(",")
330
+ if len(parts) >= 3:
331
+ x, y = int(parts[0]), int(parts[1])
332
+ direction = parts[2].strip().lower()
333
+ else:
334
+ logger.warning(f"Invalid scroll coordinates: {arg}")
335
+ return None
336
+
337
+ count = action.count or 1
338
+
339
+ return await self.call(
340
+ "scroll",
341
+ ScrollEventData(
342
+ **common,
343
+ x=x,
344
+ y=y,
345
+ direction=direction,
346
+ count=count, # type: ignore
347
+ ).model_dump(),
348
+ to=session.socket_id,
349
+ timeout=self.config.socketio_timeout,
350
+ )
351
+
352
+ case ActionType.WAIT:
353
+ try:
354
+ duration_ms = int(arg) if arg else 1000
355
+ except (ValueError, TypeError):
356
+ duration_ms = 1000
357
+
358
+ return await self.call(
359
+ "wait",
360
+ WaitEventData(**common, duration_ms=duration_ms).model_dump(),
361
+ to=session.socket_id,
362
+ timeout=self.config.socketio_timeout,
363
+ )
364
+
365
+ case ActionType.FINISH:
366
+ return await self.call(
367
+ "finish",
368
+ FinishEventData(**common).model_dump(),
369
+ to=session.socket_id,
370
+ timeout=self.config.socketio_timeout,
371
+ )
372
+
373
+ case _:
374
+ logger.warning(f"Unknown action type: {action.type}")
375
+ return None
376
+
377
+
378
+ # Dynamic namespace registration
379
+ _registered_namespaces: dict[str, SessionNamespace] = {}
380
+
381
+
382
+ def get_or_create_namespace(namespace: str, config: ServerConfig) -> SessionNamespace:
383
+ if namespace not in _registered_namespaces:
384
+ ns = SessionNamespace(namespace, config)
385
+ sio.register_namespace(ns)
386
+ _registered_namespaces[namespace] = ns
387
+ logger.info(f"Registered namespace: {namespace}")
388
+ return _registered_namespaces[namespace]
389
+
390
+
391
+ # Patch connect handler for dynamic registration
392
+ original_connect = sio._handle_connect
393
+
394
+
395
+ async def _patched_handle_connect(eio_sid: str, namespace: str, data: Any) -> Any:
396
+ if namespace and namespace.startswith("/session/"):
397
+ config = ServerConfig()
398
+ get_or_create_namespace(namespace, config)
399
+ return await original_connect(eio_sid, namespace, data)
400
+
401
+
402
+ sio._handle_connect = _patched_handle_connect
403
+
404
+ # Create ASGI app
405
+ socket_app = socketio.ASGIApp(sio, socketio_path="socket.io")
oagi/task/__init__.py ADDED
@@ -0,0 +1,21 @@
1
+ # -----------------------------------------------------------------------------
2
+ # Copyright (c) OpenAGI Foundation
3
+ # All rights reserved.
4
+ #
5
+ # This file is part of the official API project.
6
+ # Licensed under the MIT License.
7
+ # -----------------------------------------------------------------------------
8
+
9
+ from .async_ import AsyncActor, AsyncTask
10
+ from .async_short import AsyncShortTask
11
+ from .short import ShortTask
12
+ from .sync import Actor, Task
13
+
14
+ __all__ = [
15
+ "Actor",
16
+ "AsyncActor",
17
+ "Task", # Deprecated: Use Actor instead
18
+ "AsyncTask", # Deprecated: Use AsyncActor instead
19
+ "ShortTask", # Deprecated
20
+ "AsyncShortTask", # Deprecated
21
+ ]
oagi/task/async_.py ADDED
@@ -0,0 +1,101 @@
1
+ # -----------------------------------------------------------------------------
2
+ # Copyright (c) OpenAGI Foundation
3
+ # All rights reserved.
4
+ #
5
+ # This file is part of the official API project.
6
+ # Licensed under the MIT License.
7
+ # -----------------------------------------------------------------------------
8
+
9
+ import warnings
10
+
11
+ from ..client import AsyncClient
12
+ from ..types import URL, Image, Step
13
+ from .base import BaseActor
14
+
15
+
16
+ class AsyncActor(BaseActor):
17
+ """Async base class for task automation with the OAGI API."""
18
+
19
+ def __init__(
20
+ self,
21
+ api_key: str | None = None,
22
+ base_url: str | None = None,
23
+ model: str = "lux-actor-1",
24
+ temperature: float | None = None,
25
+ ):
26
+ super().__init__(api_key, base_url, model, temperature)
27
+ self.client = AsyncClient(base_url=base_url, api_key=api_key)
28
+ self.api_key = self.client.api_key
29
+ self.base_url = self.client.base_url
30
+
31
+ async def init_task(
32
+ self,
33
+ task_desc: str,
34
+ max_steps: int = 20,
35
+ ):
36
+ """Initialize a new task with the given description.
37
+
38
+ Args:
39
+ task_desc: Task description
40
+ max_steps: Maximum number of steps allowed
41
+ """
42
+ self._prepare_init_task(task_desc, max_steps)
43
+
44
+ async def step(
45
+ self,
46
+ screenshot: Image | URL | bytes,
47
+ instruction: str | None = None,
48
+ temperature: float | None = None,
49
+ ) -> Step:
50
+ """Send screenshot to the server and get the next actions.
51
+
52
+ Args:
53
+ screenshot: Screenshot as Image object or raw bytes
54
+ instruction: Optional additional instruction for this step
55
+ temperature: Sampling temperature for this step (overrides task default if provided)
56
+
57
+ Returns:
58
+ Step: The actions and reasoning for this step
59
+ """
60
+ kwargs = self._prepare_step(
61
+ screenshot, instruction, temperature, prefix="async "
62
+ )
63
+
64
+ try:
65
+ response = await self.client.create_message(**kwargs)
66
+ return self._build_step_response(response, prefix="Async ")
67
+ except Exception as e:
68
+ self._handle_step_error(e, prefix="async ")
69
+
70
+ async def close(self):
71
+ """Close the underlying HTTP client to free resources."""
72
+ await self.client.close()
73
+
74
+ async def __aenter__(self):
75
+ return self
76
+
77
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
78
+ await self.close()
79
+
80
+
81
+ class AsyncTask(AsyncActor):
82
+ """Deprecated: Use AsyncActor instead.
83
+
84
+ This class is deprecated and will be removed in a future version.
85
+ Please use AsyncActor instead.
86
+ """
87
+
88
+ def __init__(
89
+ self,
90
+ api_key: str | None = None,
91
+ base_url: str | None = None,
92
+ model: str = "lux-actor-1",
93
+ temperature: float | None = None,
94
+ ):
95
+ warnings.warn(
96
+ "AsyncTask is deprecated and will be removed in a future version. "
97
+ "Please use AsyncActor instead.",
98
+ DeprecationWarning,
99
+ stacklevel=2,
100
+ )
101
+ super().__init__(api_key, base_url, model, temperature)
@@ -0,0 +1,76 @@
1
+ # -----------------------------------------------------------------------------
2
+ # Copyright (c) OpenAGI Foundation
3
+ # All rights reserved.
4
+ #
5
+ # This file is part of the official API project.
6
+ # Licensed under the MIT License.
7
+ # -----------------------------------------------------------------------------
8
+
9
+ import warnings
10
+
11
+ from ..logging import get_logger
12
+ from ..types import AsyncActionHandler, AsyncImageProvider
13
+ from .async_ import AsyncActor
14
+ from .base import BaseAutoMode
15
+
16
+ logger = get_logger("async_short_task")
17
+
18
+
19
+ class AsyncShortTask(AsyncActor, BaseAutoMode):
20
+ """Deprecated: This class is deprecated and will be removed in a future version.
21
+
22
+ Async task implementation with automatic mode for short-duration tasks.
23
+ Please use AsyncActor directly with custom automation logic instead.
24
+ """
25
+
26
+ def __init__(
27
+ self,
28
+ api_key: str | None = None,
29
+ base_url: str | None = None,
30
+ model: str = "lux-actor-1",
31
+ temperature: float | None = None,
32
+ ):
33
+ warnings.warn(
34
+ "AsyncShortTask is deprecated and will be removed in a future version. "
35
+ "Please use AsyncActor with custom automation logic instead.",
36
+ DeprecationWarning,
37
+ stacklevel=2,
38
+ )
39
+ super().__init__(
40
+ api_key=api_key, base_url=base_url, model=model, temperature=temperature
41
+ )
42
+
43
+ async def auto_mode(
44
+ self,
45
+ task_desc: str,
46
+ max_steps: int = 20,
47
+ executor: AsyncActionHandler = None,
48
+ image_provider: AsyncImageProvider = None,
49
+ temperature: float | None = None,
50
+ ) -> bool:
51
+ """Run the task in automatic mode with the provided executor and image provider.
52
+
53
+ Args:
54
+ task_desc: Task description
55
+ max_steps: Maximum number of steps
56
+ executor: Async handler to execute actions
57
+ image_provider: Async provider for screenshots
58
+ temperature: Sampling temperature for all steps (overrides task default if provided)
59
+ """
60
+ self._log_auto_mode_start(task_desc, max_steps, prefix="async ")
61
+
62
+ await self.init_task(task_desc, max_steps=max_steps)
63
+
64
+ for i in range(max_steps):
65
+ self._log_auto_mode_step(i + 1, max_steps, prefix="async ")
66
+ image = await image_provider()
67
+ step = await self.step(image, temperature=temperature)
68
+ if executor:
69
+ self._log_auto_mode_actions(len(step.actions), prefix="async ")
70
+ await executor(step.actions)
71
+ if step.stop:
72
+ self._log_auto_mode_completion(i + 1, prefix="async ")
73
+ return True
74
+
75
+ self._log_auto_mode_max_steps(max_steps, prefix="async ")
76
+ return False