agentpool 2.1.9__py3-none-any.whl → 2.2.3__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 (174) hide show
  1. acp/__init__.py +13 -0
  2. acp/bridge/README.md +15 -2
  3. acp/bridge/__init__.py +3 -2
  4. acp/bridge/__main__.py +60 -19
  5. acp/bridge/ws_server.py +173 -0
  6. acp/bridge/ws_server_cli.py +89 -0
  7. acp/notifications.py +2 -1
  8. acp/stdio.py +39 -9
  9. acp/transports.py +362 -2
  10. acp/utils.py +15 -2
  11. agentpool/__init__.py +4 -1
  12. agentpool/agents/__init__.py +2 -0
  13. agentpool/agents/acp_agent/acp_agent.py +203 -88
  14. agentpool/agents/acp_agent/acp_converters.py +46 -21
  15. agentpool/agents/acp_agent/client_handler.py +157 -3
  16. agentpool/agents/acp_agent/session_state.py +4 -1
  17. agentpool/agents/agent.py +314 -107
  18. agentpool/agents/agui_agent/__init__.py +0 -2
  19. agentpool/agents/agui_agent/agui_agent.py +90 -21
  20. agentpool/agents/agui_agent/agui_converters.py +0 -131
  21. agentpool/agents/base_agent.py +163 -1
  22. agentpool/agents/claude_code_agent/claude_code_agent.py +626 -179
  23. agentpool/agents/claude_code_agent/converters.py +71 -3
  24. agentpool/agents/claude_code_agent/history.py +474 -0
  25. agentpool/agents/context.py +40 -0
  26. agentpool/agents/events/__init__.py +2 -0
  27. agentpool/agents/events/builtin_handlers.py +2 -1
  28. agentpool/agents/events/event_emitter.py +29 -2
  29. agentpool/agents/events/events.py +20 -0
  30. agentpool/agents/modes.py +54 -0
  31. agentpool/agents/tool_call_accumulator.py +213 -0
  32. agentpool/common_types.py +21 -0
  33. agentpool/config_resources/__init__.py +38 -1
  34. agentpool/config_resources/claude_code_agent.yml +3 -0
  35. agentpool/delegation/pool.py +37 -29
  36. agentpool/delegation/team.py +1 -0
  37. agentpool/delegation/teamrun.py +1 -0
  38. agentpool/diagnostics/__init__.py +53 -0
  39. agentpool/diagnostics/lsp_manager.py +1593 -0
  40. agentpool/diagnostics/lsp_proxy.py +41 -0
  41. agentpool/diagnostics/lsp_proxy_script.py +229 -0
  42. agentpool/diagnostics/models.py +398 -0
  43. agentpool/mcp_server/__init__.py +0 -2
  44. agentpool/mcp_server/client.py +12 -3
  45. agentpool/mcp_server/manager.py +25 -31
  46. agentpool/mcp_server/registries/official_registry_client.py +25 -0
  47. agentpool/mcp_server/tool_bridge.py +78 -66
  48. agentpool/messaging/__init__.py +0 -2
  49. agentpool/messaging/compaction.py +72 -197
  50. agentpool/messaging/message_history.py +12 -0
  51. agentpool/messaging/messages.py +52 -9
  52. agentpool/messaging/processing.py +3 -1
  53. agentpool/models/acp_agents/base.py +0 -22
  54. agentpool/models/acp_agents/mcp_capable.py +8 -148
  55. agentpool/models/acp_agents/non_mcp.py +129 -72
  56. agentpool/models/agents.py +35 -13
  57. agentpool/models/claude_code_agents.py +33 -2
  58. agentpool/models/manifest.py +43 -0
  59. agentpool/repomap.py +1 -1
  60. agentpool/resource_providers/__init__.py +9 -1
  61. agentpool/resource_providers/aggregating.py +52 -3
  62. agentpool/resource_providers/base.py +57 -1
  63. agentpool/resource_providers/mcp_provider.py +23 -0
  64. agentpool/resource_providers/plan_provider.py +130 -41
  65. agentpool/resource_providers/pool.py +2 -0
  66. agentpool/resource_providers/static.py +2 -0
  67. agentpool/sessions/__init__.py +2 -1
  68. agentpool/sessions/manager.py +31 -2
  69. agentpool/sessions/models.py +50 -0
  70. agentpool/skills/registry.py +13 -8
  71. agentpool/storage/manager.py +217 -1
  72. agentpool/testing.py +537 -19
  73. agentpool/utils/file_watcher.py +269 -0
  74. agentpool/utils/identifiers.py +121 -0
  75. agentpool/utils/pydantic_ai_helpers.py +46 -0
  76. agentpool/utils/streams.py +690 -1
  77. agentpool/utils/subprocess_utils.py +155 -0
  78. agentpool/utils/token_breakdown.py +461 -0
  79. {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/METADATA +27 -7
  80. {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/RECORD +170 -112
  81. {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/WHEEL +1 -1
  82. agentpool_cli/__main__.py +4 -0
  83. agentpool_cli/serve_acp.py +41 -20
  84. agentpool_cli/serve_agui.py +87 -0
  85. agentpool_cli/serve_opencode.py +119 -0
  86. agentpool_commands/__init__.py +30 -0
  87. agentpool_commands/agents.py +74 -1
  88. agentpool_commands/history.py +62 -0
  89. agentpool_commands/mcp.py +176 -0
  90. agentpool_commands/models.py +56 -3
  91. agentpool_commands/tools.py +57 -0
  92. agentpool_commands/utils.py +51 -0
  93. agentpool_config/builtin_tools.py +77 -22
  94. agentpool_config/commands.py +24 -1
  95. agentpool_config/compaction.py +258 -0
  96. agentpool_config/mcp_server.py +131 -1
  97. agentpool_config/storage.py +46 -1
  98. agentpool_config/tools.py +7 -1
  99. agentpool_config/toolsets.py +92 -148
  100. agentpool_server/acp_server/acp_agent.py +134 -150
  101. agentpool_server/acp_server/commands/acp_commands.py +216 -51
  102. agentpool_server/acp_server/commands/docs_commands/fetch_repo.py +10 -10
  103. agentpool_server/acp_server/server.py +23 -79
  104. agentpool_server/acp_server/session.py +181 -19
  105. agentpool_server/opencode_server/.rules +95 -0
  106. agentpool_server/opencode_server/ENDPOINTS.md +362 -0
  107. agentpool_server/opencode_server/__init__.py +27 -0
  108. agentpool_server/opencode_server/command_validation.py +172 -0
  109. agentpool_server/opencode_server/converters.py +869 -0
  110. agentpool_server/opencode_server/dependencies.py +24 -0
  111. agentpool_server/opencode_server/input_provider.py +269 -0
  112. agentpool_server/opencode_server/models/__init__.py +228 -0
  113. agentpool_server/opencode_server/models/agent.py +53 -0
  114. agentpool_server/opencode_server/models/app.py +60 -0
  115. agentpool_server/opencode_server/models/base.py +26 -0
  116. agentpool_server/opencode_server/models/common.py +23 -0
  117. agentpool_server/opencode_server/models/config.py +37 -0
  118. agentpool_server/opencode_server/models/events.py +647 -0
  119. agentpool_server/opencode_server/models/file.py +88 -0
  120. agentpool_server/opencode_server/models/mcp.py +25 -0
  121. agentpool_server/opencode_server/models/message.py +162 -0
  122. agentpool_server/opencode_server/models/parts.py +190 -0
  123. agentpool_server/opencode_server/models/provider.py +81 -0
  124. agentpool_server/opencode_server/models/pty.py +43 -0
  125. agentpool_server/opencode_server/models/session.py +99 -0
  126. agentpool_server/opencode_server/routes/__init__.py +25 -0
  127. agentpool_server/opencode_server/routes/agent_routes.py +442 -0
  128. agentpool_server/opencode_server/routes/app_routes.py +139 -0
  129. agentpool_server/opencode_server/routes/config_routes.py +241 -0
  130. agentpool_server/opencode_server/routes/file_routes.py +392 -0
  131. agentpool_server/opencode_server/routes/global_routes.py +94 -0
  132. agentpool_server/opencode_server/routes/lsp_routes.py +319 -0
  133. agentpool_server/opencode_server/routes/message_routes.py +705 -0
  134. agentpool_server/opencode_server/routes/pty_routes.py +299 -0
  135. agentpool_server/opencode_server/routes/session_routes.py +1205 -0
  136. agentpool_server/opencode_server/routes/tui_routes.py +139 -0
  137. agentpool_server/opencode_server/server.py +430 -0
  138. agentpool_server/opencode_server/state.py +121 -0
  139. agentpool_server/opencode_server/time_utils.py +8 -0
  140. agentpool_storage/__init__.py +16 -0
  141. agentpool_storage/base.py +103 -0
  142. agentpool_storage/claude_provider.py +907 -0
  143. agentpool_storage/file_provider.py +129 -0
  144. agentpool_storage/memory_provider.py +61 -0
  145. agentpool_storage/models.py +3 -0
  146. agentpool_storage/opencode_provider.py +730 -0
  147. agentpool_storage/project_store.py +325 -0
  148. agentpool_storage/session_store.py +6 -0
  149. agentpool_storage/sql_provider/__init__.py +4 -2
  150. agentpool_storage/sql_provider/models.py +48 -0
  151. agentpool_storage/sql_provider/sql_provider.py +134 -1
  152. agentpool_storage/sql_provider/utils.py +10 -1
  153. agentpool_storage/text_log_provider.py +1 -0
  154. agentpool_toolsets/builtin/__init__.py +0 -8
  155. agentpool_toolsets/builtin/code.py +95 -56
  156. agentpool_toolsets/builtin/debug.py +16 -21
  157. agentpool_toolsets/builtin/execution_environment.py +99 -103
  158. agentpool_toolsets/builtin/file_edit/file_edit.py +115 -7
  159. agentpool_toolsets/builtin/skills.py +86 -4
  160. agentpool_toolsets/fsspec_toolset/__init__.py +13 -1
  161. agentpool_toolsets/fsspec_toolset/diagnostics.py +860 -73
  162. agentpool_toolsets/fsspec_toolset/grep.py +74 -2
  163. agentpool_toolsets/fsspec_toolset/image_utils.py +161 -0
  164. agentpool_toolsets/fsspec_toolset/toolset.py +159 -38
  165. agentpool_toolsets/mcp_discovery/__init__.py +5 -0
  166. agentpool_toolsets/mcp_discovery/data/mcp_servers.parquet +0 -0
  167. agentpool_toolsets/mcp_discovery/toolset.py +454 -0
  168. agentpool_toolsets/mcp_run_toolset.py +84 -6
  169. agentpool_toolsets/builtin/agent_management.py +0 -239
  170. agentpool_toolsets/builtin/history.py +0 -36
  171. agentpool_toolsets/builtin/integration.py +0 -85
  172. agentpool_toolsets/builtin/tool_management.py +0 -90
  173. {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/entry_points.txt +0 -0
  174. {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/licenses/LICENSE +0 -0
agentpool/agents/agent.py CHANGED
@@ -19,37 +19,68 @@ from pydantic import ValidationError
19
19
  from pydantic._internal import _typing_extra
20
20
  from pydantic_ai import (
21
21
  Agent as PydanticAgent,
22
- AgentRunResultEvent,
23
22
  BaseToolCallPart,
23
+ CallToolsNode,
24
24
  FunctionToolCallEvent,
25
25
  FunctionToolResultEvent,
26
- PartDeltaEvent,
26
+ ModelRequestNode,
27
27
  PartStartEvent,
28
28
  RunContext,
29
- TextPart,
30
- TextPartDelta,
31
29
  ToolReturnPart,
32
30
  )
33
31
 
34
32
  from agentpool.agents.base_agent import BaseAgent
35
- from agentpool.agents.events import RunStartedEvent, StreamCompleteEvent, ToolCallCompleteEvent
33
+ from agentpool.agents.events import (
34
+ RunStartedEvent,
35
+ StreamCompleteEvent,
36
+ ToolCallCompleteEvent,
37
+ )
38
+ from agentpool.agents.modes import ModeInfo
36
39
  from agentpool.log import get_logger
37
40
  from agentpool.messaging import ChatMessage, MessageHistory, MessageNode
38
41
  from agentpool.messaging.processing import prepare_prompts
39
42
  from agentpool.prompts.convert import convert_prompts
40
43
  from agentpool.storage import StorageManager
41
- from agentpool.talk.stats import MessageStats
42
44
  from agentpool.tools import Tool, ToolManager
43
45
  from agentpool.tools.exceptions import ToolError
44
46
  from agentpool.utils.inspection import call_with_context, get_argument_key
45
47
  from agentpool.utils.now import get_now
48
+ from agentpool.utils.pydantic_ai_helpers import safe_args_as_dict
46
49
  from agentpool.utils.result_utils import to_type
47
- from agentpool.utils.streams import merge_queue_into_iterator
50
+ from agentpool.utils.streams import FileTracker, merge_queue_into_iterator
48
51
 
49
52
 
50
53
  TResult = TypeVar("TResult")
51
54
 
52
55
 
56
+ def _extract_text_from_messages(
57
+ messages: list[Any], include_interruption_note: bool = False
58
+ ) -> str:
59
+ """Extract text content from pydantic-ai messages.
60
+
61
+ Args:
62
+ messages: List of ModelRequest/ModelResponse messages
63
+ include_interruption_note: Whether to append interruption notice
64
+
65
+ Returns:
66
+ Concatenated text content from all ModelResponse TextParts
67
+ """
68
+ from pydantic_ai.messages import ModelResponse, TextPart as PydanticTextPart
69
+
70
+ content = "".join(
71
+ part.content
72
+ for msg in messages
73
+ if isinstance(msg, ModelResponse)
74
+ for part in msg.parts
75
+ if isinstance(part, PydanticTextPart)
76
+ )
77
+ if include_interruption_note:
78
+ if content:
79
+ content += "\n\n"
80
+ content += "[Request interrupted by user]"
81
+ return content
82
+
83
+
53
84
  if TYPE_CHECKING:
54
85
  from collections.abc import AsyncIterator, Coroutine, Sequence
55
86
  from datetime import datetime
@@ -57,13 +88,18 @@ if TYPE_CHECKING:
57
88
 
58
89
  from exxec import ExecutionEnvironment
59
90
  from pydantic_ai import UsageLimits
91
+ from pydantic_ai.builtin_tools import AbstractBuiltinTool
60
92
  from pydantic_ai.output import OutputSpec
61
93
  from pydantic_ai.settings import ModelSettings
94
+ from slashed import BaseCommand
95
+ from tokonomics.model_discovery import ProviderType
96
+ from tokonomics.model_discovery.model_info import ModelInfo
62
97
  from toprompt import AnyPromptType
63
98
  from upathtools import JoinablePathLike
64
99
 
65
100
  from agentpool.agents import AgentContext
66
101
  from agentpool.agents.events import RichAgentStreamEvent
102
+ from agentpool.agents.modes import ModeCategory
67
103
  from agentpool.common_types import (
68
104
  AgentName,
69
105
  BuiltinEventHandlerType,
@@ -77,7 +113,7 @@ if TYPE_CHECKING:
77
113
  )
78
114
  from agentpool.delegation import AgentPool, Team, TeamRun
79
115
  from agentpool.hooks import AgentHooks
80
- from agentpool.models.agents import AutoCache, NativeAgentConfig, ToolMode
116
+ from agentpool.models.agents import NativeAgentConfig, ToolMode
81
117
  from agentpool.prompts.prompts import PromptType
82
118
  from agentpool.resource_providers import ResourceProvider
83
119
  from agentpool.ui.base import InputProvider
@@ -111,9 +147,11 @@ class AgentKwargs(TypedDict, total=False):
111
147
  input_provider: InputProvider | None
112
148
  event_handlers: Sequence[IndividualEventHandler | BuiltinEventHandlerType] | None
113
149
  env: ExecutionEnvironment | None
114
- auto_cache: AutoCache
150
+
115
151
  hooks: AgentHooks | None
116
152
  model_settings: ModelSettings | None
153
+ usage_limits: UsageLimits | None
154
+ providers: Sequence[ProviderType] | None
117
155
 
118
156
 
119
157
  class Agent[TDeps = None, OutputDataT = str](BaseAgent[TDeps, OutputDataT]):
@@ -164,9 +202,12 @@ class Agent[TDeps = None, OutputDataT = str](BaseAgent[TDeps, OutputDataT]):
164
202
  knowledge: Knowledge | None = None,
165
203
  agent_config: NativeAgentConfig | None = None,
166
204
  env: ExecutionEnvironment | None = None,
167
- auto_cache: AutoCache = "off",
168
205
  hooks: AgentHooks | None = None,
169
206
  tool_confirmation_mode: ToolConfirmationMode = "per_tool",
207
+ builtin_tools: Sequence[AbstractBuiltinTool] | None = None,
208
+ usage_limits: UsageLimits | None = None,
209
+ providers: Sequence[ProviderType] | None = None,
210
+ commands: Sequence[BaseCommand] | None = None,
170
211
  ) -> None:
171
212
  """Initialize agent.
172
213
 
@@ -206,9 +247,13 @@ class Agent[TDeps = None, OutputDataT = str](BaseAgent[TDeps, OutputDataT]):
206
247
  knowledge: Knowledge sources for this agent
207
248
  agent_config: Agent configuration
208
249
  env: Execution environment for code/command execution and filesystem access
209
- auto_cache: Automatic caching configuration ("off", "5m", or "1h")
210
250
  hooks: AgentHooks instance for intercepting agent behavior at run and tool events
211
251
  tool_confirmation_mode: Tool confirmation mode
252
+ builtin_tools: PydanticAI builtin tools (WebSearchTool, CodeExecutionTool, etc.)
253
+ usage_limits: Usage limits for the agent
254
+ providers: Model providers for model discovery (e.g., ["openai", "anthropic"]).
255
+ Defaults to ["models.dev"] if not specified.
256
+ commands: Slash commands
212
257
  """
213
258
  from agentpool.agents.interactions import Interactions
214
259
  from agentpool.agents.sys_prompts import SystemPrompts
@@ -241,11 +286,15 @@ class Agent[TDeps = None, OutputDataT = str](BaseAgent[TDeps, OutputDataT]):
241
286
  output_type=to_type(output_type), # type: ignore[arg-type]
242
287
  tool_confirmation_mode=tool_confirmation_mode,
243
288
  event_handlers=event_handlers,
289
+ commands=commands,
244
290
  )
245
291
 
246
292
  # Store config for context creation
247
293
  self._agent_config = agent_config or NativeAgentConfig(name=name)
248
294
 
295
+ # Store builtin tools for pydantic-ai
296
+ self._builtin_tools = list(builtin_tools) if builtin_tools else []
297
+
249
298
  # Override tools with Agent-specific ToolManager (with tools and tool_mode)
250
299
  all_tools = list(tools or [])
251
300
  self.tools = ToolManager(all_tools, tool_mode=tool_mode)
@@ -284,8 +333,11 @@ class Agent[TDeps = None, OutputDataT = str](BaseAgent[TDeps, OutputDataT]):
284
333
  # Store hooks
285
334
  self.hooks = hooks
286
335
 
287
- # Store auto_cache setting
288
- self._auto_cache: AutoCache = auto_cache
336
+ # Store default usage limits
337
+ self._default_usage_limits = usage_limits
338
+
339
+ # Store providers for model discovery
340
+ self._providers = list(providers) if providers else None
289
341
 
290
342
  def __repr__(self) -> str:
291
343
  desc = f", {self.description!r}" if self.description else ""
@@ -574,6 +626,7 @@ class Agent[TDeps = None, OutputDataT = str](BaseAgent[TDeps, OutputDataT]):
574
626
  output_retries=self._output_retries,
575
627
  deps_type=self.deps_type or NoneType,
576
628
  output_type=final_type,
629
+ builtin_tools=self._builtin_tools,
577
630
  )
578
631
 
579
632
  base_context = self.get_context()
@@ -715,6 +768,7 @@ class Agent[TDeps = None, OutputDataT = str](BaseAgent[TDeps, OutputDataT]):
715
768
  wait_for_connections: bool | None = None,
716
769
  deps: TDeps | None = None,
717
770
  instructions: str | None = None,
771
+ event_handlers: Sequence[IndividualEventHandler | BuiltinEventHandlerType] | None = None,
718
772
  ) -> AsyncIterator[RichAgentStreamEvent[OutputDataT]]:
719
773
  """Run agent with prompt and get a streaming response.
720
774
 
@@ -734,6 +788,8 @@ class Agent[TDeps = None, OutputDataT = str](BaseAgent[TDeps, OutputDataT]):
734
788
  wait_for_connections: Whether to wait for connected agents to complete
735
789
  deps: Optional dependencies for the agent
736
790
  instructions: Optional instructions to override the agent's system prompt
791
+ event_handlers: Optional event handlers for this run (overrides agent's handlers)
792
+
737
793
  Returns:
738
794
  An async iterator yielding streaming events with final message embedded.
739
795
 
@@ -741,9 +797,22 @@ class Agent[TDeps = None, OutputDataT = str](BaseAgent[TDeps, OutputDataT]):
741
797
  UnexpectedModelBehavior: If the model fails or behaves unexpectedly
742
798
  """
743
799
  conversation = message_history if message_history is not None else self.conversation
800
+ # Use provided event handlers or fall back to agent's handlers
801
+ if event_handlers is not None:
802
+ from anyenv import MultiEventHandler
803
+
804
+ from agentpool.agents.events import resolve_event_handlers
805
+
806
+ handler: MultiEventHandler[IndividualEventHandler] = MultiEventHandler(
807
+ resolve_event_handlers(event_handlers)
808
+ )
809
+ else:
810
+ handler = self.event_handler
744
811
  message_id = message_id or str(uuid4())
745
812
  run_id = str(uuid4())
746
- user_msg, prompts, original_message = await prepare_prompts(*prompt)
813
+ # Get parent_id from last message in history for tree structure
814
+ last_msg_id = conversation.get_last_message_id()
815
+ user_msg, prompts, original_message = await prepare_prompts(*prompt, parent_id=last_msg_id)
747
816
  self.message_received.emit(user_msg)
748
817
  start_time = time.perf_counter()
749
818
  history_list = conversation.get_history()
@@ -753,9 +822,6 @@ class Agent[TDeps = None, OutputDataT = str](BaseAgent[TDeps, OutputDataT]):
753
822
  self._cancelled = False
754
823
  self._current_stream_task = asyncio.current_task()
755
824
 
756
- # Track accumulated content for partial message on cancellation
757
- accumulated_text: list[str] = []
758
-
759
825
  # Execute pre-run hooks
760
826
  if self.hooks:
761
827
  pre_run_result = await self.hooks.run_pre_run_hooks(
@@ -770,112 +836,120 @@ class Agent[TDeps = None, OutputDataT = str](BaseAgent[TDeps, OutputDataT]):
770
836
  msg = f"Run blocked: {reason}"
771
837
  raise RuntimeError(msg)
772
838
 
773
- yield RunStartedEvent(thread_id=self.conversation_id, run_id=run_id, agent_name=self.name)
839
+ run_started = RunStartedEvent(
840
+ thread_id=self.conversation_id, run_id=run_id, agent_name=self.name
841
+ )
842
+ await handler(None, run_started)
843
+ yield run_started
774
844
  try:
845
+ from pydantic_graph import End
846
+
775
847
  agentlet = await self.get_agentlet(tool_choice, model, output_type, input_provider)
776
848
  content = await convert_prompts(prompts)
777
849
  response_msg: ChatMessage[Any] | None = None
778
850
  # Prepend pending context parts (content is already pydantic-ai format)
779
851
  converted = [*pending_parts, *content]
780
852
 
781
- # Add CachePoint if auto_cache is enabled
782
- if self._auto_cache != "off":
783
- from pydantic_ai.messages import CachePoint
853
+ # Use provided usage_limits or fall back to default
854
+ effective_limits = usage_limits or self._default_usage_limits
855
+ history = [m for run in history_list for m in run.to_pydantic_ai()]
784
856
 
785
- cache_point = CachePoint(ttl=self._auto_cache)
786
- converted.append(cache_point)
787
- stream_events = agentlet.run_stream_events(
857
+ # Track tool call starts to combine with results later
858
+ pending_tcs: dict[str, BaseToolCallPart] = {}
859
+ file_tracker = FileTracker()
860
+
861
+ async with agentlet.iter(
788
862
  converted,
789
863
  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,
864
+ message_history=history,
865
+ usage_limits=effective_limits,
792
866
  instructions=instructions,
793
- )
794
-
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] = {}
867
+ ) as agent_run:
799
868
  try:
800
- async for event in events:
801
- # Check for cancellation
869
+ async for node in agent_run:
802
870
  if self._cancelled:
803
871
  self.log.info("Stream cancelled by user")
804
872
  break
873
+ if isinstance(node, End):
874
+ break
805
875
 
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)
876
+ # Stream events from model request node
877
+ if isinstance(node, ModelRequestNode):
878
+ async with (
879
+ node.stream(agent_run.ctx) as agent_stream,
880
+ merge_queue_into_iterator(
881
+ agent_stream, # type: ignore[arg-type]
882
+ self._event_queue,
883
+ ) as merged,
884
+ ):
885
+ async for event in file_tracker.track(merged):
886
+ if self._cancelled:
887
+ break
888
+ await handler(None, event)
889
+ yield event
890
+ combined = self._process_tool_event(
891
+ event, pending_tcs, message_id
892
+ )
893
+ if combined:
894
+ await handler(None, combined)
895
+ yield combined
896
+
897
+ # Stream events from tool call node
898
+ elif isinstance(node, CallToolsNode):
899
+ async with (
900
+ node.stream(agent_run.ctx) as tool_stream,
901
+ merge_queue_into_iterator(tool_stream, self._event_queue) as merged,
823
902
  ):
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,
903
+ async for event in file_tracker.track(merged):
904
+ if self._cancelled:
905
+ break
906
+ await handler(None, event)
907
+ yield event
908
+ combined = self._process_tool_event(
909
+ event, pending_tcs, message_id
839
910
  )
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
- )
911
+ if combined:
912
+ await handler(None, combined)
913
+ yield combined
851
914
  except asyncio.CancelledError:
852
915
  self.log.info("Stream cancelled via task cancellation")
853
916
  self._cancelled = True
854
917
 
855
- # Handle cancellation - emit partial message
856
- if self._cancelled:
918
+ # Build response message
857
919
  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]"
862
- response_msg = ChatMessage(
863
- content=partial_content,
864
- role="assistant",
865
- name=self.name,
866
- message_id=message_id,
867
- conversation_id=conversation_id or user_msg.conversation_id,
868
- response_time=response_time,
869
- finish_reason="stop",
870
- )
871
- yield StreamCompleteEvent(message=response_msg)
872
- self._current_stream_task = None
873
- return
874
-
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
920
+ if self._cancelled:
921
+ partial_content = _extract_text_from_messages(
922
+ agent_run.all_messages(), include_interruption_note=True
923
+ )
924
+ response_msg = ChatMessage(
925
+ content=partial_content,
926
+ role="assistant",
927
+ name=self.name,
928
+ message_id=message_id,
929
+ conversation_id=conversation_id or user_msg.conversation_id,
930
+ parent_id=user_msg.message_id,
931
+ response_time=response_time,
932
+ finish_reason="stop",
933
+ )
934
+ complete_event = StreamCompleteEvent(message=response_msg)
935
+ await handler(None, complete_event)
936
+ yield complete_event
937
+ self._current_stream_task = None
938
+ return
939
+
940
+ if agent_run.result:
941
+ response_msg = await ChatMessage.from_run_result(
942
+ agent_run.result,
943
+ agent_name=self.name,
944
+ message_id=message_id,
945
+ conversation_id=conversation_id or user_msg.conversation_id,
946
+ parent_id=user_msg.message_id,
947
+ response_time=response_time,
948
+ metadata=file_tracker.get_metadata(),
949
+ )
950
+ else:
951
+ msg = "Stream completed without producing a result"
952
+ raise RuntimeError(msg) # noqa: TRY301
879
953
 
880
954
  # Execute post-run hooks
881
955
  if self.hooks:
@@ -893,7 +967,9 @@ class Agent[TDeps = None, OutputDataT = str](BaseAgent[TDeps, OutputDataT]):
893
967
  if original_message:
894
968
  response_msg = response_msg.forwarded(original_message)
895
969
  # Send additional enriched completion event
896
- yield StreamCompleteEvent(message=response_msg)
970
+ complete_event = StreamCompleteEvent(message=response_msg)
971
+ await handler(None, complete_event)
972
+ yield complete_event
897
973
  self.message_sent.emit(response_msg)
898
974
  await self.log_message(response_msg)
899
975
  if store_history:
@@ -907,6 +983,41 @@ class Agent[TDeps = None, OutputDataT = str](BaseAgent[TDeps, OutputDataT]):
907
983
  finally:
908
984
  self._current_stream_task = None
909
985
 
986
+ def _process_tool_event(
987
+ self,
988
+ event: RichAgentStreamEvent[Any],
989
+ pending_tool_calls: dict[str, BaseToolCallPart],
990
+ message_id: str,
991
+ ) -> ToolCallCompleteEvent | None:
992
+ """Process tool-related events and return combined event when complete.
993
+
994
+ Args:
995
+ event: The streaming event to process
996
+ pending_tool_calls: Dict tracking in-progress tool calls by ID
997
+ message_id: Message ID for the combined event
998
+
999
+ Returns:
1000
+ ToolCallCompleteEvent if a tool call completed, None otherwise
1001
+ """
1002
+ match event:
1003
+ case PartStartEvent(part=BaseToolCallPart() as tool_part):
1004
+ pending_tool_calls[tool_part.tool_call_id] = tool_part
1005
+ case FunctionToolCallEvent(part=tool_part):
1006
+ pending_tool_calls[tool_part.tool_call_id] = tool_part
1007
+ case FunctionToolResultEvent(tool_call_id=call_id) as result_event:
1008
+ if call_info := pending_tool_calls.pop(call_id, None):
1009
+ return ToolCallCompleteEvent(
1010
+ tool_name=call_info.tool_name,
1011
+ tool_call_id=call_id,
1012
+ tool_input=safe_args_as_dict(call_info),
1013
+ tool_result=result_event.result.content
1014
+ if isinstance(result_event.result, ToolReturnPart)
1015
+ else result_event.result,
1016
+ agent_name=self.name,
1017
+ message_id=message_id,
1018
+ )
1019
+ return None
1020
+
910
1021
  async def run_iter(
911
1022
  self,
912
1023
  *prompt_groups: Sequence[PromptCompatible],
@@ -1131,7 +1242,7 @@ class Agent[TDeps = None, OutputDataT = str](BaseAgent[TDeps, OutputDataT]):
1131
1242
  parent=self if pass_message_history else None,
1132
1243
  )
1133
1244
 
1134
- def set_model(self, model: ModelType) -> None:
1245
+ async def set_model(self, model: ModelType) -> None:
1135
1246
  """Set the model for this agent.
1136
1247
 
1137
1248
  Args:
@@ -1166,11 +1277,6 @@ class Agent[TDeps = None, OutputDataT = str](BaseAgent[TDeps, OutputDataT]):
1166
1277
  )
1167
1278
  self.agent_reset.emit(event)
1168
1279
 
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
1280
  @asynccontextmanager
1175
1281
  async def temporary_state[T](
1176
1282
  self,
@@ -1247,6 +1353,107 @@ class Agent[TDeps = None, OutputDataT = str](BaseAgent[TDeps, OutputDataT]):
1247
1353
  else:
1248
1354
  return True
1249
1355
 
1356
+ async def get_available_models(self) -> list[ModelInfo] | None:
1357
+ """Get available models for this agent.
1358
+
1359
+ Uses tokonomics model discovery to fetch models from configured providers.
1360
+ Defaults to openai, anthropic, and gemini if no providers specified.
1361
+
1362
+ Returns:
1363
+ List of tokonomics ModelInfo, or None if discovery fails
1364
+ """
1365
+ from datetime import timedelta
1366
+
1367
+ from tokonomics.model_discovery import get_all_models
1368
+
1369
+ try:
1370
+ max_age = timedelta(days=200)
1371
+ return await get_all_models(
1372
+ providers=self._providers or ["models.dev"], max_age=max_age
1373
+ )
1374
+ except Exception:
1375
+ self.log.exception("Failed to discover models")
1376
+ return None
1377
+
1378
+ def get_modes(self) -> list[ModeCategory]:
1379
+ """Get available mode categories for this agent.
1380
+
1381
+ Native agents expose tool confirmation modes.
1382
+
1383
+ Returns:
1384
+ List with single ModeCategory for tool confirmation
1385
+ """
1386
+ from agentpool.agents.modes import ModeCategory, ModeInfo
1387
+
1388
+ # Map current confirmation mode to mode ID
1389
+ mode_id_map = {
1390
+ "per_tool": "default",
1391
+ "always": "default",
1392
+ "never": "acceptEdits",
1393
+ }
1394
+ current_id = mode_id_map.get(self.tool_confirmation_mode, "default")
1395
+
1396
+ category_id = "permissions"
1397
+ return [
1398
+ ModeCategory(
1399
+ id=category_id,
1400
+ name="Permissions",
1401
+ available_modes=[
1402
+ ModeInfo(
1403
+ id="default",
1404
+ name="Default",
1405
+ description="Require confirmation for tools marked as needing it",
1406
+ category_id=category_id,
1407
+ ),
1408
+ ModeInfo(
1409
+ id="acceptEdits",
1410
+ name="Accept Edits",
1411
+ description="Auto-approve all tool calls without confirmation",
1412
+ category_id=category_id,
1413
+ ),
1414
+ ],
1415
+ current_mode_id=current_id,
1416
+ )
1417
+ ]
1418
+
1419
+ async def set_mode(self, mode: ModeInfo | str, category_id: str | None = None) -> None:
1420
+ """Set a mode for this agent.
1421
+
1422
+ Native agents support the "permissions" category with modes:
1423
+ - "default": per_tool confirmation
1424
+ - "acceptEdits": never confirm (auto-approve)
1425
+
1426
+ Args:
1427
+ mode: Mode to activate - ModeInfo object or mode ID string
1428
+ category_id: Optional category (only "permissions" supported)
1429
+
1430
+ Raises:
1431
+ ValueError: If mode_id is invalid
1432
+ """
1433
+ # Extract mode_id and category from ModeInfo if provided
1434
+ if isinstance(mode, ModeInfo):
1435
+ mode_id = mode.id
1436
+ category_id = category_id or mode.category_id or None
1437
+ else:
1438
+ mode_id = mode
1439
+
1440
+ # Validate category if provided
1441
+ if category_id is not None and category_id != "permissions":
1442
+ msg = f"Unknown category: {category_id}. Only 'permissions' is supported."
1443
+ raise ValueError(msg)
1444
+
1445
+ # Map mode_id to confirmation mode
1446
+ mode_map: dict[str, ToolConfirmationMode] = {
1447
+ "default": "per_tool",
1448
+ "acceptEdits": "never",
1449
+ }
1450
+
1451
+ if mode_id not in mode_map:
1452
+ msg = f"Unknown mode: {mode_id}. Available: {list(mode_map.keys())}"
1453
+ raise ValueError(msg)
1454
+
1455
+ await self.set_tool_confirmation_mode(mode_map[mode_id])
1456
+
1250
1457
 
1251
1458
  if __name__ == "__main__":
1252
1459
  import logging
@@ -2,7 +2,6 @@
2
2
 
3
3
  from agentpool.agents.agui_agent.agui_agent import AGUIAgent
4
4
  from agentpool.agents.agui_agent.agui_converters import (
5
- ToolCallAccumulator,
6
5
  agui_to_native_event,
7
6
  to_agui_input_content,
8
7
  to_agui_tool,
@@ -12,7 +11,6 @@ from agentpool.agents.agui_agent.chunk_transformer import ChunkTransformer
12
11
  __all__ = [
13
12
  "AGUIAgent",
14
13
  "ChunkTransformer",
15
- "ToolCallAccumulator",
16
14
  "agui_to_native_event",
17
15
  "to_agui_input_content",
18
16
  "to_agui_tool",