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.
- acp/__init__.py +13 -0
- acp/bridge/README.md +15 -2
- acp/bridge/__init__.py +3 -2
- acp/bridge/__main__.py +60 -19
- acp/bridge/ws_server.py +173 -0
- acp/bridge/ws_server_cli.py +89 -0
- acp/notifications.py +2 -1
- acp/stdio.py +39 -9
- acp/transports.py +362 -2
- acp/utils.py +15 -2
- agentpool/__init__.py +4 -1
- agentpool/agents/__init__.py +2 -0
- agentpool/agents/acp_agent/acp_agent.py +203 -88
- agentpool/agents/acp_agent/acp_converters.py +46 -21
- agentpool/agents/acp_agent/client_handler.py +157 -3
- agentpool/agents/acp_agent/session_state.py +4 -1
- agentpool/agents/agent.py +314 -107
- agentpool/agents/agui_agent/__init__.py +0 -2
- agentpool/agents/agui_agent/agui_agent.py +90 -21
- agentpool/agents/agui_agent/agui_converters.py +0 -131
- agentpool/agents/base_agent.py +163 -1
- agentpool/agents/claude_code_agent/claude_code_agent.py +626 -179
- agentpool/agents/claude_code_agent/converters.py +71 -3
- agentpool/agents/claude_code_agent/history.py +474 -0
- agentpool/agents/context.py +40 -0
- agentpool/agents/events/__init__.py +2 -0
- agentpool/agents/events/builtin_handlers.py +2 -1
- agentpool/agents/events/event_emitter.py +29 -2
- agentpool/agents/events/events.py +20 -0
- agentpool/agents/modes.py +54 -0
- agentpool/agents/tool_call_accumulator.py +213 -0
- agentpool/common_types.py +21 -0
- agentpool/config_resources/__init__.py +38 -1
- agentpool/config_resources/claude_code_agent.yml +3 -0
- agentpool/delegation/pool.py +37 -29
- agentpool/delegation/team.py +1 -0
- agentpool/delegation/teamrun.py +1 -0
- agentpool/diagnostics/__init__.py +53 -0
- agentpool/diagnostics/lsp_manager.py +1593 -0
- agentpool/diagnostics/lsp_proxy.py +41 -0
- agentpool/diagnostics/lsp_proxy_script.py +229 -0
- agentpool/diagnostics/models.py +398 -0
- agentpool/mcp_server/__init__.py +0 -2
- agentpool/mcp_server/client.py +12 -3
- agentpool/mcp_server/manager.py +25 -31
- agentpool/mcp_server/registries/official_registry_client.py +25 -0
- agentpool/mcp_server/tool_bridge.py +78 -66
- agentpool/messaging/__init__.py +0 -2
- agentpool/messaging/compaction.py +72 -197
- agentpool/messaging/message_history.py +12 -0
- agentpool/messaging/messages.py +52 -9
- agentpool/messaging/processing.py +3 -1
- agentpool/models/acp_agents/base.py +0 -22
- agentpool/models/acp_agents/mcp_capable.py +8 -148
- agentpool/models/acp_agents/non_mcp.py +129 -72
- agentpool/models/agents.py +35 -13
- agentpool/models/claude_code_agents.py +33 -2
- agentpool/models/manifest.py +43 -0
- agentpool/repomap.py +1 -1
- agentpool/resource_providers/__init__.py +9 -1
- agentpool/resource_providers/aggregating.py +52 -3
- agentpool/resource_providers/base.py +57 -1
- agentpool/resource_providers/mcp_provider.py +23 -0
- agentpool/resource_providers/plan_provider.py +130 -41
- agentpool/resource_providers/pool.py +2 -0
- agentpool/resource_providers/static.py +2 -0
- agentpool/sessions/__init__.py +2 -1
- agentpool/sessions/manager.py +31 -2
- agentpool/sessions/models.py +50 -0
- agentpool/skills/registry.py +13 -8
- agentpool/storage/manager.py +217 -1
- agentpool/testing.py +537 -19
- agentpool/utils/file_watcher.py +269 -0
- agentpool/utils/identifiers.py +121 -0
- agentpool/utils/pydantic_ai_helpers.py +46 -0
- agentpool/utils/streams.py +690 -1
- agentpool/utils/subprocess_utils.py +155 -0
- agentpool/utils/token_breakdown.py +461 -0
- {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/METADATA +27 -7
- {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/RECORD +170 -112
- {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/WHEEL +1 -1
- agentpool_cli/__main__.py +4 -0
- agentpool_cli/serve_acp.py +41 -20
- agentpool_cli/serve_agui.py +87 -0
- agentpool_cli/serve_opencode.py +119 -0
- agentpool_commands/__init__.py +30 -0
- agentpool_commands/agents.py +74 -1
- agentpool_commands/history.py +62 -0
- agentpool_commands/mcp.py +176 -0
- agentpool_commands/models.py +56 -3
- agentpool_commands/tools.py +57 -0
- agentpool_commands/utils.py +51 -0
- agentpool_config/builtin_tools.py +77 -22
- agentpool_config/commands.py +24 -1
- agentpool_config/compaction.py +258 -0
- agentpool_config/mcp_server.py +131 -1
- agentpool_config/storage.py +46 -1
- agentpool_config/tools.py +7 -1
- agentpool_config/toolsets.py +92 -148
- agentpool_server/acp_server/acp_agent.py +134 -150
- agentpool_server/acp_server/commands/acp_commands.py +216 -51
- agentpool_server/acp_server/commands/docs_commands/fetch_repo.py +10 -10
- agentpool_server/acp_server/server.py +23 -79
- agentpool_server/acp_server/session.py +181 -19
- agentpool_server/opencode_server/.rules +95 -0
- agentpool_server/opencode_server/ENDPOINTS.md +362 -0
- agentpool_server/opencode_server/__init__.py +27 -0
- agentpool_server/opencode_server/command_validation.py +172 -0
- agentpool_server/opencode_server/converters.py +869 -0
- agentpool_server/opencode_server/dependencies.py +24 -0
- agentpool_server/opencode_server/input_provider.py +269 -0
- agentpool_server/opencode_server/models/__init__.py +228 -0
- agentpool_server/opencode_server/models/agent.py +53 -0
- agentpool_server/opencode_server/models/app.py +60 -0
- agentpool_server/opencode_server/models/base.py +26 -0
- agentpool_server/opencode_server/models/common.py +23 -0
- agentpool_server/opencode_server/models/config.py +37 -0
- agentpool_server/opencode_server/models/events.py +647 -0
- agentpool_server/opencode_server/models/file.py +88 -0
- agentpool_server/opencode_server/models/mcp.py +25 -0
- agentpool_server/opencode_server/models/message.py +162 -0
- agentpool_server/opencode_server/models/parts.py +190 -0
- agentpool_server/opencode_server/models/provider.py +81 -0
- agentpool_server/opencode_server/models/pty.py +43 -0
- agentpool_server/opencode_server/models/session.py +99 -0
- agentpool_server/opencode_server/routes/__init__.py +25 -0
- agentpool_server/opencode_server/routes/agent_routes.py +442 -0
- agentpool_server/opencode_server/routes/app_routes.py +139 -0
- agentpool_server/opencode_server/routes/config_routes.py +241 -0
- agentpool_server/opencode_server/routes/file_routes.py +392 -0
- agentpool_server/opencode_server/routes/global_routes.py +94 -0
- agentpool_server/opencode_server/routes/lsp_routes.py +319 -0
- agentpool_server/opencode_server/routes/message_routes.py +705 -0
- agentpool_server/opencode_server/routes/pty_routes.py +299 -0
- agentpool_server/opencode_server/routes/session_routes.py +1205 -0
- agentpool_server/opencode_server/routes/tui_routes.py +139 -0
- agentpool_server/opencode_server/server.py +430 -0
- agentpool_server/opencode_server/state.py +121 -0
- agentpool_server/opencode_server/time_utils.py +8 -0
- agentpool_storage/__init__.py +16 -0
- agentpool_storage/base.py +103 -0
- agentpool_storage/claude_provider.py +907 -0
- agentpool_storage/file_provider.py +129 -0
- agentpool_storage/memory_provider.py +61 -0
- agentpool_storage/models.py +3 -0
- agentpool_storage/opencode_provider.py +730 -0
- agentpool_storage/project_store.py +325 -0
- agentpool_storage/session_store.py +6 -0
- agentpool_storage/sql_provider/__init__.py +4 -2
- agentpool_storage/sql_provider/models.py +48 -0
- agentpool_storage/sql_provider/sql_provider.py +134 -1
- agentpool_storage/sql_provider/utils.py +10 -1
- agentpool_storage/text_log_provider.py +1 -0
- agentpool_toolsets/builtin/__init__.py +0 -8
- agentpool_toolsets/builtin/code.py +95 -56
- agentpool_toolsets/builtin/debug.py +16 -21
- agentpool_toolsets/builtin/execution_environment.py +99 -103
- agentpool_toolsets/builtin/file_edit/file_edit.py +115 -7
- agentpool_toolsets/builtin/skills.py +86 -4
- agentpool_toolsets/fsspec_toolset/__init__.py +13 -1
- agentpool_toolsets/fsspec_toolset/diagnostics.py +860 -73
- agentpool_toolsets/fsspec_toolset/grep.py +74 -2
- agentpool_toolsets/fsspec_toolset/image_utils.py +161 -0
- agentpool_toolsets/fsspec_toolset/toolset.py +159 -38
- agentpool_toolsets/mcp_discovery/__init__.py +5 -0
- agentpool_toolsets/mcp_discovery/data/mcp_servers.parquet +0 -0
- agentpool_toolsets/mcp_discovery/toolset.py +454 -0
- agentpool_toolsets/mcp_run_toolset.py +84 -6
- agentpool_toolsets/builtin/agent_management.py +0 -239
- agentpool_toolsets/builtin/history.py +0 -36
- agentpool_toolsets/builtin/integration.py +0 -85
- agentpool_toolsets/builtin/tool_management.py +0 -90
- {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/entry_points.txt +0 -0
- {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -34,8 +34,9 @@ from agentpool.agents.events import RunStartedEvent, StreamCompleteEvent
|
|
|
34
34
|
from agentpool.log import get_logger
|
|
35
35
|
from agentpool.messaging import ChatMessage
|
|
36
36
|
from agentpool.messaging.processing import prepare_prompts
|
|
37
|
-
from agentpool.talk.stats import MessageStats
|
|
38
37
|
from agentpool.tools import ToolManager
|
|
38
|
+
from agentpool.utils.streams import FileTracker
|
|
39
|
+
from agentpool.utils.token_breakdown import calculate_usage_from_parts
|
|
39
40
|
|
|
40
41
|
|
|
41
42
|
if TYPE_CHECKING:
|
|
@@ -45,10 +46,13 @@ if TYPE_CHECKING:
|
|
|
45
46
|
|
|
46
47
|
from ag_ui.core import Message, ToolMessage
|
|
47
48
|
from evented.configs import EventConfig
|
|
49
|
+
from slashed import BaseCommand
|
|
50
|
+
from tokonomics.model_discovery.model_info import ModelInfo
|
|
48
51
|
|
|
49
52
|
from agentpool.agents.base_agent import ToolConfirmationMode
|
|
50
53
|
from agentpool.agents.context import AgentContext
|
|
51
54
|
from agentpool.agents.events import RichAgentStreamEvent
|
|
55
|
+
from agentpool.agents.modes import ModeCategory, ModeInfo
|
|
52
56
|
from agentpool.common_types import (
|
|
53
57
|
BuiltinEventHandlerType,
|
|
54
58
|
IndividualEventHandler,
|
|
@@ -132,6 +136,7 @@ class AGUIAgent[TDeps = None](BaseAgent[TDeps, str]):
|
|
|
132
136
|
event_configs: Sequence[EventConfig] | None = None,
|
|
133
137
|
event_handlers: Sequence[IndividualEventHandler | BuiltinEventHandlerType] | None = None,
|
|
134
138
|
tool_confirmation_mode: ToolConfirmationMode = "per_tool",
|
|
139
|
+
commands: Sequence[BaseCommand] | None = None,
|
|
135
140
|
) -> None:
|
|
136
141
|
"""Initialize AG-UI agent client.
|
|
137
142
|
|
|
@@ -153,6 +158,7 @@ class AGUIAgent[TDeps = None](BaseAgent[TDeps, str]):
|
|
|
153
158
|
event_configs: Event trigger configurations
|
|
154
159
|
event_handlers: Sequence of event handlers to register
|
|
155
160
|
tool_confirmation_mode: Tool confirmation mode
|
|
161
|
+
commands: Slash commands
|
|
156
162
|
"""
|
|
157
163
|
super().__init__(
|
|
158
164
|
name=name,
|
|
@@ -164,6 +170,7 @@ class AGUIAgent[TDeps = None](BaseAgent[TDeps, str]):
|
|
|
164
170
|
event_configs=event_configs,
|
|
165
171
|
tool_confirmation_mode=tool_confirmation_mode,
|
|
166
172
|
event_handlers=event_handlers,
|
|
173
|
+
commands=commands,
|
|
167
174
|
)
|
|
168
175
|
|
|
169
176
|
# AG-UI specific configuration
|
|
@@ -379,11 +386,11 @@ class AGUIAgent[TDeps = None](BaseAgent[TDeps, str]):
|
|
|
379
386
|
)
|
|
380
387
|
|
|
381
388
|
from agentpool.agents.agui_agent.agui_converters import (
|
|
382
|
-
ToolCallAccumulator,
|
|
383
389
|
agui_to_native_event,
|
|
384
390
|
to_agui_input_content,
|
|
385
391
|
to_agui_tool,
|
|
386
392
|
)
|
|
393
|
+
from agentpool.agents.tool_call_accumulator import ToolCallAccumulator
|
|
387
394
|
|
|
388
395
|
if not self._client or not self._thread_id:
|
|
389
396
|
msg = "Agent not initialized - use async context manager"
|
|
@@ -394,7 +401,11 @@ class AGUIAgent[TDeps = None](BaseAgent[TDeps, str]):
|
|
|
394
401
|
self._current_stream_task = asyncio.current_task()
|
|
395
402
|
|
|
396
403
|
conversation = message_history if message_history is not None else self.conversation
|
|
397
|
-
|
|
404
|
+
# Get parent_id from last message in history for tree structure
|
|
405
|
+
last_msg_id = conversation.get_last_message_id()
|
|
406
|
+
user_msg, processed_prompts, _original_message = await prepare_prompts(
|
|
407
|
+
*prompts, parent_id=last_msg_id
|
|
408
|
+
)
|
|
398
409
|
self._run_id = str(uuid4()) # New run ID for each run
|
|
399
410
|
self._chunk_transformer.reset() # Reset chunk transformer
|
|
400
411
|
# Track messages in pydantic-ai format: ModelRequest -> ModelResponse -> ModelRequest...
|
|
@@ -411,10 +422,9 @@ class AGUIAgent[TDeps = None](BaseAgent[TDeps, str]):
|
|
|
411
422
|
run_id=self._run_id or str(uuid4()),
|
|
412
423
|
agent_name=self.name,
|
|
413
424
|
)
|
|
414
|
-
for handler in self.event_handler._wrapped_handlers:
|
|
415
|
-
await handler(None, run_started)
|
|
416
|
-
yield run_started
|
|
417
425
|
|
|
426
|
+
await self.event_handler(None, run_started)
|
|
427
|
+
yield run_started
|
|
418
428
|
# Get pending parts from conversation and convert them
|
|
419
429
|
pending_parts = conversation.get_pending_parts()
|
|
420
430
|
pending_content = to_agui_input_content(pending_parts)
|
|
@@ -432,6 +442,8 @@ class AGUIAgent[TDeps = None](BaseAgent[TDeps, str]):
|
|
|
432
442
|
tool_accumulator = ToolCallAccumulator()
|
|
433
443
|
pending_tool_results: list[ToolMessage] = []
|
|
434
444
|
self.log.debug("Sending prompt to AG-UI agent", tool_names=[t.name for t in agui_tools])
|
|
445
|
+
# Track files modified during this run
|
|
446
|
+
file_tracker = FileTracker()
|
|
435
447
|
# Loop to handle tool calls - agent may request multiple rounds
|
|
436
448
|
try:
|
|
437
449
|
while True:
|
|
@@ -509,25 +521,24 @@ class AGUIAgent[TDeps = None](BaseAgent[TDeps, str]):
|
|
|
509
521
|
|
|
510
522
|
# Convert to native event and distribute to handlers
|
|
511
523
|
if native_event := agui_to_native_event(event):
|
|
524
|
+
# Track file modifications
|
|
525
|
+
file_tracker.process_event(native_event)
|
|
512
526
|
# Check for queued custom events first
|
|
513
527
|
while not self._event_queue.empty():
|
|
514
528
|
try:
|
|
515
529
|
custom_event = self._event_queue.get_nowait()
|
|
516
|
-
|
|
517
|
-
await handler(None, custom_event)
|
|
530
|
+
await self.event_handler(None, custom_event)
|
|
518
531
|
yield custom_event
|
|
519
532
|
except asyncio.QueueEmpty:
|
|
520
533
|
break
|
|
521
534
|
# Distribute to handlers
|
|
522
|
-
|
|
523
|
-
await handler(None, native_event)
|
|
535
|
+
await self.event_handler(None, native_event)
|
|
524
536
|
yield native_event
|
|
525
537
|
|
|
526
538
|
# Flush any pending chunk events at end of stream
|
|
527
539
|
for event in self._chunk_transformer.flush():
|
|
528
540
|
if native_event := agui_to_native_event(event):
|
|
529
|
-
|
|
530
|
-
await handler(None, native_event)
|
|
541
|
+
await self.event_handler(None, native_event)
|
|
531
542
|
yield native_event
|
|
532
543
|
|
|
533
544
|
except httpx.HTTPError:
|
|
@@ -592,12 +603,13 @@ class AGUIAgent[TDeps = None](BaseAgent[TDeps, str]):
|
|
|
592
603
|
name=self.name,
|
|
593
604
|
message_id=message_id or str(uuid4()),
|
|
594
605
|
conversation_id=self.conversation_id,
|
|
606
|
+
parent_id=user_msg.message_id,
|
|
595
607
|
messages=model_messages,
|
|
596
608
|
finish_reason="stop",
|
|
609
|
+
metadata=file_tracker.get_metadata(),
|
|
597
610
|
)
|
|
598
611
|
complete_event = StreamCompleteEvent(message=final_message)
|
|
599
|
-
|
|
600
|
-
await handler(None, complete_event)
|
|
612
|
+
await self.event_handler(None, complete_event)
|
|
601
613
|
yield complete_event
|
|
602
614
|
self._current_stream_task = None
|
|
603
615
|
return
|
|
@@ -610,24 +622,36 @@ class AGUIAgent[TDeps = None](BaseAgent[TDeps, str]):
|
|
|
610
622
|
while not self._event_queue.empty():
|
|
611
623
|
try:
|
|
612
624
|
queued_event = self._event_queue.get_nowait()
|
|
613
|
-
|
|
614
|
-
await handler(None, queued_event)
|
|
625
|
+
await self.event_handler(None, queued_event)
|
|
615
626
|
yield queued_event
|
|
616
627
|
except asyncio.QueueEmpty:
|
|
617
628
|
break
|
|
618
629
|
|
|
619
630
|
text_content = "".join(text_chunks)
|
|
631
|
+
|
|
632
|
+
# Calculate approximate token usage from what we can observe
|
|
633
|
+
input_parts = [*processed_prompts, *pending_parts]
|
|
634
|
+
usage, cost_info = await calculate_usage_from_parts(
|
|
635
|
+
input_parts=input_parts,
|
|
636
|
+
response_parts=current_response_parts,
|
|
637
|
+
text_content=text_content,
|
|
638
|
+
model_name=self.model_name,
|
|
639
|
+
)
|
|
640
|
+
|
|
620
641
|
final_message = ChatMessage[str](
|
|
621
642
|
content=text_content,
|
|
622
643
|
role="assistant",
|
|
623
644
|
name=self.name,
|
|
624
645
|
message_id=message_id or str(uuid4()),
|
|
625
646
|
conversation_id=self.conversation_id,
|
|
647
|
+
parent_id=user_msg.message_id,
|
|
626
648
|
messages=model_messages,
|
|
649
|
+
metadata=file_tracker.get_metadata(),
|
|
650
|
+
usage=usage,
|
|
651
|
+
cost_info=cost_info,
|
|
627
652
|
)
|
|
628
653
|
complete_event = StreamCompleteEvent(message=final_message)
|
|
629
|
-
|
|
630
|
-
await handler(None, complete_event)
|
|
654
|
+
await self.event_handler(None, complete_event)
|
|
631
655
|
yield complete_event
|
|
632
656
|
# Record to conversation history
|
|
633
657
|
conversation.add_chat_messages([user_msg, final_message])
|
|
@@ -657,9 +681,54 @@ class AGUIAgent[TDeps = None](BaseAgent[TDeps, str]):
|
|
|
657
681
|
"""Get model name (AG-UI doesn't expose this)."""
|
|
658
682
|
return None
|
|
659
683
|
|
|
660
|
-
async def
|
|
661
|
-
"""
|
|
662
|
-
|
|
684
|
+
async def set_model(self, model: str) -> None:
|
|
685
|
+
"""Set model (no-op for AG-UI as model is controlled by remote server)."""
|
|
686
|
+
# AG-UI agents don't support model selection - the model is
|
|
687
|
+
# determined by the remote server configuration
|
|
688
|
+
|
|
689
|
+
async def get_available_models(self) -> list[ModelInfo] | None:
|
|
690
|
+
"""Get available models for AG-UI agent.
|
|
691
|
+
|
|
692
|
+
AG-UI doesn't expose model information, so returns a placeholder model
|
|
693
|
+
indicating the model is determined by the remote server.
|
|
694
|
+
|
|
695
|
+
Returns:
|
|
696
|
+
List with a single placeholder ModelInfo
|
|
697
|
+
"""
|
|
698
|
+
from tokonomics.model_discovery.model_info import ModelInfo
|
|
699
|
+
|
|
700
|
+
return [
|
|
701
|
+
ModelInfo(
|
|
702
|
+
id="server-determined",
|
|
703
|
+
name="Determined by server",
|
|
704
|
+
description="The model is determined by the remote AG-UI server",
|
|
705
|
+
)
|
|
706
|
+
]
|
|
707
|
+
|
|
708
|
+
def get_modes(self) -> list[ModeCategory]:
|
|
709
|
+
"""Get available modes for AG-UI agent.
|
|
710
|
+
|
|
711
|
+
AG-UI doesn't expose mode information, so returns an empty list.
|
|
712
|
+
|
|
713
|
+
Returns:
|
|
714
|
+
Empty list - no modes supported
|
|
715
|
+
"""
|
|
716
|
+
return []
|
|
717
|
+
|
|
718
|
+
async def set_mode(self, mode: ModeInfo | str, category_id: str | None = None) -> None:
|
|
719
|
+
"""Set a mode for AG-UI agent.
|
|
720
|
+
|
|
721
|
+
AG-UI doesn't support modes, so this always raises an error.
|
|
722
|
+
|
|
723
|
+
Args:
|
|
724
|
+
mode: The mode to set (not supported)
|
|
725
|
+
category_id: Optional category ID (not supported)
|
|
726
|
+
|
|
727
|
+
Raises:
|
|
728
|
+
ValueError: Always - AG-UI doesn't support modes
|
|
729
|
+
"""
|
|
730
|
+
msg = "AG-UI agent does not support modes"
|
|
731
|
+
raise ValueError(msg)
|
|
663
732
|
|
|
664
733
|
|
|
665
734
|
if __name__ == "__main__":
|
|
@@ -12,7 +12,6 @@ from __future__ import annotations
|
|
|
12
12
|
import base64
|
|
13
13
|
from typing import TYPE_CHECKING, Any
|
|
14
14
|
|
|
15
|
-
import anyenv
|
|
16
15
|
from pydantic_ai import (
|
|
17
16
|
AudioUrl,
|
|
18
17
|
BinaryContent,
|
|
@@ -291,133 +290,3 @@ def to_agui_tool(tool: Tool) -> AGUITool:
|
|
|
291
290
|
description=func_schema.get("description", ""),
|
|
292
291
|
parameters=func_schema.get("parameters", {"type": "object", "properties": {}}),
|
|
293
292
|
)
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
def _repair_partial_json(buffer: str) -> str:
|
|
297
|
-
"""Attempt to repair truncated JSON for preview purposes.
|
|
298
|
-
|
|
299
|
-
Handles common truncation cases:
|
|
300
|
-
- Unclosed strings
|
|
301
|
-
- Missing closing braces/brackets
|
|
302
|
-
- Trailing commas
|
|
303
|
-
|
|
304
|
-
Args:
|
|
305
|
-
buffer: Potentially incomplete JSON string
|
|
306
|
-
|
|
307
|
-
Returns:
|
|
308
|
-
Repaired JSON string (may still be invalid in edge cases)
|
|
309
|
-
"""
|
|
310
|
-
if not buffer:
|
|
311
|
-
return "{}"
|
|
312
|
-
|
|
313
|
-
result = buffer.rstrip()
|
|
314
|
-
|
|
315
|
-
# Check if we're in the middle of a string by counting unescaped quotes
|
|
316
|
-
in_string = False
|
|
317
|
-
i = 0
|
|
318
|
-
while i < len(result):
|
|
319
|
-
char = result[i]
|
|
320
|
-
if char == "\\" and i + 1 < len(result):
|
|
321
|
-
i += 2 # Skip escaped character
|
|
322
|
-
continue
|
|
323
|
-
if char == '"':
|
|
324
|
-
in_string = not in_string
|
|
325
|
-
i += 1
|
|
326
|
-
|
|
327
|
-
# Close unclosed string
|
|
328
|
-
if in_string:
|
|
329
|
-
result += '"'
|
|
330
|
-
|
|
331
|
-
# Remove trailing comma (invalid JSON)
|
|
332
|
-
result = result.rstrip()
|
|
333
|
-
if result.endswith(","):
|
|
334
|
-
result = result[:-1]
|
|
335
|
-
|
|
336
|
-
# Balance braces and brackets
|
|
337
|
-
open_braces = result.count("{") - result.count("}")
|
|
338
|
-
open_brackets = result.count("[") - result.count("]")
|
|
339
|
-
|
|
340
|
-
result += "]" * max(0, open_brackets)
|
|
341
|
-
result += "}" * max(0, open_braces)
|
|
342
|
-
|
|
343
|
-
return result
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
class ToolCallAccumulator:
|
|
347
|
-
"""Accumulates streamed tool call arguments.
|
|
348
|
-
|
|
349
|
-
AG-UI streams tool call arguments as deltas, this class accumulates them
|
|
350
|
-
and provides the complete arguments when the tool call ends.
|
|
351
|
-
"""
|
|
352
|
-
|
|
353
|
-
def __init__(self) -> None:
|
|
354
|
-
self._calls: dict[str, dict[str, Any]] = {}
|
|
355
|
-
|
|
356
|
-
def start(self, tool_call_id: str, tool_name: str) -> None:
|
|
357
|
-
"""Start tracking a new tool call."""
|
|
358
|
-
self._calls[tool_call_id] = {"name": tool_name, "args_buffer": ""}
|
|
359
|
-
|
|
360
|
-
def add_args(self, tool_call_id: str, delta: str) -> None:
|
|
361
|
-
"""Add argument delta to a tool call."""
|
|
362
|
-
if tool_call_id in self._calls:
|
|
363
|
-
self._calls[tool_call_id]["args_buffer"] += delta
|
|
364
|
-
|
|
365
|
-
def complete(self, tool_call_id: str) -> tuple[str, dict[str, Any]] | None:
|
|
366
|
-
"""Complete a tool call and return (tool_name, parsed_args).
|
|
367
|
-
|
|
368
|
-
Returns:
|
|
369
|
-
Tuple of (tool_name, args_dict) or None if call not found
|
|
370
|
-
"""
|
|
371
|
-
if tool_call_id not in self._calls:
|
|
372
|
-
return None
|
|
373
|
-
|
|
374
|
-
call_data = self._calls.pop(tool_call_id)
|
|
375
|
-
args_str = call_data["args_buffer"]
|
|
376
|
-
try:
|
|
377
|
-
args = anyenv.load_json(args_str) if args_str else {}
|
|
378
|
-
except anyenv.JsonLoadError:
|
|
379
|
-
args = {"raw": args_str}
|
|
380
|
-
return call_data["name"], args
|
|
381
|
-
|
|
382
|
-
def get_pending(self, tool_call_id: str) -> tuple[str, str] | None:
|
|
383
|
-
"""Get pending call data (tool_name, args_buffer) without completing."""
|
|
384
|
-
if tool_call_id not in self._calls:
|
|
385
|
-
return None
|
|
386
|
-
data = self._calls[tool_call_id]
|
|
387
|
-
return data["name"], data["args_buffer"]
|
|
388
|
-
|
|
389
|
-
def get_partial_args(self, tool_call_id: str) -> dict[str, Any]:
|
|
390
|
-
"""Get best-effort parsed args from incomplete JSON stream.
|
|
391
|
-
|
|
392
|
-
Uses heuristics to complete truncated JSON for preview purposes.
|
|
393
|
-
Handles unclosed strings, missing braces/brackets, and trailing commas.
|
|
394
|
-
|
|
395
|
-
Args:
|
|
396
|
-
tool_call_id: Tool call ID
|
|
397
|
-
|
|
398
|
-
Returns:
|
|
399
|
-
Partially parsed arguments or empty dict
|
|
400
|
-
"""
|
|
401
|
-
if tool_call_id not in self._calls:
|
|
402
|
-
return {}
|
|
403
|
-
|
|
404
|
-
buffer = self._calls[tool_call_id]["args_buffer"]
|
|
405
|
-
if not buffer:
|
|
406
|
-
return {}
|
|
407
|
-
|
|
408
|
-
# Try direct parse first
|
|
409
|
-
try:
|
|
410
|
-
return anyenv.load_json(buffer)
|
|
411
|
-
except anyenv.JsonLoadError:
|
|
412
|
-
pass
|
|
413
|
-
|
|
414
|
-
# Try to repair truncated JSON
|
|
415
|
-
try:
|
|
416
|
-
repaired = _repair_partial_json(buffer)
|
|
417
|
-
return anyenv.load_json(repaired)
|
|
418
|
-
except anyenv.JsonLoadError:
|
|
419
|
-
return {}
|
|
420
|
-
|
|
421
|
-
def clear(self) -> None:
|
|
422
|
-
"""Clear all pending tool calls."""
|
|
423
|
-
self._calls.clear()
|
agentpool/agents/base_agent.py
CHANGED
|
@@ -7,6 +7,7 @@ import asyncio
|
|
|
7
7
|
from typing import TYPE_CHECKING, Any, Literal
|
|
8
8
|
|
|
9
9
|
from anyenv import MultiEventHandler
|
|
10
|
+
from anyenv.signals import BoundSignal
|
|
10
11
|
from exxec import LocalExecutionEnvironment
|
|
11
12
|
|
|
12
13
|
from agentpool.agents.events import resolve_event_handlers
|
|
@@ -20,17 +21,30 @@ if TYPE_CHECKING:
|
|
|
20
21
|
|
|
21
22
|
from evented.configs import EventConfig
|
|
22
23
|
from exxec import ExecutionEnvironment
|
|
24
|
+
from slashed import BaseCommand, CommandStore
|
|
25
|
+
from tokonomics.model_discovery.model_info import ModelInfo
|
|
23
26
|
|
|
27
|
+
from acp.schema import AvailableCommandsUpdate, ConfigOptionUpdate
|
|
24
28
|
from agentpool.agents.context import AgentContext
|
|
25
29
|
from agentpool.agents.events import RichAgentStreamEvent
|
|
26
|
-
from agentpool.
|
|
30
|
+
from agentpool.agents.modes import ModeCategory, ModeInfo
|
|
31
|
+
from agentpool.common_types import (
|
|
32
|
+
BuiltinEventHandlerType,
|
|
33
|
+
IndividualEventHandler,
|
|
34
|
+
MCPServerStatus,
|
|
35
|
+
)
|
|
27
36
|
from agentpool.delegation import AgentPool
|
|
37
|
+
from agentpool.talk.stats import MessageStats
|
|
28
38
|
from agentpool.ui.base import InputProvider
|
|
29
39
|
from agentpool_config.mcp_server import MCPServerConfig
|
|
30
40
|
|
|
41
|
+
# Union type for state updates emitted via state_updated signal
|
|
42
|
+
type StateUpdate = ModeInfo | ModelInfo | AvailableCommandsUpdate | ConfigOptionUpdate
|
|
43
|
+
|
|
31
44
|
|
|
32
45
|
logger = get_logger(__name__)
|
|
33
46
|
|
|
47
|
+
|
|
34
48
|
ToolConfirmationMode = Literal["always", "never", "per_tool"]
|
|
35
49
|
|
|
36
50
|
|
|
@@ -64,6 +78,7 @@ class BaseAgent[TDeps = None, TResult = str](MessageNode[TDeps, TResult]):
|
|
|
64
78
|
output_type: type[TResult] = str, # type: ignore[assignment]
|
|
65
79
|
tool_confirmation_mode: ToolConfirmationMode = "per_tool",
|
|
66
80
|
event_handlers: Sequence[IndividualEventHandler | BuiltinEventHandlerType] | None = None,
|
|
81
|
+
commands: Sequence[BaseCommand] | None = None,
|
|
67
82
|
) -> None:
|
|
68
83
|
"""Initialize base agent with shared infrastructure.
|
|
69
84
|
|
|
@@ -80,6 +95,7 @@ class BaseAgent[TDeps = None, TResult = str](MessageNode[TDeps, TResult]):
|
|
|
80
95
|
output_type: Output type for this agent
|
|
81
96
|
tool_confirmation_mode: How tool execution confirmation is handled
|
|
82
97
|
event_handlers: Event handlers for this agent
|
|
98
|
+
commands: Slash commands to register with this agent
|
|
83
99
|
"""
|
|
84
100
|
super().__init__(
|
|
85
101
|
name=name,
|
|
@@ -108,6 +124,31 @@ class BaseAgent[TDeps = None, TResult = str](MessageNode[TDeps, TResult]):
|
|
|
108
124
|
self._cancelled = False
|
|
109
125
|
self._current_stream_task: asyncio.Task[Any] | None = None
|
|
110
126
|
|
|
127
|
+
# State change signal - emitted when mode/model/commands change
|
|
128
|
+
# Uses union type for different state update kinds
|
|
129
|
+
self.state_updated: BoundSignal[StateUpdate] = BoundSignal()
|
|
130
|
+
|
|
131
|
+
# Command store for slash commands
|
|
132
|
+
from slashed import CommandStore
|
|
133
|
+
|
|
134
|
+
from agentpool_commands import get_commands
|
|
135
|
+
|
|
136
|
+
self._command_store: CommandStore = CommandStore()
|
|
137
|
+
|
|
138
|
+
# Register default agent commands
|
|
139
|
+
for command in get_commands():
|
|
140
|
+
self._command_store.register_command(command)
|
|
141
|
+
|
|
142
|
+
# Register additional provided commands
|
|
143
|
+
if commands:
|
|
144
|
+
for command in commands:
|
|
145
|
+
self._command_store.register_command(command)
|
|
146
|
+
|
|
147
|
+
@property
|
|
148
|
+
def command_store(self) -> CommandStore:
|
|
149
|
+
"""Get the command store for slash commands."""
|
|
150
|
+
return self._command_store
|
|
151
|
+
|
|
111
152
|
@abstractmethod
|
|
112
153
|
def get_context(self, data: Any = None) -> AgentContext[Any]:
|
|
113
154
|
"""Create a new context for this agent.
|
|
@@ -126,6 +167,15 @@ class BaseAgent[TDeps = None, TResult = str](MessageNode[TDeps, TResult]):
|
|
|
126
167
|
"""Get the model name used by this agent."""
|
|
127
168
|
...
|
|
128
169
|
|
|
170
|
+
@abstractmethod
|
|
171
|
+
async def set_model(self, model: str) -> None:
|
|
172
|
+
"""Set the model for this agent.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
model: New model identifier to use
|
|
176
|
+
"""
|
|
177
|
+
...
|
|
178
|
+
|
|
129
179
|
@abstractmethod
|
|
130
180
|
def run_stream(
|
|
131
181
|
self,
|
|
@@ -175,3 +225,115 @@ class BaseAgent[TDeps = None, TResult = str](MessageNode[TDeps, TResult]):
|
|
|
175
225
|
if self._current_stream_task and not self._current_stream_task.done():
|
|
176
226
|
self._current_stream_task.cancel()
|
|
177
227
|
logger.info("Interrupted agent stream", agent=self.name)
|
|
228
|
+
|
|
229
|
+
async def get_stats(self) -> MessageStats:
|
|
230
|
+
"""Get message statistics."""
|
|
231
|
+
from agentpool.talk.stats import MessageStats
|
|
232
|
+
|
|
233
|
+
return MessageStats(messages=list(self.conversation.chat_messages))
|
|
234
|
+
|
|
235
|
+
def get_mcp_server_info(self) -> dict[str, MCPServerStatus]:
|
|
236
|
+
"""Get information about configured MCP servers.
|
|
237
|
+
|
|
238
|
+
Returns a dict mapping server names to their status info. Used by
|
|
239
|
+
the OpenCode /mcp endpoint to display MCP servers in the UI.
|
|
240
|
+
|
|
241
|
+
The default implementation checks external_providers on the tool manager.
|
|
242
|
+
Subclasses may override to provide agent-specific MCP server info
|
|
243
|
+
(e.g., ClaudeCodeAgent has its own MCP server handling).
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
Dict mapping server name to MCPServerStatus
|
|
247
|
+
"""
|
|
248
|
+
from agentpool.common_types import MCPServerStatus
|
|
249
|
+
from agentpool.mcp_server.manager import MCPManager
|
|
250
|
+
from agentpool.resource_providers import AggregatingResourceProvider
|
|
251
|
+
from agentpool.resource_providers.mcp_provider import MCPResourceProvider
|
|
252
|
+
|
|
253
|
+
def add_status(provider: MCPResourceProvider, result: dict[str, MCPServerStatus]) -> None:
|
|
254
|
+
status_dict = provider.get_status()
|
|
255
|
+
status_type = status_dict.get("status", "disabled")
|
|
256
|
+
if status_type == "connected":
|
|
257
|
+
result[provider.name] = MCPServerStatus(
|
|
258
|
+
name=provider.name, status="connected", server_type="stdio"
|
|
259
|
+
)
|
|
260
|
+
elif status_type == "failed":
|
|
261
|
+
error = status_dict.get("error", "Unknown error")
|
|
262
|
+
result[provider.name] = MCPServerStatus(
|
|
263
|
+
name=provider.name, status="error", error=error
|
|
264
|
+
)
|
|
265
|
+
else:
|
|
266
|
+
result[provider.name] = MCPServerStatus(name=provider.name, status="disconnected")
|
|
267
|
+
|
|
268
|
+
result: dict[str, MCPServerStatus] = {}
|
|
269
|
+
try:
|
|
270
|
+
for provider in self.tools.external_providers:
|
|
271
|
+
if isinstance(provider, MCPResourceProvider):
|
|
272
|
+
add_status(provider, result)
|
|
273
|
+
elif isinstance(provider, AggregatingResourceProvider):
|
|
274
|
+
for nested in provider.providers:
|
|
275
|
+
if isinstance(nested, MCPResourceProvider):
|
|
276
|
+
add_status(nested, result)
|
|
277
|
+
elif isinstance(provider, MCPManager):
|
|
278
|
+
for mcp_provider in provider.get_mcp_providers():
|
|
279
|
+
add_status(mcp_provider, result)
|
|
280
|
+
except Exception: # noqa: BLE001
|
|
281
|
+
pass
|
|
282
|
+
|
|
283
|
+
return result
|
|
284
|
+
|
|
285
|
+
@abstractmethod
|
|
286
|
+
async def get_available_models(self) -> list[ModelInfo] | None:
|
|
287
|
+
"""Get available models for this agent.
|
|
288
|
+
|
|
289
|
+
Returns a list of models that can be used with this agent, or None
|
|
290
|
+
if model discovery is not supported for this agent type.
|
|
291
|
+
|
|
292
|
+
Uses tokonomics.ModelInfo which includes pricing, capabilities,
|
|
293
|
+
and limits. Can be converted to protocol-specific formats (OpenCode, ACP).
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
List of tokonomics ModelInfo, or None if not supported
|
|
297
|
+
"""
|
|
298
|
+
...
|
|
299
|
+
|
|
300
|
+
@abstractmethod
|
|
301
|
+
def get_modes(self) -> list[ModeCategory]:
|
|
302
|
+
"""Get available mode categories for this agent.
|
|
303
|
+
|
|
304
|
+
Returns a list of mode categories that can be switched. Each category
|
|
305
|
+
represents a group of mutually exclusive modes (e.g., permissions,
|
|
306
|
+
behavior presets).
|
|
307
|
+
|
|
308
|
+
Different agent types expose different modes:
|
|
309
|
+
- Native Agent: Tool confirmation modes (default, acceptEdits)
|
|
310
|
+
- ClaudeCodeAgent: Claude Code SDK modes (plan, code, etc.)
|
|
311
|
+
- ACPAgent: Passthrough from remote server
|
|
312
|
+
- AGUIAgent: Empty list (no modes)
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
List of ModeCategory, empty list if no modes supported
|
|
316
|
+
"""
|
|
317
|
+
...
|
|
318
|
+
|
|
319
|
+
@abstractmethod
|
|
320
|
+
async def set_mode(self, mode: ModeInfo | str, category_id: str | None = None) -> None:
|
|
321
|
+
"""Set a mode within a category.
|
|
322
|
+
|
|
323
|
+
Each agent type handles mode switching according to its own semantics:
|
|
324
|
+
- Native Agent: Maps to tool confirmation mode
|
|
325
|
+
- ClaudeCodeAgent: Maps to SDK permission mode
|
|
326
|
+
- ACPAgent: Forwards to remote server
|
|
327
|
+
- AGUIAgent: No-op (no modes supported)
|
|
328
|
+
|
|
329
|
+
Args:
|
|
330
|
+
mode: The mode to activate - either a ModeInfo object or mode ID string.
|
|
331
|
+
If ModeInfo, category_id is extracted from it (unless overridden).
|
|
332
|
+
category_id: Optional category ID. If None and mode is a string,
|
|
333
|
+
uses the first category. If None and mode is ModeInfo,
|
|
334
|
+
uses the mode's category_id.
|
|
335
|
+
|
|
336
|
+
Raises:
|
|
337
|
+
ValueError: If mode_id or category_id is invalid
|
|
338
|
+
"""
|
|
339
|
+
...
|