agi-python 0.0.1__tar.gz → 0.5.0__tar.gz

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 (45) hide show
  1. agi_python-0.5.0/CHANGELOG.md +33 -0
  2. {agi_python-0.0.1/agi_python.egg-info → agi_python-0.5.0}/PKG-INFO +80 -2
  3. {agi_python-0.0.1 → agi_python-0.5.0}/README.md +79 -1
  4. {agi_python-0.0.1 → agi_python-0.5.0}/agi/__init__.py +48 -2
  5. {agi_python-0.0.1 → agi_python-0.5.0}/agi/_http.py +56 -0
  6. agi_python-0.5.0/agi/driver/__init__.py +85 -0
  7. agi_python-0.5.0/agi/driver/_binary.py +102 -0
  8. agi_python-0.5.0/agi/driver/_driver.py +568 -0
  9. agi_python-0.5.0/agi/driver/_protocol.py +417 -0
  10. agi_python-0.5.0/agi/loop.py +291 -0
  11. {agi_python-0.0.1 → agi_python-0.5.0}/agi/resources/sessions.py +117 -3
  12. {agi_python-0.0.1 → agi_python-0.5.0}/agi/types/__init__.py +17 -1
  13. agi_python-0.5.0/agi/types/desktop.py +68 -0
  14. {agi_python-0.0.1 → agi_python-0.5.0}/agi/types/sessions.py +13 -1
  15. {agi_python-0.0.1 → agi_python-0.5.0}/agi/types/shared.py +3 -0
  16. {agi_python-0.0.1 → agi_python-0.5.0/agi_python.egg-info}/PKG-INFO +80 -2
  17. {agi_python-0.0.1 → agi_python-0.5.0}/agi_python.egg-info/SOURCES.txt +8 -0
  18. {agi_python-0.0.1 → agi_python-0.5.0}/pyproject.toml +1 -1
  19. {agi_python-0.0.1 → agi_python-0.5.0}/tests/conftest.py +2 -1
  20. agi_python-0.5.0/tests/integration/test_driver.py +115 -0
  21. {agi_python-0.0.1 → agi_python-0.5.0}/LICENSE +0 -0
  22. {agi_python-0.0.1 → agi_python-0.5.0}/MANIFEST.in +0 -0
  23. {agi_python-0.0.1 → agi_python-0.5.0}/agi/_session_context.py +0 -0
  24. {agi_python-0.0.1 → agi_python-0.5.0}/agi/_sse.py +0 -0
  25. {agi_python-0.0.1 → agi_python-0.5.0}/agi/client.py +0 -0
  26. {agi_python-0.0.1 → agi_python-0.5.0}/agi/exceptions.py +0 -0
  27. {agi_python-0.0.1 → agi_python-0.5.0}/agi/py.typed +0 -0
  28. {agi_python-0.0.1 → agi_python-0.5.0}/agi/resources/__init__.py +0 -0
  29. {agi_python-0.0.1 → agi_python-0.5.0}/agi/types/results.py +0 -0
  30. {agi_python-0.0.1 → agi_python-0.5.0}/agi_python.egg-info/dependency_links.txt +0 -0
  31. {agi_python-0.0.1 → agi_python-0.5.0}/agi_python.egg-info/requires.txt +0 -0
  32. {agi_python-0.0.1 → agi_python-0.5.0}/agi_python.egg-info/top_level.txt +0 -0
  33. {agi_python-0.0.1 → agi_python-0.5.0}/setup.cfg +0 -0
  34. {agi_python-0.0.1 → agi_python-0.5.0}/tests/__init__.py +0 -0
  35. {agi_python-0.0.1 → agi_python-0.5.0}/tests/integration/__init__.py +0 -0
  36. {agi_python-0.0.1 → agi_python-0.5.0}/tests/integration/test_comprehensive.py +0 -0
  37. {agi_python-0.0.1 → agi_python-0.5.0}/tests/integration/test_edge_cases.py +0 -0
  38. {agi_python-0.0.1 → agi_python-0.5.0}/tests/integration/test_examples.py +0 -0
  39. {agi_python-0.0.1 → agi_python-0.5.0}/tests/integration/test_sessions.py +0 -0
  40. {agi_python-0.0.1 → agi_python-0.5.0}/tests/integration/test_smoke.py +0 -0
  41. {agi_python-0.0.1 → agi_python-0.5.0}/tests/integration/test_snapshots.py +0 -0
  42. {agi_python-0.0.1 → agi_python-0.5.0}/tests/unit/__init__.py +0 -0
  43. {agi_python-0.0.1 → agi_python-0.5.0}/tests/unit/test_client.py +0 -0
  44. {agi_python-0.0.1 → agi_python-0.5.0}/tests/unit/test_error_handling.py +0 -0
  45. {agi_python-0.0.1 → agi_python-0.5.0}/tests/unit/test_session_context.py +0 -0
@@ -0,0 +1,33 @@
1
+ # Changelog
2
+
3
+ ## [0.4.1](https://github.com/agi-inc/agi-python/compare/v0.4.0...v0.4.1) (2026-02-06)
4
+
5
+
6
+ ### Features
7
+
8
+ * add agi python sdk ([1440466](https://github.com/agi-inc/agi-python/commit/14404667032849e3835e3d40db7ca346a03c8474))
9
+ * add client-driven session support with AgentLoop ([0165ae7](https://github.com/agi-inc/agi-python/commit/0165ae73adceaefd7008f8bb47e01780a58982eb))
10
+ * Add client-driven session support with AgentLoop ([f3d024e](https://github.com/agi-inc/agi-python/commit/f3d024e1a0cfbc49a761f9ebc8745bdcb5af67a0))
11
+ * add Task response models ([1021036](https://github.com/agi-inc/agi-python/commit/10210367ba4df2022131dadb40901fa923c9d357))
12
+ * agi-python sdk ([0fe2e32](https://github.com/agi-inc/agi-python/commit/0fe2e3227d6a254aeb302e9b5262aa5013320d35))
13
+ * **ci:** add release-please workflow for automated releases ([f25ca3f](https://github.com/agi-inc/agi-python/commit/f25ca3fe8601873dc8f96408900a3a0bdd7b3c59))
14
+ * **ci:** add release-please workflow for automated releases ([9da9c57](https://github.com/agi-inc/agi-python/commit/9da9c57ccbe4b51fa0ac3b72438587eb362c2d06))
15
+ * **driver:** Add driver module for binary execution ([#5](https://github.com/agi-inc/agi-python/issues/5)) ([33588e6](https://github.com/agi-inc/agi-python/commit/33588e65d84e0eebca611ac52ed4529a7345e964))
16
+ * update step method in SessionsResource to include session_id ([560e268](https://github.com/agi-inc/agi-python/commit/560e26892101b272db93011da58de8dc55d5abae))
17
+
18
+
19
+ ### Bug Fixes
20
+
21
+ * **ci:** Fix security check false positive and add workflow permissions ([442e993](https://github.com/agi-inc/agi-python/commit/442e993ef7317f5458b7b6721d207ccb2e55ecd4))
22
+ * lint and test issues ([502d104](https://github.com/agi-inc/agi-python/commit/502d104bb75e9903b87766b071d8f47a10eb885f))
23
+ * lint issues ([fd15035](https://github.com/agi-inc/agi-python/commit/fd15035c06d125cb65d5a92299788d8cf1750844))
24
+ * **loop:** Add session_id parameter to AgentLoop ([de1ac36](https://github.com/agi-inc/agi-python/commit/de1ac36224bda410415713aab46ae081ac5e3d44))
25
+ * update session context to handle question event ([d062367](https://github.com/agi-inc/agi-python/commit/d0623672a4a0c6dd51100792e8fe71842624b4c2))
26
+
27
+
28
+ ### Documentation
29
+
30
+ * add CHANGELOG.md ([f9316c7](https://github.com/agi-inc/agi-python/commit/f9316c762f170750642974074c327cdc1e2b63bc))
31
+ * add enterprise notice for desktop mode ([237eb5e](https://github.com/agi-inc/agi-python/commit/237eb5ea12b3c0015af86c24e9df92687a3454cc))
32
+ * Add enterprise notice for desktop mode ([a2df41e](https://github.com/agi-inc/agi-python/commit/a2df41ed0523131f2ac53f39b76defc3cbb83e31))
33
+ * update readme ([cf05e24](https://github.com/agi-inc/agi-python/commit/cf05e248e0e06164e8475c76dc8bf4fc3d33beb8))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agi-python
3
- Version: 0.0.1
3
+ Version: 0.5.0
4
4
  Summary: Official Python SDK for AGI.tech API
5
5
  Author-email: AGI Inc <kaushik@theagi.company>
6
6
  Maintainer-email: AGI Inc <kaushik@theagi.company>
@@ -51,7 +51,7 @@ Dynamic: license-file
51
51
 
52
52
  ---
53
53
 
54
- **AI agent that actually works on the web.**
54
+ **Universal Computer-Use AI**
55
55
 
56
56
  <br />
57
57
 
@@ -305,6 +305,84 @@ session = client.sessions.create(
305
305
 
306
306
  </details>
307
307
 
308
+ <details>
309
+ <summary><b>Client-Driven Sessions</b> – Run agents on desktop, mobile, or custom environments</summary>
310
+
311
+ <br />
312
+
313
+ > **Note:** Desktop mode is currently feature-gated. For enterprise access, contact [`partner@theagi.company`](mailto:partner@theagi.company).
314
+
315
+ For scenarios where you control the execution environment (desktop automation, mobile apps, custom browsers), use client-driven sessions with `AgentLoop`:
316
+
317
+ ```python
318
+ import asyncio
319
+ from agi import AGIClient, AgentLoop
320
+
321
+ async def main():
322
+ client = AGIClient()
323
+
324
+ # Create a client-driven session
325
+ session = client.sessions.create(
326
+ agent_name="agi-2-claude",
327
+ agent_session_type="desktop",
328
+ goal="Open calculator and compute 2+2"
329
+ )
330
+
331
+ # Define your callbacks
332
+ async def capture_screenshot() -> str:
333
+ # Return base64-encoded screenshot from your environment
334
+ return "..."
335
+
336
+ async def execute_actions(actions):
337
+ for action in actions:
338
+ print(f"Executing: {action['type']}")
339
+ # Execute action in your environment
340
+
341
+ # Create and run the loop
342
+ loop = AgentLoop(
343
+ client=client,
344
+ agent_url=session.agent_url,
345
+ capture_screenshot=capture_screenshot,
346
+ execute_actions=execute_actions,
347
+ on_thinking=lambda t: print(f"💭 {t}"),
348
+ )
349
+
350
+ result = await loop.start()
351
+ print(f"Finished: {result.finished}")
352
+
353
+ asyncio.run(main())
354
+ ```
355
+
356
+ **Loop Control:**
357
+
358
+ ```python
359
+ # Start in background
360
+ task = asyncio.create_task(loop.start())
361
+
362
+ # Pause/resume/stop
363
+ loop.pause() # Pause after current step
364
+ loop.resume() # Continue execution
365
+ loop.stop() # Stop the loop
366
+
367
+ # Check state
368
+ loop.state # LoopState.RUNNING, PAUSED, STOPPED, FINISHED
369
+ loop.current_step # Current step number
370
+ loop.last_result # Last StepDesktopResponse
371
+ ```
372
+
373
+ **Manual Step Control:**
374
+
375
+ ```python
376
+ # Or manage the loop yourself
377
+ while not finished:
378
+ screenshot = capture_screenshot()
379
+ result = client.sessions.step(session.agent_url, screenshot)
380
+ execute_actions(result.actions)
381
+ finished = result.finished
382
+ ```
383
+
384
+ </details>
385
+
308
386
  ---
309
387
 
310
388
  ## Error Handling
@@ -13,7 +13,7 @@
13
13
 
14
14
  ---
15
15
 
16
- **AI agent that actually works on the web.**
16
+ **Universal Computer-Use AI**
17
17
 
18
18
  <br />
19
19
 
@@ -267,6 +267,84 @@ session = client.sessions.create(
267
267
 
268
268
  </details>
269
269
 
270
+ <details>
271
+ <summary><b>Client-Driven Sessions</b> – Run agents on desktop, mobile, or custom environments</summary>
272
+
273
+ <br />
274
+
275
+ > **Note:** Desktop mode is currently feature-gated. For enterprise access, contact [`partner@theagi.company`](mailto:partner@theagi.company).
276
+
277
+ For scenarios where you control the execution environment (desktop automation, mobile apps, custom browsers), use client-driven sessions with `AgentLoop`:
278
+
279
+ ```python
280
+ import asyncio
281
+ from agi import AGIClient, AgentLoop
282
+
283
+ async def main():
284
+ client = AGIClient()
285
+
286
+ # Create a client-driven session
287
+ session = client.sessions.create(
288
+ agent_name="agi-2-claude",
289
+ agent_session_type="desktop",
290
+ goal="Open calculator and compute 2+2"
291
+ )
292
+
293
+ # Define your callbacks
294
+ async def capture_screenshot() -> str:
295
+ # Return base64-encoded screenshot from your environment
296
+ return "..."
297
+
298
+ async def execute_actions(actions):
299
+ for action in actions:
300
+ print(f"Executing: {action['type']}")
301
+ # Execute action in your environment
302
+
303
+ # Create and run the loop
304
+ loop = AgentLoop(
305
+ client=client,
306
+ agent_url=session.agent_url,
307
+ capture_screenshot=capture_screenshot,
308
+ execute_actions=execute_actions,
309
+ on_thinking=lambda t: print(f"💭 {t}"),
310
+ )
311
+
312
+ result = await loop.start()
313
+ print(f"Finished: {result.finished}")
314
+
315
+ asyncio.run(main())
316
+ ```
317
+
318
+ **Loop Control:**
319
+
320
+ ```python
321
+ # Start in background
322
+ task = asyncio.create_task(loop.start())
323
+
324
+ # Pause/resume/stop
325
+ loop.pause() # Pause after current step
326
+ loop.resume() # Continue execution
327
+ loop.stop() # Stop the loop
328
+
329
+ # Check state
330
+ loop.state # LoopState.RUNNING, PAUSED, STOPPED, FINISHED
331
+ loop.current_step # Current step number
332
+ loop.last_result # Last StepDesktopResponse
333
+ ```
334
+
335
+ **Manual Step Control:**
336
+
337
+ ```python
338
+ # Or manage the loop yourself
339
+ while not finished:
340
+ screenshot = capture_screenshot()
341
+ result = client.sessions.step(session.agent_url, screenshot)
342
+ execute_actions(result.actions)
343
+ finished = result.finished
344
+ ```
345
+
346
+ </details>
347
+
270
348
  ---
271
349
 
272
350
  ## Error Handling
@@ -4,7 +4,7 @@ The agi package provides a complete Python SDK for the AGI.tech API,
4
4
  enabling developers to create and manage AI agent sessions that can perform
5
5
  complex web tasks.
6
6
 
7
- Example:
7
+ Example (server-driven session):
8
8
  >>> from agi import AGIClient
9
9
  >>>
10
10
  >>> client = AGIClient(api_key="your_api_key")
@@ -14,9 +14,25 @@ Example:
14
14
  ... "Find three nonstop SFO→JFK flights next month under $450"
15
15
  ... )
16
16
  ... print(result)
17
+
18
+ Example (local desktop session using driver):
19
+ >>> from agi import AgentDriver, DriverOptions
20
+ >>>
21
+ >>> driver = AgentDriver(DriverOptions(mode="local"))
22
+ >>> result = await driver.start(goal="Open calculator and compute 2+2")
23
+ >>> print(result.summary)
17
24
  """
18
25
 
19
26
  from agi.client import AGIClient
27
+ from agi.driver import (
28
+ AgentDriver,
29
+ DriverAction,
30
+ DriverOptions,
31
+ DriverResult,
32
+ DriverState,
33
+ find_binary_path,
34
+ is_binary_available,
35
+ )
20
36
  from agi.exceptions import (
21
37
  AgentExecutionError,
22
38
  AGIError,
@@ -26,6 +42,14 @@ from agi.exceptions import (
26
42
  PermissionError,
27
43
  RateLimitError,
28
44
  )
45
+ from agi.loop import AgentLoop, LoopState
46
+ from agi.types.desktop import (
47
+ DesktopAction,
48
+ DesktopActionType,
49
+ ModelsResponse,
50
+ StepDesktopRequest,
51
+ StepDesktopResponse,
52
+ )
29
53
  from agi.types.results import Screenshot, TaskMetadata, TaskResult
30
54
  from agi.types.sessions import (
31
55
  DeleteResponse,
@@ -38,12 +62,17 @@ from agi.types.sessions import (
38
62
  SSEEvent,
39
63
  SuccessResponse,
40
64
  )
41
- from agi.types.shared import EventType, MessageType, SessionStatus, SnapshotMode
65
+ from agi.types.shared import AgentSessionType, EventType, MessageType, SessionStatus, SnapshotMode
42
66
 
43
67
  __version__ = "0.0.1"
44
68
 
45
69
  __all__ = [
70
+ # Client
46
71
  "AGIClient",
72
+ # Loop
73
+ "AgentLoop",
74
+ "LoopState",
75
+ # Exceptions
47
76
  "AGIError",
48
77
  "APIError",
49
78
  "AgentExecutionError",
@@ -51,6 +80,7 @@ __all__ = [
51
80
  "NotFoundError",
52
81
  "PermissionError",
53
82
  "RateLimitError",
83
+ # Session types
54
84
  "SessionResponse",
55
85
  "SSEEvent",
56
86
  "MessageResponse",
@@ -63,8 +93,24 @@ __all__ = [
63
93
  "SuccessResponse",
64
94
  "TaskResult",
65
95
  "TaskMetadata",
96
+ # Desktop types
97
+ "DesktopAction",
98
+ "DesktopActionType",
99
+ "ModelsResponse",
100
+ "StepDesktopRequest",
101
+ "StepDesktopResponse",
102
+ # Shared types
103
+ "AgentSessionType",
66
104
  "EventType",
67
105
  "MessageType",
68
106
  "SessionStatus",
69
107
  "SnapshotMode",
108
+ # Driver
109
+ "AgentDriver",
110
+ "DriverOptions",
111
+ "DriverResult",
112
+ "DriverAction",
113
+ "DriverState",
114
+ "find_binary_path",
115
+ "is_binary_available",
70
116
  ]
@@ -94,6 +94,62 @@ class HTTPClient:
94
94
 
95
95
  raise APIError("Request failed")
96
96
 
97
+ def request_url(
98
+ self,
99
+ method: str,
100
+ url: str,
101
+ **kwargs: Any,
102
+ ) -> httpx.Response:
103
+ """Make HTTP request to an absolute URL with retry logic.
104
+
105
+ Args:
106
+ method: HTTP method (GET, POST, etc.)
107
+ url: Absolute URL to request
108
+ **kwargs: Additional arguments for httpx.request()
109
+
110
+ Returns:
111
+ HTTP response
112
+
113
+ Raises:
114
+ AGIError: On API errors
115
+ """
116
+ last_exception: httpx.HTTPStatusError | None = None
117
+
118
+ for attempt in range(self._max_retries):
119
+ try:
120
+ # Use httpx directly for absolute URLs
121
+ response = httpx.request(
122
+ method,
123
+ url,
124
+ headers=self._client.headers,
125
+ timeout=self._client.timeout,
126
+ **kwargs,
127
+ )
128
+ response.raise_for_status()
129
+ return response
130
+
131
+ except httpx.HTTPStatusError as e:
132
+ if e.response.status_code >= 500 and attempt < self._max_retries - 1:
133
+ wait_time = 2**attempt
134
+ time.sleep(wait_time)
135
+ last_exception = e
136
+ continue
137
+
138
+ self._handle_error(e.response)
139
+ raise
140
+
141
+ except (httpx.RequestError, httpx.TimeoutException) as e:
142
+ if attempt < self._max_retries - 1:
143
+ wait_time = 2**attempt
144
+ time.sleep(wait_time)
145
+ continue
146
+ raise APIError(f"Request failed: {str(e)}") from e
147
+
148
+ if last_exception:
149
+ raise APIError(f"Max retries exceeded: {str(last_exception)}") from last_exception
150
+
151
+ raise APIError("Request failed")
152
+
97
153
  def _handle_error(self, response: httpx.Response) -> None:
98
154
  """Map HTTP errors to SDK exceptions.
99
155
 
@@ -0,0 +1,85 @@
1
+ """Driver module for spawning and managing the agi-driver binary.
2
+
3
+ The driver communicates via JSON lines over stdin/stdout and provides
4
+ an event-based interface for agent control.
5
+ """
6
+
7
+ from agi.driver._binary import (
8
+ PlatformId,
9
+ find_binary_path,
10
+ get_platform_id,
11
+ is_binary_available,
12
+ )
13
+ from agi.driver._driver import AgentDriver, DriverOptions, DriverResult
14
+ from agi.driver._protocol import (
15
+ ActionEvent,
16
+ AnswerCommand,
17
+ AskQuestionEvent,
18
+ AudioTranscriptEvent,
19
+ CommandType,
20
+ ConfirmEvent,
21
+ ConfirmResponseCommand,
22
+ DriverAction,
23
+ DriverCommand,
24
+ DriverEvent,
25
+ DriverState,
26
+ ErrorEvent,
27
+ EventType,
28
+ FinishedEvent,
29
+ GetAudioTranscriptCommand,
30
+ GetVideoFrameCommand,
31
+ PauseCommand,
32
+ ReadyEvent,
33
+ ResumeCommand,
34
+ ScreenshotCapturedEvent,
35
+ ScreenshotCommand,
36
+ SessionCreatedEvent,
37
+ SpeechFinishedEvent,
38
+ SpeechStartedEvent,
39
+ StartCommand,
40
+ StateChangeEvent,
41
+ StopCommand,
42
+ ThinkingEvent,
43
+ TurnDetectedEvent,
44
+ VideoFrameEvent,
45
+ )
46
+
47
+ __all__ = [
48
+ "AgentDriver",
49
+ "DriverOptions",
50
+ "DriverResult",
51
+ "DriverEvent",
52
+ "DriverAction",
53
+ "DriverState",
54
+ "DriverCommand",
55
+ "EventType",
56
+ "CommandType",
57
+ "ReadyEvent",
58
+ "StateChangeEvent",
59
+ "ThinkingEvent",
60
+ "ActionEvent",
61
+ "ConfirmEvent",
62
+ "AskQuestionEvent",
63
+ "FinishedEvent",
64
+ "ErrorEvent",
65
+ "ScreenshotCapturedEvent",
66
+ "SessionCreatedEvent",
67
+ "AudioTranscriptEvent",
68
+ "VideoFrameEvent",
69
+ "SpeechStartedEvent",
70
+ "SpeechFinishedEvent",
71
+ "TurnDetectedEvent",
72
+ "GetAudioTranscriptCommand",
73
+ "GetVideoFrameCommand",
74
+ "StartCommand",
75
+ "ScreenshotCommand",
76
+ "PauseCommand",
77
+ "ResumeCommand",
78
+ "StopCommand",
79
+ "ConfirmResponseCommand",
80
+ "AnswerCommand",
81
+ "find_binary_path",
82
+ "is_binary_available",
83
+ "get_platform_id",
84
+ "PlatformId",
85
+ ]
@@ -0,0 +1,102 @@
1
+ """Binary locator for the agi-driver.
2
+
3
+ Finds the platform-specific binary bundled with the package
4
+ or a global installation.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ import platform
11
+ import sys
12
+ from pathlib import Path
13
+ from typing import Literal
14
+
15
+ PlatformId = Literal["darwin-arm64", "darwin-x64", "linux-x64", "windows-x64"]
16
+
17
+
18
+ def get_platform_id() -> PlatformId:
19
+ """Get the current platform identifier."""
20
+ os_name = platform.system().lower()
21
+ arch = platform.machine().lower()
22
+
23
+ if os_name == "darwin":
24
+ if arch == "arm64":
25
+ return "darwin-arm64"
26
+ elif arch in ("x86_64", "amd64"):
27
+ return "darwin-x64"
28
+ else:
29
+ raise RuntimeError(f"Unsupported architecture for macOS: {arch}")
30
+ elif os_name == "linux":
31
+ if arch not in ("x86_64", "amd64"):
32
+ raise RuntimeError(f"Unsupported architecture for Linux: {arch}")
33
+ return "linux-x64"
34
+ elif os_name == "windows":
35
+ if arch not in ("x86_64", "amd64"):
36
+ raise RuntimeError(f"Unsupported architecture for Windows: {arch}")
37
+ return "windows-x64"
38
+ else:
39
+ raise RuntimeError(f"Unsupported platform: {os_name}-{arch}")
40
+
41
+
42
+ def get_binary_filename(platform_id: PlatformId | None = None) -> str:
43
+ """Get the binary filename for the given platform."""
44
+ pid = platform_id or get_platform_id()
45
+ if pid == "windows-x64":
46
+ return "agi-driver.exe"
47
+ return "agi-driver"
48
+
49
+
50
+ def _get_search_paths(platform_id: PlatformId) -> list[Path]:
51
+ """Get the list of paths to search for the binary."""
52
+ filename = get_binary_filename(platform_id)
53
+ paths: list[Path] = []
54
+
55
+ # 1. Bundled in package's bin directory
56
+ package_dir = Path(__file__).parent.parent
57
+ paths.append(package_dir / "bin" / filename)
58
+
59
+ # 2. In the package root's bin directory
60
+ paths.append(package_dir.parent / "bin" / filename)
61
+
62
+ # 3. In site-packages bin directory
63
+ for site_pkg in sys.path:
64
+ if "site-packages" in site_pkg:
65
+ paths.append(Path(site_pkg).parent / "bin" / filename)
66
+
67
+ # 4. Global installation (in PATH)
68
+ env_path = os.environ.get("PATH", "")
69
+ for dir_path in env_path.split(os.pathsep):
70
+ if dir_path:
71
+ paths.append(Path(dir_path) / filename)
72
+
73
+ return paths
74
+
75
+
76
+ def find_binary_path() -> str:
77
+ """Find the agi-driver binary path.
78
+
79
+ Raises:
80
+ RuntimeError: If the binary cannot be found
81
+ """
82
+ platform_id = get_platform_id()
83
+ search_paths = _get_search_paths(platform_id)
84
+
85
+ for path in search_paths:
86
+ if path.exists() and path.is_file():
87
+ return str(path)
88
+
89
+ raise RuntimeError(
90
+ f"Could not find agi-driver binary for {platform_id}. "
91
+ f"Searched: {', '.join(str(p) for p in search_paths[:5])}... "
92
+ f"Ensure agi-driver is installed or in PATH."
93
+ )
94
+
95
+
96
+ def is_binary_available() -> bool:
97
+ """Check if the binary is available."""
98
+ try:
99
+ find_binary_path()
100
+ return True
101
+ except RuntimeError:
102
+ return False