codebuddy-agent-sdk 0.1.13__py3-none-musllinux_1_1_x86_64.whl → 0.2.2__py3-none-musllinux_1_1_x86_64.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.

@@ -5,10 +5,22 @@ from ._errors import (
5
5
  CLIJSONDecodeError,
6
6
  CLINotFoundError,
7
7
  CodeBuddySDKError,
8
+ ExecutionError,
8
9
  ProcessError,
9
10
  )
10
11
  from ._version import __version__
11
12
  from .client import CodeBuddySDKClient
13
+ from .mcp import (
14
+ CallToolResult,
15
+ SdkControlServerTransport,
16
+ SdkMcpServerOptions,
17
+ SdkMcpServerResult,
18
+ SdkMcpToolDefinition,
19
+ TextContent,
20
+ ToolHandler,
21
+ create_sdk_mcp_server,
22
+ tool,
23
+ )
12
24
  from .query import query
13
25
  from .transport import Transport
14
26
  from .types import (
@@ -20,14 +32,20 @@ from .types import (
20
32
  AssistantMessage,
21
33
  CanUseTool,
22
34
  CanUseToolOptions,
35
+ Checkpoint,
36
+ CheckpointFileChangeStats,
23
37
  CodeBuddyAgentOptions,
24
38
  ContentBlock,
39
+ ErrorMessage,
40
+ FileVersion,
25
41
  HookCallback,
26
42
  HookContext,
27
43
  HookEvent,
28
44
  HookJSONOutput,
29
45
  HookMatcher,
46
+ McpSdkServerConfig,
30
47
  McpServerConfig,
48
+ McpStdioServerConfig,
31
49
  Message,
32
50
  PermissionMode,
33
51
  PermissionResult,
@@ -50,6 +68,16 @@ __all__ = [
50
68
  "CodeBuddySDKClient",
51
69
  "Transport",
52
70
  "__version__",
71
+ # MCP Server API
72
+ "create_sdk_mcp_server",
73
+ "tool",
74
+ "SdkControlServerTransport",
75
+ "SdkMcpServerOptions",
76
+ "SdkMcpServerResult",
77
+ "SdkMcpToolDefinition",
78
+ "ToolHandler",
79
+ "CallToolResult",
80
+ "TextContent",
53
81
  # Types - Permission
54
82
  "PermissionMode",
55
83
  # Types - Messages
@@ -59,6 +87,7 @@ __all__ = [
59
87
  "SystemMessage",
60
88
  "ResultMessage",
61
89
  "StreamEvent",
90
+ "ErrorMessage",
62
91
  # Types - Content blocks
63
92
  "ContentBlock",
64
93
  "TextBlock",
@@ -86,12 +115,19 @@ __all__ = [
86
115
  "HookMatcher",
87
116
  "HookJSONOutput",
88
117
  "HookContext",
118
+ # Types - Checkpoint
119
+ "Checkpoint",
120
+ "CheckpointFileChangeStats",
121
+ "FileVersion",
89
122
  # Types - MCP
90
123
  "McpServerConfig",
124
+ "McpStdioServerConfig",
125
+ "McpSdkServerConfig",
91
126
  # Errors
92
127
  "CodeBuddySDKError",
93
128
  "CLIConnectionError",
94
129
  "CLINotFoundError",
95
130
  "CLIJSONDecodeError",
96
131
  "ProcessError",
132
+ "ExecutionError",
97
133
  ]
@@ -5,6 +5,8 @@ This module provides functions to locate the CodeBuddy CLI binary.
5
5
  The binary is bundled in platform-specific wheels.
6
6
  """
7
7
 
8
+ from __future__ import annotations
9
+
8
10
  import os
9
11
  import platform
10
12
  import sys
@@ -1,5 +1,7 @@
1
1
  """Error definitions for CodeBuddy Agent SDK."""
2
2
 
3
+ from __future__ import annotations
4
+
3
5
 
4
6
  class CodeBuddySDKError(Exception):
5
7
  """Base exception for CodeBuddy SDK errors."""
@@ -37,3 +39,16 @@ class ProcessError(CodeBuddySDKError):
37
39
  """Raised when CLI process encounters an error."""
38
40
 
39
41
  pass
42
+
43
+
44
+ class ExecutionError(CodeBuddySDKError):
45
+ """Raised when execution fails (e.g., authentication error, API error).
46
+
47
+ Contains the errors array from the ResultMessage.
48
+ """
49
+
50
+ def __init__(self, errors: list[str], subtype: str):
51
+ message = errors[0] if errors else "Execution failed"
52
+ super().__init__(message)
53
+ self.errors = errors
54
+ self.subtype = subtype
@@ -1,10 +1,13 @@
1
1
  """Message parser for CLI output."""
2
2
 
3
+ from __future__ import annotations
4
+
3
5
  from typing import Any
4
6
 
5
7
  from .types import (
6
8
  AssistantMessage,
7
9
  ContentBlock,
10
+ ErrorMessage,
8
11
  Message,
9
12
  ResultMessage,
10
13
  StreamEvent,
@@ -99,6 +102,7 @@ def parse_message(data: dict[str, Any]) -> Message | None:
99
102
  total_cost_usd=data.get("total_cost_usd"),
100
103
  usage=data.get("usage"),
101
104
  result=data.get("result"),
105
+ errors=data.get("errors"),
102
106
  )
103
107
 
104
108
  if msg_type == "stream_event":
@@ -109,4 +113,10 @@ def parse_message(data: dict[str, Any]) -> Message | None:
109
113
  parent_tool_use_id=data.get("parent_tool_use_id"),
110
114
  )
111
115
 
116
+ if msg_type == "error":
117
+ return ErrorMessage(
118
+ error=data.get("error", ""),
119
+ session_id=data.get("session_id"),
120
+ )
121
+
112
122
  return None
@@ -1,3 +1,3 @@
1
1
  """Version information for CodeBuddy Agent SDK."""
2
2
 
3
- __version__ = "0.1.13"
3
+ __version__ = "0.2.2"
Binary file
@@ -1,14 +1,17 @@
1
1
  """CodeBuddy SDK Client for interactive conversations."""
2
2
 
3
+ from __future__ import annotations
4
+
3
5
  import json
4
6
  import os
5
7
  from collections.abc import AsyncIterable, AsyncIterator
8
+ from types import TracebackType
6
9
  from typing import Any
7
10
 
8
- from ._errors import CLIConnectionError
11
+ from ._errors import CLIConnectionError, ExecutionError
9
12
  from ._message_parser import parse_message
10
13
  from .transport import SubprocessTransport, Transport
11
- from .types import CanUseToolOptions, CodeBuddyAgentOptions, Message, ResultMessage
14
+ from .types import CanUseToolOptions, CodeBuddyAgentOptions, ErrorMessage, HookMatcher, Message, ResultMessage
12
15
 
13
16
 
14
17
  class CodeBuddySDKClient:
@@ -44,12 +47,12 @@ class CodeBuddySDKClient:
44
47
  self._custom_transport = transport
45
48
  self._transport: Transport | None = None
46
49
  self._connected = False
50
+ # Hook callback registry: callback_id -> hook function
51
+ self._hook_callbacks: dict[str, Any] = {}
47
52
 
48
53
  os.environ["CODEBUDDY_CODE_ENTRYPOINT"] = "sdk-py-client"
49
54
 
50
- async def connect(
51
- self, prompt: str | AsyncIterable[dict[str, Any]] | None = None
52
- ) -> None:
55
+ async def connect(self, prompt: str | AsyncIterable[dict[str, Any]] | None = None) -> None:
53
56
  """Connect to CodeBuddy with an optional initial prompt."""
54
57
  if self._custom_transport:
55
58
  self._transport = self._custom_transport
@@ -68,13 +71,52 @@ class CodeBuddySDKClient:
68
71
  if not self._transport:
69
72
  return
70
73
 
74
+ hooks_config, self._hook_callbacks = self._build_hooks_config()
75
+
71
76
  request = {
72
77
  "type": "control_request",
73
78
  "request_id": f"init_{id(self)}",
74
- "request": {"subtype": "initialize"},
79
+ "request": {
80
+ "subtype": "initialize",
81
+ "hooks": hooks_config,
82
+ },
75
83
  }
76
84
  await self._transport.write(json.dumps(request))
77
85
 
86
+ def _build_hooks_config(self) -> tuple[dict[str, list[dict[str, Any]]] | None, dict[str, Any]]:
87
+ """Build hooks configuration for CLI and callback registry.
88
+
89
+ Returns:
90
+ Tuple of (config for CLI, callback_id -> hook function mapping)
91
+ """
92
+ callbacks: dict[str, Any] = {}
93
+
94
+ if not self.options.hooks:
95
+ return None, callbacks
96
+
97
+ config: dict[str, list[dict[str, Any]]] = {}
98
+
99
+ for event, matchers in self.options.hooks.items():
100
+ event_str = str(event)
101
+ matcher_configs = []
102
+
103
+ for i, m in enumerate(matchers):
104
+ callback_ids = []
105
+ for j, hook in enumerate(m.hooks):
106
+ callback_id = f"hook_{event_str}_{i}_{j}"
107
+ callback_ids.append(callback_id)
108
+ callbacks[callback_id] = hook
109
+
110
+ matcher_configs.append({
111
+ "matcher": m.matcher,
112
+ "hookCallbackIds": callback_ids,
113
+ "timeout": m.timeout,
114
+ })
115
+
116
+ config[event_str] = matcher_configs
117
+
118
+ return (config if config else None), callbacks
119
+
78
120
  async def query(
79
121
  self,
80
122
  prompt: str | AsyncIterable[dict[str, Any]],
@@ -139,20 +181,24 @@ class CodeBuddySDKClient:
139
181
  if subtype == "can_use_tool":
140
182
  await self._handle_permission_request(request_id, request)
141
183
  elif subtype == "hook_callback":
142
- # Default: continue
184
+ callback_id = request.get("callback_id", "")
185
+ hook_input = request.get("input", {})
186
+ tool_use_id = request.get("tool_use_id")
187
+
188
+ # Execute the hook
189
+ hook_response = await self._execute_hook(callback_id, hook_input, tool_use_id)
190
+
143
191
  response = {
144
192
  "type": "control_response",
145
193
  "response": {
146
194
  "subtype": "success",
147
195
  "request_id": request_id,
148
- "response": {"continue": True},
196
+ "response": hook_response,
149
197
  },
150
198
  }
151
199
  await self._transport.write(json.dumps(response))
152
200
 
153
- async def _handle_permission_request(
154
- self, request_id: str, request: dict[str, Any]
155
- ) -> None:
201
+ async def _handle_permission_request(self, request_id: str, request: dict[str, Any]) -> None:
156
202
  """Handle permission request from CLI."""
157
203
  if not self._transport:
158
204
  return
@@ -232,16 +278,42 @@ class CodeBuddySDKClient:
232
278
  }
233
279
  await self._transport.write(json.dumps(response))
234
280
 
281
+ async def _execute_hook(
282
+ self,
283
+ callback_id: str,
284
+ hook_input: dict[str, Any],
285
+ tool_use_id: str | None,
286
+ ) -> dict[str, Any]:
287
+ """Execute a hook callback by looking up in the callback registry."""
288
+ hook = self._hook_callbacks.get(callback_id)
289
+ if not hook:
290
+ return {"continue": True}
291
+
292
+ try:
293
+ result = await hook(hook_input, tool_use_id, {"signal": None})
294
+ return dict(result)
295
+ except Exception as e:
296
+ return {"continue": False, "stopReason": str(e)}
297
+
235
298
  async def receive_response(self) -> AsyncIterator[Message]:
236
299
  """
237
- Receive messages until and including a ResultMessage.
300
+ Receive messages until and including a ResultMessage or ErrorMessage.
238
301
 
239
302
  Yields each message as it's received and terminates after
240
- yielding a ResultMessage.
303
+ yielding a ResultMessage or ErrorMessage.
304
+ Raises ExecutionError if ResultMessage indicates an error.
241
305
  """
242
306
  async for message in self.receive_messages():
243
- yield message
307
+ # Check for execution error BEFORE yielding
244
308
  if isinstance(message, ResultMessage):
309
+ if message.is_error and message.errors and len(message.errors) > 0:
310
+ raise ExecutionError(message.errors, message.subtype)
311
+ yield message
312
+ return
313
+
314
+ yield message
315
+
316
+ if isinstance(message, ErrorMessage):
245
317
  return
246
318
 
247
319
  async def interrupt(self) -> None:
@@ -287,12 +359,17 @@ class CodeBuddySDKClient:
287
359
  self._transport = None
288
360
  self._connected = False
289
361
 
290
- async def __aenter__(self) -> "CodeBuddySDKClient":
362
+ async def __aenter__(self) -> CodeBuddySDKClient:
291
363
  """Enter async context - automatically connects."""
292
364
  await self.connect()
293
365
  return self
294
366
 
295
- async def __aexit__(self, *args: Any) -> bool:
367
+ async def __aexit__(
368
+ self,
369
+ exc_type: type[BaseException] | None,
370
+ exc_val: BaseException | None,
371
+ exc_tb: TracebackType | None,
372
+ ) -> bool:
296
373
  """Exit async context - always disconnects."""
297
374
  await self.disconnect()
298
375
  return False
@@ -0,0 +1,35 @@
1
+ """
2
+ MCP (Model Context Protocol) Integration
3
+
4
+ This module provides utilities for creating and managing SDK MCP servers
5
+ that can be integrated with the CLI via the control protocol.
6
+ """
7
+
8
+ from .create_sdk_mcp_server import (
9
+ create_sdk_mcp_server,
10
+ tool,
11
+ )
12
+ from .sdk_control_server_transport import SdkControlServerTransport
13
+ from .types import (
14
+ CallToolResult,
15
+ SdkMcpServerOptions,
16
+ SdkMcpServerResult,
17
+ SdkMcpToolDefinition,
18
+ TextContent,
19
+ ToolHandler,
20
+ )
21
+
22
+ __all__ = [
23
+ # Factory functions
24
+ "create_sdk_mcp_server",
25
+ "tool",
26
+ # Transport
27
+ "SdkControlServerTransport",
28
+ # Types
29
+ "SdkMcpServerOptions",
30
+ "SdkMcpServerResult",
31
+ "SdkMcpToolDefinition",
32
+ "ToolHandler",
33
+ "CallToolResult",
34
+ "TextContent",
35
+ ]
@@ -0,0 +1,154 @@
1
+ """
2
+ create_sdk_mcp_server - Create an SDK MCP Server
3
+
4
+ This function creates an MCP server that can be integrated into the SDK
5
+ and used with the CLI via the control protocol.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from collections.abc import Awaitable, Callable
11
+ from typing import Any, TypeVar
12
+
13
+ from .types import (
14
+ CallToolResult,
15
+ SdkMcpServer,
16
+ SdkMcpServerOptions,
17
+ SdkMcpServerResult,
18
+ SdkMcpToolDefinition,
19
+ ToolInputSchema,
20
+ )
21
+
22
+ # Tool handler type
23
+ ToolHandler = Callable[[dict[str, Any]], CallToolResult | Awaitable[CallToolResult]]
24
+
25
+ # Type variable for the decorated function
26
+ F = TypeVar("F", bound=ToolHandler)
27
+
28
+
29
+ def _python_type_to_json_schema(py_type: type) -> dict[str, Any]:
30
+ """Convert Python type to JSON Schema type."""
31
+ type_mapping = {
32
+ str: {"type": "string"},
33
+ int: {"type": "integer"},
34
+ float: {"type": "number"},
35
+ bool: {"type": "boolean"},
36
+ list: {"type": "array"},
37
+ dict: {"type": "object"},
38
+ }
39
+ return type_mapping.get(py_type, {"type": "string"})
40
+
41
+
42
+ def _convert_schema(input_schema: dict[str, Any]) -> tuple[dict[str, dict[str, Any]], list[str]]:
43
+ """
44
+ Convert input schema to JSON Schema format.
45
+
46
+ Supports multiple formats:
47
+ 1. Simple type mapping: {"latitude": float, "longitude": float}
48
+ 2. JSON Schema format: {"properties": {...}, "required": [...]}
49
+
50
+ Returns:
51
+ Tuple of (properties dict, required list)
52
+ """
53
+ # Check if it's already in JSON Schema format
54
+ if "properties" in input_schema:
55
+ return input_schema.get("properties", {}), input_schema.get("required", [])
56
+
57
+ if "type" in input_schema and input_schema.get("type") == "object":
58
+ return input_schema.get("properties", {}), input_schema.get("required", [])
59
+
60
+ # Simple type mapping format: {"param_name": type}
61
+ properties = {}
62
+ required = []
63
+
64
+ for param_name, param_type in input_schema.items():
65
+ if isinstance(param_type, type):
66
+ properties[param_name] = _python_type_to_json_schema(param_type)
67
+ required.append(param_name)
68
+ elif isinstance(param_type, dict):
69
+ properties[param_name] = param_type
70
+ if "default" not in param_type:
71
+ required.append(param_name)
72
+ else:
73
+ properties[param_name] = {"type": "string"}
74
+ required.append(param_name)
75
+
76
+ return properties, required
77
+
78
+
79
+ def tool(name: str, description: str, input_schema: dict[str, Any]) -> Callable[[F], F]:
80
+ """
81
+ Decorator to define an MCP tool.
82
+
83
+ Example:
84
+ ```python
85
+ @tool("get_weather", "Get current weather", {"latitude": float, "longitude": float})
86
+ async def get_weather(args: dict[str, Any]) -> dict[str, Any]:
87
+ return {"content": [{"type": "text", "text": f"Weather: sunny"}]}
88
+ ```
89
+
90
+ Args:
91
+ name: Tool name (unique within the server)
92
+ description: Tool description
93
+ input_schema: Input parameters schema. Supports:
94
+ - Simple types: {"latitude": float, "longitude": float}
95
+ - JSON Schema: {"properties": {...}, "required": [...]}
96
+
97
+ Returns:
98
+ Decorated function with tool metadata attached
99
+ """
100
+ properties, required = _convert_schema(input_schema)
101
+ tool_schema = ToolInputSchema(type="object", properties=properties, required=required)
102
+
103
+ def decorator(func: F) -> F:
104
+ func._tool_definition = SdkMcpToolDefinition( # type: ignore[attr-defined]
105
+ name=name,
106
+ description=description,
107
+ input_schema=tool_schema,
108
+ handler=func,
109
+ )
110
+ return func
111
+
112
+ return decorator
113
+
114
+
115
+ def create_sdk_mcp_server(
116
+ name: str,
117
+ version: str = "1.0.0",
118
+ tools: list[Callable[..., Any]] | None = None,
119
+ ) -> SdkMcpServerResult:
120
+ """
121
+ Create an SDK MCP Server.
122
+
123
+ Args:
124
+ name: Server name (unique within the session)
125
+ version: Server version (defaults to "1.0.0")
126
+ tools: List of functions decorated with @tool
127
+
128
+ Returns:
129
+ SDK MCP server result for use with query()
130
+
131
+ Example:
132
+ ```python
133
+ from codebuddy_agent_sdk import create_sdk_mcp_server, tool, query
134
+
135
+ @tool("get_weather", "Get weather", {"location": str})
136
+ async def get_weather(args: dict) -> dict:
137
+ return {"content": [{"type": "text", "text": f"Weather: sunny"}]}
138
+
139
+ server = create_sdk_mcp_server(
140
+ name="my-server",
141
+ tools=[get_weather]
142
+ )
143
+ ```
144
+ """
145
+ tool_definitions: list[SdkMcpToolDefinition] = []
146
+ if tools:
147
+ for func in tools:
148
+ if not hasattr(func, "_tool_definition"):
149
+ raise ValueError(f"Function {func.__name__} is not decorated with @tool")
150
+ tool_definitions.append(func._tool_definition)
151
+
152
+ server = SdkMcpServer(SdkMcpServerOptions(name=name, version=version, tools=tool_definitions))
153
+
154
+ return SdkMcpServerResult(type="sdk", name=name, server=server)
@@ -0,0 +1,95 @@
1
+ """
2
+ SDK Control Server Transport
3
+
4
+ Custom transport implementation that bridges SDK MCP servers to CLI process.
5
+ This transport forwards MCP messages through the control protocol.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from collections.abc import Callable
11
+
12
+ from .types import JSONRPCMessage
13
+
14
+ # Callback function type for sending MCP messages to CLI
15
+ SendMcpMessageCallback = Callable[[JSONRPCMessage], None]
16
+
17
+
18
+ class SdkControlServerTransport:
19
+ """
20
+ SdkControlServerTransport - bridges MCP servers to CLI via control messages.
21
+
22
+ This transport implements a simple interface for forwarding MCP messages
23
+ between the SDK MCP server and the CLI via the control protocol.
24
+ """
25
+
26
+ def __init__(self, send_mcp_message: SendMcpMessageCallback):
27
+ """
28
+ Create a new SDK Control Server Transport.
29
+
30
+ Args:
31
+ send_mcp_message: Callback function to forward MCP messages to CLI
32
+ """
33
+ self._send_mcp_message = send_mcp_message
34
+ self._is_closed = False
35
+ self._on_message: Callable[[JSONRPCMessage], None] | None = None
36
+ self._on_close: Callable[[], None] | None = None
37
+ self._on_error: Callable[[Exception], None] | None = None
38
+
39
+ @property
40
+ def closed(self) -> bool:
41
+ """Check if the transport is closed."""
42
+ return self._is_closed
43
+
44
+ def set_on_message(self, callback: Callable[[JSONRPCMessage], None] | None) -> None:
45
+ """Set the message callback."""
46
+ self._on_message = callback
47
+
48
+ def set_on_close(self, callback: Callable[[], None] | None) -> None:
49
+ """Set the close callback."""
50
+ self._on_close = callback
51
+
52
+ def set_on_error(self, callback: Callable[[Exception], None] | None) -> None:
53
+ """Set the error callback."""
54
+ self._on_error = callback
55
+
56
+ async def start(self) -> None:
57
+ """
58
+ Start the transport.
59
+ No-op since connection is already established via stdio.
60
+ """
61
+ pass
62
+
63
+ async def send(self, message: JSONRPCMessage) -> None:
64
+ """
65
+ Send a message to the CLI via control_request.
66
+
67
+ Args:
68
+ message: The JSON-RPC message to send
69
+ """
70
+ if self._is_closed:
71
+ raise RuntimeError("Transport is closed")
72
+ # Forward message to CLI via control_request
73
+ self._send_mcp_message(message)
74
+
75
+ async def close(self) -> None:
76
+ """Close the transport."""
77
+ if self._is_closed:
78
+ return
79
+
80
+ self._is_closed = True
81
+ if self._on_close:
82
+ self._on_close()
83
+
84
+ def handle_incoming_message(self, message: JSONRPCMessage) -> None:
85
+ """
86
+ Handle incoming message from CLI.
87
+ This method should be called when the CLI sends a message to this server.
88
+
89
+ Args:
90
+ message: The JSON-RPC message from CLI
91
+ """
92
+ if self._is_closed:
93
+ return
94
+ if self._on_message:
95
+ self._on_message(message)