agentpool 2.1.9__py3-none-any.whl → 2.5.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 (311) hide show
  1. acp/__init__.py +13 -4
  2. acp/acp_requests.py +20 -77
  3. acp/agent/connection.py +8 -0
  4. acp/agent/implementations/debug_server/debug_server.py +6 -2
  5. acp/agent/protocol.py +6 -0
  6. acp/bridge/README.md +15 -2
  7. acp/bridge/__init__.py +3 -2
  8. acp/bridge/__main__.py +60 -19
  9. acp/bridge/ws_server.py +173 -0
  10. acp/bridge/ws_server_cli.py +89 -0
  11. acp/client/connection.py +38 -29
  12. acp/client/implementations/default_client.py +3 -2
  13. acp/client/implementations/headless_client.py +2 -2
  14. acp/connection.py +2 -2
  15. acp/notifications.py +20 -50
  16. acp/schema/__init__.py +2 -0
  17. acp/schema/agent_responses.py +21 -0
  18. acp/schema/client_requests.py +3 -3
  19. acp/schema/session_state.py +63 -29
  20. acp/stdio.py +39 -9
  21. acp/task/supervisor.py +2 -2
  22. acp/transports.py +362 -2
  23. acp/utils.py +17 -4
  24. agentpool/__init__.py +6 -1
  25. agentpool/agents/__init__.py +2 -0
  26. agentpool/agents/acp_agent/acp_agent.py +407 -277
  27. agentpool/agents/acp_agent/acp_converters.py +196 -38
  28. agentpool/agents/acp_agent/client_handler.py +191 -26
  29. agentpool/agents/acp_agent/session_state.py +17 -6
  30. agentpool/agents/agent.py +607 -572
  31. agentpool/agents/agui_agent/__init__.py +0 -2
  32. agentpool/agents/agui_agent/agui_agent.py +176 -110
  33. agentpool/agents/agui_agent/agui_converters.py +0 -131
  34. agentpool/agents/agui_agent/helpers.py +3 -4
  35. agentpool/agents/base_agent.py +632 -17
  36. agentpool/agents/claude_code_agent/FORKING.md +191 -0
  37. agentpool/agents/claude_code_agent/__init__.py +13 -1
  38. agentpool/agents/claude_code_agent/claude_code_agent.py +1058 -291
  39. agentpool/agents/claude_code_agent/converters.py +74 -143
  40. agentpool/agents/claude_code_agent/history.py +474 -0
  41. agentpool/agents/claude_code_agent/models.py +77 -0
  42. agentpool/agents/claude_code_agent/static_info.py +100 -0
  43. agentpool/agents/claude_code_agent/usage.py +242 -0
  44. agentpool/agents/context.py +40 -0
  45. agentpool/agents/events/__init__.py +24 -0
  46. agentpool/agents/events/builtin_handlers.py +67 -1
  47. agentpool/agents/events/event_emitter.py +32 -2
  48. agentpool/agents/events/events.py +104 -3
  49. agentpool/agents/events/infer_info.py +145 -0
  50. agentpool/agents/events/processors.py +254 -0
  51. agentpool/agents/interactions.py +41 -6
  52. agentpool/agents/modes.py +67 -0
  53. agentpool/agents/slashed_agent.py +5 -4
  54. agentpool/agents/tool_call_accumulator.py +213 -0
  55. agentpool/agents/tool_wrapping.py +18 -6
  56. agentpool/common_types.py +56 -21
  57. agentpool/config_resources/__init__.py +38 -1
  58. agentpool/config_resources/acp_assistant.yml +2 -2
  59. agentpool/config_resources/agents.yml +3 -0
  60. agentpool/config_resources/agents_template.yml +1 -0
  61. agentpool/config_resources/claude_code_agent.yml +10 -6
  62. agentpool/config_resources/external_acp_agents.yml +2 -1
  63. agentpool/delegation/base_team.py +4 -30
  64. agentpool/delegation/pool.py +136 -289
  65. agentpool/delegation/team.py +58 -57
  66. agentpool/delegation/teamrun.py +51 -55
  67. agentpool/diagnostics/__init__.py +53 -0
  68. agentpool/diagnostics/lsp_manager.py +1593 -0
  69. agentpool/diagnostics/lsp_proxy.py +41 -0
  70. agentpool/diagnostics/lsp_proxy_script.py +229 -0
  71. agentpool/diagnostics/models.py +398 -0
  72. agentpool/functional/run.py +10 -4
  73. agentpool/mcp_server/__init__.py +0 -2
  74. agentpool/mcp_server/client.py +76 -32
  75. agentpool/mcp_server/conversions.py +54 -13
  76. agentpool/mcp_server/manager.py +34 -54
  77. agentpool/mcp_server/registries/official_registry_client.py +35 -1
  78. agentpool/mcp_server/tool_bridge.py +186 -139
  79. agentpool/messaging/__init__.py +0 -2
  80. agentpool/messaging/compaction.py +72 -197
  81. agentpool/messaging/connection_manager.py +11 -10
  82. agentpool/messaging/event_manager.py +5 -5
  83. agentpool/messaging/message_container.py +6 -30
  84. agentpool/messaging/message_history.py +99 -8
  85. agentpool/messaging/messagenode.py +52 -14
  86. agentpool/messaging/messages.py +54 -35
  87. agentpool/messaging/processing.py +12 -22
  88. agentpool/models/__init__.py +1 -1
  89. agentpool/models/acp_agents/base.py +6 -24
  90. agentpool/models/acp_agents/mcp_capable.py +126 -157
  91. agentpool/models/acp_agents/non_mcp.py +129 -95
  92. agentpool/models/agents.py +98 -76
  93. agentpool/models/agui_agents.py +1 -1
  94. agentpool/models/claude_code_agents.py +144 -19
  95. agentpool/models/file_parsing.py +0 -1
  96. agentpool/models/manifest.py +113 -50
  97. agentpool/prompts/conversion_manager.py +1 -1
  98. agentpool/prompts/prompts.py +5 -2
  99. agentpool/repomap.py +1 -1
  100. agentpool/resource_providers/__init__.py +11 -1
  101. agentpool/resource_providers/aggregating.py +56 -5
  102. agentpool/resource_providers/base.py +70 -4
  103. agentpool/resource_providers/codemode/code_executor.py +72 -5
  104. agentpool/resource_providers/codemode/helpers.py +2 -2
  105. agentpool/resource_providers/codemode/provider.py +64 -12
  106. agentpool/resource_providers/codemode/remote_mcp_execution.py +2 -2
  107. agentpool/resource_providers/codemode/remote_provider.py +9 -12
  108. agentpool/resource_providers/filtering.py +3 -1
  109. agentpool/resource_providers/mcp_provider.py +89 -12
  110. agentpool/resource_providers/plan_provider.py +228 -46
  111. agentpool/resource_providers/pool.py +7 -3
  112. agentpool/resource_providers/resource_info.py +111 -0
  113. agentpool/resource_providers/static.py +4 -2
  114. agentpool/sessions/__init__.py +4 -1
  115. agentpool/sessions/manager.py +33 -5
  116. agentpool/sessions/models.py +59 -6
  117. agentpool/sessions/protocol.py +28 -0
  118. agentpool/sessions/session.py +11 -55
  119. agentpool/skills/registry.py +13 -8
  120. agentpool/storage/manager.py +572 -49
  121. agentpool/talk/registry.py +4 -4
  122. agentpool/talk/talk.py +9 -10
  123. agentpool/testing.py +538 -20
  124. agentpool/tool_impls/__init__.py +6 -0
  125. agentpool/tool_impls/agent_cli/__init__.py +42 -0
  126. agentpool/tool_impls/agent_cli/tool.py +95 -0
  127. agentpool/tool_impls/bash/__init__.py +64 -0
  128. agentpool/tool_impls/bash/helpers.py +35 -0
  129. agentpool/tool_impls/bash/tool.py +171 -0
  130. agentpool/tool_impls/delete_path/__init__.py +70 -0
  131. agentpool/tool_impls/delete_path/tool.py +142 -0
  132. agentpool/tool_impls/download_file/__init__.py +80 -0
  133. agentpool/tool_impls/download_file/tool.py +183 -0
  134. agentpool/tool_impls/execute_code/__init__.py +55 -0
  135. agentpool/tool_impls/execute_code/tool.py +163 -0
  136. agentpool/tool_impls/grep/__init__.py +80 -0
  137. agentpool/tool_impls/grep/tool.py +200 -0
  138. agentpool/tool_impls/list_directory/__init__.py +73 -0
  139. agentpool/tool_impls/list_directory/tool.py +197 -0
  140. agentpool/tool_impls/question/__init__.py +42 -0
  141. agentpool/tool_impls/question/tool.py +127 -0
  142. agentpool/tool_impls/read/__init__.py +104 -0
  143. agentpool/tool_impls/read/tool.py +305 -0
  144. agentpool/tools/__init__.py +2 -1
  145. agentpool/tools/base.py +114 -34
  146. agentpool/tools/manager.py +57 -1
  147. agentpool/ui/base.py +2 -2
  148. agentpool/ui/mock_provider.py +2 -2
  149. agentpool/ui/stdlib_provider.py +2 -2
  150. agentpool/utils/file_watcher.py +269 -0
  151. agentpool/utils/identifiers.py +121 -0
  152. agentpool/utils/pydantic_ai_helpers.py +46 -0
  153. agentpool/utils/streams.py +616 -2
  154. agentpool/utils/subprocess_utils.py +155 -0
  155. agentpool/utils/token_breakdown.py +461 -0
  156. agentpool/vfs_registry.py +7 -2
  157. {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/METADATA +41 -27
  158. agentpool-2.5.0.dist-info/RECORD +579 -0
  159. {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/WHEEL +1 -1
  160. agentpool_cli/__main__.py +24 -0
  161. agentpool_cli/create.py +1 -1
  162. agentpool_cli/serve_acp.py +100 -21
  163. agentpool_cli/serve_agui.py +87 -0
  164. agentpool_cli/serve_opencode.py +119 -0
  165. agentpool_cli/ui.py +557 -0
  166. agentpool_commands/__init__.py +42 -5
  167. agentpool_commands/agents.py +75 -2
  168. agentpool_commands/history.py +62 -0
  169. agentpool_commands/mcp.py +176 -0
  170. agentpool_commands/models.py +56 -3
  171. agentpool_commands/pool.py +260 -0
  172. agentpool_commands/session.py +1 -1
  173. agentpool_commands/text_sharing/__init__.py +119 -0
  174. agentpool_commands/text_sharing/base.py +123 -0
  175. agentpool_commands/text_sharing/github_gist.py +80 -0
  176. agentpool_commands/text_sharing/opencode.py +462 -0
  177. agentpool_commands/text_sharing/paste_rs.py +59 -0
  178. agentpool_commands/text_sharing/pastebin.py +116 -0
  179. agentpool_commands/text_sharing/shittycodingagent.py +112 -0
  180. agentpool_commands/tools.py +57 -0
  181. agentpool_commands/utils.py +80 -30
  182. agentpool_config/__init__.py +30 -2
  183. agentpool_config/agentpool_tools.py +498 -0
  184. agentpool_config/builtin_tools.py +77 -22
  185. agentpool_config/commands.py +24 -1
  186. agentpool_config/compaction.py +258 -0
  187. agentpool_config/converters.py +1 -1
  188. agentpool_config/event_handlers.py +42 -0
  189. agentpool_config/events.py +1 -1
  190. agentpool_config/forward_targets.py +1 -4
  191. agentpool_config/jinja.py +3 -3
  192. agentpool_config/mcp_server.py +132 -6
  193. agentpool_config/nodes.py +1 -1
  194. agentpool_config/observability.py +44 -0
  195. agentpool_config/session.py +0 -3
  196. agentpool_config/storage.py +82 -38
  197. agentpool_config/task.py +3 -3
  198. agentpool_config/tools.py +11 -22
  199. agentpool_config/toolsets.py +109 -233
  200. agentpool_server/a2a_server/agent_worker.py +307 -0
  201. agentpool_server/a2a_server/server.py +23 -18
  202. agentpool_server/acp_server/acp_agent.py +234 -181
  203. agentpool_server/acp_server/commands/acp_commands.py +151 -156
  204. agentpool_server/acp_server/commands/docs_commands/fetch_repo.py +18 -17
  205. agentpool_server/acp_server/event_converter.py +651 -0
  206. agentpool_server/acp_server/input_provider.py +53 -10
  207. agentpool_server/acp_server/server.py +24 -90
  208. agentpool_server/acp_server/session.py +173 -331
  209. agentpool_server/acp_server/session_manager.py +8 -34
  210. agentpool_server/agui_server/server.py +3 -1
  211. agentpool_server/mcp_server/server.py +5 -2
  212. agentpool_server/opencode_server/.rules +95 -0
  213. agentpool_server/opencode_server/ENDPOINTS.md +401 -0
  214. agentpool_server/opencode_server/OPENCODE_UI_TOOLS_COMPLETE.md +202 -0
  215. agentpool_server/opencode_server/__init__.py +19 -0
  216. agentpool_server/opencode_server/command_validation.py +172 -0
  217. agentpool_server/opencode_server/converters.py +975 -0
  218. agentpool_server/opencode_server/dependencies.py +24 -0
  219. agentpool_server/opencode_server/input_provider.py +421 -0
  220. agentpool_server/opencode_server/models/__init__.py +250 -0
  221. agentpool_server/opencode_server/models/agent.py +53 -0
  222. agentpool_server/opencode_server/models/app.py +72 -0
  223. agentpool_server/opencode_server/models/base.py +26 -0
  224. agentpool_server/opencode_server/models/common.py +23 -0
  225. agentpool_server/opencode_server/models/config.py +37 -0
  226. agentpool_server/opencode_server/models/events.py +821 -0
  227. agentpool_server/opencode_server/models/file.py +88 -0
  228. agentpool_server/opencode_server/models/mcp.py +44 -0
  229. agentpool_server/opencode_server/models/message.py +179 -0
  230. agentpool_server/opencode_server/models/parts.py +323 -0
  231. agentpool_server/opencode_server/models/provider.py +81 -0
  232. agentpool_server/opencode_server/models/pty.py +43 -0
  233. agentpool_server/opencode_server/models/question.py +56 -0
  234. agentpool_server/opencode_server/models/session.py +111 -0
  235. agentpool_server/opencode_server/routes/__init__.py +29 -0
  236. agentpool_server/opencode_server/routes/agent_routes.py +473 -0
  237. agentpool_server/opencode_server/routes/app_routes.py +202 -0
  238. agentpool_server/opencode_server/routes/config_routes.py +302 -0
  239. agentpool_server/opencode_server/routes/file_routes.py +571 -0
  240. agentpool_server/opencode_server/routes/global_routes.py +94 -0
  241. agentpool_server/opencode_server/routes/lsp_routes.py +319 -0
  242. agentpool_server/opencode_server/routes/message_routes.py +761 -0
  243. agentpool_server/opencode_server/routes/permission_routes.py +63 -0
  244. agentpool_server/opencode_server/routes/pty_routes.py +300 -0
  245. agentpool_server/opencode_server/routes/question_routes.py +128 -0
  246. agentpool_server/opencode_server/routes/session_routes.py +1276 -0
  247. agentpool_server/opencode_server/routes/tui_routes.py +139 -0
  248. agentpool_server/opencode_server/server.py +475 -0
  249. agentpool_server/opencode_server/state.py +151 -0
  250. agentpool_server/opencode_server/time_utils.py +8 -0
  251. agentpool_storage/__init__.py +12 -0
  252. agentpool_storage/base.py +184 -2
  253. agentpool_storage/claude_provider/ARCHITECTURE.md +433 -0
  254. agentpool_storage/claude_provider/__init__.py +42 -0
  255. agentpool_storage/claude_provider/provider.py +1089 -0
  256. agentpool_storage/file_provider.py +278 -15
  257. agentpool_storage/memory_provider.py +193 -12
  258. agentpool_storage/models.py +3 -0
  259. agentpool_storage/opencode_provider/ARCHITECTURE.md +386 -0
  260. agentpool_storage/opencode_provider/__init__.py +16 -0
  261. agentpool_storage/opencode_provider/helpers.py +414 -0
  262. agentpool_storage/opencode_provider/provider.py +895 -0
  263. agentpool_storage/project_store.py +325 -0
  264. agentpool_storage/session_store.py +26 -6
  265. agentpool_storage/sql_provider/__init__.py +4 -2
  266. agentpool_storage/sql_provider/models.py +48 -0
  267. agentpool_storage/sql_provider/sql_provider.py +269 -3
  268. agentpool_storage/sql_provider/utils.py +12 -13
  269. agentpool_storage/zed_provider/__init__.py +16 -0
  270. agentpool_storage/zed_provider/helpers.py +281 -0
  271. agentpool_storage/zed_provider/models.py +130 -0
  272. agentpool_storage/zed_provider/provider.py +442 -0
  273. agentpool_storage/zed_provider.py +803 -0
  274. agentpool_toolsets/__init__.py +0 -2
  275. agentpool_toolsets/builtin/__init__.py +2 -12
  276. agentpool_toolsets/builtin/code.py +96 -57
  277. agentpool_toolsets/builtin/debug.py +118 -48
  278. agentpool_toolsets/builtin/execution_environment.py +115 -230
  279. agentpool_toolsets/builtin/file_edit/file_edit.py +115 -7
  280. agentpool_toolsets/builtin/skills.py +9 -4
  281. agentpool_toolsets/builtin/subagent_tools.py +64 -51
  282. agentpool_toolsets/builtin/workers.py +4 -2
  283. agentpool_toolsets/composio_toolset.py +2 -2
  284. agentpool_toolsets/entry_points.py +3 -1
  285. agentpool_toolsets/fsspec_toolset/__init__.py +13 -1
  286. agentpool_toolsets/fsspec_toolset/diagnostics.py +860 -73
  287. agentpool_toolsets/fsspec_toolset/grep.py +99 -7
  288. agentpool_toolsets/fsspec_toolset/helpers.py +3 -2
  289. agentpool_toolsets/fsspec_toolset/image_utils.py +161 -0
  290. agentpool_toolsets/fsspec_toolset/toolset.py +500 -95
  291. agentpool_toolsets/mcp_discovery/__init__.py +5 -0
  292. agentpool_toolsets/mcp_discovery/data/mcp_servers.parquet +0 -0
  293. agentpool_toolsets/mcp_discovery/toolset.py +511 -0
  294. agentpool_toolsets/mcp_run_toolset.py +87 -12
  295. agentpool_toolsets/notifications.py +33 -33
  296. agentpool_toolsets/openapi.py +3 -1
  297. agentpool_toolsets/search_toolset.py +3 -1
  298. agentpool-2.1.9.dist-info/RECORD +0 -474
  299. agentpool_config/resources.py +0 -33
  300. agentpool_server/acp_server/acp_tools.py +0 -43
  301. agentpool_server/acp_server/commands/spawn.py +0 -210
  302. agentpool_storage/text_log_provider.py +0 -275
  303. agentpool_toolsets/builtin/agent_management.py +0 -239
  304. agentpool_toolsets/builtin/chain.py +0 -288
  305. agentpool_toolsets/builtin/history.py +0 -36
  306. agentpool_toolsets/builtin/integration.py +0 -85
  307. agentpool_toolsets/builtin/tool_management.py +0 -90
  308. agentpool_toolsets/builtin/user_interaction.py +0 -52
  309. agentpool_toolsets/semantic_memory_toolset.py +0 -536
  310. {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/entry_points.txt +0 -0
  311. {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/licenses/LICENSE +0 -0
agentpool/agents/agent.py CHANGED
@@ -3,83 +3,86 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
- from collections.abc import Awaitable, Callable
7
- from contextlib import AsyncExitStack, asynccontextmanager, suppress
8
- from dataclasses import dataclass, field, replace
6
+ from collections.abc import Awaitable
7
+ from contextlib import AsyncExitStack, asynccontextmanager
8
+ from dataclasses import replace
9
9
  import time
10
10
  from typing import TYPE_CHECKING, Any, Self, TypedDict, TypeVar, overload
11
11
  from uuid import uuid4
12
12
 
13
13
  from anyenv import method_spawner
14
- import anyio
15
- from llmling_models import function_to_model, infer_model
16
14
  import logfire
17
- from psygnal import Signal
18
15
  from pydantic import ValidationError
19
16
  from pydantic._internal import _typing_extra
20
17
  from pydantic_ai import (
21
18
  Agent as PydanticAgent,
22
- AgentRunResultEvent,
23
19
  BaseToolCallPart,
20
+ CallToolsNode,
24
21
  FunctionToolCallEvent,
25
22
  FunctionToolResultEvent,
26
- PartDeltaEvent,
23
+ ModelRequestNode,
27
24
  PartStartEvent,
28
25
  RunContext,
29
- TextPart,
30
- TextPartDelta,
31
26
  ToolReturnPart,
32
27
  )
28
+ from pydantic_ai.models import Model
33
29
 
34
30
  from agentpool.agents.base_agent import BaseAgent
35
- from agentpool.agents.events import RunStartedEvent, StreamCompleteEvent, ToolCallCompleteEvent
31
+ from agentpool.agents.events import (
32
+ RunStartedEvent,
33
+ StreamCompleteEvent,
34
+ ToolCallCompleteEvent,
35
+ )
36
+ from agentpool.agents.events.processors import FileTracker
37
+ from agentpool.agents.modes import ModeInfo
36
38
  from agentpool.log import get_logger
37
- from agentpool.messaging import ChatMessage, MessageHistory, MessageNode
38
- from agentpool.messaging.processing import prepare_prompts
39
+ from agentpool.messaging import ChatMessage, MessageHistory
39
40
  from agentpool.prompts.convert import convert_prompts
40
41
  from agentpool.storage import StorageManager
41
- from agentpool.talk.stats import MessageStats
42
42
  from agentpool.tools import Tool, ToolManager
43
43
  from agentpool.tools.exceptions import ToolError
44
- from agentpool.utils.inspection import call_with_context, get_argument_key
45
- from agentpool.utils.now import get_now
44
+ from agentpool.utils.inspection import get_argument_key
45
+ from agentpool.utils.pydantic_ai_helpers import safe_args_as_dict
46
46
  from agentpool.utils.result_utils import to_type
47
47
  from agentpool.utils.streams import merge_queue_into_iterator
48
48
 
49
49
 
50
- TResult = TypeVar("TResult")
51
-
52
-
53
50
  if TYPE_CHECKING:
54
- from collections.abc import AsyncIterator, Coroutine, Sequence
55
- from datetime import datetime
51
+ from collections.abc import AsyncIterator, Callable, Coroutine, Sequence
56
52
  from types import TracebackType
57
53
 
58
54
  from exxec import ExecutionEnvironment
59
- from pydantic_ai import UsageLimits
55
+ from llmling_models_config import AnyModelConfig
56
+ from pydantic_ai import UsageLimits, UserContent
57
+ from pydantic_ai.builtin_tools import AbstractBuiltinTool
60
58
  from pydantic_ai.output import OutputSpec
61
59
  from pydantic_ai.settings import ModelSettings
60
+ from slashed import BaseCommand
61
+ from tokonomics.model_discovery import ProviderType
62
+ from tokonomics.model_discovery.model_info import ModelInfo
62
63
  from toprompt import AnyPromptType
63
64
  from upathtools import JoinablePathLike
64
65
 
65
66
  from agentpool.agents import AgentContext
66
67
  from agentpool.agents.events import RichAgentStreamEvent
68
+ from agentpool.agents.modes import ModeCategory
67
69
  from agentpool.common_types import (
68
- AgentName,
69
70
  BuiltinEventHandlerType,
70
71
  EndStrategy,
71
72
  IndividualEventHandler,
72
73
  ModelType,
73
74
  ProcessorCallback,
74
- PromptCompatible,
75
75
  SessionIdType,
76
76
  ToolType,
77
77
  )
78
- from agentpool.delegation import AgentPool, Team, TeamRun
78
+ from agentpool.delegation import AgentPool
79
79
  from agentpool.hooks import AgentHooks
80
- from agentpool.models.agents import AutoCache, NativeAgentConfig, ToolMode
80
+ from agentpool.messaging import MessageNode
81
+ from agentpool.models.agents import NativeAgentConfig, ToolMode
82
+ from agentpool.models.manifest import AgentsManifest
81
83
  from agentpool.prompts.prompts import PromptType
82
84
  from agentpool.resource_providers import ResourceProvider
85
+ from agentpool.tools.base import FunctionTool
83
86
  from agentpool.ui.base import InputProvider
84
87
  from agentpool_config.knowledge import Knowledge
85
88
  from agentpool_config.mcp_server import MCPServerConfig
@@ -92,6 +95,36 @@ logger = get_logger(__name__)
92
95
  # OutputDataT = TypeVar('OutputDataT', default=str, covariant=True)
93
96
  NoneType = type(None)
94
97
 
98
+ TResult = TypeVar("TResult")
99
+
100
+
101
+ def _extract_text_from_messages(
102
+ messages: list[Any], include_interruption_note: bool = False
103
+ ) -> str:
104
+ """Extract text content from pydantic-ai messages.
105
+
106
+ Args:
107
+ messages: List of ModelRequest/ModelResponse messages
108
+ include_interruption_note: Whether to append interruption notice
109
+
110
+ Returns:
111
+ Concatenated text content from all ModelResponse TextParts
112
+ """
113
+ from pydantic_ai.messages import ModelResponse, TextPart as PydanticTextPart
114
+
115
+ content = "".join(
116
+ part.content
117
+ for msg in messages
118
+ if isinstance(msg, ModelResponse)
119
+ for part in msg.parts
120
+ if isinstance(part, PydanticTextPart)
121
+ )
122
+ if include_interruption_note:
123
+ if content:
124
+ content += "\n\n"
125
+ content += "[Request interrupted by user]"
126
+ return content
127
+
95
128
 
96
129
  class AgentKwargs(TypedDict, total=False):
97
130
  """Keyword arguments for configuring an Agent instance."""
@@ -111,9 +144,11 @@ class AgentKwargs(TypedDict, total=False):
111
144
  input_provider: InputProvider | None
112
145
  event_handlers: Sequence[IndividualEventHandler | BuiltinEventHandlerType] | None
113
146
  env: ExecutionEnvironment | None
114
- auto_cache: AutoCache
147
+
115
148
  hooks: AgentHooks | None
116
149
  model_settings: ModelSettings | None
150
+ usage_limits: UsageLimits | None
151
+ providers: Sequence[ProviderType] | None
117
152
 
118
153
 
119
154
  class Agent[TDeps = None, OutputDataT = str](BaseAgent[TDeps, OutputDataT]):
@@ -122,25 +157,13 @@ class Agent[TDeps = None, OutputDataT = str](BaseAgent[TDeps, OutputDataT]):
122
157
  Generically typed with: Agent[Type of Dependencies, Type of Result]
123
158
  """
124
159
 
125
- @dataclass(frozen=True)
126
- class AgentReset:
127
- """Emitted when agent is reset."""
128
-
129
- agent_name: AgentName
130
- previous_tools: dict[str, bool]
131
- new_tools: dict[str, bool]
132
- timestamp: datetime = field(default_factory=get_now)
133
-
134
- run_failed = Signal(str, Exception)
135
- agent_reset = Signal(AgentReset)
136
-
137
- def __init__(
160
+ def __init__( # noqa: PLR0915
138
161
  # we dont use AgentKwargs here so that we can work with explicit ones in the ctor
139
162
  self,
140
163
  name: str = "agentpool",
141
164
  *,
142
165
  deps_type: type[TDeps] | None = None,
143
- model: ModelType = None,
166
+ model: ModelType,
144
167
  output_type: OutputSpec[OutputDataT] = str, # type: ignore[assignment]
145
168
  # context: AgentContext[TDeps] | None = None,
146
169
  session: SessionIdType | SessionQuery | MemoryConfig | bool | int = None,
@@ -164,9 +187,12 @@ class Agent[TDeps = None, OutputDataT = str](BaseAgent[TDeps, OutputDataT]):
164
187
  knowledge: Knowledge | None = None,
165
188
  agent_config: NativeAgentConfig | None = None,
166
189
  env: ExecutionEnvironment | None = None,
167
- auto_cache: AutoCache = "off",
168
190
  hooks: AgentHooks | None = None,
169
191
  tool_confirmation_mode: ToolConfirmationMode = "per_tool",
192
+ builtin_tools: Sequence[AbstractBuiltinTool] | None = None,
193
+ usage_limits: UsageLimits | None = None,
194
+ providers: Sequence[ProviderType] | None = None,
195
+ commands: Sequence[BaseCommand] | None = None,
170
196
  ) -> None:
171
197
  """Initialize agent.
172
198
 
@@ -206,17 +232,24 @@ class Agent[TDeps = None, OutputDataT = str](BaseAgent[TDeps, OutputDataT]):
206
232
  knowledge: Knowledge sources for this agent
207
233
  agent_config: Agent configuration
208
234
  env: Execution environment for code/command execution and filesystem access
209
- auto_cache: Automatic caching configuration ("off", "5m", or "1h")
210
235
  hooks: AgentHooks instance for intercepting agent behavior at run and tool events
211
236
  tool_confirmation_mode: Tool confirmation mode
237
+ builtin_tools: PydanticAI builtin tools (WebSearchTool, CodeExecutionTool, etc.)
238
+ usage_limits: Per-request usage limits (applied to each run() call independently,
239
+ not cumulative across the session)
240
+ providers: Model providers for model discovery (e.g., ["openai", "anthropic"]).
241
+ Defaults to ["models.dev"] if not specified.
242
+ commands: Slash commands
212
243
  """
244
+ from llmling_models_config import StringModelConfig
245
+
213
246
  from agentpool.agents.interactions import Interactions
214
247
  from agentpool.agents.sys_prompts import SystemPrompts
215
248
  from agentpool.models.agents import NativeAgentConfig
216
249
  from agentpool.prompts.conversion_manager import ConversionManager
250
+ from agentpool_commands.pool import CompactCommand
217
251
  from agentpool_config.session import MemoryConfig
218
252
 
219
- self._infinite = False
220
253
  self.deps_type = deps_type
221
254
  self.model_settings = model_settings
222
255
  memory_cfg = (
@@ -226,7 +259,10 @@ class Agent[TDeps = None, OutputDataT = str](BaseAgent[TDeps, OutputDataT]):
226
259
  all_mcp_servers = list(mcp_servers) if mcp_servers else []
227
260
  if agent_config and agent_config.mcp_servers:
228
261
  all_mcp_servers.extend(agent_config.get_mcp_servers())
229
-
262
+ # Add CompactCommand - only makes sense for Native Agent (has own history)
263
+ # Other agents (ClaudeCode, ACP, AGUI) don't control their history directly
264
+ all_commands = list(commands) if commands else []
265
+ all_commands.append(CompactCommand())
230
266
  # Call base class with shared parameters
231
267
  super().__init__(
232
268
  name=name,
@@ -241,11 +277,28 @@ class Agent[TDeps = None, OutputDataT = str](BaseAgent[TDeps, OutputDataT]):
241
277
  output_type=to_type(output_type), # type: ignore[arg-type]
242
278
  tool_confirmation_mode=tool_confirmation_mode,
243
279
  event_handlers=event_handlers,
280
+ commands=all_commands,
244
281
  )
245
282
 
246
283
  # Store config for context creation
247
- self._agent_config = agent_config or NativeAgentConfig(name=name)
284
+ # Convert model to proper config type for NativeAgentConfig
248
285
 
286
+ config_model: AnyModelConfig
287
+ if isinstance(model, Model):
288
+ config_model = StringModelConfig(
289
+ identifier=model.model_name,
290
+ **({"model_settings": model._settings} if model._settings else {}),
291
+ )
292
+ elif isinstance(model, str):
293
+ config_model = StringModelConfig(
294
+ identifier=model,
295
+ **({"model_settings": model_settings} if model_settings else {}),
296
+ )
297
+ else:
298
+ config_model = model
299
+ self._agent_config = agent_config or NativeAgentConfig(name=name, model=config_model)
300
+ # Store builtin tools for pydantic-ai
301
+ self._builtin_tools = list(builtin_tools) if builtin_tools else []
249
302
  # Override tools with Agent-specific ToolManager (with tools and tool_mode)
250
303
  all_tools = list(tools or [])
251
304
  self.tools = ToolManager(all_tools, tool_mode=tool_mode)
@@ -253,7 +306,6 @@ class Agent[TDeps = None, OutputDataT = str](BaseAgent[TDeps, OutputDataT]):
253
306
  self.tools.add_provider(toolset_provider)
254
307
  aggregating_provider = self.mcp.get_aggregating_provider()
255
308
  self.tools.add_provider(aggregating_provider)
256
-
257
309
  # Override conversation with Agent-specific MessageHistory (with storage, etc.)
258
310
  resources = list(resources)
259
311
  if knowledge:
@@ -265,14 +317,17 @@ class Agent[TDeps = None, OutputDataT = str](BaseAgent[TDeps, OutputDataT]):
265
317
  session_config=memory_cfg,
266
318
  resources=resources,
267
319
  )
268
- self._model = infer_model(model) if isinstance(model, str) else model
320
+ if isinstance(model, str):
321
+ self._model, settings = self._resolve_model_string(model)
322
+ if settings:
323
+ self.model_settings = settings
324
+ else:
325
+ self._model = model
269
326
  self._retries = retries
270
327
  self._end_strategy: EndStrategy = end_strategy
271
328
  self._output_retries = output_retries
272
329
  self.parallel_init = parallel_init
273
- self._background_task: asyncio.Task[ChatMessage[Any]] | None = None
274
330
  self.talk = Interactions(self)
275
-
276
331
  # Set up system prompts
277
332
  all_prompts: list[AnyPromptType] = []
278
333
  if isinstance(system_prompt, (list, tuple)):
@@ -280,12 +335,12 @@ class Agent[TDeps = None, OutputDataT = str](BaseAgent[TDeps, OutputDataT]):
280
335
  elif system_prompt:
281
336
  all_prompts.append(system_prompt)
282
337
  self.sys_prompts = SystemPrompts(all_prompts, prompt_manager=self._manifest.prompt_manager)
283
-
284
338
  # Store hooks
285
339
  self.hooks = hooks
286
-
287
- # Store auto_cache setting
288
- self._auto_cache: AutoCache = auto_cache
340
+ # Store default usage limits
341
+ self._default_usage_limits = usage_limits
342
+ # Store providers for model discovery
343
+ self._providers = list(providers) if providers else None
289
344
 
290
345
  def __repr__(self) -> str:
291
346
  desc = f", {self.description!r}" if self.description else ""
@@ -300,6 +355,143 @@ class Agent[TDeps = None, OutputDataT = str](BaseAgent[TDeps, OutputDataT]):
300
355
  parts.extend([await self.tools.__prompt__(), self.conversation.__prompt__()])
301
356
  return "\n".join(parts)
302
357
 
358
+ @classmethod
359
+ def from_config( # noqa: PLR0915
360
+ cls,
361
+ config: NativeAgentConfig,
362
+ *,
363
+ name: str | None = None,
364
+ manifest: AgentsManifest | None = None,
365
+ event_handlers: Sequence[IndividualEventHandler | BuiltinEventHandlerType] | None = None,
366
+ input_provider: InputProvider | None = None,
367
+ agent_pool: AgentPool[Any] | None = None,
368
+ deps_type: type[TDeps] | None = None,
369
+ ) -> Self:
370
+ """Create a native Agent from a config object.
371
+
372
+ This is the preferred way to instantiate an Agent from configuration.
373
+ Handles system prompt resolution, model resolution, toolsets setup, etc.
374
+
375
+ Args:
376
+ config: Native agent configuration
377
+ name: Optional name override (used for manifest lookups, defaults to config.name)
378
+ manifest: Optional manifest for resolving prompts, models, output types.
379
+ If not provided, uses agent_pool.manifest or creates empty one.
380
+ event_handlers: Optional event handlers (merged with config handlers)
381
+ input_provider: Optional input provider for user interactions
382
+ agent_pool: Optional agent pool for coordination
383
+ deps_type: Optional dependency type
384
+
385
+ Returns:
386
+ Configured Agent instance
387
+ """
388
+ from pathlib import Path
389
+
390
+ from agentpool.models.manifest import AgentsManifest
391
+ from agentpool.utils.result_utils import to_type
392
+ from agentpool_config.system_prompts import (
393
+ FilePromptConfig,
394
+ FunctionPromptConfig,
395
+ LibraryPromptConfig,
396
+ StaticPromptConfig,
397
+ )
398
+
399
+ # Get manifest from pool or create empty one
400
+ if manifest is None:
401
+ manifest = agent_pool.manifest if agent_pool else AgentsManifest()
402
+
403
+ # Use provided name, fall back to config.name, then default
404
+ name = name or config.name or "agent"
405
+
406
+ # Normalize system_prompt to a list for iteration
407
+ sys_prompts: list[str] = []
408
+ prompt_source = config.system_prompt
409
+ if prompt_source is not None:
410
+ prompts_to_process = (
411
+ [prompt_source] if isinstance(prompt_source, str) else prompt_source
412
+ )
413
+ for prompt in prompts_to_process:
414
+ match prompt:
415
+ case (str() as sys_prompt) | StaticPromptConfig(content=sys_prompt):
416
+ sys_prompts.append(sys_prompt)
417
+ case FilePromptConfig(path=path, variables=variables):
418
+ template_path = Path(path)
419
+ if not template_path.is_absolute() and config.config_file_path:
420
+ template_path = Path(config.config_file_path).parent / path
421
+ template_content = template_path.read_text("utf-8")
422
+ if variables:
423
+ from jinja2 import Template
424
+
425
+ template = Template(template_content)
426
+ content = template.render(**variables)
427
+ else:
428
+ content = template_content
429
+ sys_prompts.append(content)
430
+ case LibraryPromptConfig(reference=reference):
431
+ try:
432
+ content = manifest.prompt_manager.get.sync(reference)
433
+ sys_prompts.append(content)
434
+ except Exception as e:
435
+ msg = f"Failed to load library prompt {reference!r} for agent {name}"
436
+ logger.exception(msg)
437
+ raise ValueError(msg) from e
438
+ case FunctionPromptConfig(function=function, arguments=arguments):
439
+ content = function(**arguments)
440
+ sys_prompts.append(content)
441
+
442
+ # Prepare toolsets list
443
+ toolsets_list = config.get_toolsets()
444
+ if config_tool_provider := config.get_tool_provider():
445
+ toolsets_list.append(config_tool_provider)
446
+ # Convert workers config to a toolset (backwards compatibility)
447
+ if config.workers:
448
+ from agentpool_toolsets.builtin.workers import WorkersTools
449
+
450
+ workers_provider = WorkersTools(workers=list(config.workers), name="workers")
451
+ toolsets_list.append(workers_provider)
452
+ # Resolve output type
453
+ agent_output_type = manifest.get_output_type(name) or str
454
+ resolved_output_type = to_type(agent_output_type, manifest.responses)
455
+ # Merge event handlers
456
+ config_handlers = config.get_event_handlers()
457
+ merged_handlers: list[IndividualEventHandler | BuiltinEventHandlerType] = [
458
+ *config_handlers,
459
+ *(event_handlers or []),
460
+ ]
461
+ # Resolve model
462
+ resolved_model = manifest.resolve_model(config.model)
463
+ model = resolved_model.get_model()
464
+ model_settings = resolved_model.get_model_settings()
465
+ # Extract builtin tools
466
+ builtin_tools = config.get_builtin_tools()
467
+ return cls(
468
+ model=model,
469
+ model_settings=model_settings,
470
+ system_prompt=sys_prompts,
471
+ name=name,
472
+ display_name=config.display_name,
473
+ deps_type=deps_type,
474
+ env=config.environment.get_provider() if config.environment else None,
475
+ description=config.description,
476
+ retries=config.retries,
477
+ session=config.get_session_config(),
478
+ output_retries=config.output_retries,
479
+ end_strategy=config.end_strategy,
480
+ agent_config=config,
481
+ input_provider=input_provider,
482
+ output_type=resolved_output_type, # type: ignore[arg-type]
483
+ event_handlers=merged_handlers or None,
484
+ agent_pool=agent_pool,
485
+ tool_mode=config.tool_mode,
486
+ knowledge=config.knowledge,
487
+ toolsets=toolsets_list,
488
+ hooks=config.hooks.get_agent_hooks() if config.hooks else None,
489
+ tool_confirmation_mode=config.requires_tool_confirmation,
490
+ builtin_tools=builtin_tools or None,
491
+ usage_limits=config.usage_limits,
492
+ providers=config.model_providers,
493
+ )
494
+
303
495
  async def __aenter__(self) -> Self:
304
496
  """Enter async context and set up MCP servers."""
305
497
  try:
@@ -327,57 +519,6 @@ class Agent[TDeps = None, OutputDataT = str](BaseAgent[TDeps, OutputDataT]):
327
519
  """Exit async context."""
328
520
  await super().__aexit__(exc_type, exc_val, exc_tb)
329
521
 
330
- @overload
331
- def __and__( # if other doesnt define deps, we take the agents one
332
- self, other: ProcessorCallback[Any] | Team[TDeps] | Agent[TDeps, Any]
333
- ) -> Team[TDeps]: ...
334
-
335
- @overload
336
- def __and__( # otherwise, we dont know and deps is Any
337
- self, other: ProcessorCallback[Any] | Team[Any] | Agent[Any, Any]
338
- ) -> Team[Any]: ...
339
-
340
- def __and__(self, other: MessageNode[Any, Any] | ProcessorCallback[Any]) -> Team[Any]:
341
- """Create sequential team using & operator.
342
-
343
- Example:
344
- group = analyzer & planner & executor # Create group of 3
345
- group = analyzer & existing_group # Add to existing group
346
- """
347
- from agentpool.delegation.team import Team
348
-
349
- match other:
350
- case Team():
351
- return Team([self, *other.nodes])
352
- case Callable():
353
- agent_2 = Agent.from_callback(other)
354
- agent_2.agent_pool = self.agent_pool
355
- return Team([self, agent_2])
356
- case MessageNode():
357
- return Team([self, other])
358
- case _:
359
- msg = f"Invalid agent type: {type(other)}"
360
- raise ValueError(msg)
361
-
362
- @overload
363
- def __or__(self, other: MessageNode[TDeps, Any]) -> TeamRun[TDeps, Any]: ...
364
-
365
- @overload
366
- def __or__[TOtherDeps](self, other: MessageNode[TOtherDeps, Any]) -> TeamRun[Any, Any]: ...
367
-
368
- @overload
369
- def __or__(self, other: ProcessorCallback[Any]) -> TeamRun[Any, Any]: ...
370
-
371
- def __or__(self, other: MessageNode[Any, Any] | ProcessorCallback[Any]) -> TeamRun[Any, Any]:
372
- # Create new execution with sequential mode (for piping)
373
- from agentpool import TeamRun
374
-
375
- if callable(other):
376
- other = Agent.from_callback(other)
377
- other.agent_pool = self.agent_pool
378
-
379
- return TeamRun([self, other])
380
-
381
522
  @overload
382
523
  @classmethod
383
524
  def from_callback(
@@ -416,16 +557,18 @@ class Agent[TDeps = None, OutputDataT = str](BaseAgent[TDeps, OutputDataT]):
416
557
  name: Optional name for the agent
417
558
  kwargs: Additional arguments for agent
418
559
  """
560
+ from llmling_models import function_to_model
561
+
419
562
  name = name or callback.__name__ or "processor"
420
563
  model = function_to_model(callback)
421
- return_type = _typing_extra.get_function_type_hints(callback).get("return")
564
+ output_type = _typing_extra.get_function_type_hints(callback).get("return")
422
565
  if ( # If async, unwrap from Awaitable
423
- return_type
424
- and hasattr(return_type, "__origin__")
425
- and return_type.__origin__ is Awaitable
566
+ output_type
567
+ and hasattr(output_type, "__origin__")
568
+ and output_type.__origin__ is Awaitable
426
569
  ):
427
- return_type = return_type.__args__[0]
428
- return Agent(model=model, name=name, output_type=return_type or str, **kwargs)
570
+ output_type = output_type.__args__[0]
571
+ return Agent(model=model, name=name, output_type=output_type or str, **kwargs)
429
572
 
430
573
  @property
431
574
  def name(self) -> str:
@@ -457,15 +600,34 @@ class Agent[TDeps = None, OutputDataT = str](BaseAgent[TDeps, OutputDataT]):
457
600
  data=data,
458
601
  )
459
602
 
603
+ def _resolve_model_string(self, model: str) -> tuple[Model, ModelSettings | None]:
604
+ """Resolve a model string, checking variants first.
605
+
606
+ Args:
607
+ model: Model identifier or variant name
608
+
609
+ Returns:
610
+ Tuple of (Model instance, ModelSettings or None)
611
+ Settings are only returned for variants.
612
+ """
613
+ from llmling_models import infer_model
614
+
615
+ # Check if it's a variant
616
+ if self.agent_pool and model in self.agent_pool.manifest.model_variants:
617
+ config = self.agent_pool.manifest.model_variants[model]
618
+ return config.get_model(), config.get_model_settings()
619
+ # Regular model string - no settings
620
+ return infer_model(model), None
621
+
460
622
  def to_structured[NewOutputDataT](
461
623
  self,
462
624
  output_type: type[NewOutputDataT],
463
- *,
464
- tool_name: str | None = None,
465
- tool_description: str | None = None,
466
625
  ) -> Agent[TDeps, NewOutputDataT]:
467
626
  """Convert this agent to a structured agent.
468
627
 
628
+ Warning: This method mutates the agent in place and breaks caching.
629
+ Changing output type modifies tool definitions sent to the API.
630
+
469
631
  Args:
470
632
  output_type: Type for structured responses. Can be:
471
633
  - A Python type (Pydantic model)
@@ -473,23 +635,17 @@ class Agent[TDeps = None, OutputDataT = str](BaseAgent[TDeps, OutputDataT]):
473
635
  tool_description: Optional override for result tool description
474
636
 
475
637
  Returns:
476
- Typed Agent
638
+ Self (same instance, not a copy)
477
639
  """
478
640
  self.log.debug("Setting result type", output_type=output_type)
479
641
  self._output_type = to_type(output_type) # type: ignore[assignment]
480
642
  return self # type: ignore
481
643
 
482
- def is_busy(self) -> bool:
483
- """Check if agent is currently processing tasks."""
484
- return bool(self.task_manager._pending_tasks or self._background_task)
485
-
486
644
  @property
487
645
  def model_name(self) -> str | None:
488
646
  """Get the model name in a consistent format (provider:model_name)."""
489
- if self._model:
490
- # Construct full model ID with provider prefix (e.g., "anthropic:claude-haiku-4-5")
491
- return f"{self._model.system}:{self._model.model_name}"
492
- return None
647
+ # Construct full model ID with provider prefix (e.g., "anthropic:claude-haiku-4-5")
648
+ return f"{self._model.system}:{self._model.model_name}" if self._model else None
493
649
 
494
650
  def to_tool(
495
651
  self,
@@ -500,7 +656,7 @@ class Agent[TDeps = None, OutputDataT = str](BaseAgent[TDeps, OutputDataT]):
500
656
  pass_message_history: bool = False,
501
657
  parent: Agent[Any, Any] | None = None,
502
658
  **_kwargs: Any,
503
- ) -> Tool[OutputDataT]:
659
+ ) -> FunctionTool[OutputDataT]:
504
660
  """Create a tool from this agent.
505
661
 
506
662
  Args:
@@ -517,7 +673,7 @@ class Agent[TDeps = None, OutputDataT = str](BaseAgent[TDeps, OutputDataT]):
517
673
  raise ToolError(msg)
518
674
 
519
675
  if reset_history_on_run:
520
- self.conversation.clear()
676
+ await self.conversation.clear()
521
677
 
522
678
  history = None
523
679
  if pass_message_history and parent:
@@ -550,8 +706,7 @@ class Agent[TDeps = None, OutputDataT = str](BaseAgent[TDeps, OutputDataT]):
550
706
 
551
707
  async def get_agentlet[AgentOutputType](
552
708
  self,
553
- tool_choice: str | list[str] | None,
554
- model: ModelType,
709
+ model: ModelType | None,
555
710
  output_type: type[AgentOutputType] | None,
556
711
  input_provider: InputProvider | None = None,
557
712
  ) -> PydanticAgent[TDeps, AgentOutputType]:
@@ -560,10 +715,13 @@ class Agent[TDeps = None, OutputDataT = str](BaseAgent[TDeps, OutputDataT]):
560
715
 
561
716
  from agentpool.agents.tool_wrapping import wrap_tool
562
717
 
563
- tools = await self.tools.get_tools(state="enabled", names=tool_choice)
718
+ tools = await self.tools.get_tools(state="enabled")
564
719
  final_type = to_type(output_type) if output_type not in [None, str] else self._output_type
565
720
  actual_model = model or self._model
566
- model_ = infer_model(actual_model) if isinstance(actual_model, str) else actual_model
721
+ if isinstance(actual_model, str):
722
+ model_, _settings = self._resolve_model_string(actual_model)
723
+ else:
724
+ model_ = actual_model
567
725
  agent = PydanticAgent(
568
726
  name=self.name,
569
727
  model=model_,
@@ -574,6 +732,7 @@ class Agent[TDeps = None, OutputDataT = str](BaseAgent[TDeps, OutputDataT]):
574
732
  output_retries=self._output_retries,
575
733
  deps_type=self.deps_type or NoneType,
576
734
  output_type=final_type,
735
+ builtin_tools=self._builtin_tools,
577
736
  )
578
737
 
579
738
  base_context = self.get_context()
@@ -594,168 +753,46 @@ class Agent[TDeps = None, OutputDataT = str](BaseAgent[TDeps, OutputDataT]):
594
753
 
595
754
  return agent # type: ignore[return-value]
596
755
 
597
- @overload
598
- async def run(
599
- self,
600
- *prompts: PromptCompatible | ChatMessage[Any],
601
- output_type: None = None,
602
- model: ModelType = None,
603
- store_history: bool = True,
604
- tool_choice: str | list[str] | None = None,
605
- usage_limits: UsageLimits | None = None,
606
- message_id: str | None = None,
607
- conversation_id: str | None = None,
608
- message_history: MessageHistory | None = None,
609
- deps: TDeps | None = None,
610
- input_provider: InputProvider | None = None,
611
- wait_for_connections: bool | None = None,
612
- instructions: str | None = None,
613
- ) -> ChatMessage[OutputDataT]: ...
614
-
615
- @overload
616
- async def run[OutputTypeT](
617
- self,
618
- *prompts: PromptCompatible | ChatMessage[Any],
619
- output_type: type[OutputTypeT],
620
- model: ModelType = None,
621
- store_history: bool = True,
622
- tool_choice: str | list[str] | None = None,
623
- usage_limits: UsageLimits | None = None,
624
- message_id: str | None = None,
625
- conversation_id: str | None = None,
626
- message_history: MessageHistory | None = None,
627
- deps: TDeps | None = None,
628
- input_provider: InputProvider | None = None,
629
- wait_for_connections: bool | None = None,
630
- instructions: str | None = None,
631
- ) -> ChatMessage[OutputTypeT]: ...
632
-
633
- @method_spawner # type: ignore[misc]
634
- async def run(
635
- self,
636
- *prompts: PromptCompatible | ChatMessage[Any],
637
- output_type: type[Any] | None = None,
638
- model: ModelType = None,
639
- store_history: bool = True,
640
- tool_choice: str | list[str] | None = None,
641
- usage_limits: UsageLimits | None = None,
642
- message_id: str | None = None,
643
- conversation_id: str | None = None,
644
- message_history: MessageHistory | None = None,
645
- deps: TDeps | None = None,
646
- input_provider: InputProvider | None = None,
647
- wait_for_connections: bool | None = None,
648
- instructions: str | None = None,
649
- ) -> ChatMessage[Any]:
650
- """Run agent with prompt and get response.
651
-
652
- Args:
653
- prompts: User query or instruction
654
- output_type: Optional type for structured responses
655
- model: Optional model override
656
- store_history: Whether the message exchange should be added to the
657
- context window
658
- tool_choice: Filter tool choice by name
659
- usage_limits: Optional usage limits for the model
660
- message_id: Optional message id for the returned message.
661
- Automatically generated if not provided.
662
- conversation_id: Optional conversation id for the returned message.
663
- message_history: Optional MessageHistory object to
664
- use instead of agent's own conversation
665
- deps: Optional dependencies for the agent
666
- input_provider: Optional input provider for the agent
667
- wait_for_connections: Whether to wait for connected agents to complete
668
- instructions: Optional instructions to override the agent's system prompt
669
-
670
- Returns:
671
- Result containing response and run information
672
-
673
- Raises:
674
- UnexpectedModelBehavior: If the model fails or behaves unexpectedly
675
- """
676
- # Collect all events through run_stream
677
- final_message: ChatMessage[Any] | None = None
678
- async for event in self.run_stream(
679
- *prompts,
680
- output_type=output_type,
681
- model=model,
682
- store_history=store_history,
683
- tool_choice=tool_choice,
684
- usage_limits=usage_limits,
685
- message_id=message_id,
686
- conversation_id=conversation_id,
687
- message_history=message_history,
688
- deps=deps,
689
- input_provider=input_provider,
690
- wait_for_connections=wait_for_connections,
691
- instructions=instructions,
692
- ):
693
- if isinstance(event, StreamCompleteEvent):
694
- final_message = event.message
695
-
696
- if final_message is None:
697
- msg = "No final message received from stream"
698
- raise RuntimeError(msg)
699
-
700
- return final_message
701
-
702
- @method_spawner
703
- async def run_stream( # noqa: PLR0915
756
+ async def _stream_events( # noqa: PLR0915
704
757
  self,
705
- *prompt: PromptCompatible,
706
- output_type: type[OutputDataT] | None = None,
707
- model: ModelType = None,
708
- tool_choice: str | list[str] | None = None,
758
+ prompts: list[UserContent],
759
+ *,
760
+ user_msg: ChatMessage[Any],
761
+ effective_parent_id: str | None,
709
762
  store_history: bool = True,
710
- usage_limits: UsageLimits | None = None,
711
763
  message_id: str | None = None,
712
764
  conversation_id: str | None = None,
765
+ parent_id: str | None = None,
713
766
  message_history: MessageHistory | None = None,
714
767
  input_provider: InputProvider | None = None,
715
768
  wait_for_connections: bool | None = None,
716
769
  deps: TDeps | None = None,
717
- instructions: str | None = None,
770
+ event_handlers: Sequence[IndividualEventHandler | BuiltinEventHandlerType] | None = None,
718
771
  ) -> AsyncIterator[RichAgentStreamEvent[OutputDataT]]:
719
- """Run agent with prompt and get a streaming response.
772
+ from anyenv import MultiEventHandler
773
+ from pydantic_graph import End
720
774
 
721
- Args:
722
- prompt: User query or instruction
723
- output_type: Optional type for structured responses
724
- model: Optional model override
725
- tool_choice: Filter tool choice by name
726
- store_history: Whether the message exchange should be added to the
727
- context window
728
- usage_limits: Optional usage limits for the model
729
- message_id: Optional message id for the returned message.
730
- Automatically generated if not provided.
731
- conversation_id: Optional conversation id for the returned message.
732
- message_history: Optional MessageHistory to use instead of agent's own
733
- input_provider: Optional input provider for the agent
734
- wait_for_connections: Whether to wait for connected agents to complete
735
- deps: Optional dependencies for the agent
736
- instructions: Optional instructions to override the agent's system prompt
737
- Returns:
738
- An async iterator yielding streaming events with final message embedded.
775
+ from agentpool.agents.events import resolve_event_handlers
739
776
 
740
- Raises:
741
- UnexpectedModelBehavior: If the model fails or behaves unexpectedly
742
- """
743
777
  conversation = message_history if message_history is not None else self.conversation
778
+ # Use provided event handlers or fall back to agent's handlers
779
+ if event_handlers is not None:
780
+ handler: MultiEventHandler[IndividualEventHandler] = MultiEventHandler(
781
+ resolve_event_handlers(event_handlers)
782
+ )
783
+ else:
784
+ handler = self.event_handler
744
785
  message_id = message_id or str(uuid4())
745
786
  run_id = str(uuid4())
746
- user_msg, prompts, original_message = await prepare_prompts(*prompt)
747
- self.message_received.emit(user_msg)
787
+ # Reset cancellation state
788
+ self._cancelled = False
789
+ # Initialize conversation_id on first run and log to storage
790
+ # Conversation ID initialization handled by BaseAgent
791
+ processed_prompts = prompts
792
+ await self.message_received.emit(user_msg)
748
793
  start_time = time.perf_counter()
749
794
  history_list = conversation.get_history()
750
795
  pending_parts = conversation.get_pending_parts()
751
-
752
- # Reset cancellation state and track current task
753
- self._cancelled = False
754
- self._current_stream_task = asyncio.current_task()
755
-
756
- # Track accumulated content for partial message on cancellation
757
- accumulated_text: list[str] = []
758
-
759
796
  # Execute pre-run hooks
760
797
  if self.hooks:
761
798
  pre_run_result = await self.hooks.run_pre_run_hooks(
@@ -763,188 +800,167 @@ class Agent[TDeps = None, OutputDataT = str](BaseAgent[TDeps, OutputDataT]):
763
800
  prompt=user_msg.content
764
801
  if isinstance(user_msg.content, str)
765
802
  else str(user_msg.content),
766
- conversation_id=conversation_id,
803
+ conversation_id=self.conversation_id,
767
804
  )
768
805
  if pre_run_result.get("decision") == "deny":
769
806
  reason = pre_run_result.get("reason", "Blocked by pre-run hook")
770
807
  msg = f"Run blocked: {reason}"
771
808
  raise RuntimeError(msg)
772
809
 
773
- yield RunStartedEvent(thread_id=self.conversation_id, run_id=run_id, agent_name=self.name)
774
- try:
775
- agentlet = await self.get_agentlet(tool_choice, model, output_type, input_provider)
776
- content = await convert_prompts(prompts)
777
- response_msg: ChatMessage[Any] | None = None
778
- # Prepend pending context parts (content is already pydantic-ai format)
779
- converted = [*pending_parts, *content]
780
-
781
- # Add CachePoint if auto_cache is enabled
782
- if self._auto_cache != "off":
783
- from pydantic_ai.messages import CachePoint
784
-
785
- cache_point = CachePoint(ttl=self._auto_cache)
786
- converted.append(cache_point)
787
- stream_events = agentlet.run_stream_events(
788
- converted,
789
- deps=deps, # type: ignore[arg-type]
790
- message_history=[m for run in history_list for m in run.to_pydantic_ai()],
791
- usage_limits=usage_limits,
792
- instructions=instructions,
793
- )
810
+ assert self.conversation_id is not None # Initialized by BaseAgent.run_stream()
811
+ run_started = RunStartedEvent(
812
+ thread_id=self.conversation_id, run_id=run_id, agent_name=self.name
813
+ )
814
+ await handler(None, run_started)
815
+ yield run_started
816
+
817
+ agentlet = await self.get_agentlet(None, self._output_type, input_provider)
818
+ content = await convert_prompts(processed_prompts)
819
+ response_msg: ChatMessage[Any] | None = None
820
+ # Prepend pending context parts (content is already pydantic-ai format)
821
+ converted = [*pending_parts, *content]
822
+ history = [m for run in history_list for m in run.to_pydantic_ai()]
823
+ # Track tool call starts to combine with results later
824
+ pending_tcs: dict[str, BaseToolCallPart] = {}
825
+ file_tracker = FileTracker()
826
+ async with agentlet.iter(
827
+ converted,
828
+ deps=deps, # type: ignore[arg-type]
829
+ message_history=history,
830
+ usage_limits=self._default_usage_limits,
831
+ ) as agent_run:
832
+ try:
833
+ async for node in agent_run:
834
+ if self._cancelled:
835
+ self.log.info("Stream cancelled by user")
836
+ break
837
+ if isinstance(node, End):
838
+ break
794
839
 
795
- # Stream events through merge_queue for progress events
796
- async with merge_queue_into_iterator(stream_events, self._event_queue) as events:
797
- # Track tool call starts to combine with results later
798
- pending_tcs: dict[str, BaseToolCallPart] = {}
799
- try:
800
- async for event in events:
801
- # Check for cancellation
802
- if self._cancelled:
803
- self.log.info("Stream cancelled by user")
804
- break
805
-
806
- # Call event handlers for all events
807
- for handler in self.event_handler._wrapped_handlers:
808
- await handler(None, event)
809
-
810
- yield event # type: ignore[misc]
811
-
812
- # Accumulate text content for partial message
813
- match event:
814
- case PartDeltaEvent(delta=TextPartDelta(content_delta=delta)):
815
- accumulated_text.append(delta)
816
- case PartStartEvent(part=TextPart(content=text)) if text:
817
- accumulated_text.append(text)
818
-
819
- match event:
820
- case (
821
- PartStartEvent(part=BaseToolCallPart() as tool_part)
822
- | FunctionToolCallEvent(part=tool_part)
823
- ):
824
- # Store tool call start info for later combination with result
825
- pending_tcs[tool_part.tool_call_id] = tool_part
826
- case FunctionToolResultEvent(tool_call_id=call_id) as result_event:
827
- # Check if we have a pending tool call to combine with
828
- if call_info := pending_tcs.pop(call_id, None):
829
- # Create and yield combined event
830
- combined_event = ToolCallCompleteEvent(
831
- tool_name=call_info.tool_name,
832
- tool_call_id=call_id,
833
- tool_input=call_info.args_as_dict(),
834
- tool_result=result_event.result.content
835
- if isinstance(result_event.result, ToolReturnPart)
836
- else result_event.result,
837
- agent_name=self.name,
838
- message_id=message_id,
839
- )
840
- yield combined_event
841
- case AgentRunResultEvent():
842
- # Capture final result data, Build final response message
843
- response_time = time.perf_counter() - start_time
844
- response_msg = await ChatMessage.from_run_result(
845
- event.result,
846
- agent_name=self.name,
847
- message_id=message_id,
848
- conversation_id=conversation_id or user_msg.conversation_id,
849
- response_time=response_time,
850
- )
851
- except asyncio.CancelledError:
852
- self.log.info("Stream cancelled via task cancellation")
853
- self._cancelled = True
854
-
855
- # Handle cancellation - emit partial message
840
+ # Stream events from model request node
841
+ if isinstance(node, ModelRequestNode):
842
+ async with (
843
+ node.stream(agent_run.ctx) as agent_stream,
844
+ merge_queue_into_iterator(
845
+ agent_stream, # type: ignore[arg-type]
846
+ self._event_queue,
847
+ ) as merged,
848
+ ):
849
+ async for event in file_tracker(merged):
850
+ if self._cancelled:
851
+ break
852
+ await handler(None, event)
853
+ yield event
854
+ combined = self._process_tool_event(event, pending_tcs, message_id)
855
+ if combined:
856
+ await handler(None, combined)
857
+ yield combined
858
+
859
+ # Stream events from tool call node
860
+ elif isinstance(node, CallToolsNode):
861
+ async with (
862
+ node.stream(agent_run.ctx) as tool_stream,
863
+ merge_queue_into_iterator(tool_stream, self._event_queue) as merged,
864
+ ):
865
+ async for event in file_tracker(merged):
866
+ if self._cancelled:
867
+ break
868
+ await handler(None, event)
869
+ yield event
870
+ combined = self._process_tool_event(event, pending_tcs, message_id)
871
+ if combined:
872
+ await handler(None, combined)
873
+ yield combined
874
+ except asyncio.CancelledError:
875
+ self.log.info("Stream cancelled via task cancellation")
876
+ self._cancelled = True
877
+
878
+ # Build response message
879
+ response_time = time.perf_counter() - start_time
856
880
  if self._cancelled:
857
- response_time = time.perf_counter() - start_time
858
- partial_content = "".join(accumulated_text)
859
- if partial_content:
860
- partial_content += "\n\n"
861
- partial_content += "[Request interrupted by user]"
881
+ partial_content = _extract_text_from_messages(
882
+ agent_run.all_messages(), include_interruption_note=True
883
+ )
862
884
  response_msg = ChatMessage(
863
885
  content=partial_content,
864
886
  role="assistant",
865
887
  name=self.name,
866
888
  message_id=message_id,
867
- conversation_id=conversation_id or user_msg.conversation_id,
889
+ conversation_id=self.conversation_id,
890
+ parent_id=user_msg.message_id,
868
891
  response_time=response_time,
869
892
  finish_reason="stop",
870
893
  )
871
- yield StreamCompleteEvent(message=response_msg)
872
- self._current_stream_task = None
894
+ complete_event = StreamCompleteEvent(message=response_msg)
895
+ await handler(None, complete_event)
896
+ yield complete_event
873
897
  return
874
898
 
875
- # Only finalize if we got a result (stream may exit early on error)
876
- if response_msg is None:
877
- msg = "Stream completed without producing a result"
878
- raise RuntimeError(msg) # noqa: TRY301
879
-
880
- # Execute post-run hooks
881
- if self.hooks:
882
- prompt_str = (
883
- user_msg.content if isinstance(user_msg.content, str) else str(user_msg.content)
884
- )
885
- await self.hooks.run_post_run_hooks(
899
+ if agent_run.result:
900
+ response_msg = await ChatMessage.from_run_result(
901
+ agent_run.result,
886
902
  agent_name=self.name,
887
- prompt=prompt_str,
888
- result=response_msg.content,
889
- conversation_id=conversation_id,
903
+ message_id=message_id,
904
+ conversation_id=self.conversation_id,
905
+ parent_id=user_msg.message_id,
906
+ response_time=response_time,
907
+ metadata=file_tracker.get_metadata(),
890
908
  )
909
+ else:
910
+ msg = "Stream completed without producing a result"
911
+ raise RuntimeError(msg)
891
912
 
892
- # Apply forwarding logic if needed
893
- if original_message:
894
- response_msg = response_msg.forwarded(original_message)
895
- # Send additional enriched completion event
896
- yield StreamCompleteEvent(message=response_msg)
897
- self.message_sent.emit(response_msg)
898
- await self.log_message(response_msg)
899
- if store_history:
900
- conversation.add_chat_messages([user_msg, response_msg])
901
- await self.connections.route_message(response_msg, wait=wait_for_connections)
913
+ # Execute post-run hooks
914
+ if self.hooks:
915
+ prompt_str = (
916
+ user_msg.content if isinstance(user_msg.content, str) else str(user_msg.content)
917
+ )
918
+ await self.hooks.run_post_run_hooks(
919
+ agent_name=self.name,
920
+ prompt=prompt_str,
921
+ result=response_msg.content,
922
+ conversation_id=self.conversation_id,
923
+ )
902
924
 
903
- except Exception as e:
904
- self.log.exception("Agent stream failed")
905
- self.run_failed.emit("Agent stream failed", e)
906
- raise
907
- finally:
908
- self._current_stream_task = None
925
+ # Send additional enriched completion event
926
+ complete_event = StreamCompleteEvent(message=response_msg)
927
+ await handler(None, complete_event)
928
+ yield complete_event
909
929
 
910
- async def run_iter(
930
+ def _process_tool_event(
911
931
  self,
912
- *prompt_groups: Sequence[PromptCompatible],
913
- output_type: type[OutputDataT] | None = None,
914
- model: ModelType = None,
915
- store_history: bool = True,
916
- wait_for_connections: bool | None = None,
917
- ) -> AsyncIterator[ChatMessage[OutputDataT]]:
918
- """Run agent sequentially on multiple prompt groups.
932
+ event: RichAgentStreamEvent[Any],
933
+ pending_tool_calls: dict[str, BaseToolCallPart],
934
+ message_id: str,
935
+ ) -> ToolCallCompleteEvent | None:
936
+ """Process tool-related events and return combined event when complete.
919
937
 
920
938
  Args:
921
- prompt_groups: Groups of prompts to process sequentially
922
- output_type: Optional type for structured responses
923
- model: Optional model override
924
- store_history: Whether to store in conversation history
925
- wait_for_connections: Whether to wait for connected agents
926
-
927
- Yields:
928
- Response messages in sequence
929
-
930
- Example:
931
- questions = [
932
- ["What is your name?"],
933
- ["How old are you?", image1],
934
- ["Describe this image", image2],
935
- ]
936
- async for response in agent.run_iter(*questions):
937
- print(response.content)
939
+ event: The streaming event to process
940
+ pending_tool_calls: Dict tracking in-progress tool calls by ID
941
+ message_id: Message ID for the combined event
942
+
943
+ Returns:
944
+ ToolCallCompleteEvent if a tool call completed, None otherwise
938
945
  """
939
- for prompts in prompt_groups:
940
- response = await self.run(
941
- *prompts,
942
- output_type=output_type,
943
- model=model,
944
- store_history=store_history,
945
- wait_for_connections=wait_for_connections,
946
- )
947
- yield response # pyright: ignore
946
+ match event:
947
+ case PartStartEvent(part=BaseToolCallPart() as tool_part):
948
+ pending_tool_calls[tool_part.tool_call_id] = tool_part
949
+ case FunctionToolCallEvent(part=tool_part):
950
+ pending_tool_calls[tool_part.tool_call_id] = tool_part
951
+ case FunctionToolResultEvent(tool_call_id=call_id) as result_event:
952
+ if call_info := pending_tool_calls.pop(call_id, None):
953
+ return ToolCallCompleteEvent(
954
+ tool_name=call_info.tool_name,
955
+ tool_call_id=call_id,
956
+ tool_input=safe_args_as_dict(call_info),
957
+ tool_result=result_event.result.content
958
+ if isinstance(result_event.result, ToolReturnPart)
959
+ else result_event.result,
960
+ agent_name=self.name,
961
+ message_id=message_id,
962
+ )
963
+ return None
948
964
 
949
965
  @method_spawner
950
966
  async def run_job(
@@ -998,122 +1014,6 @@ class Agent[TDeps = None, OutputDataT = str](BaseAgent[TDeps, OutputDataT]):
998
1014
  msg = f"Task execution failed: {e}"
999
1015
  raise JobError(msg) from e
1000
1016
 
1001
- async def run_in_background(
1002
- self,
1003
- *prompt: PromptCompatible,
1004
- max_count: int | None = None,
1005
- interval: float = 1.0,
1006
- **kwargs: Any,
1007
- ) -> asyncio.Task[ChatMessage[OutputDataT] | None]:
1008
- """Run agent continuously in background with prompt or dynamic prompt function.
1009
-
1010
- Args:
1011
- prompt: Static prompt or function that generates prompts
1012
- max_count: Maximum number of runs (None = infinite)
1013
- interval: Seconds between runs
1014
- **kwargs: Arguments passed to run()
1015
- """
1016
- self._infinite = max_count is None
1017
-
1018
- async def _continuous() -> ChatMessage[Any]:
1019
- count = 0
1020
- self.log.debug("Starting continuous run", max_count=max_count, interval=interval)
1021
- latest = None
1022
- while (max_count is None or count < max_count) and not self._cancelled:
1023
- try:
1024
- agent_ctx = self.get_context()
1025
- current_prompts = [
1026
- call_with_context(p, agent_ctx, **kwargs) if callable(p) else p
1027
- for p in prompt
1028
- ]
1029
- self.log.debug("Generated prompt", iteration=count)
1030
- latest = await self.run(current_prompts, **kwargs)
1031
- self.log.debug("Run continuous result", iteration=count)
1032
-
1033
- count += 1
1034
- await anyio.sleep(interval)
1035
- except asyncio.CancelledError:
1036
- self.log.debug("Continuous run cancelled")
1037
- break
1038
- except Exception:
1039
- # Check if we were cancelled (may surface as other exceptions)
1040
- if self._cancelled:
1041
- self.log.debug("Continuous run cancelled via flag")
1042
- break
1043
- count += 1
1044
- self.log.exception("Background run failed")
1045
- await anyio.sleep(interval)
1046
- self.log.debug("Continuous run completed", iterations=count)
1047
- return latest # type: ignore[return-value]
1048
-
1049
- await self.stop() # Cancel any existing background task
1050
- self._cancelled = False # Reset cancellation flag for new run
1051
- task = asyncio.create_task(_continuous(), name=f"background_{self.name}")
1052
- self.log.debug("Started background task", task_name=task.get_name())
1053
- self._background_task = task
1054
- return task
1055
-
1056
- async def stop(self) -> None:
1057
- """Stop continuous execution if running."""
1058
- self._cancelled = True # Signal cancellation via flag
1059
- if self._background_task and not self._background_task.done():
1060
- self._background_task.cancel()
1061
- with suppress(asyncio.CancelledError): # Expected when we cancel the task
1062
- await self._background_task
1063
- self._background_task = None
1064
-
1065
- async def wait(self) -> ChatMessage[OutputDataT]:
1066
- """Wait for background execution to complete."""
1067
- if not self._background_task:
1068
- msg = "No background task running"
1069
- raise RuntimeError(msg)
1070
- if self._infinite:
1071
- msg = "Cannot wait on infinite execution"
1072
- raise RuntimeError(msg)
1073
- try:
1074
- return await self._background_task
1075
- finally:
1076
- self._background_task = None
1077
-
1078
- async def share(
1079
- self,
1080
- target: Agent[TDeps, Any],
1081
- *,
1082
- tools: list[str] | None = None,
1083
- history: bool | int | None = None, # bool or number of messages
1084
- token_limit: int | None = None,
1085
- ) -> None:
1086
- """Share capabilities and knowledge with another agent.
1087
-
1088
- Args:
1089
- target: Agent to share with
1090
- tools: List of tool names to share
1091
- history: Share conversation history:
1092
- - True: Share full history
1093
- - int: Number of most recent messages to share
1094
- - None: Don't share history
1095
- token_limit: Optional max tokens for history
1096
-
1097
- Raises:
1098
- ValueError: If requested items don't exist
1099
- RuntimeError: If runtime not available for resources
1100
- """
1101
- # Share tools if requested
1102
- for name in tools or []:
1103
- tool = await self.tools.get_tool(name)
1104
- meta = {"shared_from": self.name}
1105
- target.tools.register_tool(tool.callable, metadata=meta)
1106
-
1107
- # Share history if requested
1108
- if history:
1109
- history_text = await self.conversation.format_history(
1110
- max_tokens=token_limit,
1111
- num_messages=history if isinstance(history, int) else None,
1112
- )
1113
- target.conversation.add_context_message(
1114
- history_text, source=self.name, metadata={"type": "shared_history"}
1115
- )
1116
-
1117
1017
  def register_worker(
1118
1018
  self,
1119
1019
  worker: MessageNode[Any, Any],
@@ -1131,14 +1031,19 @@ class Agent[TDeps = None, OutputDataT = str](BaseAgent[TDeps, OutputDataT]):
1131
1031
  parent=self if pass_message_history else None,
1132
1032
  )
1133
1033
 
1134
- def set_model(self, model: ModelType) -> None:
1034
+ async def set_model(self, model: Model | str) -> None:
1135
1035
  """Set the model for this agent.
1136
1036
 
1137
1037
  Args:
1138
1038
  model: New model to use (name or instance)
1139
1039
 
1140
1040
  """
1141
- self._model = infer_model(model) if isinstance(model, str) else model
1041
+ if isinstance(model, str):
1042
+ self._model, settings = self._resolve_model_string(model)
1043
+ if settings:
1044
+ self.model_settings = settings
1045
+ else:
1046
+ self._model = model
1142
1047
 
1143
1048
  async def set_tool_confirmation_mode(self, mode: ToolConfirmationMode) -> None:
1144
1049
  """Set the tool confirmation mode for this agent.
@@ -1152,25 +1057,6 @@ class Agent[TDeps = None, OutputDataT = str](BaseAgent[TDeps, OutputDataT]):
1152
1057
  self.tool_confirmation_mode = mode
1153
1058
  self.log.info("Tool confirmation mode changed", mode=mode)
1154
1059
 
1155
- async def reset(self) -> None:
1156
- """Reset agent state (conversation history and tool states)."""
1157
- old_tools = await self.tools.list_tools()
1158
- self.conversation.clear()
1159
- await self.tools.reset_states()
1160
- new_tools = await self.tools.list_tools()
1161
-
1162
- event = self.AgentReset(
1163
- agent_name=self.name,
1164
- previous_tools=old_tools,
1165
- new_tools=new_tools,
1166
- )
1167
- self.agent_reset.emit(event)
1168
-
1169
- async def get_stats(self) -> MessageStats:
1170
- """Get message statistics (async version)."""
1171
- messages = await self.get_message_history()
1172
- return MessageStats(messages=messages)
1173
-
1174
1060
  @asynccontextmanager
1175
1061
  async def temporary_state[T](
1176
1062
  self,
@@ -1199,6 +1085,7 @@ class Agent[TDeps = None, OutputDataT = str](BaseAgent[TDeps, OutputDataT]):
1199
1085
  model: Temporary model override
1200
1086
  """
1201
1087
  old_model = self._model
1088
+ old_settings = self.model_settings
1202
1089
  if output_type:
1203
1090
  old_type = self._output_type
1204
1091
  self.to_structured(output_type)
@@ -1221,14 +1108,21 @@ class Agent[TDeps = None, OutputDataT = str](BaseAgent[TDeps, OutputDataT]):
1221
1108
  if pause_routing: # Routing
1222
1109
  await stack.enter_async_context(self.connections.paused_routing())
1223
1110
 
1224
- elif model is not None: # Model
1225
- self._model = infer_model(model) if isinstance(model, str) else model
1111
+ if model is not None: # Model
1112
+ if isinstance(model, str):
1113
+ self._model, settings = self._resolve_model_string(model)
1114
+ if settings:
1115
+ self.model_settings = settings
1116
+ else:
1117
+ self._model = model
1226
1118
 
1227
1119
  try:
1228
1120
  yield self
1229
- finally: # Restore model
1230
- if model is not None and old_model:
1231
- self._model = old_model
1121
+ finally: # Restore model and settings
1122
+ if model is not None:
1123
+ if old_model:
1124
+ self._model = old_model
1125
+ self.model_settings = old_settings
1232
1126
  if output_type:
1233
1127
  self.to_structured(old_type)
1234
1128
 
@@ -1247,6 +1141,147 @@ class Agent[TDeps = None, OutputDataT = str](BaseAgent[TDeps, OutputDataT]):
1247
1141
  else:
1248
1142
  return True
1249
1143
 
1144
+ async def get_available_models(self) -> list[ModelInfo] | None:
1145
+ """Get available models for this agent.
1146
+
1147
+ Uses tokonomics model discovery to fetch models from configured providers.
1148
+ Defaults to openai, anthropic, and gemini if no providers specified.
1149
+
1150
+ Returns:
1151
+ List of tokonomics ModelInfo, or None if discovery fails
1152
+ """
1153
+ from datetime import timedelta
1154
+
1155
+ from tokonomics.model_discovery import get_all_models
1156
+
1157
+ try:
1158
+ max_age = timedelta(days=200)
1159
+ return await get_all_models(
1160
+ providers=self._providers or ["models.dev"], max_age=max_age
1161
+ )
1162
+ except Exception:
1163
+ self.log.exception("Failed to discover models")
1164
+ return None
1165
+
1166
+ async def get_modes(self) -> list[ModeCategory]:
1167
+ """Get available mode categories for this agent.
1168
+
1169
+ Native agents expose permission modes and model selection.
1170
+
1171
+ Returns:
1172
+ List of ModeCategory for permissions and models
1173
+ """
1174
+ from agentpool.agents.modes import ModeCategory, ModeInfo
1175
+
1176
+ categories: list[ModeCategory] = []
1177
+
1178
+ # Permission modes
1179
+ mode_id_map = {
1180
+ "per_tool": "default",
1181
+ "always": "default",
1182
+ "never": "acceptEdits",
1183
+ }
1184
+ current_id = mode_id_map.get(self.tool_confirmation_mode, "default")
1185
+
1186
+ categories.append(
1187
+ ModeCategory(
1188
+ id="permissions",
1189
+ name="Permissions",
1190
+ available_modes=[
1191
+ ModeInfo(
1192
+ id="default",
1193
+ name="Default",
1194
+ description="Require confirmation for tools marked as needing it",
1195
+ category_id="permissions",
1196
+ ),
1197
+ ModeInfo(
1198
+ id="acceptEdits",
1199
+ name="Accept Edits",
1200
+ description="Auto-approve all tool calls without confirmation",
1201
+ category_id="permissions",
1202
+ ),
1203
+ ],
1204
+ current_mode_id=current_id,
1205
+ category="mode",
1206
+ )
1207
+ )
1208
+
1209
+ # Model selection
1210
+ models = await self.get_available_models()
1211
+ if models:
1212
+ current_model = self.model_name or (models[0].id if models else "")
1213
+ categories.append(
1214
+ ModeCategory(
1215
+ id="model",
1216
+ name="Model",
1217
+ available_modes=[
1218
+ ModeInfo(
1219
+ id=m.id,
1220
+ name=m.name or m.id,
1221
+ description=m.description or "",
1222
+ category_id="model",
1223
+ )
1224
+ for m in models
1225
+ ],
1226
+ current_mode_id=current_model,
1227
+ category="model",
1228
+ )
1229
+ )
1230
+
1231
+ return categories
1232
+
1233
+ async def set_mode(self, mode: ModeInfo | str, category_id: str | None = None) -> None:
1234
+ """Set a mode for this agent.
1235
+
1236
+ Native agents support:
1237
+ - "permissions" category: default, acceptEdits
1238
+ - "model" category: any available model ID
1239
+
1240
+ Args:
1241
+ mode: Mode to activate - ModeInfo object or mode ID string
1242
+ category_id: Category ID ("permissions" or "model")
1243
+
1244
+ Raises:
1245
+ ValueError: If mode_id or category_id is invalid
1246
+ """
1247
+ # Extract mode_id and category from ModeInfo if provided
1248
+ if isinstance(mode, ModeInfo):
1249
+ mode_id = mode.id
1250
+ category_id = category_id or mode.category_id
1251
+ else:
1252
+ mode_id = mode
1253
+
1254
+ # Default to permissions if no category specified
1255
+ if category_id is None:
1256
+ category_id = "permissions"
1257
+
1258
+ if category_id == "permissions":
1259
+ # Map mode_id to confirmation mode
1260
+ mode_map: dict[str, ToolConfirmationMode] = {
1261
+ "default": "per_tool",
1262
+ "acceptEdits": "never",
1263
+ }
1264
+ if mode_id not in mode_map:
1265
+ msg = f"Unknown permission mode: {mode_id}. Available: {list(mode_map.keys())}"
1266
+ raise ValueError(msg)
1267
+ await self.set_tool_confirmation_mode(mode_map[mode_id])
1268
+
1269
+ elif category_id == "model":
1270
+ # Validate model exists
1271
+ models = await self.get_available_models()
1272
+ if models:
1273
+ valid_ids = {m.id for m in models}
1274
+ if mode_id not in valid_ids:
1275
+ msg = f"Unknown model: {mode_id}. Available: {valid_ids}"
1276
+ raise ValueError(msg)
1277
+ # Set the model using set_model method
1278
+ await self.set_model(mode_id)
1279
+ self.log.info("Model changed", model=mode_id)
1280
+
1281
+ else:
1282
+ msg = f"Unknown category: {category_id}. Available: permissions, model"
1283
+ raise ValueError(msg)
1284
+
1250
1285
 
1251
1286
  if __name__ == "__main__":
1252
1287
  import logging
@@ -1261,4 +1296,4 @@ if __name__ == "__main__":
1261
1296
  print(f"[EVENT] {type(event).__name__}: {event}")
1262
1297
 
1263
1298
  agent = Agent(model=_model, tools=["webbrowser.open"], event_handlers=[handle_events])
1264
- result = agent.run.sync(sys_prompt) # type: ignore[attr-defined]
1299
+ result = agent.run.sync(sys_prompt)