open-agent-sdk-py 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.
Files changed (57) hide show
  1. open_agent_sdk/__init__.py +129 -0
  2. open_agent_sdk/agent.py +391 -0
  3. open_agent_sdk/engine.py +600 -0
  4. open_agent_sdk/hooks.py +142 -0
  5. open_agent_sdk/mcp/__init__.py +0 -0
  6. open_agent_sdk/mcp/client.py +130 -0
  7. open_agent_sdk/providers/__init__.py +60 -0
  8. open_agent_sdk/providers/anthropic.py +99 -0
  9. open_agent_sdk/providers/openai.py +266 -0
  10. open_agent_sdk/providers/types.py +102 -0
  11. open_agent_sdk/sdk_mcp_server.py +61 -0
  12. open_agent_sdk/session.py +104 -0
  13. open_agent_sdk/skills/__init__.py +33 -0
  14. open_agent_sdk/skills/bundled/__init__.py +29 -0
  15. open_agent_sdk/skills/bundled/commit.py +45 -0
  16. open_agent_sdk/skills/bundled/debug.py +55 -0
  17. open_agent_sdk/skills/bundled/review.py +48 -0
  18. open_agent_sdk/skills/bundled/simplify.py +62 -0
  19. open_agent_sdk/skills/bundled/test_skill.py +50 -0
  20. open_agent_sdk/skills/registry.py +107 -0
  21. open_agent_sdk/skills/types.py +48 -0
  22. open_agent_sdk/tools/__init__.py +161 -0
  23. open_agent_sdk/tools/agent_tool.py +144 -0
  24. open_agent_sdk/tools/ask_user.py +73 -0
  25. open_agent_sdk/tools/bash.py +76 -0
  26. open_agent_sdk/tools/config_tool.py +78 -0
  27. open_agent_sdk/tools/cron_tools.py +164 -0
  28. open_agent_sdk/tools/edit.py +95 -0
  29. open_agent_sdk/tools/glob_tool.py +76 -0
  30. open_agent_sdk/tools/grep.py +158 -0
  31. open_agent_sdk/tools/lsp_tool.py +187 -0
  32. open_agent_sdk/tools/mcp_resource_tools.py +149 -0
  33. open_agent_sdk/tools/notebook_edit.py +124 -0
  34. open_agent_sdk/tools/plan_tools.py +84 -0
  35. open_agent_sdk/tools/read.py +84 -0
  36. open_agent_sdk/tools/send_message.py +87 -0
  37. open_agent_sdk/tools/skill_tool.py +117 -0
  38. open_agent_sdk/tools/task_tools.py +237 -0
  39. open_agent_sdk/tools/team_tools.py +98 -0
  40. open_agent_sdk/tools/todo_tool.py +112 -0
  41. open_agent_sdk/tools/tool_search.py +85 -0
  42. open_agent_sdk/tools/types.py +74 -0
  43. open_agent_sdk/tools/web_fetch.py +85 -0
  44. open_agent_sdk/tools/web_search.py +100 -0
  45. open_agent_sdk/tools/worktree_tools.py +160 -0
  46. open_agent_sdk/tools/write.py +54 -0
  47. open_agent_sdk/types.py +323 -0
  48. open_agent_sdk/utils/__init__.py +0 -0
  49. open_agent_sdk/utils/compact.py +200 -0
  50. open_agent_sdk/utils/context.py +152 -0
  51. open_agent_sdk/utils/messages.py +117 -0
  52. open_agent_sdk/utils/retry.py +114 -0
  53. open_agent_sdk/utils/tokens.py +119 -0
  54. open_agent_sdk_py-0.2.0.dist-info/METADATA +390 -0
  55. open_agent_sdk_py-0.2.0.dist-info/RECORD +57 -0
  56. open_agent_sdk_py-0.2.0.dist-info/WHEEL +4 -0
  57. open_agent_sdk_py-0.2.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,129 @@
1
+ """
2
+ open-agent-sdk - Python
3
+
4
+ Open-source Agent SDK by CodeAny (https://codeany.ai).
5
+ Runs the full agent loop in-process without spawning subprocesses.
6
+
7
+ Features:
8
+ - 10+ built-in tools (file I/O, shell, web, agents, etc.)
9
+ - MCP server integration (stdio, SSE, HTTP)
10
+ - Context compression (auto-compact, micro-compact)
11
+ - Retry with exponential backoff
12
+ - Git status & project context injection
13
+ - Multi-turn session persistence
14
+ - Permission system (allow/deny/bypass modes)
15
+ - Subagent spawning
16
+ - Hook system (pre/post tool use, lifecycle events)
17
+ - Token estimation & cost tracking
18
+ """
19
+
20
+ try:
21
+ from dotenv import load_dotenv
22
+
23
+ # Try to load .env from package root, parent dirs, or current dir
24
+ import pathlib
25
+
26
+ current_dir = pathlib.Path(__file__).parent.parent.parent
27
+ load_dotenv(str(current_dir / ".env"))
28
+ except ImportError:
29
+ pass
30
+
31
+
32
+ from open_agent_sdk.agent import Agent, create_agent, query
33
+ from open_agent_sdk.types import (
34
+ AgentOptions,
35
+ ToolDefinition,
36
+ ToolInputSchema,
37
+ ToolContext,
38
+ ToolResult,
39
+ SDKMessage,
40
+ SDKAssistantMessage,
41
+ SDKToolResultMessage,
42
+ SDKResultMessage,
43
+ SDKPartialMessage,
44
+ SDKSystemMessage,
45
+ TokenUsage,
46
+ ConversationMessage,
47
+ UserMessage,
48
+ AssistantMessage,
49
+ Message,
50
+ PermissionMode,
51
+ McpStdioConfig,
52
+ McpSseConfig,
53
+ McpHttpConfig,
54
+ McpServerConfig,
55
+ AgentDefinition,
56
+ ThinkingConfig,
57
+ QueryResult,
58
+ PermissionBehavior,
59
+ )
60
+ from open_agent_sdk.providers.types import ApiType, LLMProvider
61
+ from open_agent_sdk.providers import create_provider
62
+ from open_agent_sdk.hooks import HookRegistry, HookInput, HookOutput
63
+ from open_agent_sdk.skills import (
64
+ SkillDefinition,
65
+ SkillResult,
66
+ register_skill,
67
+ get_skill,
68
+ get_all_skills,
69
+ init_bundled_skills,
70
+ )
71
+ from open_agent_sdk.tools.types import define_tool, to_api_tool
72
+ from open_agent_sdk.tools import get_all_base_tools, filter_tools
73
+ from open_agent_sdk.sdk_mcp_server import create_sdk_mcp_server, is_sdk_server_config
74
+
75
+ __all__ = [
76
+ # Agent
77
+ "Agent",
78
+ "create_agent",
79
+ "query",
80
+ # Types
81
+ "AgentOptions",
82
+ "ToolDefinition",
83
+ "ToolInputSchema",
84
+ "ToolContext",
85
+ "ToolResult",
86
+ "SDKMessage",
87
+ "SDKAssistantMessage",
88
+ "SDKToolResultMessage",
89
+ "SDKResultMessage",
90
+ "SDKPartialMessage",
91
+ "SDKSystemMessage",
92
+ "TokenUsage",
93
+ "ConversationMessage",
94
+ "UserMessage",
95
+ "AssistantMessage",
96
+ "Message",
97
+ "PermissionMode",
98
+ "PermissionBehavior",
99
+ "McpStdioConfig",
100
+ "McpSseConfig",
101
+ "McpHttpConfig",
102
+ "McpServerConfig",
103
+ "AgentDefinition",
104
+ "ThinkingConfig",
105
+ "QueryResult",
106
+ # Provider
107
+ "ApiType",
108
+ "LLMProvider",
109
+ "create_provider",
110
+ # Hooks
111
+ "HookRegistry",
112
+ "HookInput",
113
+ "HookOutput",
114
+ # Skills
115
+ "SkillDefinition",
116
+ "SkillResult",
117
+ "register_skill",
118
+ "get_skill",
119
+ "get_all_skills",
120
+ "init_bundled_skills",
121
+ # Tool helpers
122
+ "define_tool",
123
+ "to_api_tool",
124
+ "get_all_base_tools",
125
+ "filter_tools",
126
+ # MCP
127
+ "create_sdk_mcp_server",
128
+ "is_sdk_server_config",
129
+ ]
@@ -0,0 +1,391 @@
1
+ """
2
+ Agent - High-level API.
3
+
4
+ Provides Agent class, create_agent(), and query() interfaces.
5
+
6
+ Usage:
7
+ from open_agent_sdk import create_agent
8
+
9
+ agent = create_agent(model="claude-sonnet-4-6")
10
+ async for event in agent.query("Hello"):
11
+ ...
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import os
17
+ import time
18
+ import traceback
19
+ import uuid
20
+ from typing import Any, AsyncGenerator
21
+
22
+ from open_agent_sdk.types import (
23
+ AgentOptions,
24
+ Message,
25
+ QueryEngineConfig,
26
+ QueryResult,
27
+ TokenUsage,
28
+ ToolDefinition,
29
+ UserMessage,
30
+ AssistantMessage,
31
+ ConversationMessage,
32
+ )
33
+ from open_agent_sdk.providers import create_provider, ApiType
34
+ from open_agent_sdk.engine import QueryEngine
35
+ from open_agent_sdk.tools import get_all_base_tools, filter_tools
36
+ from open_agent_sdk.tools.agent_tool import register_agents
37
+ from open_agent_sdk.session import save_session, load_session
38
+ from open_agent_sdk.sdk_mcp_server import is_sdk_server_config
39
+
40
+
41
+ class Agent:
42
+ """High-level agent interface with multi-turn support."""
43
+
44
+ def __init__(self, options: AgentOptions | None = None) -> None:
45
+ self._cfg = options or AgentOptions()
46
+ self._api_credentials = self._pick_credentials()
47
+ self._model_id = (
48
+ self._cfg.model or self._read_env("CODEANY_MODEL") or "claude-sonnet-4-6"
49
+ )
50
+ self._api_type = self._resolve_api_type()
51
+ self._provider = create_provider(
52
+ self._api_type,
53
+ api_key=self._api_credentials.get("key"),
54
+ base_url=self._api_credentials.get("base_url"),
55
+ )
56
+ self._sid = self._cfg.session_id or str(uuid.uuid4())
57
+ self._history: list[dict[str, Any]] = []
58
+ self._message_log: list[Message] = []
59
+ self._tool_pool: list[ToolDefinition] = self._build_tool_pool()
60
+ self._setup_done = False
61
+ self._hook_registry = self._cfg.hook_registry
62
+
63
+ def _pick_credentials(self) -> dict[str, str | None]:
64
+ env_map = self._cfg.env or {}
65
+ return {
66
+ "key": (
67
+ self._cfg.api_key
68
+ or env_map.get("CODEANY_API_KEY")
69
+ or env_map.get("CODEANY_AUTH_TOKEN")
70
+ or self._read_env("CODEANY_API_KEY")
71
+ or self._read_env("CODEANY_AUTH_TOKEN")
72
+ ),
73
+ "base_url": (
74
+ self._cfg.base_url
75
+ or env_map.get("CODEANY_BASE_URL")
76
+ or self._read_env("CODEANY_BASE_URL")
77
+ ),
78
+ }
79
+
80
+ @staticmethod
81
+ def _read_env(key: str) -> str | None:
82
+ val = os.environ.get(key)
83
+ return val if val else None
84
+
85
+ def _resolve_api_type(self) -> ApiType:
86
+ """Resolve API type from options, env, or model name heuristic."""
87
+ # Explicit option
88
+ if self._cfg.api_type:
89
+ return self._cfg.api_type
90
+
91
+ # Env var
92
+ env_map = self._cfg.env or {}
93
+ env_type = env_map.get("CODEANY_API_TYPE") or self._read_env("CODEANY_API_TYPE")
94
+ if env_type in ("openai-completions", "anthropic-messages"):
95
+ return env_type # type: ignore[return-value]
96
+
97
+ # Heuristic from model name
98
+ model = self._model_id.lower()
99
+ openai_patterns = (
100
+ "gpt-",
101
+ "o1",
102
+ "o3",
103
+ "o4",
104
+ "deepseek",
105
+ "qwen",
106
+ "yi-",
107
+ "glm",
108
+ "mistral",
109
+ "gemma",
110
+ )
111
+ if any(p in model for p in openai_patterns):
112
+ return "openai-completions"
113
+
114
+ return "anthropic-messages"
115
+
116
+ def _resolve_api_type(self) -> ApiType:
117
+ """Resolve API type from options, env, or model name heuristic."""
118
+ # Explicit option
119
+ if self._cfg.api_type:
120
+ return self._cfg.api_type
121
+
122
+ # Env var
123
+ env_map = self._cfg.env or {}
124
+ env_type = env_map.get("CODEANY_API_TYPE") or self._read_env("CODEANY_API_TYPE")
125
+ if env_type in ("openai-completions", "anthropic-messages"):
126
+ return env_type # type: ignore[return-value]
127
+
128
+ # Heuristic from model name
129
+ model = self._model_id.lower()
130
+ openai_patterns = (
131
+ "gpt-",
132
+ "o1",
133
+ "o3",
134
+ "o4",
135
+ "deepseek",
136
+ "qwen",
137
+ "yi-",
138
+ "glm",
139
+ "mistral",
140
+ "gemma",
141
+ )
142
+ if any(p in model for p in openai_patterns):
143
+ return "openai-completions"
144
+
145
+ return "anthropic-messages"
146
+
147
+ def _build_tool_pool(self) -> list[ToolDefinition]:
148
+ raw = self._cfg.tools
149
+ if raw is None:
150
+ pool = get_all_base_tools()
151
+ elif isinstance(raw, list) and raw and isinstance(raw[0], str):
152
+ pool = filter_tools(get_all_base_tools(), raw) # type: ignore[arg-type]
153
+ elif isinstance(raw, list):
154
+ pool = list(raw) # type: ignore[arg-type]
155
+ else:
156
+ pool = get_all_base_tools()
157
+
158
+ return filter_tools(pool, self._cfg.allowed_tools, self._cfg.disallowed_tools)
159
+
160
+ async def _setup(self) -> None:
161
+ if self._setup_done:
162
+ return
163
+ self._setup_done = True
164
+
165
+ # Register custom agent definitions
166
+ if self._cfg.agents:
167
+ register_agents(self._cfg.agents)
168
+
169
+ # Connect MCP servers
170
+ if self._cfg.mcp_servers:
171
+ for name, config in self._cfg.mcp_servers.items():
172
+ try:
173
+ if is_sdk_server_config(config):
174
+ self._tool_pool = [*self._tool_pool, *config.tools]
175
+ else:
176
+ from open_agent_sdk.mcp.client import connect_mcp_server
177
+
178
+ connection = await connect_mcp_server(name, config)
179
+ if connection.status == "connected" and connection.tools:
180
+ self._tool_pool = [*self._tool_pool, *connection.tools]
181
+ except Exception as e:
182
+ print(f"[MCP] Failed to connect to '{name}': {e}")
183
+
184
+ # Resume session
185
+ if self._cfg.resume:
186
+ session_data = await load_session(self._cfg.resume)
187
+ if session_data:
188
+ self._history = session_data.get("messages", [])
189
+ self._sid = self._cfg.resume
190
+
191
+ async def query(
192
+ self,
193
+ prompt: str,
194
+ **overrides: Any,
195
+ ) -> AsyncGenerator[dict[str, Any], None]:
196
+ """Run a query with streaming events."""
197
+ await self._setup()
198
+
199
+ cwd = overrides.get("cwd") or self._cfg.cwd or os.getcwd()
200
+ model = overrides.get("model") or self._cfg.model or self._model_id
201
+
202
+ # Resolve tools with overrides
203
+ tools = self._tool_pool
204
+ if "allowed_tools" in overrides or "disallowed_tools" in overrides:
205
+ tools = filter_tools(
206
+ tools,
207
+ overrides.get("allowed_tools"),
208
+ overrides.get("disallowed_tools"),
209
+ )
210
+ if "tools" in overrides:
211
+ ot = overrides["tools"]
212
+ if isinstance(ot, list) and ot and isinstance(ot[0], str):
213
+ tools = filter_tools(self._tool_pool, ot)
214
+ elif isinstance(ot, list):
215
+ tools = ot
216
+
217
+ # Recreate provider if overrides change credentials or apiType
218
+ provider = self._provider
219
+ if (
220
+ overrides.get("api_type")
221
+ or overrides.get("api_key")
222
+ or overrides.get("base_url")
223
+ ):
224
+ resolved_api_type = overrides.get("api_type") or self._api_type
225
+ provider = create_provider(
226
+ resolved_api_type,
227
+ api_key=overrides.get("api_key") or self._api_credentials.get("key"),
228
+ base_url=overrides.get("base_url")
229
+ or self._api_credentials.get("base_url"),
230
+ )
231
+
232
+ # Build query engine config
233
+ config = QueryEngineConfig(
234
+ cwd=cwd,
235
+ model=model,
236
+ provider=provider,
237
+ tools=tools,
238
+ system_prompt=overrides.get("system_prompt") or self._cfg.system_prompt,
239
+ append_system_prompt=overrides.get("append_system_prompt")
240
+ or self._cfg.append_system_prompt,
241
+ max_turns=overrides.get("max_turns", self._cfg.max_turns),
242
+ max_budget_usd=overrides.get("max_budget_usd") or self._cfg.max_budget_usd,
243
+ max_tokens=overrides.get("max_tokens", self._cfg.max_tokens),
244
+ thinking=overrides.get("thinking") or self._cfg.thinking,
245
+ include_partial_messages=overrides.get(
246
+ "include_partial_messages", self._cfg.include_partial_messages
247
+ ),
248
+ agents=overrides.get("agents") or self._cfg.agents,
249
+ hook_registry=self._hook_registry,
250
+ session_id=self._sid,
251
+ )
252
+
253
+ engine = QueryEngine(config)
254
+
255
+ # Inject existing conversation history
256
+ for msg in self._history:
257
+ engine.messages.append(msg)
258
+
259
+ # Run the engine
260
+ try:
261
+ async for event in engine.submit_message(prompt):
262
+ yield event
263
+
264
+ # Track assistant messages
265
+ if isinstance(event, dict) and event.get("type") == "assistant":
266
+ self._message_log.append(
267
+ AssistantMessage(
268
+ message=event.get("message", {}),
269
+ )
270
+ )
271
+ except Exception as exc:
272
+ yield {
273
+ "type": "result",
274
+ "subtype": "error_during_execution",
275
+ "usage": {
276
+ "input_tokens": 0,
277
+ "output_tokens": 0,
278
+ "cache_creation_input_tokens": 0,
279
+ "cache_read_input_tokens": 0,
280
+ },
281
+ "num_turns": 0,
282
+ "cost": 0.0,
283
+ "is_error": True,
284
+ "error": str(exc),
285
+ "traceback": traceback.format_exc(),
286
+ }
287
+
288
+ # Persist conversation state for multi-turn
289
+ self._history = engine.get_messages()
290
+
291
+ # Track user message
292
+ self._message_log.append(
293
+ UserMessage(
294
+ message=ConversationMessage(role="user", content=prompt),
295
+ )
296
+ )
297
+
298
+ # Save session if configured
299
+ if self._cfg.persist_session:
300
+ await save_session(
301
+ self._sid,
302
+ self._history,
303
+ {"model": model, "cwd": cwd},
304
+ )
305
+
306
+ async def prompt(
307
+ self,
308
+ text: str,
309
+ **overrides: Any,
310
+ ) -> QueryResult:
311
+ """Send a prompt and collect the final answer."""
312
+ t0 = time.monotonic()
313
+ collected_text = ""
314
+ collected_turns = 0
315
+ collected_usage = TokenUsage()
316
+ collected_error: str | None = None
317
+ is_error = False
318
+
319
+ async for ev in self.query(text, **overrides):
320
+ if not isinstance(ev, dict):
321
+ continue
322
+ if ev.get("type") == "assistant":
323
+ msg = ev.get("message", {})
324
+ fragments = [
325
+ b.get("text", "")
326
+ for b in msg.get("content", [])
327
+ if isinstance(b, dict) and b.get("type") == "text"
328
+ ]
329
+ if fragments:
330
+ collected_text = "".join(fragments)
331
+
332
+ elif ev.get("type") == "result":
333
+ collected_turns = ev.get("num_turns", 0)
334
+ usage_data = ev.get("usage", {})
335
+ if isinstance(usage_data, dict):
336
+ collected_usage = TokenUsage(**usage_data)
337
+ if ev.get("is_error"):
338
+ is_error = True
339
+ collected_error = ev.get("error")
340
+
341
+ duration_ms = round((time.monotonic() - t0) * 1000)
342
+
343
+ return QueryResult(
344
+ text=collected_text,
345
+ usage=collected_usage,
346
+ num_turns=collected_turns,
347
+ duration_ms=duration_ms,
348
+ messages=list(self._message_log),
349
+ is_error=is_error,
350
+ error=collected_error,
351
+ )
352
+
353
+ def get_messages(self) -> list[Message]:
354
+ """Get conversation messages."""
355
+ return list(self._message_log)
356
+
357
+ def clear(self) -> None:
358
+ """Reset conversation history."""
359
+ self._history = []
360
+ self._message_log = []
361
+
362
+ async def set_model(self, model: str) -> None:
363
+ self._model_id = model
364
+ self._cfg.model = model
365
+
366
+ @property
367
+ def session_id(self) -> str:
368
+ return self._sid
369
+
370
+
371
+ def create_agent(
372
+ model: str | None = None,
373
+ **kwargs: Any,
374
+ ) -> Agent:
375
+ """Create a new Agent instance.
376
+
377
+ Usage:
378
+ agent = create_agent(model="claude-sonnet-4-6", max_turns=10)
379
+ """
380
+ opts = AgentOptions(model=model, **kwargs)
381
+ return Agent(opts)
382
+
383
+
384
+ async def query(
385
+ prompt: str,
386
+ model: str | None = None,
387
+ **kwargs: Any,
388
+ ) -> QueryResult:
389
+ """One-shot query: create agent, send prompt, return result."""
390
+ agent = create_agent(model=model, **kwargs)
391
+ return await agent.prompt(prompt)