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.
- open_agent_sdk/__init__.py +129 -0
- open_agent_sdk/agent.py +391 -0
- open_agent_sdk/engine.py +600 -0
- open_agent_sdk/hooks.py +142 -0
- open_agent_sdk/mcp/__init__.py +0 -0
- open_agent_sdk/mcp/client.py +130 -0
- open_agent_sdk/providers/__init__.py +60 -0
- open_agent_sdk/providers/anthropic.py +99 -0
- open_agent_sdk/providers/openai.py +266 -0
- open_agent_sdk/providers/types.py +102 -0
- open_agent_sdk/sdk_mcp_server.py +61 -0
- open_agent_sdk/session.py +104 -0
- open_agent_sdk/skills/__init__.py +33 -0
- open_agent_sdk/skills/bundled/__init__.py +29 -0
- open_agent_sdk/skills/bundled/commit.py +45 -0
- open_agent_sdk/skills/bundled/debug.py +55 -0
- open_agent_sdk/skills/bundled/review.py +48 -0
- open_agent_sdk/skills/bundled/simplify.py +62 -0
- open_agent_sdk/skills/bundled/test_skill.py +50 -0
- open_agent_sdk/skills/registry.py +107 -0
- open_agent_sdk/skills/types.py +48 -0
- open_agent_sdk/tools/__init__.py +161 -0
- open_agent_sdk/tools/agent_tool.py +144 -0
- open_agent_sdk/tools/ask_user.py +73 -0
- open_agent_sdk/tools/bash.py +76 -0
- open_agent_sdk/tools/config_tool.py +78 -0
- open_agent_sdk/tools/cron_tools.py +164 -0
- open_agent_sdk/tools/edit.py +95 -0
- open_agent_sdk/tools/glob_tool.py +76 -0
- open_agent_sdk/tools/grep.py +158 -0
- open_agent_sdk/tools/lsp_tool.py +187 -0
- open_agent_sdk/tools/mcp_resource_tools.py +149 -0
- open_agent_sdk/tools/notebook_edit.py +124 -0
- open_agent_sdk/tools/plan_tools.py +84 -0
- open_agent_sdk/tools/read.py +84 -0
- open_agent_sdk/tools/send_message.py +87 -0
- open_agent_sdk/tools/skill_tool.py +117 -0
- open_agent_sdk/tools/task_tools.py +237 -0
- open_agent_sdk/tools/team_tools.py +98 -0
- open_agent_sdk/tools/todo_tool.py +112 -0
- open_agent_sdk/tools/tool_search.py +85 -0
- open_agent_sdk/tools/types.py +74 -0
- open_agent_sdk/tools/web_fetch.py +85 -0
- open_agent_sdk/tools/web_search.py +100 -0
- open_agent_sdk/tools/worktree_tools.py +160 -0
- open_agent_sdk/tools/write.py +54 -0
- open_agent_sdk/types.py +323 -0
- open_agent_sdk/utils/__init__.py +0 -0
- open_agent_sdk/utils/compact.py +200 -0
- open_agent_sdk/utils/context.py +152 -0
- open_agent_sdk/utils/messages.py +117 -0
- open_agent_sdk/utils/retry.py +114 -0
- open_agent_sdk/utils/tokens.py +119 -0
- open_agent_sdk_py-0.2.0.dist-info/METADATA +390 -0
- open_agent_sdk_py-0.2.0.dist-info/RECORD +57 -0
- open_agent_sdk_py-0.2.0.dist-info/WHEEL +4 -0
- 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
|
+
]
|
open_agent_sdk/agent.py
ADDED
|
@@ -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)
|