evolv-agent-sdk 0.2.0__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.
@@ -0,0 +1,285 @@
1
+ """evolv Agent SDK for Python."""
2
+
3
+ from collections.abc import Awaitable, Callable
4
+ from dataclasses import dataclass
5
+ from typing import Any, Generic, TypeVar
6
+
7
+ from ._errors import (
8
+ CLIConnectionError,
9
+ CLIJSONDecodeError,
10
+ CLINotFoundError,
11
+ EvolvSDKError,
12
+ ProcessError,
13
+ )
14
+ from ._internal.transport import Transport
15
+ from ._version import __version__
16
+ from .client import EvolvSDKClient
17
+ from .query import query
18
+ from .types import (
19
+ AgentDefinition,
20
+ AssistantMessage,
21
+ CanUseTool,
22
+ ContentBlock,
23
+ EvolvAgentOptions,
24
+ HookCallback,
25
+ HookContext,
26
+ HookEvent,
27
+ HookInput,
28
+ HookJSONOutput,
29
+ HookMatcher,
30
+ McpSdkServerConfig,
31
+ McpServerConfig,
32
+ Message,
33
+ PermissionMode,
34
+ PermissionResult,
35
+ PermissionResultAllow,
36
+ PermissionResultDeny,
37
+ PermissionUpdate,
38
+ PostToolUseHookInput,
39
+ PreToolUseHookInput,
40
+ ResultMessage,
41
+ SdkBeta,
42
+ SettingSource,
43
+ SyncHookJSONOutput,
44
+ SystemMessage,
45
+ TextBlock,
46
+ ThinkingBlock,
47
+ ToolPermissionContext,
48
+ ToolResultBlock,
49
+ ToolUseBlock,
50
+ UserMessage,
51
+ )
52
+
53
+ # MCP Server Support
54
+
55
+ T = TypeVar("T")
56
+
57
+
58
+ @dataclass
59
+ class SdkMcpTool(Generic[T]):
60
+ """Definition for an SDK MCP tool."""
61
+
62
+ name: str
63
+ description: str
64
+ input_schema: type[T] | dict[str, Any]
65
+ handler: Callable[[T], Awaitable[dict[str, Any]]]
66
+
67
+
68
+ def tool(
69
+ name: str, description: str, input_schema: type | dict[str, Any]
70
+ ) -> Callable[[Callable[[Any], Awaitable[dict[str, Any]]]], SdkMcpTool[Any]]:
71
+ """Decorator for defining MCP tools with type safety.
72
+
73
+ Creates a tool that can be used with SDK MCP servers. The tool runs
74
+ in-process within your Python application, providing better performance
75
+ than external MCP servers.
76
+
77
+ Args:
78
+ name: Unique identifier for the tool.
79
+ description: Human-readable description of what the tool does.
80
+ input_schema: Schema defining the tool's input parameters.
81
+
82
+ Returns:
83
+ A decorator function that wraps the tool implementation.
84
+
85
+ Example:
86
+ ```python
87
+ @tool("greet", "Greet a user", {"name": str})
88
+ async def greet(args):
89
+ return {"content": [{"type": "text", "text": f"Hello, {args['name']}!"}]}
90
+ ```
91
+ """
92
+
93
+ def decorator(
94
+ handler: Callable[[Any], Awaitable[dict[str, Any]]],
95
+ ) -> SdkMcpTool[Any]:
96
+ return SdkMcpTool(
97
+ name=name,
98
+ description=description,
99
+ input_schema=input_schema,
100
+ handler=handler,
101
+ )
102
+
103
+ return decorator
104
+
105
+
106
+ def create_sdk_mcp_server(
107
+ name: str, version: str = "1.0.0", tools: list[SdkMcpTool[Any]] | None = None
108
+ ) -> McpSdkServerConfig:
109
+ """Create an in-process MCP server that runs within your Python application.
110
+
111
+ Unlike external MCP servers that run as separate processes, SDK MCP servers
112
+ run directly in your application's process.
113
+
114
+ Args:
115
+ name: Unique identifier for the server.
116
+ version: Server version string. Defaults to "1.0.0".
117
+ tools: List of SdkMcpTool instances created with the @tool decorator.
118
+
119
+ Returns:
120
+ McpSdkServerConfig: A configuration object for use with EvolvAgentOptions.
121
+
122
+ Example:
123
+ ```python
124
+ @tool("add", "Add numbers", {"a": float, "b": float})
125
+ async def add(args):
126
+ return {"content": [{"type": "text", "text": f"Sum: {args['a'] + args['b']}"}]}
127
+
128
+ calculator = create_sdk_mcp_server(
129
+ name="calculator",
130
+ tools=[add]
131
+ )
132
+
133
+ options = EvolvAgentOptions(
134
+ mcp_servers={"calc": calculator},
135
+ )
136
+ ```
137
+ """
138
+ from mcp.server import Server
139
+ from mcp.types import ImageContent, TextContent, Tool
140
+
141
+ server = Server(name, version=version)
142
+
143
+ if tools:
144
+ tool_map = {tool_def.name: tool_def for tool_def in tools}
145
+
146
+ @server.list_tools() # type: ignore[no-untyped-call,untyped-decorator]
147
+ async def list_tools() -> list[Tool]:
148
+ """Return the list of available tools."""
149
+ tool_list = []
150
+ for tool_def in tools:
151
+ if isinstance(tool_def.input_schema, dict):
152
+ if (
153
+ "type" in tool_def.input_schema
154
+ and "properties" in tool_def.input_schema
155
+ ):
156
+ schema = tool_def.input_schema
157
+ else:
158
+ properties = {}
159
+ for param_name, param_type in tool_def.input_schema.items():
160
+ if param_type is str:
161
+ properties[param_name] = {"type": "string"}
162
+ elif param_type is int:
163
+ properties[param_name] = {"type": "integer"}
164
+ elif param_type is float:
165
+ properties[param_name] = {"type": "number"}
166
+ elif param_type is bool:
167
+ properties[param_name] = {"type": "boolean"}
168
+ else:
169
+ properties[param_name] = {"type": "string"}
170
+ schema = {
171
+ "type": "object",
172
+ "properties": properties,
173
+ "required": list(properties.keys()),
174
+ }
175
+ else:
176
+ schema = {"type": "object", "properties": {}}
177
+
178
+ tool_list.append(
179
+ Tool(
180
+ name=tool_def.name,
181
+ description=tool_def.description,
182
+ inputSchema=schema,
183
+ )
184
+ )
185
+ return tool_list
186
+
187
+ @server.call_tool() # type: ignore[untyped-decorator]
188
+ async def call_tool(name: str, arguments: dict[str, Any]) -> Any:
189
+ """Execute a tool by name with given arguments."""
190
+ if name not in tool_map:
191
+ raise ValueError(f"Tool '{name}' not found")
192
+
193
+ tool_def = tool_map[name]
194
+ result = await tool_def.handler(arguments)
195
+
196
+ content: list[TextContent | ImageContent] = []
197
+ if "content" in result:
198
+ for item in result["content"]:
199
+ if item.get("type") == "text":
200
+ content.append(TextContent(type="text", text=item["text"]))
201
+ if item.get("type") == "image":
202
+ content.append(
203
+ ImageContent(
204
+ type="image",
205
+ data=item["data"],
206
+ mimeType=item["mimeType"],
207
+ )
208
+ )
209
+
210
+ return content
211
+
212
+ return McpSdkServerConfig(type="sdk", name=name, instance=server)
213
+
214
+
215
+ # Backward compatibility aliases
216
+ CortexSDKClient = EvolvSDKClient
217
+ CortexAgentOptions = EvolvAgentOptions
218
+ CortexSDKError = EvolvSDKError
219
+ ClaudeSDKClient = EvolvSDKClient
220
+ ClaudeAgentOptions = EvolvAgentOptions
221
+ ClaudeSDKError = EvolvSDKError
222
+
223
+
224
+ __all__ = [
225
+ # Main exports
226
+ "query",
227
+ "__version__",
228
+ # Transport
229
+ "Transport",
230
+ "EvolvSDKClient",
231
+ # Types
232
+ "PermissionMode",
233
+ "McpServerConfig",
234
+ "McpSdkServerConfig",
235
+ "UserMessage",
236
+ "AssistantMessage",
237
+ "SystemMessage",
238
+ "ResultMessage",
239
+ "Message",
240
+ "EvolvAgentOptions",
241
+ "TextBlock",
242
+ "ThinkingBlock",
243
+ "ToolUseBlock",
244
+ "ToolResultBlock",
245
+ "ContentBlock",
246
+ # Tool callbacks
247
+ "CanUseTool",
248
+ "ToolPermissionContext",
249
+ "PermissionResult",
250
+ "PermissionResultAllow",
251
+ "PermissionResultDeny",
252
+ "PermissionUpdate",
253
+ # Hooks
254
+ "HookCallback",
255
+ "HookContext",
256
+ "HookEvent",
257
+ "HookInput",
258
+ "HookJSONOutput",
259
+ "HookMatcher",
260
+ "PreToolUseHookInput",
261
+ "PostToolUseHookInput",
262
+ "SyncHookJSONOutput",
263
+ # Agent support
264
+ "AgentDefinition",
265
+ "SettingSource",
266
+ # Beta support
267
+ "SdkBeta",
268
+ # MCP Server Support
269
+ "create_sdk_mcp_server",
270
+ "tool",
271
+ "SdkMcpTool",
272
+ # Errors
273
+ "EvolvSDKError",
274
+ "CLIConnectionError",
275
+ "CLINotFoundError",
276
+ "ProcessError",
277
+ "CLIJSONDecodeError",
278
+ # Backward compatibility aliases
279
+ "CortexSDKClient",
280
+ "CortexAgentOptions",
281
+ "CortexSDKError",
282
+ "ClaudeSDKClient",
283
+ "ClaudeAgentOptions",
284
+ "ClaudeSDKError",
285
+ ]
@@ -0,0 +1 @@
1
+ # Placeholder for bundled CLI binary
@@ -0,0 +1,3 @@
1
+ """evolv CLI version tracking."""
2
+
3
+ CLI_VERSION = "1.0.5"
@@ -0,0 +1,61 @@
1
+ """Error types for evolv Agent SDK."""
2
+
3
+ from typing import Any
4
+
5
+
6
+ class EvolvSDKError(Exception):
7
+ """Base exception for all evolv Agent SDK errors."""
8
+
9
+
10
+ class CLIConnectionError(EvolvSDKError):
11
+ """Raised when unable to connect to evolv Code."""
12
+
13
+
14
+ class CLINotFoundError(CLIConnectionError):
15
+ """Raised when evolv Code is not found or not installed."""
16
+
17
+ def __init__(
18
+ self, message: str = "evolv Code not found", cli_path: str | None = None
19
+ ):
20
+ if cli_path:
21
+ message = f"{message}: {cli_path}"
22
+ super().__init__(message)
23
+
24
+
25
+ class ProcessError(EvolvSDKError):
26
+ """Raised when the CLI process fails."""
27
+
28
+ def __init__(
29
+ self, message: str, exit_code: int | None = None, stderr: str | None = None
30
+ ):
31
+ self.exit_code = exit_code
32
+ self.stderr = stderr
33
+
34
+ if exit_code is not None:
35
+ message = f"{message} (exit code: {exit_code})"
36
+ if stderr:
37
+ message = f"{message}\nError output: {stderr}"
38
+
39
+ super().__init__(message)
40
+
41
+
42
+ class CLIJSONDecodeError(EvolvSDKError):
43
+ """Raised when unable to decode JSON from CLI output."""
44
+
45
+ def __init__(self, line: str, original_error: Exception):
46
+ self.line = line
47
+ self.original_error = original_error
48
+ super().__init__(f"Failed to decode JSON: {line[:100]}...")
49
+
50
+
51
+ class MessageParseError(EvolvSDKError):
52
+ """Raised when unable to parse a message from CLI output."""
53
+
54
+ def __init__(self, message: str, data: dict[str, Any] | None = None):
55
+ self.data = data
56
+ super().__init__(message)
57
+
58
+
59
+ # Backward compatibility aliases
60
+ CortexSDKError = EvolvSDKError
61
+ ClaudeSDKError = EvolvSDKError
@@ -0,0 +1 @@
1
+ """Internal modules for evolv Agent SDK."""
@@ -0,0 +1,108 @@
1
+ """Internal client implementation."""
2
+
3
+ from collections.abc import AsyncIterable, AsyncIterator
4
+ from dataclasses import replace
5
+ from typing import Any
6
+
7
+ from ..types import (
8
+ EvolvAgentOptions,
9
+ HookEvent,
10
+ HookMatcher,
11
+ Message,
12
+ )
13
+ from .message_parser import parse_message
14
+ from .query import Query
15
+ from .transport import Transport
16
+ from .transport.subprocess_cli import SubprocessCLITransport
17
+
18
+
19
+ class InternalClient:
20
+ """Internal client implementation."""
21
+
22
+ def __init__(self) -> None:
23
+ """Initialize the internal client."""
24
+
25
+ def _convert_hooks_to_internal_format(
26
+ self, hooks: dict[HookEvent, list[HookMatcher]]
27
+ ) -> dict[str, list[dict[str, Any]]]:
28
+ """Convert HookMatcher format to internal Query format."""
29
+ internal_hooks: dict[str, list[dict[str, Any]]] = {}
30
+ for event, matchers in hooks.items():
31
+ internal_hooks[event] = []
32
+ for matcher in matchers:
33
+ internal_matcher: dict[str, Any] = {
34
+ "matcher": matcher.matcher if hasattr(matcher, "matcher") else None,
35
+ "hooks": matcher.hooks if hasattr(matcher, "hooks") else [],
36
+ }
37
+ if hasattr(matcher, "timeout") and matcher.timeout is not None:
38
+ internal_matcher["timeout"] = matcher.timeout
39
+ internal_hooks[event].append(internal_matcher)
40
+ return internal_hooks
41
+
42
+ async def process_query(
43
+ self,
44
+ prompt: str | AsyncIterable[dict[str, Any]],
45
+ options: EvolvAgentOptions,
46
+ transport: Transport | None = None,
47
+ ) -> AsyncIterator[Message]:
48
+ """Process a query through transport and Query."""
49
+
50
+ configured_options = options
51
+ if options.can_use_tool:
52
+ if isinstance(prompt, str):
53
+ raise ValueError(
54
+ "can_use_tool callback requires streaming mode. "
55
+ "Please provide prompt as an AsyncIterable instead of a string."
56
+ )
57
+
58
+ if options.permission_prompt_tool_name:
59
+ raise ValueError(
60
+ "can_use_tool callback cannot be used with permission_prompt_tool_name. "
61
+ "Please use one or the other."
62
+ )
63
+
64
+ configured_options = replace(options, permission_prompt_tool_name="stdio")
65
+
66
+ if transport is not None:
67
+ chosen_transport = transport
68
+ else:
69
+ chosen_transport = SubprocessCLITransport(
70
+ prompt=prompt,
71
+ options=configured_options,
72
+ )
73
+
74
+ await chosen_transport.connect()
75
+
76
+ sdk_mcp_servers = {}
77
+ if configured_options.mcp_servers and isinstance(
78
+ configured_options.mcp_servers, dict
79
+ ):
80
+ for name, config in configured_options.mcp_servers.items():
81
+ if isinstance(config, dict) and config.get("type") == "sdk":
82
+ sdk_mcp_servers[name] = config["instance"] # type: ignore[typeddict-item]
83
+
84
+ is_streaming = not isinstance(prompt, str)
85
+ query = Query(
86
+ transport=chosen_transport,
87
+ is_streaming_mode=is_streaming,
88
+ can_use_tool=configured_options.can_use_tool,
89
+ hooks=self._convert_hooks_to_internal_format(configured_options.hooks)
90
+ if configured_options.hooks
91
+ else None,
92
+ sdk_mcp_servers=sdk_mcp_servers,
93
+ )
94
+
95
+ try:
96
+ await query.start()
97
+
98
+ if is_streaming:
99
+ await query.initialize()
100
+
101
+ if isinstance(prompt, AsyncIterable) and query._tg:
102
+ query._tg.start_soon(query.stream_input, prompt)
103
+
104
+ async for data in query.receive_messages():
105
+ yield parse_message(data)
106
+
107
+ finally:
108
+ await query.close()
@@ -0,0 +1,181 @@
1
+ """Message parser for evolv Agent SDK responses."""
2
+
3
+ import logging
4
+ from typing import Any
5
+
6
+ from .._errors import MessageParseError
7
+ from ..types import (
8
+ AssistantMessage,
9
+ ContentBlock,
10
+ Message,
11
+ ResultMessage,
12
+ StreamEvent,
13
+ SystemMessage,
14
+ TextBlock,
15
+ ThinkingBlock,
16
+ ToolResultBlock,
17
+ ToolUseBlock,
18
+ UserMessage,
19
+ )
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ def parse_message(data: dict[str, Any]) -> Message:
25
+ """
26
+ Parse message from CLI output into typed Message objects.
27
+
28
+ Args:
29
+ data: Raw message dictionary from CLI output
30
+
31
+ Returns:
32
+ Parsed Message object
33
+
34
+ Raises:
35
+ MessageParseError: If parsing fails or message type is unrecognized
36
+ """
37
+ if not isinstance(data, dict):
38
+ raise MessageParseError(
39
+ f"Invalid message data type (expected dict, got {type(data).__name__})",
40
+ data,
41
+ )
42
+
43
+ message_type = data.get("type")
44
+ if not message_type:
45
+ raise MessageParseError("Message missing 'type' field", data)
46
+
47
+ match message_type:
48
+ case "user":
49
+ try:
50
+ parent_tool_use_id = data.get("parent_tool_use_id")
51
+ tool_use_result = data.get("tool_use_result")
52
+ uuid = data.get("uuid")
53
+ if isinstance(data["message"]["content"], list):
54
+ user_content_blocks: list[ContentBlock] = []
55
+ for block in data["message"]["content"]:
56
+ match block["type"]:
57
+ case "text":
58
+ user_content_blocks.append(
59
+ TextBlock(text=block["text"])
60
+ )
61
+ case "tool_use":
62
+ user_content_blocks.append(
63
+ ToolUseBlock(
64
+ id=block["id"],
65
+ name=block["name"],
66
+ input=block["input"],
67
+ )
68
+ )
69
+ case "tool_result":
70
+ user_content_blocks.append(
71
+ ToolResultBlock(
72
+ tool_use_id=block["tool_use_id"],
73
+ content=block.get("content"),
74
+ is_error=block.get("is_error"),
75
+ )
76
+ )
77
+ return UserMessage(
78
+ content=user_content_blocks,
79
+ uuid=uuid,
80
+ parent_tool_use_id=parent_tool_use_id,
81
+ tool_use_result=tool_use_result,
82
+ )
83
+ return UserMessage(
84
+ content=data["message"]["content"],
85
+ uuid=uuid,
86
+ parent_tool_use_id=parent_tool_use_id,
87
+ tool_use_result=tool_use_result,
88
+ )
89
+ except KeyError as e:
90
+ raise MessageParseError(
91
+ f"Missing required field in user message: {e}", data
92
+ ) from e
93
+
94
+ case "assistant":
95
+ try:
96
+ content_blocks: list[ContentBlock] = []
97
+ for block in data["message"]["content"]:
98
+ match block["type"]:
99
+ case "text":
100
+ content_blocks.append(TextBlock(text=block["text"]))
101
+ case "thinking":
102
+ # evolv might not provide signature, use empty string
103
+ content_blocks.append(
104
+ ThinkingBlock(
105
+ thinking=block["thinking"],
106
+ signature=block.get("signature", ""),
107
+ )
108
+ )
109
+ case "tool_use":
110
+ content_blocks.append(
111
+ ToolUseBlock(
112
+ id=block["id"],
113
+ name=block["name"],
114
+ input=block["input"],
115
+ )
116
+ )
117
+ case "tool_result":
118
+ content_blocks.append(
119
+ ToolResultBlock(
120
+ tool_use_id=block["tool_use_id"],
121
+ content=block.get("content"),
122
+ is_error=block.get("is_error"),
123
+ )
124
+ )
125
+
126
+ return AssistantMessage(
127
+ content=content_blocks,
128
+ model=data["message"].get("model", "evolv"),
129
+ parent_tool_use_id=data.get("parent_tool_use_id"),
130
+ error=data.get("error"),
131
+ )
132
+ except KeyError as e:
133
+ raise MessageParseError(
134
+ f"Missing required field in assistant message: {e}", data
135
+ ) from e
136
+
137
+ case "system":
138
+ try:
139
+ return SystemMessage(
140
+ subtype=data["subtype"],
141
+ data=data,
142
+ )
143
+ except KeyError as e:
144
+ raise MessageParseError(
145
+ f"Missing required field in system message: {e}", data
146
+ ) from e
147
+
148
+ case "result":
149
+ try:
150
+ return ResultMessage(
151
+ subtype=data["subtype"],
152
+ duration_ms=data.get("duration_ms", 0),
153
+ duration_api_ms=data.get("duration_api_ms", 0),
154
+ is_error=data.get("is_error", data.get("subtype") != "success"),
155
+ num_turns=data.get("num_turns", 1),
156
+ session_id=data.get("session_id", "evolv-session"),
157
+ total_cost_usd=data.get("total_cost_usd", 0),
158
+ usage=data.get("usage"),
159
+ result=data.get("result"),
160
+ structured_output=data.get("structured_output"),
161
+ )
162
+ except KeyError as e:
163
+ raise MessageParseError(
164
+ f"Missing required field in result message: {e}", data
165
+ ) from e
166
+
167
+ case "stream_event":
168
+ try:
169
+ return StreamEvent(
170
+ uuid=data["uuid"],
171
+ session_id=data["session_id"],
172
+ event=data["event"],
173
+ parent_tool_use_id=data.get("parent_tool_use_id"),
174
+ )
175
+ except KeyError as e:
176
+ raise MessageParseError(
177
+ f"Missing required field in stream_event message: {e}", data
178
+ ) from e
179
+
180
+ case _:
181
+ raise MessageParseError(f"Unknown message type: {message_type}", data)