augment-sdk 0.1.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.
- augment/__init__.py +30 -0
- augment/acp/__init__.py +11 -0
- augment/acp/claude_code_client.py +365 -0
- augment/acp/client.py +640 -0
- augment/acp/test_client_e2e.py +472 -0
- augment/agent.py +1139 -0
- augment/exceptions.py +92 -0
- augment/function_tools.py +265 -0
- augment/listener.py +186 -0
- augment/listener_adapter.py +83 -0
- augment/prompt_formatter.py +343 -0
- augment_sdk-0.1.1.dist-info/METADATA +841 -0
- augment_sdk-0.1.1.dist-info/RECORD +17 -0
- augment_sdk-0.1.1.dist-info/WHEEL +5 -0
- augment_sdk-0.1.1.dist-info/entry_points.txt +2 -0
- augment_sdk-0.1.1.dist-info/licenses/LICENSE +22 -0
- augment_sdk-0.1.1.dist-info/top_level.txt +1 -0
augment/__init__.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Augment SDK - Python client for Augment CLI agent"""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.1"
|
|
4
|
+
|
|
5
|
+
from .agent import Agent, Model, VerificationResult
|
|
6
|
+
from .exceptions import (
|
|
7
|
+
AugmentError,
|
|
8
|
+
AugmentCLIError,
|
|
9
|
+
AugmentJSONError,
|
|
10
|
+
AugmentNotFoundError,
|
|
11
|
+
AugmentParseError,
|
|
12
|
+
AugmentWorkspaceError,
|
|
13
|
+
AugmentVerificationError,
|
|
14
|
+
)
|
|
15
|
+
from .listener import AgentListener, LoggingAgentListener
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"Agent",
|
|
19
|
+
"Model",
|
|
20
|
+
"VerificationResult",
|
|
21
|
+
"AgentListener",
|
|
22
|
+
"LoggingAgentListener",
|
|
23
|
+
"AugmentError",
|
|
24
|
+
"AugmentCLIError",
|
|
25
|
+
"AugmentJSONError",
|
|
26
|
+
"AugmentNotFoundError",
|
|
27
|
+
"AugmentParseError",
|
|
28
|
+
"AugmentWorkspaceError",
|
|
29
|
+
"AugmentVerificationError",
|
|
30
|
+
]
|
augment/acp/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ACP (Agent Client Protocol) client for Augment CLI.
|
|
3
|
+
|
|
4
|
+
This module provides a synchronous Python client for communicating with
|
|
5
|
+
the Augment CLI agent via the Agent Client Protocol.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from augment.acp.client import ACPClient, AuggieACPClient, AgentEventListener
|
|
9
|
+
from augment.acp.claude_code_client import ClaudeCodeACPClient
|
|
10
|
+
|
|
11
|
+
__all__ = ["ACPClient", "AuggieACPClient", "ClaudeCodeACPClient", "AgentEventListener"]
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ACP Client for Claude Code via Zed's ACP Adapter
|
|
3
|
+
|
|
4
|
+
This module provides a client for communicating with Claude Code through
|
|
5
|
+
the @zed-industries/claude-code-acp adapter, which wraps the Claude Code SDK
|
|
6
|
+
to speak the Agent Client Protocol.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import os
|
|
11
|
+
import shutil
|
|
12
|
+
import subprocess
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from queue import Empty, Queue
|
|
15
|
+
from threading import Thread
|
|
16
|
+
from typing import Optional
|
|
17
|
+
|
|
18
|
+
from acp import (
|
|
19
|
+
ClientSideConnection,
|
|
20
|
+
InitializeRequest,
|
|
21
|
+
NewSessionRequest,
|
|
22
|
+
PromptRequest,
|
|
23
|
+
PROTOCOL_VERSION,
|
|
24
|
+
spawn_agent_process,
|
|
25
|
+
text_block,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
from augment.acp.client import ACPClient, AgentEventListener, _InternalACPClient
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ClaudeCodeACPClient(ACPClient):
|
|
32
|
+
"""
|
|
33
|
+
Synchronous ACP client for Claude Code via Zed's adapter.
|
|
34
|
+
|
|
35
|
+
This client provides a simple interface for:
|
|
36
|
+
- Starting/stopping Claude Code agent
|
|
37
|
+
- Sending messages and getting responses
|
|
38
|
+
- Listening to agent events (messages, tool calls, etc.)
|
|
39
|
+
- Clearing session context
|
|
40
|
+
|
|
41
|
+
Example:
|
|
42
|
+
```python
|
|
43
|
+
# Create a client with API key
|
|
44
|
+
client = ClaudeCodeACPClient(
|
|
45
|
+
api_key="...",
|
|
46
|
+
model="claude-3-5-sonnet-latest"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Start the agent
|
|
50
|
+
client.start()
|
|
51
|
+
|
|
52
|
+
# Send a message
|
|
53
|
+
response = client.send_message("What is 2 + 2?")
|
|
54
|
+
print(response)
|
|
55
|
+
|
|
56
|
+
# Stop the agent
|
|
57
|
+
client.stop()
|
|
58
|
+
```
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
def __init__(
|
|
62
|
+
self,
|
|
63
|
+
api_key: Optional[str] = None,
|
|
64
|
+
listener: Optional[AgentEventListener] = None,
|
|
65
|
+
model: Optional[str] = None,
|
|
66
|
+
workspace_root: Optional[str] = None,
|
|
67
|
+
adapter_path: Optional[str] = None,
|
|
68
|
+
):
|
|
69
|
+
"""
|
|
70
|
+
Initialize the Claude Code ACP client.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
api_key: Anthropic API key. If None, uses ANTHROPIC_API_KEY env var.
|
|
74
|
+
listener: Optional event listener to receive agent events.
|
|
75
|
+
model: AI model to use (e.g., "claude-3-5-sonnet-latest").
|
|
76
|
+
If None, uses Claude Code's default model.
|
|
77
|
+
workspace_root: Workspace root directory. If None, uses current directory.
|
|
78
|
+
adapter_path: Path to claude-code-acp executable. If None, uses 'npx @zed-industries/claude-code-acp'.
|
|
79
|
+
|
|
80
|
+
Raises:
|
|
81
|
+
ValueError: If API key is not provided and not in environment
|
|
82
|
+
"""
|
|
83
|
+
self.api_key = api_key or os.getenv("ANTHROPIC_API_KEY")
|
|
84
|
+
if not self.api_key:
|
|
85
|
+
raise ValueError(
|
|
86
|
+
"ANTHROPIC_API_KEY must be provided either as argument or environment variable"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
self.listener = listener
|
|
90
|
+
self.model = model
|
|
91
|
+
self.workspace_root = workspace_root
|
|
92
|
+
self.adapter_path = adapter_path
|
|
93
|
+
|
|
94
|
+
self._client: Optional[_InternalACPClient] = None
|
|
95
|
+
self._conn: Optional[ClientSideConnection] = None
|
|
96
|
+
self._session_id: Optional[str] = None
|
|
97
|
+
self._loop: Optional[asyncio.AbstractEventLoop] = None
|
|
98
|
+
self._thread: Optional[Thread] = None
|
|
99
|
+
self._context = None
|
|
100
|
+
self._ready_queue: Optional[Queue] = None
|
|
101
|
+
|
|
102
|
+
def start(self, timeout: float = 30.0) -> None:
|
|
103
|
+
"""
|
|
104
|
+
Start the Claude Code agent process and establish ACP connection.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
timeout: Maximum time to wait for the agent to start (seconds)
|
|
108
|
+
|
|
109
|
+
Raises:
|
|
110
|
+
RuntimeError: If the agent is already started
|
|
111
|
+
TimeoutError: If the agent fails to start within the timeout
|
|
112
|
+
Exception: If initialization fails
|
|
113
|
+
"""
|
|
114
|
+
if self._thread is not None:
|
|
115
|
+
raise RuntimeError("Agent already started")
|
|
116
|
+
|
|
117
|
+
self._ready_queue = Queue()
|
|
118
|
+
self._thread = Thread(target=self._run_async_loop, daemon=True)
|
|
119
|
+
self._thread.start()
|
|
120
|
+
|
|
121
|
+
# Wait for initialization with timeout
|
|
122
|
+
try:
|
|
123
|
+
result = self._ready_queue.get(timeout=timeout)
|
|
124
|
+
if isinstance(result, Exception):
|
|
125
|
+
raise result
|
|
126
|
+
except Empty:
|
|
127
|
+
# Queue.get() timed out - no result received within timeout
|
|
128
|
+
raise TimeoutError(
|
|
129
|
+
f"Claude Code agent failed to start within {timeout} seconds. "
|
|
130
|
+
f"Make sure @zed-industries/claude-code-acp is installed: "
|
|
131
|
+
f"npm install -g @zed-industries/claude-code-acp"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
def stop(self) -> None:
|
|
135
|
+
"""Stop the Claude Code agent process and cleanup resources."""
|
|
136
|
+
if self._loop is not None:
|
|
137
|
+
asyncio.run_coroutine_threadsafe(self._async_stop(), self._loop)
|
|
138
|
+
self._loop.call_soon_threadsafe(self._loop.stop)
|
|
139
|
+
if self._thread is not None:
|
|
140
|
+
self._thread.join(timeout=2.0)
|
|
141
|
+
|
|
142
|
+
self._client = None
|
|
143
|
+
self._conn = None
|
|
144
|
+
self._session_id = None
|
|
145
|
+
self._loop = None
|
|
146
|
+
self._thread = None
|
|
147
|
+
self._context = None
|
|
148
|
+
|
|
149
|
+
def send_message(self, message: str, timeout: float = 30.0) -> str:
|
|
150
|
+
"""
|
|
151
|
+
Send a message to Claude Code and get the response.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
message: The message to send
|
|
155
|
+
timeout: Maximum time to wait for response (seconds)
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
The agent's response as a string
|
|
159
|
+
|
|
160
|
+
Raises:
|
|
161
|
+
RuntimeError: If the agent is not started
|
|
162
|
+
TimeoutError: If the response takes too long
|
|
163
|
+
"""
|
|
164
|
+
if self._loop is None or self._conn is None or self._client is None:
|
|
165
|
+
raise RuntimeError("Agent not started. Call start() first.")
|
|
166
|
+
|
|
167
|
+
# Reset the response and completion flag
|
|
168
|
+
self._client.last_response = ""
|
|
169
|
+
self._client.message_complete = False
|
|
170
|
+
|
|
171
|
+
# Schedule the async query
|
|
172
|
+
future = asyncio.run_coroutine_threadsafe(
|
|
173
|
+
self._async_send_message(message), self._loop
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# Wait for completion
|
|
177
|
+
future.result(timeout=timeout)
|
|
178
|
+
|
|
179
|
+
# Wait for message_complete flag (with timeout)
|
|
180
|
+
import time
|
|
181
|
+
|
|
182
|
+
start_time = time.time()
|
|
183
|
+
while not self._client.message_complete:
|
|
184
|
+
if time.time() - start_time > 2.0: # Max 2 seconds to wait for completion
|
|
185
|
+
# If we don't get message_end event, just return what we have
|
|
186
|
+
break
|
|
187
|
+
time.sleep(0.05) # Small sleep to avoid busy waiting
|
|
188
|
+
|
|
189
|
+
return self._client.get_last_response()
|
|
190
|
+
|
|
191
|
+
def clear_context(self) -> None:
|
|
192
|
+
"""
|
|
193
|
+
Clear the session context by restarting the agent.
|
|
194
|
+
|
|
195
|
+
This stops the current agent and starts a new one with a fresh session.
|
|
196
|
+
"""
|
|
197
|
+
self.stop()
|
|
198
|
+
self.start()
|
|
199
|
+
|
|
200
|
+
@property
|
|
201
|
+
def session_id(self) -> Optional[str]:
|
|
202
|
+
"""Get the current session ID."""
|
|
203
|
+
return self._session_id
|
|
204
|
+
|
|
205
|
+
@property
|
|
206
|
+
def is_running(self) -> bool:
|
|
207
|
+
"""Check if the agent is currently running."""
|
|
208
|
+
return self._thread is not None and self._loop is not None
|
|
209
|
+
|
|
210
|
+
def _run_async_loop(self):
|
|
211
|
+
"""Run the asyncio event loop in a background thread."""
|
|
212
|
+
try:
|
|
213
|
+
self._loop = asyncio.new_event_loop()
|
|
214
|
+
asyncio.set_event_loop(self._loop)
|
|
215
|
+
self._loop.run_until_complete(self._async_start())
|
|
216
|
+
self._ready_queue.put(True)
|
|
217
|
+
self._loop.run_forever()
|
|
218
|
+
except Exception as e:
|
|
219
|
+
self._ready_queue.put(e)
|
|
220
|
+
|
|
221
|
+
async def _async_start(self):
|
|
222
|
+
"""Async initialization."""
|
|
223
|
+
self._client = _InternalACPClient(self.listener)
|
|
224
|
+
|
|
225
|
+
# Build command to spawn claude-code-acp
|
|
226
|
+
if self.adapter_path:
|
|
227
|
+
# Use explicit path if provided
|
|
228
|
+
cli_args = [self.adapter_path]
|
|
229
|
+
else:
|
|
230
|
+
# Use npx to run the package
|
|
231
|
+
cli_args = ["npx", "@zed-industries/claude-code-acp"]
|
|
232
|
+
|
|
233
|
+
# Check if npx is available
|
|
234
|
+
if not self.adapter_path and not shutil.which("npx"):
|
|
235
|
+
raise RuntimeError(
|
|
236
|
+
"npx not found. Please install Node.js or provide adapter_path explicitly."
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
# Set up environment variables
|
|
240
|
+
env = os.environ.copy()
|
|
241
|
+
env["ANTHROPIC_API_KEY"] = self.api_key
|
|
242
|
+
|
|
243
|
+
# Note: The adapter may use different env vars for model configuration
|
|
244
|
+
# We'll need to check the adapter's documentation for the exact variable name
|
|
245
|
+
if self.model:
|
|
246
|
+
# Try common patterns - the adapter will use what it supports
|
|
247
|
+
env["CLAUDE_CODE_MODEL"] = self.model
|
|
248
|
+
env["MODEL"] = self.model
|
|
249
|
+
|
|
250
|
+
# Spawn the agent process with environment variables
|
|
251
|
+
self._context = spawn_agent_process(
|
|
252
|
+
lambda _agent: self._client, *cli_args, env=env
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
# Start the process and get connection
|
|
256
|
+
conn_proc = await self._context.__aenter__()
|
|
257
|
+
self._conn, self._proc = conn_proc
|
|
258
|
+
|
|
259
|
+
# Create a task to monitor if the process exits early
|
|
260
|
+
async def wait_for_process_exit():
|
|
261
|
+
"""Wait for the process to exit and raise an error if it does."""
|
|
262
|
+
await self._proc.wait()
|
|
263
|
+
stderr = ""
|
|
264
|
+
if self._proc.stderr:
|
|
265
|
+
try:
|
|
266
|
+
stderr_bytes = await asyncio.wait_for(
|
|
267
|
+
self._proc.stderr.read(), timeout=1.0
|
|
268
|
+
)
|
|
269
|
+
stderr = stderr_bytes.decode("utf-8", errors="replace")
|
|
270
|
+
except Exception:
|
|
271
|
+
pass
|
|
272
|
+
raise RuntimeError(
|
|
273
|
+
f"Claude Code agent process exited with code {self._proc.returncode}.\n"
|
|
274
|
+
f"Stderr: {stderr}"
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
# Check if process has already exited
|
|
278
|
+
if self._proc.returncode is not None:
|
|
279
|
+
# Process already exited
|
|
280
|
+
stderr = ""
|
|
281
|
+
if self._proc.stderr:
|
|
282
|
+
stderr_bytes = await self._proc.stderr.read()
|
|
283
|
+
stderr = stderr_bytes.decode("utf-8", errors="replace")
|
|
284
|
+
raise RuntimeError(
|
|
285
|
+
f"Claude Code agent process exited immediately with code {self._proc.returncode}.\n"
|
|
286
|
+
f"Stderr: {stderr}"
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
# Create process monitor task
|
|
290
|
+
monitor_task = asyncio.create_task(wait_for_process_exit())
|
|
291
|
+
|
|
292
|
+
try:
|
|
293
|
+
# Race between initialization and process exit
|
|
294
|
+
init_task = asyncio.create_task(
|
|
295
|
+
self._conn.initialize(
|
|
296
|
+
InitializeRequest(
|
|
297
|
+
protocolVersion=PROTOCOL_VERSION, clientCapabilities=None
|
|
298
|
+
)
|
|
299
|
+
)
|
|
300
|
+
)
|
|
301
|
+
done, pending = await asyncio.wait(
|
|
302
|
+
[init_task, monitor_task], return_when=asyncio.FIRST_COMPLETED
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
# If monitor task completed first, it means process exited
|
|
306
|
+
if monitor_task in done:
|
|
307
|
+
# Cancel the init task
|
|
308
|
+
init_task.cancel()
|
|
309
|
+
# Re-raise the exception from monitor_task
|
|
310
|
+
await monitor_task
|
|
311
|
+
|
|
312
|
+
# Otherwise, initialization succeeded
|
|
313
|
+
await init_task
|
|
314
|
+
|
|
315
|
+
# Use workspace_root as cwd if provided, otherwise use current directory
|
|
316
|
+
cwd = self.workspace_root if self.workspace_root else os.getcwd()
|
|
317
|
+
|
|
318
|
+
# Race between session creation and process exit
|
|
319
|
+
session_task = asyncio.create_task(
|
|
320
|
+
self._conn.newSession(NewSessionRequest(mcpServers=[], cwd=cwd))
|
|
321
|
+
)
|
|
322
|
+
done, pending = await asyncio.wait(
|
|
323
|
+
[session_task, monitor_task], return_when=asyncio.FIRST_COMPLETED
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
# If monitor task completed first, it means process exited
|
|
327
|
+
if monitor_task in done:
|
|
328
|
+
# Cancel the session task
|
|
329
|
+
session_task.cancel()
|
|
330
|
+
# Re-raise the exception from monitor_task
|
|
331
|
+
await monitor_task
|
|
332
|
+
|
|
333
|
+
# Otherwise, session creation succeeded
|
|
334
|
+
session = await session_task
|
|
335
|
+
self._session_id = session.sessionId
|
|
336
|
+
|
|
337
|
+
# Keep the monitor task running in the background
|
|
338
|
+
# (don't cancel it, it will keep monitoring the process)
|
|
339
|
+
except Exception:
|
|
340
|
+
# If anything fails, cancel the monitor task
|
|
341
|
+
monitor_task.cancel()
|
|
342
|
+
raise
|
|
343
|
+
|
|
344
|
+
async def _async_send_message(self, message: str):
|
|
345
|
+
"""Async message sending."""
|
|
346
|
+
await self._conn.prompt(
|
|
347
|
+
PromptRequest(
|
|
348
|
+
sessionId=self._session_id,
|
|
349
|
+
prompt=[text_block(message)],
|
|
350
|
+
)
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
async def _async_stop(self):
|
|
354
|
+
"""Async cleanup."""
|
|
355
|
+
if self._context is not None:
|
|
356
|
+
await self._context.__aexit__(None, None, None)
|
|
357
|
+
|
|
358
|
+
def __enter__(self):
|
|
359
|
+
"""Context manager entry."""
|
|
360
|
+
self.start()
|
|
361
|
+
return self
|
|
362
|
+
|
|
363
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
364
|
+
"""Context manager exit."""
|
|
365
|
+
self.stop()
|