codebuddy-agent-sdk 0.1.8__py3-none-manylinux_2_17_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.
- codebuddy_agent_sdk/__init__.py +97 -0
- codebuddy_agent_sdk/_binary.py +148 -0
- codebuddy_agent_sdk/_errors.py +39 -0
- codebuddy_agent_sdk/_message_parser.py +112 -0
- codebuddy_agent_sdk/_version.py +3 -0
- codebuddy_agent_sdk/bin/codebuddy +0 -0
- codebuddy_agent_sdk/client.py +298 -0
- codebuddy_agent_sdk/py.typed +0 -0
- codebuddy_agent_sdk/query.py +316 -0
- codebuddy_agent_sdk/transport/__init__.py +6 -0
- codebuddy_agent_sdk/transport/base.py +24 -0
- codebuddy_agent_sdk/transport/subprocess.py +167 -0
- codebuddy_agent_sdk/types.py +303 -0
- codebuddy_agent_sdk-0.1.8.dist-info/METADATA +89 -0
- codebuddy_agent_sdk-0.1.8.dist-info/RECORD +16 -0
- codebuddy_agent_sdk-0.1.8.dist-info/WHEEL +4 -0
|
@@ -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
|
|
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
|