agy-connect 0.1.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 (36) hide show
  1. agy_connect-0.1.0/LICENSE +21 -0
  2. agy_connect-0.1.0/PKG-INFO +188 -0
  3. agy_connect-0.1.0/README.md +169 -0
  4. agy_connect-0.1.0/agy_connect/__init__.py +17 -0
  5. agy_connect-0.1.0/agy_connect/adapter.py +106 -0
  6. agy_connect-0.1.0/agy_connect/checks.py +52 -0
  7. agy_connect-0.1.0/agy_connect/client.py +122 -0
  8. agy_connect-0.1.0/agy_connect/completion.py +55 -0
  9. agy_connect-0.1.0/agy_connect/config.py +53 -0
  10. agy_connect-0.1.0/agy_connect/constants.py +37 -0
  11. agy_connect-0.1.0/agy_connect/events.py +46 -0
  12. agy_connect-0.1.0/agy_connect/exceptions.py +45 -0
  13. agy_connect-0.1.0/agy_connect/health.py +64 -0
  14. agy_connect-0.1.0/agy_connect/history.py +47 -0
  15. agy_connect-0.1.0/agy_connect/logger.py +38 -0
  16. agy_connect-0.1.0/agy_connect/manager.py +95 -0
  17. agy_connect-0.1.0/agy_connect/process.py +103 -0
  18. agy_connect-0.1.0/agy_connect/queue.py +61 -0
  19. agy_connect-0.1.0/agy_connect/session.py +60 -0
  20. agy_connect-0.1.0/agy_connect/state.py +64 -0
  21. agy_connect-0.1.0/agy_connect/stream.py +117 -0
  22. agy_connect-0.1.0/agy_connect/types.py +12 -0
  23. agy_connect-0.1.0/agy_connect/utils.py +29 -0
  24. agy_connect-0.1.0/agy_connect.egg-info/PKG-INFO +188 -0
  25. agy_connect-0.1.0/agy_connect.egg-info/SOURCES.txt +34 -0
  26. agy_connect-0.1.0/agy_connect.egg-info/dependency_links.txt +1 -0
  27. agy_connect-0.1.0/agy_connect.egg-info/requires.txt +7 -0
  28. agy_connect-0.1.0/agy_connect.egg-info/top_level.txt +1 -0
  29. agy_connect-0.1.0/pyproject.toml +46 -0
  30. agy_connect-0.1.0/setup.cfg +4 -0
  31. agy_connect-0.1.0/tests/test_adapter.py +19 -0
  32. agy_connect-0.1.0/tests/test_chat.py +22 -0
  33. agy_connect-0.1.0/tests/test_config.py +21 -0
  34. agy_connect-0.1.0/tests/test_history.py +30 -0
  35. agy_connect-0.1.0/tests/test_manager.py +27 -0
  36. agy_connect-0.1.0/tests/test_state.py +23 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Antigravity Team
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,188 @@
1
+ Metadata-Version: 2.4
2
+ Name: agy-connect
3
+ Version: 0.1.0
4
+ Summary: A production-grade Python runtime for Antigravity CLI
5
+ Author-email: Antigravity Team <team@example.com>
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: License :: OSI Approved :: MIT License
8
+ Classifier: Operating System :: OS Independent
9
+ Requires-Python: >=3.9
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Provides-Extra: dev
13
+ Requires-Dist: pytest>=7.0; extra == "dev"
14
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
15
+ Requires-Dist: black>=23.0; extra == "dev"
16
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
17
+ Requires-Dist: mypy>=1.0; extra == "dev"
18
+ Dynamic: license-file
19
+
20
+ # agy-connect
21
+
22
+ ![Python Version](https://img.shields.io/badge/python-3.9+-blue.svg)
23
+ ![License](https://img.shields.io/badge/license-MIT-green.svg)
24
+
25
+ **agy-connect** is a production-grade Python runtime and SDK that bridges your Python applications seamlessly with the Antigravity CLI (`agy`).
26
+
27
+ It completely abstracts away all subprocess management, inter-process communication, and session state tracking, providing a clean, robust, and strongly-typed API for interacting with the `agy` AI coding assistant natively in Python.
28
+
29
+ ---
30
+
31
+ ## Key Features
32
+
33
+ * **Zero API Key Requirement:** `agy-connect` securely piggybacks off your local Antigravity CLI's existing credentials and setup.
34
+ * **Native Context Memory:** Takes full advantage of `agy`'s native memory management by isolating chats into physical directory structures. Conversations maintain perfect memory across multiple requests!
35
+ * **Real-time Streaming:** Fetch tokens as they arrive instantly using the async generators (`chat.stream()`).
36
+ * **Session Management:** Built-in `SessionManager` utilizes LRU (Least Recently Used) caching with configurable auto-expiration to manage hundreds of simultaneous chats efficiently.
37
+ * **Robust Process Management:** Handles batch processing intelligently. If `agy` hangs or crashes, the SDK detects it and optionally performs auto-recovery.
38
+ * **Dual API Support:** Complete support for both highly scalable `asyncio` applications (via `SessionManager`) and simple synchronous scripts (via `Chat`).
39
+ * **Health Metrics:** Check CPU/process uptime and state machine transitions in real time (`chat.health()`).
40
+
41
+ ---
42
+
43
+ ## Installation
44
+
45
+ To install `agy-connect`, use `pip`:
46
+
47
+ ```bash
48
+ pip install agy-connect
49
+ ```
50
+
51
+ **Prerequisites:** You must have the [Antigravity CLI](https://antigravity.google/product/antigravity-cli) installed on your system.
52
+ *If `agy` is missing from your system PATH, the library will gracefully raise an `AgyNotInstalled` exception guiding the user on how to install it.*
53
+
54
+ ---
55
+
56
+ ## Usage & Quick Start
57
+
58
+ ### 1. Synchronous Chatbot (Simple Scripts)
59
+ The `Chat` class provides an easy-to-use, blocking interface perfect for automation scripts.
60
+
61
+ ```python
62
+ from agy_connect import Chat
63
+
64
+ def main():
65
+ chat = Chat()
66
+ print("User: Hello!")
67
+
68
+ # Send a prompt and wait for the complete response
69
+ response = chat.send("Hello!")
70
+ print(f"Agy: {response}")
71
+
72
+ # Context memory is naturally maintained!
73
+ response2 = chat.send("What did I just say?")
74
+ print(f"Agy: {response2}")
75
+
76
+ if __name__ == "__main__":
77
+ main()
78
+ ```
79
+
80
+ ### 2. Streaming Responses
81
+ If you want to render text back to a user in real-time (like ChatGPT):
82
+
83
+ ```python
84
+ import sys
85
+ from agy_connect import Chat
86
+
87
+ chat = Chat()
88
+
89
+ print("Agy: ", end="")
90
+ for chunk in chat.stream("Write a short poem about code."):
91
+ sys.stdout.write(chunk)
92
+ sys.stdout.flush()
93
+ print()
94
+ ```
95
+
96
+ ### 3. Asynchronous APIs (FastAPI / Web Servers)
97
+ For high-performance non-blocking code, use the `SessionManager`.
98
+
99
+ ```python
100
+ import asyncio
101
+ from agy_connect import SessionManager, Config
102
+
103
+ async def run_server():
104
+ # Configure session limits and timeouts
105
+ config = Config(max_sessions=10, idle_timeout=300)
106
+ manager = SessionManager(config)
107
+
108
+ # Request a specific session (Memory is tied to "user_123")
109
+ session = await manager.get("user_123")
110
+
111
+ # Stream asynchronously
112
+ async for chunk in session.stream("Explain asyncio."):
113
+ print(chunk, end="", flush=True)
114
+
115
+ # Cleanup memory
116
+ await manager.shutdown()
117
+
118
+ asyncio.run(run_server())
119
+ ```
120
+
121
+ ---
122
+
123
+ ## API Reference
124
+
125
+ The `Chat` and `Session` objects expose the following methods:
126
+
127
+ | Method | Description |
128
+ |--------|-------------|
129
+ | `send(prompt: str)` | Sends a prompt synchronously and returns the complete string response. |
130
+ | `stream(prompt: str)` | Generator that yields response tokens in real-time as they arrive. |
131
+ | `save(filename: str)` | Saves the entire conversation history to a JSON file on disk. |
132
+ | `load(filename: str)` | Reloads a previous conversation history from a JSON file. |
133
+ | `reset()` | Wipes the current session's memory/history completely. |
134
+ | `history()` | Returns the raw list of messages (dictionaries) in the current session. |
135
+ | `health()` | Returns real-time health metrics (PID, uptime, state, last errors). |
136
+ | `status()` | Returns the current string state of the adapter (e.g. `READY`, `BUSY`). |
137
+ | `restart()` | Forces a reboot of the underlying state machine and adapter. |
138
+ | `close()` / `shutdown()` | Gracefully cleans up all resources and background tasks. |
139
+
140
+ ---
141
+
142
+ ## Architecture & Design
143
+
144
+ `agy-connect` uses an event-driven, state-machine architecture under the hood to ensure predictable process execution.
145
+
146
+ ### The Problem it Solves
147
+ The Antigravity CLI intentionally disables its interactive REPL mode when standard streams are piped (waiting for an `EOF` before processing anything). This makes a long-running persistent subprocess impossible.
148
+
149
+ ### The Solution (Batch-Mode Strategy)
150
+ `agy-connect` embraces batch-mode processing using isolated workspaces.
151
+ 1. When a new session is requested, a unique session folder is dynamically created (`/sessions/<session_id>`).
152
+ 2. When a prompt is sent, `agy-connect` explicitly prepends the entire Python-side tracked history to ensure context isn't lost.
153
+ 3. It spawns a fresh, ephemeral `agy` process in that directory, immediately flushes `stdin`, and sends an `EOF`.
154
+ 4. It attaches an async reader to `stdout` to yield the tokens back to your application as `agy` generates them, then terminates the ephemeral process cleanly.
155
+
156
+ ### State Machine
157
+ Every adapter tracks its state strictly using `agy_connect.constants`:
158
+ `STOPPED -> STARTING -> READY -> STREAMING -> READY`
159
+
160
+ ---
161
+
162
+ ## Configuration
163
+
164
+ You can customize the library's behavior entirely by injecting a `Config` object:
165
+
166
+ ```python
167
+ from agy_connect import Chat, Config
168
+
169
+ config = Config(
170
+ executable_path="/custom/path/to/agy", # Override auto-discovery
171
+ idle_timeout=60, # Clean up session memory after 60s
172
+ stream_chunk_size=512, # Byte sizes yielded during streams
173
+ debug_mode=True # Enable verbose logger traces
174
+ )
175
+
176
+ chat = Chat(config)
177
+ ```
178
+
179
+ ---
180
+
181
+ ## Contributing
182
+
183
+ Contributions, issues, and feature requests are welcome!
184
+ Feel free to check the [issues page](https://github.com/Prince-1652/agy-connect/issues).
185
+
186
+ ## License
187
+
188
+ This project is [MIT](https://github.com/Prince-1652/agy-connect/blob/main/LICENSE) licensed.
@@ -0,0 +1,169 @@
1
+ # agy-connect
2
+
3
+ ![Python Version](https://img.shields.io/badge/python-3.9+-blue.svg)
4
+ ![License](https://img.shields.io/badge/license-MIT-green.svg)
5
+
6
+ **agy-connect** is a production-grade Python runtime and SDK that bridges your Python applications seamlessly with the Antigravity CLI (`agy`).
7
+
8
+ It completely abstracts away all subprocess management, inter-process communication, and session state tracking, providing a clean, robust, and strongly-typed API for interacting with the `agy` AI coding assistant natively in Python.
9
+
10
+ ---
11
+
12
+ ## Key Features
13
+
14
+ * **Zero API Key Requirement:** `agy-connect` securely piggybacks off your local Antigravity CLI's existing credentials and setup.
15
+ * **Native Context Memory:** Takes full advantage of `agy`'s native memory management by isolating chats into physical directory structures. Conversations maintain perfect memory across multiple requests!
16
+ * **Real-time Streaming:** Fetch tokens as they arrive instantly using the async generators (`chat.stream()`).
17
+ * **Session Management:** Built-in `SessionManager` utilizes LRU (Least Recently Used) caching with configurable auto-expiration to manage hundreds of simultaneous chats efficiently.
18
+ * **Robust Process Management:** Handles batch processing intelligently. If `agy` hangs or crashes, the SDK detects it and optionally performs auto-recovery.
19
+ * **Dual API Support:** Complete support for both highly scalable `asyncio` applications (via `SessionManager`) and simple synchronous scripts (via `Chat`).
20
+ * **Health Metrics:** Check CPU/process uptime and state machine transitions in real time (`chat.health()`).
21
+
22
+ ---
23
+
24
+ ## Installation
25
+
26
+ To install `agy-connect`, use `pip`:
27
+
28
+ ```bash
29
+ pip install agy-connect
30
+ ```
31
+
32
+ **Prerequisites:** You must have the [Antigravity CLI](https://antigravity.google/product/antigravity-cli) installed on your system.
33
+ *If `agy` is missing from your system PATH, the library will gracefully raise an `AgyNotInstalled` exception guiding the user on how to install it.*
34
+
35
+ ---
36
+
37
+ ## Usage & Quick Start
38
+
39
+ ### 1. Synchronous Chatbot (Simple Scripts)
40
+ The `Chat` class provides an easy-to-use, blocking interface perfect for automation scripts.
41
+
42
+ ```python
43
+ from agy_connect import Chat
44
+
45
+ def main():
46
+ chat = Chat()
47
+ print("User: Hello!")
48
+
49
+ # Send a prompt and wait for the complete response
50
+ response = chat.send("Hello!")
51
+ print(f"Agy: {response}")
52
+
53
+ # Context memory is naturally maintained!
54
+ response2 = chat.send("What did I just say?")
55
+ print(f"Agy: {response2}")
56
+
57
+ if __name__ == "__main__":
58
+ main()
59
+ ```
60
+
61
+ ### 2. Streaming Responses
62
+ If you want to render text back to a user in real-time (like ChatGPT):
63
+
64
+ ```python
65
+ import sys
66
+ from agy_connect import Chat
67
+
68
+ chat = Chat()
69
+
70
+ print("Agy: ", end="")
71
+ for chunk in chat.stream("Write a short poem about code."):
72
+ sys.stdout.write(chunk)
73
+ sys.stdout.flush()
74
+ print()
75
+ ```
76
+
77
+ ### 3. Asynchronous APIs (FastAPI / Web Servers)
78
+ For high-performance non-blocking code, use the `SessionManager`.
79
+
80
+ ```python
81
+ import asyncio
82
+ from agy_connect import SessionManager, Config
83
+
84
+ async def run_server():
85
+ # Configure session limits and timeouts
86
+ config = Config(max_sessions=10, idle_timeout=300)
87
+ manager = SessionManager(config)
88
+
89
+ # Request a specific session (Memory is tied to "user_123")
90
+ session = await manager.get("user_123")
91
+
92
+ # Stream asynchronously
93
+ async for chunk in session.stream("Explain asyncio."):
94
+ print(chunk, end="", flush=True)
95
+
96
+ # Cleanup memory
97
+ await manager.shutdown()
98
+
99
+ asyncio.run(run_server())
100
+ ```
101
+
102
+ ---
103
+
104
+ ## API Reference
105
+
106
+ The `Chat` and `Session` objects expose the following methods:
107
+
108
+ | Method | Description |
109
+ |--------|-------------|
110
+ | `send(prompt: str)` | Sends a prompt synchronously and returns the complete string response. |
111
+ | `stream(prompt: str)` | Generator that yields response tokens in real-time as they arrive. |
112
+ | `save(filename: str)` | Saves the entire conversation history to a JSON file on disk. |
113
+ | `load(filename: str)` | Reloads a previous conversation history from a JSON file. |
114
+ | `reset()` | Wipes the current session's memory/history completely. |
115
+ | `history()` | Returns the raw list of messages (dictionaries) in the current session. |
116
+ | `health()` | Returns real-time health metrics (PID, uptime, state, last errors). |
117
+ | `status()` | Returns the current string state of the adapter (e.g. `READY`, `BUSY`). |
118
+ | `restart()` | Forces a reboot of the underlying state machine and adapter. |
119
+ | `close()` / `shutdown()` | Gracefully cleans up all resources and background tasks. |
120
+
121
+ ---
122
+
123
+ ## Architecture & Design
124
+
125
+ `agy-connect` uses an event-driven, state-machine architecture under the hood to ensure predictable process execution.
126
+
127
+ ### The Problem it Solves
128
+ The Antigravity CLI intentionally disables its interactive REPL mode when standard streams are piped (waiting for an `EOF` before processing anything). This makes a long-running persistent subprocess impossible.
129
+
130
+ ### The Solution (Batch-Mode Strategy)
131
+ `agy-connect` embraces batch-mode processing using isolated workspaces.
132
+ 1. When a new session is requested, a unique session folder is dynamically created (`/sessions/<session_id>`).
133
+ 2. When a prompt is sent, `agy-connect` explicitly prepends the entire Python-side tracked history to ensure context isn't lost.
134
+ 3. It spawns a fresh, ephemeral `agy` process in that directory, immediately flushes `stdin`, and sends an `EOF`.
135
+ 4. It attaches an async reader to `stdout` to yield the tokens back to your application as `agy` generates them, then terminates the ephemeral process cleanly.
136
+
137
+ ### State Machine
138
+ Every adapter tracks its state strictly using `agy_connect.constants`:
139
+ `STOPPED -> STARTING -> READY -> STREAMING -> READY`
140
+
141
+ ---
142
+
143
+ ## Configuration
144
+
145
+ You can customize the library's behavior entirely by injecting a `Config` object:
146
+
147
+ ```python
148
+ from agy_connect import Chat, Config
149
+
150
+ config = Config(
151
+ executable_path="/custom/path/to/agy", # Override auto-discovery
152
+ idle_timeout=60, # Clean up session memory after 60s
153
+ stream_chunk_size=512, # Byte sizes yielded during streams
154
+ debug_mode=True # Enable verbose logger traces
155
+ )
156
+
157
+ chat = Chat(config)
158
+ ```
159
+
160
+ ---
161
+
162
+ ## Contributing
163
+
164
+ Contributions, issues, and feature requests are welcome!
165
+ Feel free to check the [issues page](https://github.com/Prince-1652/agy-connect/issues).
166
+
167
+ ## License
168
+
169
+ This project is [MIT](https://github.com/Prince-1652/agy-connect/blob/main/LICENSE) licensed.
@@ -0,0 +1,17 @@
1
+ """
2
+ agy-connect
3
+
4
+ A production-grade Python runtime for Antigravity CLI.
5
+ """
6
+
7
+ from .client import Chat
8
+ from .manager import SessionManager
9
+ from .config import Config
10
+ from . import exceptions
11
+
12
+ __all__ = [
13
+ "Chat",
14
+ "SessionManager",
15
+ "Config",
16
+ "exceptions"
17
+ ]
@@ -0,0 +1,106 @@
1
+ """AntigravityAdapter orchestrating process and communication."""
2
+
3
+ import asyncio
4
+ from typing import AsyncGenerator, Optional, Callable, Coroutine, Any, List
5
+ from .config import Config
6
+ from .process import ProcessManager
7
+ from .state import StateMachine
8
+ from .events import EventDispatcher, EVENT_PROCESS_CRASHED, EVENT_PROMPT_COMPLETED
9
+ from .stream import SafeStreamer
10
+ from .completion import CompletionDetector
11
+ from .constants import (
12
+ STATE_STOPPED, STATE_STARTING, STATE_READY, STATE_BUSY,
13
+ STATE_STREAMING, STATE_ERROR, STATE_RESTARTING, DEFAULT_PROMPT_MARKER
14
+ )
15
+ from .exceptions import ProcessCrash, StreamError, InvalidState
16
+ from .logger import get_logger
17
+
18
+ logger = get_logger()
19
+
20
+ class AntigravityAdapter:
21
+ """
22
+ The heart of the runtime. Manages state, process, and streaming.
23
+ Hides all subprocess implementation details from the rest of the library.
24
+ """
25
+
26
+ def __init__(self, session_id: str, config: Config) -> None:
27
+ self.session_id = session_id
28
+ self.config = config
29
+ self.state = StateMachine()
30
+ self.events = EventDispatcher()
31
+ self.process = ProcessManager(session_id, config)
32
+ self.detector = CompletionDetector(marker=DEFAULT_PROMPT_MARKER)
33
+ self._background_tasks: List[asyncio.Task[Any]] = []
34
+
35
+ async def start(self) -> None:
36
+ """Initialize the adapter."""
37
+ await self.state.transition(STATE_STARTING)
38
+ await self.state.transition(STATE_READY)
39
+
40
+ async def stop(self) -> None:
41
+ """Stop the adapter."""
42
+ for task in self._background_tasks:
43
+ if not task.done():
44
+ task.cancel()
45
+ await self.process.terminate()
46
+ await self.state.transition(STATE_STOPPED)
47
+
48
+ async def restart(self) -> None:
49
+ """Restart the adapter."""
50
+ await self.state.transition(STATE_RESTARTING)
51
+ await self.stop()
52
+ await self.start()
53
+
54
+ async def _ensure_ready(self) -> None:
55
+ """Ensures the adapter is ready to process a prompt."""
56
+ if self.state.current != STATE_READY:
57
+ if self.state.current in (STATE_BUSY, STATE_STREAMING):
58
+ raise InvalidState("Adapter is currently busy with another prompt.")
59
+ if self.state.current == STATE_ERROR and self.config.auto_recover:
60
+ await self.restart()
61
+ else:
62
+ raise InvalidState(f"Adapter is in state: {self.state.current}")
63
+
64
+ async def stream(self, prompt: str) -> AsyncGenerator[str, None]:
65
+ """
66
+ Sends a prompt and yields the response tokens.
67
+ """
68
+ await self._ensure_ready()
69
+
70
+ await self.state.transition(STATE_BUSY)
71
+
72
+ try:
73
+ # Spawn a new batch process
74
+ process = await self.process.spawn()
75
+ await self.state.transition(STATE_STREAMING)
76
+
77
+ # Write prompt and close stdin so agy executes
78
+ await self.process.write_stdin_and_close(process, prompt)
79
+
80
+ if not process.stdout:
81
+ raise ProcessCrash("No stdout available.")
82
+
83
+ streamer = SafeStreamer(marker="", chunk_size=self.config.stream_chunk_size)
84
+
85
+ async for chunk in streamer.stream(process.stdout):
86
+ yield chunk
87
+
88
+ await process.wait()
89
+
90
+ await self.events.emit(EVENT_PROMPT_COMPLETED)
91
+ await self.state.transition(STATE_READY)
92
+
93
+ except Exception as e:
94
+ logger.error(f"Error during stream: {e}")
95
+ await self.state.transition(STATE_ERROR)
96
+ await self.events.emit(EVENT_PROCESS_CRASHED, error=str(e))
97
+ raise
98
+
99
+ async def send(self, prompt: str) -> str:
100
+ """
101
+ Sends a prompt and returns the full response string.
102
+ """
103
+ response_parts = []
104
+ async for chunk in self.stream(prompt):
105
+ response_parts.append(chunk)
106
+ return "".join(response_parts)
@@ -0,0 +1,52 @@
1
+ """Authentication checks for agy-connect."""
2
+
3
+ import asyncio
4
+ from typing import Optional
5
+ from .exceptions import AuthenticationRequired, AgyNotInstalled
6
+ from .utils import find_executable
7
+ from .logger import get_logger
8
+
9
+ logger = get_logger()
10
+
11
+ async def check_authentication(executable_path: Optional[str] = None) -> bool:
12
+ """
13
+ Checks if the agy CLI is authenticated.
14
+ Runs an arbitrary command (e.g., `agy auth status`) to verify.
15
+ Returns True if authenticated, otherwise raises AuthenticationRequired.
16
+ """
17
+ try:
18
+ exe = find_executable(executable_path)
19
+ except AgyNotInstalled as e:
20
+ raise e
21
+
22
+ logger.debug(f"Checking authentication using executable: {exe}")
23
+
24
+ # Note: In a real integration, the exact command `agy auth status` or similar would be used.
25
+ # For this robust SDK, we will attempt to run it and parse stderr/stdout.
26
+ # If the tool just exits with a known auth error code, we catch it.
27
+
28
+ try:
29
+ # Mocking the command structure since agy internal details are abstracted.
30
+ # This will need to be adapted based on real agy behavior.
31
+ process = await asyncio.create_subprocess_exec(
32
+ exe, "--help", # Using help as a safe fallback command to see if it even runs
33
+ stdout=asyncio.subprocess.PIPE,
34
+ stderr=asyncio.subprocess.PIPE
35
+ )
36
+ stdout, stderr = await process.communicate()
37
+
38
+ output = stdout.decode() + stderr.decode()
39
+
40
+ # If output explicitly mentions "login" or "authentication required"
41
+ if "authenticate" in output.lower() or "login" in output.lower():
42
+ raise AuthenticationRequired("Antigravity CLI is not authenticated. Please login first.")
43
+
44
+ return True
45
+ except FileNotFoundError:
46
+ raise AgyNotInstalled("Executable not found during auth check.")
47
+ except Exception as e:
48
+ if isinstance(e, AuthenticationRequired):
49
+ raise
50
+ logger.warning(f"Could not verify authentication status: {e}")
51
+ # Proceed cautiously
52
+ return True
@@ -0,0 +1,122 @@
1
+ """Synchronous client interface for agy-connect."""
2
+
3
+ import asyncio
4
+ import threading
5
+ import uuid
6
+ from typing import Iterator, Optional, Dict, Any, List
7
+ from .config import Config
8
+ from .session import Session
9
+ from .manager import SessionManager
10
+
11
+ # A global thread to run the asyncio loop for synchronous API
12
+ _loop_thread: Optional[threading.Thread] = None
13
+ _loop: Optional[asyncio.AbstractEventLoop] = None
14
+ _loop_lock = threading.Lock()
15
+
16
+ def _start_background_loop(loop: asyncio.AbstractEventLoop) -> None:
17
+ asyncio.set_event_loop(loop)
18
+ loop.run_forever()
19
+
20
+ def _get_loop() -> asyncio.AbstractEventLoop:
21
+ global _loop, _loop_thread
22
+ with _loop_lock:
23
+ if _loop is None:
24
+ _loop = asyncio.new_event_loop()
25
+ _loop_thread = threading.Thread(target=_start_background_loop, args=(_loop,), daemon=True)
26
+ _loop_thread.start()
27
+ return _loop
28
+
29
+ def _run_coroutine(coro: Any) -> Any:
30
+ """Run a coroutine thread-safely in the background loop."""
31
+ loop = _get_loop()
32
+ future = asyncio.run_coroutine_threadsafe(coro, loop)
33
+ return future.result()
34
+
35
+ class Chat:
36
+ """
37
+ Synchronous Chat interface representing one independent conversation.
38
+ """
39
+
40
+ def __init__(self, config: Optional[Config] = None) -> None:
41
+ self.config = config or Config()
42
+ self.session_id = str(uuid.uuid4())
43
+ self._session = Session(self.session_id, self.config)
44
+ _run_coroutine(self._session.initialize())
45
+
46
+ def send(self, prompt: str) -> str:
47
+ """Send a prompt and wait for the full response."""
48
+ return _run_coroutine(self._session.send(prompt))
49
+
50
+ def stream(self, prompt: str) -> Iterator[str]:
51
+ """Stream the response chunks synchronously."""
52
+ loop = _get_loop()
53
+
54
+ # We use a thread-safe queue to pass chunks from async generator to sync iterator
55
+ q: "asyncio.Queue[Optional[str]]" = asyncio.Queue()
56
+
57
+ async def _consume_stream() -> None:
58
+ try:
59
+ async for chunk in self._session.stream(prompt):
60
+ await q.put(chunk)
61
+ await q.put(None) # EOF
62
+ except Exception as e:
63
+ # We could put the exception in the queue, but for simplicity
64
+ await q.put(None)
65
+
66
+ asyncio.run_coroutine_threadsafe(_consume_stream(), loop)
67
+
68
+ while True:
69
+ # We must wait for items in the queue thread-safely
70
+ future = asyncio.run_coroutine_threadsafe(q.get(), loop)
71
+ chunk = future.result()
72
+ if chunk is None:
73
+ break
74
+ yield chunk
75
+
76
+ def history(self) -> List[Dict[str, str]]:
77
+ """Retrieve the conversation history."""
78
+ return self._session.history.get_all()
79
+
80
+ def save(self, filepath: str) -> None:
81
+ """Save history to a file."""
82
+ self._session.history.save(filepath)
83
+
84
+ def load(self, filepath: str) -> None:
85
+ """Load history from a file."""
86
+ self._session.history.load(filepath)
87
+
88
+ def reset(self) -> None:
89
+ """Reset the conversation history."""
90
+ self._session.history.clear()
91
+
92
+ def restart(self) -> None:
93
+ """Restart the underlying process."""
94
+ _run_coroutine(self._session.adapter.restart())
95
+
96
+ def interrupt(self) -> None:
97
+ """Interrupt the current generation (restarts the process)."""
98
+ self.restart()
99
+
100
+ def status(self) -> str:
101
+ """Get the current state of the adapter."""
102
+ return self._session.adapter.state.current
103
+
104
+ def health(self) -> Dict[str, Any]:
105
+ """Get health metrics."""
106
+ h = self._session.health.get_status(
107
+ process_alive=self._session.adapter.process.is_running,
108
+ pid=self._session.adapter.process.pid,
109
+ queue_length=self._session.queue.qsize() if hasattr(self._session, 'queue') else 0, # Since we removed RequestQueue for Lock, queue length is N/A
110
+ current_state=self._session.adapter.state.current
111
+ )
112
+ return {
113
+ "process_alive": h.process_alive,
114
+ "pid": h.pid,
115
+ "uptime": h.uptime,
116
+ "current_state": h.current_state,
117
+ "last_error": h.last_error
118
+ }
119
+
120
+ def close(self) -> None:
121
+ """Close the session and terminate the process."""
122
+ _run_coroutine(self._session.cleanup())