codebuddy-agent-sdk 0.1.15__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.

Potentially problematic release.


This version of codebuddy-agent-sdk might be problematic. Click here for more details.

@@ -0,0 +1,97 @@
1
+ """CodeBuddy Agent SDK for Python."""
2
+
3
+ from ._errors import (
4
+ CLIConnectionError,
5
+ CLIJSONDecodeError,
6
+ CLINotFoundError,
7
+ CodeBuddySDKError,
8
+ ProcessError,
9
+ )
10
+ from ._version import __version__
11
+ from .client import CodeBuddySDKClient
12
+ from .query import query
13
+ from .transport import Transport
14
+ from .types import (
15
+ AgentDefinition,
16
+ AppendSystemPrompt,
17
+ AskUserQuestionInput,
18
+ AskUserQuestionOption,
19
+ AskUserQuestionQuestion,
20
+ AssistantMessage,
21
+ CanUseTool,
22
+ CanUseToolOptions,
23
+ CodeBuddyAgentOptions,
24
+ ContentBlock,
25
+ HookCallback,
26
+ HookContext,
27
+ HookEvent,
28
+ HookJSONOutput,
29
+ HookMatcher,
30
+ McpServerConfig,
31
+ Message,
32
+ PermissionMode,
33
+ PermissionResult,
34
+ PermissionResultAllow,
35
+ PermissionResultDeny,
36
+ ResultMessage,
37
+ SettingSource,
38
+ StreamEvent,
39
+ SystemMessage,
40
+ TextBlock,
41
+ ThinkingBlock,
42
+ ToolResultBlock,
43
+ ToolUseBlock,
44
+ UserMessage,
45
+ )
46
+
47
+ __all__ = [
48
+ # Main API
49
+ "query",
50
+ "CodeBuddySDKClient",
51
+ "Transport",
52
+ "__version__",
53
+ # Types - Permission
54
+ "PermissionMode",
55
+ # Types - Messages
56
+ "Message",
57
+ "UserMessage",
58
+ "AssistantMessage",
59
+ "SystemMessage",
60
+ "ResultMessage",
61
+ "StreamEvent",
62
+ # Types - Content blocks
63
+ "ContentBlock",
64
+ "TextBlock",
65
+ "ThinkingBlock",
66
+ "ToolUseBlock",
67
+ "ToolResultBlock",
68
+ # Types - Configuration
69
+ "CodeBuddyAgentOptions",
70
+ "AgentDefinition",
71
+ "AppendSystemPrompt",
72
+ "SettingSource",
73
+ # Types - Permission
74
+ "CanUseTool",
75
+ "CanUseToolOptions",
76
+ "PermissionResult",
77
+ "PermissionResultAllow",
78
+ "PermissionResultDeny",
79
+ # Types - AskUserQuestion
80
+ "AskUserQuestionOption",
81
+ "AskUserQuestionQuestion",
82
+ "AskUserQuestionInput",
83
+ # Types - Hooks
84
+ "HookEvent",
85
+ "HookCallback",
86
+ "HookMatcher",
87
+ "HookJSONOutput",
88
+ "HookContext",
89
+ # Types - MCP
90
+ "McpServerConfig",
91
+ # Errors
92
+ "CodeBuddySDKError",
93
+ "CLIConnectionError",
94
+ "CLINotFoundError",
95
+ "CLIJSONDecodeError",
96
+ "ProcessError",
97
+ ]
@@ -0,0 +1,148 @@
1
+ """
2
+ Binary locator for CodeBuddy CLI.
3
+
4
+ This module provides functions to locate the CodeBuddy CLI binary.
5
+ The binary is bundled in platform-specific wheels.
6
+ """
7
+
8
+ import os
9
+ import platform
10
+ import sys
11
+ from pathlib import Path
12
+
13
+ from ._errors import CLINotFoundError
14
+
15
+ __all__ = ["get_cli_path", "try_cli_path", "get_platform_info"]
16
+
17
+
18
+ # Platform mapping for goreleaser output directories (for monorepo development)
19
+ # Note: For musl (Alpine), the correct binary is bundled in the wheel at build time
20
+ _PLATFORM_DIRS: dict[tuple[str, str], str] = {
21
+ ("Darwin", "arm64"): "darwin-arm64_bun-darwin-arm64",
22
+ ("Darwin", "x86_64"): "darwin-x64_bun-darwin-x64",
23
+ ("Linux", "x86_64"): "linux-x64_bun-linux-x64",
24
+ ("Linux", "aarch64"): "linux-arm64_bun-linux-arm64",
25
+ ("Windows", "AMD64"): "windows-x64_bun-windows-x64",
26
+ }
27
+
28
+ # Binary name per platform
29
+ _BINARY_NAMES: dict[str, str] = {
30
+ "Darwin": "codebuddy",
31
+ "Linux": "codebuddy",
32
+ "Windows": "codebuddy.exe",
33
+ }
34
+
35
+
36
+ def _get_package_binary_path() -> Path | None:
37
+ """Get the binary path bundled in the package."""
38
+ # Binary is bundled at package_root/bin/codebuddy
39
+ pkg_root = Path(__file__).parent
40
+ system = platform.system()
41
+ binary_name = _BINARY_NAMES.get(system, "codebuddy")
42
+ bin_path = pkg_root / "bin" / binary_name
43
+
44
+ if bin_path.exists() and bin_path.is_file():
45
+ return bin_path
46
+
47
+ return None
48
+
49
+
50
+ def _get_monorepo_binary_path() -> Path | None:
51
+ """Get the binary path from monorepo development structure."""
52
+ system = platform.system()
53
+ machine = platform.machine()
54
+ key = (system, machine)
55
+
56
+ dir_name = _PLATFORM_DIRS.get(key)
57
+ if not dir_name:
58
+ return None
59
+
60
+ binary_name = _BINARY_NAMES.get(system, "codebuddy")
61
+
62
+ # Try relative to this package (agent-sdk-python -> agent-cli)
63
+ pkg_root = Path(__file__).parent
64
+ monorepo_path = pkg_root.parent.parent.parent / "agent-cli" / "out" / dir_name / binary_name
65
+
66
+ if monorepo_path.exists() and monorepo_path.is_file():
67
+ return monorepo_path
68
+
69
+ return None
70
+
71
+
72
+ def get_cli_path() -> str:
73
+ """
74
+ Get the path to the CodeBuddy CLI binary.
75
+
76
+ Resolution order:
77
+ 1. CODEBUDDY_CODE_PATH environment variable
78
+ 2. Binary bundled in the package (wheel)
79
+ 3. Monorepo development path
80
+
81
+ Returns:
82
+ The absolute path to the CLI binary.
83
+
84
+ Raises:
85
+ CLINotFoundError: If the binary cannot be found.
86
+ """
87
+ system = platform.system()
88
+ machine = platform.machine()
89
+
90
+ # 1. Environment variable takes precedence
91
+ env_path = os.environ.get("CODEBUDDY_CODE_PATH")
92
+ if env_path:
93
+ path = Path(env_path)
94
+ if path.exists():
95
+ return str(path)
96
+ # Warn but continue to try other methods
97
+ print(
98
+ f"Warning: CODEBUDDY_CODE_PATH is set to '{env_path}' but file does not exist. "
99
+ "Falling back to other resolution methods.",
100
+ file=sys.stderr,
101
+ )
102
+
103
+ # 2. Try package bundled binary
104
+ pkg_path = _get_package_binary_path()
105
+ if pkg_path:
106
+ return str(pkg_path)
107
+
108
+ # 3. Try monorepo development path
109
+ monorepo_path = _get_monorepo_binary_path()
110
+ if monorepo_path:
111
+ return str(monorepo_path)
112
+
113
+ # 4. Nothing found - raise helpful error
114
+ supported = ", ".join(f"{s}-{m}" for s, m in _PLATFORM_DIRS)
115
+ raise CLINotFoundError(
116
+ f"CodeBuddy CLI binary not found for platform '{system}-{machine}'.\n\n"
117
+ f"Possible solutions:\n"
118
+ f" 1. Reinstall the package to get the correct platform wheel:\n"
119
+ f" pip install --force-reinstall codebuddy-agent-sdk\n\n"
120
+ f" 2. Set CODEBUDDY_CODE_PATH environment variable to the CLI binary path\n\n"
121
+ f" 3. Install the CLI separately and set the path\n\n"
122
+ f"Supported platforms: {supported}",
123
+ system,
124
+ machine,
125
+ )
126
+
127
+
128
+ def try_cli_path() -> str | None:
129
+ """
130
+ Try to get the CLI path without raising an exception.
131
+
132
+ Returns:
133
+ The CLI path if found, None otherwise.
134
+ """
135
+ try:
136
+ return get_cli_path()
137
+ except CLINotFoundError:
138
+ return None
139
+
140
+
141
+ def get_platform_info() -> tuple[str, str]:
142
+ """
143
+ Get the current platform information.
144
+
145
+ Returns:
146
+ A tuple of (system, machine).
147
+ """
148
+ return platform.system(), platform.machine()
@@ -0,0 +1,39 @@
1
+ """Error definitions for CodeBuddy Agent SDK."""
2
+
3
+
4
+ class CodeBuddySDKError(Exception):
5
+ """Base exception for CodeBuddy SDK errors."""
6
+
7
+ pass
8
+
9
+
10
+ class CLIConnectionError(CodeBuddySDKError):
11
+ """Raised when connection to CLI fails or is not established."""
12
+
13
+ pass
14
+
15
+
16
+ class CLINotFoundError(CodeBuddySDKError):
17
+ """Raised when CLI executable is not found."""
18
+
19
+ def __init__(
20
+ self,
21
+ message: str,
22
+ platform: str | None = None,
23
+ arch: str | None = None,
24
+ ):
25
+ super().__init__(message)
26
+ self.platform = platform
27
+ self.arch = arch
28
+
29
+
30
+ class CLIJSONDecodeError(CodeBuddySDKError):
31
+ """Raised when JSON decoding from CLI output fails."""
32
+
33
+ pass
34
+
35
+
36
+ class ProcessError(CodeBuddySDKError):
37
+ """Raised when CLI process encounters an error."""
38
+
39
+ pass
@@ -0,0 +1,112 @@
1
+ """Message parser for CLI output."""
2
+
3
+ from typing import Any
4
+
5
+ from .types import (
6
+ AssistantMessage,
7
+ ContentBlock,
8
+ Message,
9
+ ResultMessage,
10
+ StreamEvent,
11
+ SystemMessage,
12
+ TextBlock,
13
+ ThinkingBlock,
14
+ ToolResultBlock,
15
+ ToolUseBlock,
16
+ UserMessage,
17
+ )
18
+
19
+
20
+ def parse_content_block(data: dict[str, Any]) -> ContentBlock | None:
21
+ """Parse a content block from raw data."""
22
+ block_type = data.get("type")
23
+
24
+ if block_type == "text":
25
+ return TextBlock(text=data.get("text", ""))
26
+
27
+ if block_type == "thinking":
28
+ return ThinkingBlock(
29
+ thinking=data.get("thinking", ""),
30
+ signature=data.get("signature", ""),
31
+ )
32
+
33
+ if block_type == "tool_use":
34
+ return ToolUseBlock(
35
+ id=data.get("id", ""),
36
+ name=data.get("name", ""),
37
+ input=data.get("input", {}),
38
+ )
39
+
40
+ if block_type == "tool_result":
41
+ return ToolResultBlock(
42
+ tool_use_id=data.get("tool_use_id", ""),
43
+ content=data.get("content"),
44
+ is_error=data.get("is_error"),
45
+ )
46
+
47
+ return None
48
+
49
+
50
+ def parse_content_blocks(content: list[dict[str, Any]]) -> list[ContentBlock]:
51
+ """Parse a list of content blocks."""
52
+ blocks: list[ContentBlock] = []
53
+ for item in content:
54
+ block = parse_content_block(item)
55
+ if block:
56
+ blocks.append(block)
57
+ return blocks
58
+
59
+
60
+ def parse_message(data: dict[str, Any]) -> Message | None:
61
+ """Parse a message from raw JSON data."""
62
+ msg_type = data.get("type")
63
+
64
+ if msg_type == "user":
65
+ message_data = data.get("message", {})
66
+ content = message_data.get("content", "")
67
+ if isinstance(content, list):
68
+ content = parse_content_blocks(content)
69
+ return UserMessage(
70
+ content=content,
71
+ uuid=data.get("uuid"),
72
+ parent_tool_use_id=data.get("parent_tool_use_id"),
73
+ )
74
+
75
+ if msg_type == "assistant":
76
+ message_data = data.get("message", {})
77
+ content = message_data.get("content", [])
78
+ return AssistantMessage(
79
+ content=parse_content_blocks(content) if isinstance(content, list) else [],
80
+ model=data.get("model", ""),
81
+ parent_tool_use_id=data.get("parent_tool_use_id"),
82
+ error=data.get("error"),
83
+ )
84
+
85
+ if msg_type == "system":
86
+ return SystemMessage(
87
+ subtype=data.get("subtype", ""),
88
+ data=data,
89
+ )
90
+
91
+ if msg_type == "result":
92
+ return ResultMessage(
93
+ subtype=data.get("subtype", ""),
94
+ duration_ms=data.get("duration_ms", 0),
95
+ duration_api_ms=data.get("duration_api_ms", 0),
96
+ is_error=data.get("is_error", False),
97
+ num_turns=data.get("num_turns", 0),
98
+ session_id=data.get("session_id", ""),
99
+ total_cost_usd=data.get("total_cost_usd"),
100
+ usage=data.get("usage"),
101
+ result=data.get("result"),
102
+ )
103
+
104
+ if msg_type == "stream_event":
105
+ return StreamEvent(
106
+ uuid=data.get("uuid", ""),
107
+ session_id=data.get("session_id", ""),
108
+ event=data.get("event", {}),
109
+ parent_tool_use_id=data.get("parent_tool_use_id"),
110
+ )
111
+
112
+ return None
@@ -0,0 +1,3 @@
1
+ """Version information for CodeBuddy Agent SDK."""
2
+
3
+ __version__ = "0.1.15"
Binary file
@@ -0,0 +1,298 @@
1
+ """CodeBuddy SDK Client for interactive conversations."""
2
+
3
+ import json
4
+ import os
5
+ from collections.abc import AsyncIterable, AsyncIterator
6
+ from typing import Any
7
+
8
+ from ._errors import CLIConnectionError
9
+ from ._message_parser import parse_message
10
+ from .transport import SubprocessTransport, Transport
11
+ from .types import CanUseToolOptions, CodeBuddyAgentOptions, Message, ResultMessage
12
+
13
+
14
+ class CodeBuddySDKClient:
15
+ """
16
+ Client for bidirectional, interactive conversations with CodeBuddy.
17
+
18
+ This client provides full control over the conversation flow with support
19
+ for streaming, interrupts, and dynamic message sending. For simple one-shot
20
+ queries, consider using the query() function instead.
21
+
22
+ Key features:
23
+ - Bidirectional: Send and receive messages at any time
24
+ - Stateful: Maintains conversation context across messages
25
+ - Interactive: Send follow-ups based on responses
26
+ - Control flow: Support for interrupts and session management
27
+
28
+ Example:
29
+ ```python
30
+ async with CodeBuddySDKClient() as client:
31
+ await client.query("Hello!")
32
+ async for msg in client.receive_response():
33
+ print(msg)
34
+ ```
35
+ """
36
+
37
+ def __init__(
38
+ self,
39
+ options: CodeBuddyAgentOptions | None = None,
40
+ transport: Transport | None = None,
41
+ ):
42
+ """Initialize CodeBuddy SDK client."""
43
+ self.options = options or CodeBuddyAgentOptions()
44
+ self._custom_transport = transport
45
+ self._transport: Transport | None = None
46
+ self._connected = False
47
+
48
+ os.environ["CODEBUDDY_CODE_ENTRYPOINT"] = "sdk-py-client"
49
+
50
+ async def connect(
51
+ self, prompt: str | AsyncIterable[dict[str, Any]] | None = None
52
+ ) -> None:
53
+ """Connect to CodeBuddy with an optional initial prompt."""
54
+ if self._custom_transport:
55
+ self._transport = self._custom_transport
56
+ else:
57
+ self._transport = SubprocessTransport(
58
+ options=self.options,
59
+ prompt=prompt,
60
+ )
61
+
62
+ await self._transport.connect()
63
+ self._connected = True
64
+ await self._send_initialize()
65
+
66
+ async def _send_initialize(self) -> None:
67
+ """Send initialization control request."""
68
+ if not self._transport:
69
+ return
70
+
71
+ request = {
72
+ "type": "control_request",
73
+ "request_id": f"init_{id(self)}",
74
+ "request": {"subtype": "initialize"},
75
+ }
76
+ await self._transport.write(json.dumps(request))
77
+
78
+ async def query(
79
+ self,
80
+ prompt: str | AsyncIterable[dict[str, Any]],
81
+ session_id: str = "default",
82
+ ) -> None:
83
+ """
84
+ Send a new request.
85
+
86
+ Args:
87
+ prompt: Either a string message or an async iterable of message dicts
88
+ session_id: Session identifier for the conversation
89
+ """
90
+ if not self._connected or not self._transport:
91
+ raise CLIConnectionError("Not connected. Call connect() first.")
92
+
93
+ if isinstance(prompt, str):
94
+ message = {
95
+ "type": "user",
96
+ "message": {"role": "user", "content": prompt},
97
+ "parent_tool_use_id": None,
98
+ "session_id": session_id,
99
+ }
100
+ await self._transport.write(json.dumps(message))
101
+ else:
102
+ async for msg in prompt:
103
+ if "session_id" not in msg:
104
+ msg["session_id"] = session_id
105
+ await self._transport.write(json.dumps(msg))
106
+
107
+ async def receive_messages(self) -> AsyncIterator[Message]:
108
+ """Receive all messages from CodeBuddy."""
109
+ if not self._transport:
110
+ raise CLIConnectionError("Not connected.")
111
+
112
+ async for line in self._transport.read():
113
+ if not line:
114
+ continue
115
+
116
+ try:
117
+ data = json.loads(line)
118
+
119
+ # Handle control requests (permissions, hooks)
120
+ if data.get("type") == "control_request":
121
+ await self._handle_control_request(data)
122
+ continue
123
+
124
+ message = parse_message(data)
125
+ if message:
126
+ yield message
127
+ except json.JSONDecodeError:
128
+ continue
129
+
130
+ async def _handle_control_request(self, data: dict[str, Any]) -> None:
131
+ """Handle control request from CLI."""
132
+ if not self._transport:
133
+ return
134
+
135
+ request_id = data.get("request_id", "")
136
+ request = data.get("request", {})
137
+ subtype = request.get("subtype", "")
138
+
139
+ if subtype == "can_use_tool":
140
+ await self._handle_permission_request(request_id, request)
141
+ elif subtype == "hook_callback":
142
+ # Default: continue
143
+ response = {
144
+ "type": "control_response",
145
+ "response": {
146
+ "subtype": "success",
147
+ "request_id": request_id,
148
+ "response": {"continue": True},
149
+ },
150
+ }
151
+ await self._transport.write(json.dumps(response))
152
+
153
+ async def _handle_permission_request(
154
+ self, request_id: str, request: dict[str, Any]
155
+ ) -> None:
156
+ """Handle permission request from CLI."""
157
+ if not self._transport:
158
+ return
159
+
160
+ tool_name = request.get("tool_name", "")
161
+ input_data = request.get("input", {})
162
+ tool_use_id = request.get("tool_use_id", "")
163
+ agent_id = request.get("agent_id")
164
+
165
+ can_use_tool = self.options.can_use_tool
166
+
167
+ # Default deny if no callback provided
168
+ if not can_use_tool:
169
+ response = {
170
+ "type": "control_response",
171
+ "response": {
172
+ "subtype": "success",
173
+ "request_id": request_id,
174
+ "response": {
175
+ "allowed": False,
176
+ "reason": "No permission handler provided",
177
+ "tool_use_id": tool_use_id,
178
+ },
179
+ },
180
+ }
181
+ await self._transport.write(json.dumps(response))
182
+ return
183
+
184
+ try:
185
+ callback_options = CanUseToolOptions(
186
+ tool_use_id=tool_use_id,
187
+ signal=None,
188
+ agent_id=agent_id,
189
+ suggestions=request.get("permission_suggestions"),
190
+ blocked_path=request.get("blocked_path"),
191
+ decision_reason=request.get("decision_reason"),
192
+ )
193
+
194
+ result = await can_use_tool(tool_name, input_data, callback_options)
195
+
196
+ if result.behavior == "allow":
197
+ response_data = {
198
+ "allowed": True,
199
+ "updatedInput": result.updated_input,
200
+ "tool_use_id": tool_use_id,
201
+ }
202
+ else:
203
+ response_data = {
204
+ "allowed": False,
205
+ "reason": result.message,
206
+ "interrupt": result.interrupt,
207
+ "tool_use_id": tool_use_id,
208
+ }
209
+
210
+ response = {
211
+ "type": "control_response",
212
+ "response": {
213
+ "subtype": "success",
214
+ "request_id": request_id,
215
+ "response": response_data,
216
+ },
217
+ }
218
+ await self._transport.write(json.dumps(response))
219
+
220
+ except Exception as e:
221
+ response = {
222
+ "type": "control_response",
223
+ "response": {
224
+ "subtype": "success",
225
+ "request_id": request_id,
226
+ "response": {
227
+ "allowed": False,
228
+ "reason": str(e),
229
+ "tool_use_id": tool_use_id,
230
+ },
231
+ },
232
+ }
233
+ await self._transport.write(json.dumps(response))
234
+
235
+ async def receive_response(self) -> AsyncIterator[Message]:
236
+ """
237
+ Receive messages until and including a ResultMessage.
238
+
239
+ Yields each message as it's received and terminates after
240
+ yielding a ResultMessage.
241
+ """
242
+ async for message in self.receive_messages():
243
+ yield message
244
+ if isinstance(message, ResultMessage):
245
+ return
246
+
247
+ async def interrupt(self) -> None:
248
+ """Send interrupt signal."""
249
+ if not self._transport:
250
+ raise CLIConnectionError("Not connected.")
251
+
252
+ request = {
253
+ "type": "control_request",
254
+ "request_id": f"interrupt_{id(self)}",
255
+ "request": {"subtype": "interrupt"},
256
+ }
257
+ await self._transport.write(json.dumps(request))
258
+
259
+ async def set_permission_mode(self, mode: str) -> None:
260
+ """Change permission mode during conversation."""
261
+ if not self._transport:
262
+ raise CLIConnectionError("Not connected.")
263
+
264
+ request = {
265
+ "type": "control_request",
266
+ "request_id": f"perm_{id(self)}",
267
+ "request": {"subtype": "set_permission_mode", "mode": mode},
268
+ }
269
+ await self._transport.write(json.dumps(request))
270
+
271
+ async def set_model(self, model: str | None = None) -> None:
272
+ """Change the AI model during conversation."""
273
+ if not self._transport:
274
+ raise CLIConnectionError("Not connected.")
275
+
276
+ request = {
277
+ "type": "control_request",
278
+ "request_id": f"model_{id(self)}",
279
+ "request": {"subtype": "set_model", "model": model},
280
+ }
281
+ await self._transport.write(json.dumps(request))
282
+
283
+ async def disconnect(self) -> None:
284
+ """Disconnect from CodeBuddy."""
285
+ if self._transport:
286
+ await self._transport.close()
287
+ self._transport = None
288
+ self._connected = False
289
+
290
+ async def __aenter__(self) -> "CodeBuddySDKClient":
291
+ """Enter async context - automatically connects."""
292
+ await self.connect()
293
+ return self
294
+
295
+ async def __aexit__(self, *args: Any) -> bool:
296
+ """Exit async context - always disconnects."""
297
+ await self.disconnect()
298
+ return False
File without changes