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.
- acp/__init__.py +13 -4
- acp/acp_requests.py +20 -77
- acp/agent/connection.py +8 -0
- acp/agent/implementations/debug_server/debug_server.py +6 -2
- acp/agent/protocol.py +6 -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/client/connection.py +38 -29
- acp/client/implementations/default_client.py +3 -2
- acp/client/implementations/headless_client.py +2 -2
- acp/connection.py +2 -2
- acp/notifications.py +20 -50
- acp/schema/__init__.py +2 -0
- acp/schema/agent_responses.py +21 -0
- acp/schema/client_requests.py +3 -3
- acp/schema/session_state.py +63 -29
- acp/stdio.py +39 -9
- acp/task/supervisor.py +2 -2
- acp/transports.py +362 -2
- acp/utils.py +17 -4
- agentpool/__init__.py +6 -1
- agentpool/agents/__init__.py +2 -0
- agentpool/agents/acp_agent/acp_agent.py +407 -277
- agentpool/agents/acp_agent/acp_converters.py +196 -38
- agentpool/agents/acp_agent/client_handler.py +191 -26
- agentpool/agents/acp_agent/session_state.py +17 -6
- agentpool/agents/agent.py +607 -572
- agentpool/agents/agui_agent/__init__.py +0 -2
- agentpool/agents/agui_agent/agui_agent.py +176 -110
- agentpool/agents/agui_agent/agui_converters.py +0 -131
- agentpool/agents/agui_agent/helpers.py +3 -4
- agentpool/agents/base_agent.py +632 -17
- agentpool/agents/claude_code_agent/FORKING.md +191 -0
- agentpool/agents/claude_code_agent/__init__.py +13 -1
- agentpool/agents/claude_code_agent/claude_code_agent.py +1058 -291
- agentpool/agents/claude_code_agent/converters.py +74 -143
- agentpool/agents/claude_code_agent/history.py +474 -0
- agentpool/agents/claude_code_agent/models.py +77 -0
- agentpool/agents/claude_code_agent/static_info.py +100 -0
- agentpool/agents/claude_code_agent/usage.py +242 -0
- agentpool/agents/context.py +40 -0
- agentpool/agents/events/__init__.py +24 -0
- agentpool/agents/events/builtin_handlers.py +67 -1
- agentpool/agents/events/event_emitter.py +32 -2
- agentpool/agents/events/events.py +104 -3
- agentpool/agents/events/infer_info.py +145 -0
- agentpool/agents/events/processors.py +254 -0
- agentpool/agents/interactions.py +41 -6
- agentpool/agents/modes.py +67 -0
- agentpool/agents/slashed_agent.py +5 -4
- agentpool/agents/tool_call_accumulator.py +213 -0
- agentpool/agents/tool_wrapping.py +18 -6
- agentpool/common_types.py +56 -21
- agentpool/config_resources/__init__.py +38 -1
- agentpool/config_resources/acp_assistant.yml +2 -2
- agentpool/config_resources/agents.yml +3 -0
- agentpool/config_resources/agents_template.yml +1 -0
- agentpool/config_resources/claude_code_agent.yml +10 -6
- agentpool/config_resources/external_acp_agents.yml +2 -1
- agentpool/delegation/base_team.py +4 -30
- agentpool/delegation/pool.py +136 -289
- agentpool/delegation/team.py +58 -57
- agentpool/delegation/teamrun.py +51 -55
- 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/functional/run.py +10 -4
- agentpool/mcp_server/__init__.py +0 -2
- agentpool/mcp_server/client.py +76 -32
- agentpool/mcp_server/conversions.py +54 -13
- agentpool/mcp_server/manager.py +34 -54
- agentpool/mcp_server/registries/official_registry_client.py +35 -1
- agentpool/mcp_server/tool_bridge.py +186 -139
- agentpool/messaging/__init__.py +0 -2
- agentpool/messaging/compaction.py +72 -197
- agentpool/messaging/connection_manager.py +11 -10
- agentpool/messaging/event_manager.py +5 -5
- agentpool/messaging/message_container.py +6 -30
- agentpool/messaging/message_history.py +99 -8
- agentpool/messaging/messagenode.py +52 -14
- agentpool/messaging/messages.py +54 -35
- agentpool/messaging/processing.py +12 -22
- agentpool/models/__init__.py +1 -1
- agentpool/models/acp_agents/base.py +6 -24
- agentpool/models/acp_agents/mcp_capable.py +126 -157
- agentpool/models/acp_agents/non_mcp.py +129 -95
- agentpool/models/agents.py +98 -76
- agentpool/models/agui_agents.py +1 -1
- agentpool/models/claude_code_agents.py +144 -19
- agentpool/models/file_parsing.py +0 -1
- agentpool/models/manifest.py +113 -50
- agentpool/prompts/conversion_manager.py +1 -1
- agentpool/prompts/prompts.py +5 -2
- agentpool/repomap.py +1 -1
- agentpool/resource_providers/__init__.py +11 -1
- agentpool/resource_providers/aggregating.py +56 -5
- agentpool/resource_providers/base.py +70 -4
- agentpool/resource_providers/codemode/code_executor.py +72 -5
- agentpool/resource_providers/codemode/helpers.py +2 -2
- agentpool/resource_providers/codemode/provider.py +64 -12
- agentpool/resource_providers/codemode/remote_mcp_execution.py +2 -2
- agentpool/resource_providers/codemode/remote_provider.py +9 -12
- agentpool/resource_providers/filtering.py +3 -1
- agentpool/resource_providers/mcp_provider.py +89 -12
- agentpool/resource_providers/plan_provider.py +228 -46
- agentpool/resource_providers/pool.py +7 -3
- agentpool/resource_providers/resource_info.py +111 -0
- agentpool/resource_providers/static.py +4 -2
- agentpool/sessions/__init__.py +4 -1
- agentpool/sessions/manager.py +33 -5
- agentpool/sessions/models.py +59 -6
- agentpool/sessions/protocol.py +28 -0
- agentpool/sessions/session.py +11 -55
- agentpool/skills/registry.py +13 -8
- agentpool/storage/manager.py +572 -49
- agentpool/talk/registry.py +4 -4
- agentpool/talk/talk.py +9 -10
- agentpool/testing.py +538 -20
- agentpool/tool_impls/__init__.py +6 -0
- agentpool/tool_impls/agent_cli/__init__.py +42 -0
- agentpool/tool_impls/agent_cli/tool.py +95 -0
- agentpool/tool_impls/bash/__init__.py +64 -0
- agentpool/tool_impls/bash/helpers.py +35 -0
- agentpool/tool_impls/bash/tool.py +171 -0
- agentpool/tool_impls/delete_path/__init__.py +70 -0
- agentpool/tool_impls/delete_path/tool.py +142 -0
- agentpool/tool_impls/download_file/__init__.py +80 -0
- agentpool/tool_impls/download_file/tool.py +183 -0
- agentpool/tool_impls/execute_code/__init__.py +55 -0
- agentpool/tool_impls/execute_code/tool.py +163 -0
- agentpool/tool_impls/grep/__init__.py +80 -0
- agentpool/tool_impls/grep/tool.py +200 -0
- agentpool/tool_impls/list_directory/__init__.py +73 -0
- agentpool/tool_impls/list_directory/tool.py +197 -0
- agentpool/tool_impls/question/__init__.py +42 -0
- agentpool/tool_impls/question/tool.py +127 -0
- agentpool/tool_impls/read/__init__.py +104 -0
- agentpool/tool_impls/read/tool.py +305 -0
- agentpool/tools/__init__.py +2 -1
- agentpool/tools/base.py +114 -34
- agentpool/tools/manager.py +57 -1
- agentpool/ui/base.py +2 -2
- agentpool/ui/mock_provider.py +2 -2
- agentpool/ui/stdlib_provider.py +2 -2
- 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 +616 -2
- agentpool/utils/subprocess_utils.py +155 -0
- agentpool/utils/token_breakdown.py +461 -0
- agentpool/vfs_registry.py +7 -2
- {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/METADATA +41 -27
- agentpool-2.5.0.dist-info/RECORD +579 -0
- {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/WHEEL +1 -1
- agentpool_cli/__main__.py +24 -0
- agentpool_cli/create.py +1 -1
- agentpool_cli/serve_acp.py +100 -21
- agentpool_cli/serve_agui.py +87 -0
- agentpool_cli/serve_opencode.py +119 -0
- agentpool_cli/ui.py +557 -0
- agentpool_commands/__init__.py +42 -5
- agentpool_commands/agents.py +75 -2
- agentpool_commands/history.py +62 -0
- agentpool_commands/mcp.py +176 -0
- agentpool_commands/models.py +56 -3
- agentpool_commands/pool.py +260 -0
- agentpool_commands/session.py +1 -1
- agentpool_commands/text_sharing/__init__.py +119 -0
- agentpool_commands/text_sharing/base.py +123 -0
- agentpool_commands/text_sharing/github_gist.py +80 -0
- agentpool_commands/text_sharing/opencode.py +462 -0
- agentpool_commands/text_sharing/paste_rs.py +59 -0
- agentpool_commands/text_sharing/pastebin.py +116 -0
- agentpool_commands/text_sharing/shittycodingagent.py +112 -0
- agentpool_commands/tools.py +57 -0
- agentpool_commands/utils.py +80 -30
- agentpool_config/__init__.py +30 -2
- agentpool_config/agentpool_tools.py +498 -0
- agentpool_config/builtin_tools.py +77 -22
- agentpool_config/commands.py +24 -1
- agentpool_config/compaction.py +258 -0
- agentpool_config/converters.py +1 -1
- agentpool_config/event_handlers.py +42 -0
- agentpool_config/events.py +1 -1
- agentpool_config/forward_targets.py +1 -4
- agentpool_config/jinja.py +3 -3
- agentpool_config/mcp_server.py +132 -6
- agentpool_config/nodes.py +1 -1
- agentpool_config/observability.py +44 -0
- agentpool_config/session.py +0 -3
- agentpool_config/storage.py +82 -38
- agentpool_config/task.py +3 -3
- agentpool_config/tools.py +11 -22
- agentpool_config/toolsets.py +109 -233
- agentpool_server/a2a_server/agent_worker.py +307 -0
- agentpool_server/a2a_server/server.py +23 -18
- agentpool_server/acp_server/acp_agent.py +234 -181
- agentpool_server/acp_server/commands/acp_commands.py +151 -156
- agentpool_server/acp_server/commands/docs_commands/fetch_repo.py +18 -17
- agentpool_server/acp_server/event_converter.py +651 -0
- agentpool_server/acp_server/input_provider.py +53 -10
- agentpool_server/acp_server/server.py +24 -90
- agentpool_server/acp_server/session.py +173 -331
- agentpool_server/acp_server/session_manager.py +8 -34
- agentpool_server/agui_server/server.py +3 -1
- agentpool_server/mcp_server/server.py +5 -2
- agentpool_server/opencode_server/.rules +95 -0
- agentpool_server/opencode_server/ENDPOINTS.md +401 -0
- agentpool_server/opencode_server/OPENCODE_UI_TOOLS_COMPLETE.md +202 -0
- agentpool_server/opencode_server/__init__.py +19 -0
- agentpool_server/opencode_server/command_validation.py +172 -0
- agentpool_server/opencode_server/converters.py +975 -0
- agentpool_server/opencode_server/dependencies.py +24 -0
- agentpool_server/opencode_server/input_provider.py +421 -0
- agentpool_server/opencode_server/models/__init__.py +250 -0
- agentpool_server/opencode_server/models/agent.py +53 -0
- agentpool_server/opencode_server/models/app.py +72 -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 +821 -0
- agentpool_server/opencode_server/models/file.py +88 -0
- agentpool_server/opencode_server/models/mcp.py +44 -0
- agentpool_server/opencode_server/models/message.py +179 -0
- agentpool_server/opencode_server/models/parts.py +323 -0
- agentpool_server/opencode_server/models/provider.py +81 -0
- agentpool_server/opencode_server/models/pty.py +43 -0
- agentpool_server/opencode_server/models/question.py +56 -0
- agentpool_server/opencode_server/models/session.py +111 -0
- agentpool_server/opencode_server/routes/__init__.py +29 -0
- agentpool_server/opencode_server/routes/agent_routes.py +473 -0
- agentpool_server/opencode_server/routes/app_routes.py +202 -0
- agentpool_server/opencode_server/routes/config_routes.py +302 -0
- agentpool_server/opencode_server/routes/file_routes.py +571 -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 +761 -0
- agentpool_server/opencode_server/routes/permission_routes.py +63 -0
- agentpool_server/opencode_server/routes/pty_routes.py +300 -0
- agentpool_server/opencode_server/routes/question_routes.py +128 -0
- agentpool_server/opencode_server/routes/session_routes.py +1276 -0
- agentpool_server/opencode_server/routes/tui_routes.py +139 -0
- agentpool_server/opencode_server/server.py +475 -0
- agentpool_server/opencode_server/state.py +151 -0
- agentpool_server/opencode_server/time_utils.py +8 -0
- agentpool_storage/__init__.py +12 -0
- agentpool_storage/base.py +184 -2
- agentpool_storage/claude_provider/ARCHITECTURE.md +433 -0
- agentpool_storage/claude_provider/__init__.py +42 -0
- agentpool_storage/claude_provider/provider.py +1089 -0
- agentpool_storage/file_provider.py +278 -15
- agentpool_storage/memory_provider.py +193 -12
- agentpool_storage/models.py +3 -0
- agentpool_storage/opencode_provider/ARCHITECTURE.md +386 -0
- agentpool_storage/opencode_provider/__init__.py +16 -0
- agentpool_storage/opencode_provider/helpers.py +414 -0
- agentpool_storage/opencode_provider/provider.py +895 -0
- agentpool_storage/project_store.py +325 -0
- agentpool_storage/session_store.py +26 -6
- agentpool_storage/sql_provider/__init__.py +4 -2
- agentpool_storage/sql_provider/models.py +48 -0
- agentpool_storage/sql_provider/sql_provider.py +269 -3
- agentpool_storage/sql_provider/utils.py +12 -13
- agentpool_storage/zed_provider/__init__.py +16 -0
- agentpool_storage/zed_provider/helpers.py +281 -0
- agentpool_storage/zed_provider/models.py +130 -0
- agentpool_storage/zed_provider/provider.py +442 -0
- agentpool_storage/zed_provider.py +803 -0
- agentpool_toolsets/__init__.py +0 -2
- agentpool_toolsets/builtin/__init__.py +2 -12
- agentpool_toolsets/builtin/code.py +96 -57
- agentpool_toolsets/builtin/debug.py +118 -48
- agentpool_toolsets/builtin/execution_environment.py +115 -230
- agentpool_toolsets/builtin/file_edit/file_edit.py +115 -7
- agentpool_toolsets/builtin/skills.py +9 -4
- agentpool_toolsets/builtin/subagent_tools.py +64 -51
- agentpool_toolsets/builtin/workers.py +4 -2
- agentpool_toolsets/composio_toolset.py +2 -2
- agentpool_toolsets/entry_points.py +3 -1
- agentpool_toolsets/fsspec_toolset/__init__.py +13 -1
- agentpool_toolsets/fsspec_toolset/diagnostics.py +860 -73
- agentpool_toolsets/fsspec_toolset/grep.py +99 -7
- agentpool_toolsets/fsspec_toolset/helpers.py +3 -2
- agentpool_toolsets/fsspec_toolset/image_utils.py +161 -0
- agentpool_toolsets/fsspec_toolset/toolset.py +500 -95
- agentpool_toolsets/mcp_discovery/__init__.py +5 -0
- agentpool_toolsets/mcp_discovery/data/mcp_servers.parquet +0 -0
- agentpool_toolsets/mcp_discovery/toolset.py +511 -0
- agentpool_toolsets/mcp_run_toolset.py +87 -12
- agentpool_toolsets/notifications.py +33 -33
- agentpool_toolsets/openapi.py +3 -1
- agentpool_toolsets/search_toolset.py +3 -1
- agentpool-2.1.9.dist-info/RECORD +0 -474
- agentpool_config/resources.py +0 -33
- agentpool_server/acp_server/acp_tools.py +0 -43
- agentpool_server/acp_server/commands/spawn.py +0 -210
- agentpool_storage/text_log_provider.py +0 -275
- agentpool_toolsets/builtin/agent_management.py +0 -239
- agentpool_toolsets/builtin/chain.py +0 -288
- agentpool_toolsets/builtin/history.py +0 -36
- agentpool_toolsets/builtin/integration.py +0 -85
- agentpool_toolsets/builtin/tool_management.py +0 -90
- agentpool_toolsets/builtin/user_interaction.py +0 -52
- agentpool_toolsets/semantic_memory_toolset.py +0 -536
- {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/entry_points.txt +0 -0
- {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -20,7 +20,15 @@ from __future__ import annotations
|
|
|
20
20
|
from dataclasses import dataclass, field
|
|
21
21
|
from typing import TYPE_CHECKING, Any, Literal
|
|
22
22
|
|
|
23
|
-
from pydantic_ai import
|
|
23
|
+
from pydantic_ai import (
|
|
24
|
+
AgentStreamEvent,
|
|
25
|
+
PartDeltaEvent as PyAIPartDeltaEvent,
|
|
26
|
+
PartStartEvent as PyAIPartStartEvent,
|
|
27
|
+
TextPart,
|
|
28
|
+
TextPartDelta,
|
|
29
|
+
ThinkingPart,
|
|
30
|
+
ThinkingPartDelta,
|
|
31
|
+
)
|
|
24
32
|
|
|
25
33
|
from agentpool.messaging import ChatMessage # noqa: TC001
|
|
26
34
|
|
|
@@ -35,6 +43,30 @@ if TYPE_CHECKING:
|
|
|
35
43
|
# Lifecycle events (aligned with AG-UI protocol)
|
|
36
44
|
|
|
37
45
|
|
|
46
|
+
class PartStartEvent(PyAIPartStartEvent):
|
|
47
|
+
"""Part start event."""
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def thinking(cls, index: int, content: str) -> PartStartEvent:
|
|
51
|
+
return cls(index=index, part=ThinkingPart(content=content))
|
|
52
|
+
|
|
53
|
+
@classmethod
|
|
54
|
+
def text(cls, index: int, content: str) -> PartStartEvent:
|
|
55
|
+
return cls(index=index, part=TextPart(content=content))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class PartDeltaEvent(PyAIPartDeltaEvent):
|
|
59
|
+
"""Part start event."""
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def thinking(cls, index: int, content: str) -> PartDeltaEvent:
|
|
63
|
+
return cls(index=index, delta=ThinkingPartDelta(content_delta=content))
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def text(cls, index: int, content: str) -> PartDeltaEvent:
|
|
67
|
+
return cls(index=index, delta=TextPartDelta(content_delta=content))
|
|
68
|
+
|
|
69
|
+
|
|
38
70
|
@dataclass(kw_only=True)
|
|
39
71
|
class RunStartedEvent:
|
|
40
72
|
"""Signals the start of an agent run."""
|
|
@@ -409,6 +441,7 @@ class ToolCallProgressEvent:
|
|
|
409
441
|
success: bool,
|
|
410
442
|
error: str | None = None,
|
|
411
443
|
tool_name: str | None = None,
|
|
444
|
+
line: int = 0,
|
|
412
445
|
) -> ToolCallProgressEvent:
|
|
413
446
|
"""Create event for file operation.
|
|
414
447
|
|
|
@@ -419,13 +452,14 @@ class ToolCallProgressEvent:
|
|
|
419
452
|
success: Whether operation succeeded
|
|
420
453
|
error: Error message if failed
|
|
421
454
|
tool_name: Optional tool name
|
|
455
|
+
line: Line number for navigation (0 = beginning)
|
|
422
456
|
"""
|
|
423
457
|
status: Literal["completed", "failed"] = "completed" if success else "failed"
|
|
424
458
|
title = f"{operation.capitalize()}: {path}"
|
|
425
459
|
if error:
|
|
426
460
|
title = f"{title} - {error}"
|
|
427
461
|
|
|
428
|
-
items: list[ToolCallContentItem] = [LocationContentItem(path=path)]
|
|
462
|
+
items: list[ToolCallContentItem] = [LocationContentItem(path=path, line=line)]
|
|
429
463
|
if error:
|
|
430
464
|
items.append(TextContentItem(text=f"Error: {error}"))
|
|
431
465
|
|
|
@@ -460,7 +494,6 @@ class ToolCallProgressEvent:
|
|
|
460
494
|
"""
|
|
461
495
|
items: list[ToolCallContentItem] = [
|
|
462
496
|
DiffContentItem(path=path, old_text=old_text, new_text=new_text),
|
|
463
|
-
LocationContentItem(path=path),
|
|
464
497
|
]
|
|
465
498
|
|
|
466
499
|
return cls(
|
|
@@ -513,10 +546,36 @@ class ToolCallCompleteEvent:
|
|
|
513
546
|
"""The name of the agent that made the tool call."""
|
|
514
547
|
message_id: str
|
|
515
548
|
"""The message ID associated with this tool call."""
|
|
549
|
+
metadata: dict[str, Any] | None = None
|
|
550
|
+
"""Optional metadata for UI/client use (diffs, diagnostics, etc.)."""
|
|
516
551
|
event_kind: Literal["tool_call_complete"] = "tool_call_complete"
|
|
517
552
|
"""Event type identifier."""
|
|
518
553
|
|
|
519
554
|
|
|
555
|
+
@dataclass(kw_only=True)
|
|
556
|
+
class ToolResultMetadataEvent:
|
|
557
|
+
"""Sidechannel event carrying tool result metadata stripped by Claude SDK.
|
|
558
|
+
|
|
559
|
+
The Claude SDK strips the `_meta` field from MCP CallToolResult when converting
|
|
560
|
+
to ToolResultBlock, losing UI-only metadata (diffs, diagnostics, etc.).
|
|
561
|
+
|
|
562
|
+
This event provides a sidechannel to preserve that metadata:
|
|
563
|
+
- Tool returns ToolResult with metadata
|
|
564
|
+
- ToolManagerBridge emits this event with metadata before converting
|
|
565
|
+
- ClaudeCodeAgent correlates by tool_call_id and enriches ToolCallCompleteEvent
|
|
566
|
+
- Downstream consumers (OpenCode, ACP) receive complete events with metadata
|
|
567
|
+
|
|
568
|
+
This avoids polluting LLM context with UI-only data while preserving it for clients.
|
|
569
|
+
"""
|
|
570
|
+
|
|
571
|
+
tool_call_id: str
|
|
572
|
+
"""The ID of the tool call this metadata belongs to."""
|
|
573
|
+
metadata: dict[str, Any]
|
|
574
|
+
"""Metadata for UI/client use (diffs, diagnostics, etc.)."""
|
|
575
|
+
event_kind: Literal["tool_result_metadata"] = "tool_result_metadata"
|
|
576
|
+
"""Event type identifier."""
|
|
577
|
+
|
|
578
|
+
|
|
520
579
|
@dataclass(kw_only=True)
|
|
521
580
|
class CustomEvent[T]:
|
|
522
581
|
"""Generic custom event that can be emitted during tool execution."""
|
|
@@ -543,6 +602,45 @@ class PlanUpdateEvent:
|
|
|
543
602
|
"""Event type identifier."""
|
|
544
603
|
|
|
545
604
|
|
|
605
|
+
@dataclass(kw_only=True)
|
|
606
|
+
class SubAgentEvent:
|
|
607
|
+
"""Event wrapping activity from a subagent or team member.
|
|
608
|
+
|
|
609
|
+
Used to propagate events from delegated agents/teams into the parent stream,
|
|
610
|
+
allowing the consumer (UI/server) to decide how to render nested activity.
|
|
611
|
+
"""
|
|
612
|
+
|
|
613
|
+
source_name: str
|
|
614
|
+
"""Name of the agent or team that produced this event."""
|
|
615
|
+
source_type: Literal["agent", "team_parallel", "team_sequential"]
|
|
616
|
+
"""Type of source: agent, parallel team, or sequential team."""
|
|
617
|
+
event: RichAgentStreamEvent[Any]
|
|
618
|
+
"""The actual event from the subagent/team."""
|
|
619
|
+
depth: int = 1
|
|
620
|
+
"""Nesting depth (1 = direct child, 2 = grandchild, etc.)."""
|
|
621
|
+
event_kind: Literal["subagent"] = "subagent"
|
|
622
|
+
"""Event type identifier."""
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
@dataclass(kw_only=True)
|
|
626
|
+
class CompactionEvent:
|
|
627
|
+
"""Event indicating context compaction is starting or completed.
|
|
628
|
+
|
|
629
|
+
This is a semantic event that consumers (ACP, OpenCode) handle differently:
|
|
630
|
+
- ACP: Converts to a text message for display
|
|
631
|
+
- OpenCode: Emits session.compacted SSE event
|
|
632
|
+
"""
|
|
633
|
+
|
|
634
|
+
session_id: str
|
|
635
|
+
"""The session ID being compacted."""
|
|
636
|
+
trigger: Literal["auto", "manual"] = "auto"
|
|
637
|
+
"""What triggered the compaction (auto = context overflow, manual = slash command)."""
|
|
638
|
+
phase: Literal["starting", "completed"] = "starting"
|
|
639
|
+
"""Current phase of compaction."""
|
|
640
|
+
event_kind: Literal["compaction"] = "compaction"
|
|
641
|
+
"""Event type identifier."""
|
|
642
|
+
|
|
643
|
+
|
|
546
644
|
type RichAgentStreamEvent[OutputDataT] = (
|
|
547
645
|
AgentStreamEvent
|
|
548
646
|
| StreamCompleteEvent[OutputDataT]
|
|
@@ -552,6 +650,9 @@ type RichAgentStreamEvent[OutputDataT] = (
|
|
|
552
650
|
| ToolCallProgressEvent
|
|
553
651
|
| ToolCallCompleteEvent
|
|
554
652
|
| PlanUpdateEvent
|
|
653
|
+
| CompactionEvent
|
|
654
|
+
| SubAgentEvent
|
|
655
|
+
| ToolResultMetadataEvent
|
|
555
656
|
| CustomEvent[Any]
|
|
556
657
|
)
|
|
557
658
|
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""AgentPool event helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
from agentpool.agents.events import DiffContentItem, LocationContentItem
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from agentpool.agents.events import ToolCallContentItem
|
|
13
|
+
from agentpool.tools.base import ToolKind
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class RichToolInfo:
|
|
18
|
+
"""Rich display information derived from tool name and input."""
|
|
19
|
+
|
|
20
|
+
title: str
|
|
21
|
+
"""Human-readable title for the tool call."""
|
|
22
|
+
kind: ToolKind = "other"
|
|
23
|
+
"""Category of tool operation."""
|
|
24
|
+
locations: list[LocationContentItem] = field(default_factory=list)
|
|
25
|
+
"""File locations involved in the operation."""
|
|
26
|
+
content: list[ToolCallContentItem] = field(default_factory=list)
|
|
27
|
+
"""Rich content items (diffs, text, etc.)."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def derive_rich_tool_info(name: str, input_data: dict[str, Any]) -> RichToolInfo: # noqa: PLR0911, PLR0915
|
|
31
|
+
"""Derive rich display info from tool name and input arguments.
|
|
32
|
+
|
|
33
|
+
Maps MCP tool names and their inputs to human-readable titles, kinds,
|
|
34
|
+
and location information for rich UI display. Handles both Claude Code
|
|
35
|
+
built-in tools and MCP bridge tools.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
name: The tool name (e.g., "Read", "mcp__server__read")
|
|
39
|
+
input_data: The tool input arguments
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
RichToolInfo with derived display information
|
|
43
|
+
"""
|
|
44
|
+
# Extract the actual tool name if it's an MCP bridge tool
|
|
45
|
+
# Format: mcp__{server_name}__{tool_name}
|
|
46
|
+
actual_name = name
|
|
47
|
+
if name.startswith("mcp__") and "__" in name[5:]:
|
|
48
|
+
parts = name.split("__")
|
|
49
|
+
if len(parts) >= 3: # noqa: PLR2004
|
|
50
|
+
actual_name = parts[-1] # Get the last part (actual tool name)
|
|
51
|
+
|
|
52
|
+
# Normalize to lowercase for matching
|
|
53
|
+
tool_lower = actual_name.lower()
|
|
54
|
+
# Read operations
|
|
55
|
+
if tool_lower in ("read", "read_file"):
|
|
56
|
+
path = input_data.get("file_path") or input_data.get("path", "")
|
|
57
|
+
offset = input_data.get("offset") or input_data.get("line")
|
|
58
|
+
limit = input_data.get("limit")
|
|
59
|
+
|
|
60
|
+
suffix = ""
|
|
61
|
+
if limit:
|
|
62
|
+
start = (offset or 0) + 1
|
|
63
|
+
end = (offset or 0) + limit
|
|
64
|
+
suffix = f" ({start}-{end})"
|
|
65
|
+
elif offset:
|
|
66
|
+
suffix = f" (from line {offset + 1})"
|
|
67
|
+
title = f"Read {path}{suffix}" if path else "Read File"
|
|
68
|
+
locations = [LocationContentItem(path=path, line=offset or 0)] if path else []
|
|
69
|
+
return RichToolInfo(title=title, kind="read", locations=locations)
|
|
70
|
+
|
|
71
|
+
# Write operations
|
|
72
|
+
if tool_lower in ("write", "write_file"):
|
|
73
|
+
path = input_data.get("file_path") or input_data.get("path", "")
|
|
74
|
+
content = input_data.get("content", "")
|
|
75
|
+
return RichToolInfo(
|
|
76
|
+
title=f"Write {path}" if path else "Write File",
|
|
77
|
+
kind="edit",
|
|
78
|
+
locations=[LocationContentItem(path=path)] if path else [],
|
|
79
|
+
content=[DiffContentItem(path=path, old_text=None, new_text=content)] if path else [],
|
|
80
|
+
)
|
|
81
|
+
# Edit operations
|
|
82
|
+
if tool_lower in ("edit", "edit_file"):
|
|
83
|
+
path = input_data.get("file_path") or input_data.get("path", "")
|
|
84
|
+
old_string = input_data.get("old_string") or input_data.get("old_text", "")
|
|
85
|
+
new_string = input_data.get("new_string") or input_data.get("new_text", "")
|
|
86
|
+
return RichToolInfo(
|
|
87
|
+
title=f"Edit {path}" if path else "Edit File",
|
|
88
|
+
kind="edit",
|
|
89
|
+
locations=[LocationContentItem(path=path)] if path else [],
|
|
90
|
+
content=[DiffContentItem(path=path, old_text=old_string, new_text=new_string)]
|
|
91
|
+
if path
|
|
92
|
+
else [],
|
|
93
|
+
)
|
|
94
|
+
# Delete operations
|
|
95
|
+
if tool_lower in ("delete", "delete_path", "delete_file"):
|
|
96
|
+
path = input_data.get("file_path") or input_data.get("path", "")
|
|
97
|
+
locations = [LocationContentItem(path=path)] if path else []
|
|
98
|
+
title = f"Delete {path}" if path else "Delete"
|
|
99
|
+
return RichToolInfo(title=title, kind="delete", locations=locations)
|
|
100
|
+
# Bash/terminal operations
|
|
101
|
+
if tool_lower in ("bash", "execute", "run_command", "execute_command", "execute_code"):
|
|
102
|
+
command = input_data.get("command") or input_data.get("code", "")
|
|
103
|
+
# Escape backticks in command
|
|
104
|
+
escaped_cmd = command.replace("`", "\\`") if command else ""
|
|
105
|
+
title = f"`{escaped_cmd}`" if escaped_cmd else "Terminal"
|
|
106
|
+
return RichToolInfo(title=title, kind="execute")
|
|
107
|
+
# Search operations
|
|
108
|
+
if tool_lower in ("grep", "search", "glob", "find"):
|
|
109
|
+
pattern = input_data.get("pattern") or input_data.get("query", "")
|
|
110
|
+
path = input_data.get("path", "")
|
|
111
|
+
title = f"Search for '{pattern}'" if pattern else "Search"
|
|
112
|
+
if path:
|
|
113
|
+
title += f" in {path}"
|
|
114
|
+
locations = [LocationContentItem(path=path)] if path else []
|
|
115
|
+
return RichToolInfo(title=title, kind="search", locations=locations)
|
|
116
|
+
# List directory
|
|
117
|
+
if tool_lower in ("ls", "list", "list_directory"):
|
|
118
|
+
path = input_data.get("path", ".")
|
|
119
|
+
title = f"List {path}" if path != "." else "List current directory"
|
|
120
|
+
locations = [LocationContentItem(path=path)] if path else []
|
|
121
|
+
return RichToolInfo(title=title, kind="search", locations=locations)
|
|
122
|
+
# Web operations
|
|
123
|
+
if tool_lower in ("webfetch", "web_fetch", "fetch"):
|
|
124
|
+
url = input_data.get("url", "")
|
|
125
|
+
return RichToolInfo(title=f"Fetch {url}" if url else "Web Fetch", kind="fetch")
|
|
126
|
+
if tool_lower in ("websearch", "web_search", "search_web"):
|
|
127
|
+
query = input_data.get("query", "")
|
|
128
|
+
return RichToolInfo(title=f"Search: {query}" if query else "Web Search", kind="fetch")
|
|
129
|
+
# Task/subagent operations
|
|
130
|
+
if tool_lower == "task":
|
|
131
|
+
description = input_data.get("description", "")
|
|
132
|
+
return RichToolInfo(title=description if description else "Task", kind="think")
|
|
133
|
+
# Notebook operations
|
|
134
|
+
if tool_lower in ("notebookread", "notebook_read"):
|
|
135
|
+
path = input_data.get("notebook_path", "")
|
|
136
|
+
title = f"Read Notebook {path}" if path else "Read Notebook"
|
|
137
|
+
locations = [LocationContentItem(path=path)] if path else []
|
|
138
|
+
return RichToolInfo(title=title, kind="read", locations=locations)
|
|
139
|
+
if tool_lower in ("notebookedit", "notebook_edit"):
|
|
140
|
+
path = input_data.get("notebook_path", "")
|
|
141
|
+
title = f"Edit Notebook {path}" if path else "Edit Notebook"
|
|
142
|
+
locations = [LocationContentItem(path=path)] if path else []
|
|
143
|
+
return RichToolInfo(title=title, kind="edit", locations=locations)
|
|
144
|
+
# Default: use the tool name as title
|
|
145
|
+
return RichToolInfo(title=actual_name, kind="other")
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"""Stream processors for event pipelines.
|
|
2
|
+
|
|
3
|
+
This module provides composable processors that can transform, filter, or observe
|
|
4
|
+
event streams. Processors wrap AsyncIterators and can be chained together.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
```python
|
|
8
|
+
# Simple function processor
|
|
9
|
+
async def log_events(stream):
|
|
10
|
+
async for event in stream:
|
|
11
|
+
print(f"Event: {type(event).__name__}")
|
|
12
|
+
yield event
|
|
13
|
+
|
|
14
|
+
# Class-based processor with state
|
|
15
|
+
tracker = FileTrackingProcessor()
|
|
16
|
+
|
|
17
|
+
# Compose into pipeline
|
|
18
|
+
pipeline = StreamPipeline([tracker, log_events])
|
|
19
|
+
|
|
20
|
+
async for event in pipeline(raw_events):
|
|
21
|
+
yield event
|
|
22
|
+
|
|
23
|
+
# Access state directly
|
|
24
|
+
print(tracker.get_metadata())
|
|
25
|
+
```
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
from collections.abc import Callable
|
|
31
|
+
from dataclasses import dataclass, field
|
|
32
|
+
from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
if TYPE_CHECKING:
|
|
36
|
+
from collections.abc import AsyncIterator, Coroutine
|
|
37
|
+
|
|
38
|
+
from agentpool.agents.events.events import RichAgentStreamEvent
|
|
39
|
+
from agentpool.common_types import SimpleJsonType
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# Type alias for processor callables
|
|
43
|
+
type StreamProcessorCallable = Callable[
|
|
44
|
+
[AsyncIterator[RichAgentStreamEvent[Any]]], AsyncIterator[RichAgentStreamEvent[Any]]
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@runtime_checkable
|
|
49
|
+
class StreamProcessor(Protocol):
|
|
50
|
+
"""Protocol for stream processors.
|
|
51
|
+
|
|
52
|
+
Processors can be:
|
|
53
|
+
- Callables: `(AsyncIterator[RichAgentStreamEvent]) -> AsyncIterator[RichAgentStreamEvent]`
|
|
54
|
+
- Classes with `__call__`: Same signature, but can hold state
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def __call__(
|
|
58
|
+
self, stream: AsyncIterator[RichAgentStreamEvent[Any]]
|
|
59
|
+
) -> AsyncIterator[RichAgentStreamEvent[Any]]:
|
|
60
|
+
"""Process an event stream.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
stream: Input event stream
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Transformed/filtered event stream
|
|
67
|
+
"""
|
|
68
|
+
...
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass
|
|
72
|
+
class StreamPipeline:
|
|
73
|
+
"""Composable pipeline for processing event streams.
|
|
74
|
+
|
|
75
|
+
Chains multiple processors together, passing the output of each
|
|
76
|
+
to the input of the next.
|
|
77
|
+
|
|
78
|
+
Example:
|
|
79
|
+
```python
|
|
80
|
+
tracker = FileTrackingProcessor()
|
|
81
|
+
pipeline = StreamPipeline([
|
|
82
|
+
tracker,
|
|
83
|
+
event_handler_processor(handler),
|
|
84
|
+
])
|
|
85
|
+
|
|
86
|
+
async for event in pipeline(raw_events):
|
|
87
|
+
yield event
|
|
88
|
+
|
|
89
|
+
# Access state directly from processor instances
|
|
90
|
+
print(tracker.get_metadata())
|
|
91
|
+
```
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
processors: list[StreamProcessorCallable | StreamProcessor] = field(default_factory=list)
|
|
95
|
+
|
|
96
|
+
def __call__(
|
|
97
|
+
self, stream: AsyncIterator[RichAgentStreamEvent[Any]]
|
|
98
|
+
) -> AsyncIterator[RichAgentStreamEvent[Any]]:
|
|
99
|
+
"""Run events through all processors in sequence.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
stream: Input event stream
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Processed event stream
|
|
106
|
+
"""
|
|
107
|
+
result = stream
|
|
108
|
+
for processor in self.processors:
|
|
109
|
+
result = processor(result)
|
|
110
|
+
return result
|
|
111
|
+
|
|
112
|
+
def add(self, processor: StreamProcessorCallable | StreamProcessor) -> None:
|
|
113
|
+
"""Add a processor to the pipeline.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
processor: Processor to add
|
|
117
|
+
"""
|
|
118
|
+
self.processors.append(processor)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def extract_file_path_from_tool_call(tool_name: str, raw_input: dict[str, Any]) -> str | None:
|
|
122
|
+
"""Extract file path from a tool call if it's a file-writing tool.
|
|
123
|
+
|
|
124
|
+
Uses simple heuristics:
|
|
125
|
+
- Tool name contains 'write' or 'edit' (case-insensitive)
|
|
126
|
+
- Input contains 'path' or 'file_path' key
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
tool_name: Name of the tool being called
|
|
130
|
+
raw_input: Tool call arguments
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
File path if this is a file-writing tool, None otherwise
|
|
134
|
+
"""
|
|
135
|
+
name_lower = tool_name.lower()
|
|
136
|
+
if "write" not in name_lower and "edit" not in name_lower:
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
# Try common path argument names
|
|
140
|
+
for key in ("file_path", "path", "filepath", "filename", "file"):
|
|
141
|
+
if key in raw_input and isinstance(val := raw_input[key], str):
|
|
142
|
+
return val
|
|
143
|
+
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@dataclass
|
|
148
|
+
class FileTrackingProcessor:
|
|
149
|
+
"""Tracks files modified during a stream of events.
|
|
150
|
+
|
|
151
|
+
Observes ToolCallStartEvent and extracts file paths from write/edit operations.
|
|
152
|
+
Does not modify events - just passes them through while collecting metadata.
|
|
153
|
+
|
|
154
|
+
Example:
|
|
155
|
+
```python
|
|
156
|
+
tracker = FileTrackingProcessor()
|
|
157
|
+
|
|
158
|
+
async for event in tracker(events):
|
|
159
|
+
yield event
|
|
160
|
+
|
|
161
|
+
print(f"Modified files: {tracker.touched_files}")
|
|
162
|
+
print(f"Metadata: {tracker.get_metadata()}")
|
|
163
|
+
```
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
touched_files: set[str] = field(default_factory=set)
|
|
167
|
+
"""Set of file paths that were modified by tool calls."""
|
|
168
|
+
|
|
169
|
+
extractor: Callable[[str, dict[str, Any]], str | None] = extract_file_path_from_tool_call
|
|
170
|
+
"""Function to extract file path from tool call. Can be customized."""
|
|
171
|
+
|
|
172
|
+
def process_event(self, event: RichAgentStreamEvent[Any]) -> None:
|
|
173
|
+
"""Process an event and track any file modifications.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
event: The event to process (checks for ToolCallStartEvent)
|
|
177
|
+
"""
|
|
178
|
+
from agentpool.agents.events import ToolCallStartEvent
|
|
179
|
+
|
|
180
|
+
if isinstance(event, ToolCallStartEvent) and (
|
|
181
|
+
file_path := self.extractor(event.tool_name or "", event.raw_input or {})
|
|
182
|
+
):
|
|
183
|
+
self.touched_files.add(file_path)
|
|
184
|
+
|
|
185
|
+
def __call__(
|
|
186
|
+
self, stream: AsyncIterator[RichAgentStreamEvent[Any]]
|
|
187
|
+
) -> AsyncIterator[RichAgentStreamEvent[Any]]:
|
|
188
|
+
"""Wrap a stream to track file modifications.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
stream: Input event stream
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
Same events, unmodified
|
|
195
|
+
"""
|
|
196
|
+
return self._process(stream)
|
|
197
|
+
|
|
198
|
+
async def _process(
|
|
199
|
+
self, stream: AsyncIterator[RichAgentStreamEvent[Any]]
|
|
200
|
+
) -> AsyncIterator[RichAgentStreamEvent[Any]]:
|
|
201
|
+
"""Internal async generator for processing."""
|
|
202
|
+
async for event in stream:
|
|
203
|
+
self.process_event(event)
|
|
204
|
+
yield event
|
|
205
|
+
|
|
206
|
+
def get_metadata(self) -> SimpleJsonType:
|
|
207
|
+
"""Get metadata dict with touched files (if any).
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
Dict with 'touched_files' key if files were modified, else empty dict
|
|
211
|
+
"""
|
|
212
|
+
if self.touched_files:
|
|
213
|
+
return {"touched_files": sorted(self.touched_files)}
|
|
214
|
+
return {}
|
|
215
|
+
|
|
216
|
+
def reset(self) -> None:
|
|
217
|
+
"""Clear tracked files for reuse."""
|
|
218
|
+
self.touched_files.clear()
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def event_handler_processor(
|
|
222
|
+
handler: Callable[[Any, RichAgentStreamEvent[Any]], Coroutine[Any, Any, None]],
|
|
223
|
+
) -> StreamProcessorCallable:
|
|
224
|
+
"""Create a processor that calls an event handler for each event.
|
|
225
|
+
|
|
226
|
+
The handler is called with (None, event) to match the existing
|
|
227
|
+
MultiEventHandler signature.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
handler: Async callable with signature (ctx, event) -> None
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
Processor function that calls the handler
|
|
234
|
+
|
|
235
|
+
Example:
|
|
236
|
+
```python
|
|
237
|
+
pipeline = StreamPipeline([
|
|
238
|
+
event_handler_processor(self.event_handler),
|
|
239
|
+
])
|
|
240
|
+
```
|
|
241
|
+
"""
|
|
242
|
+
|
|
243
|
+
async def process(
|
|
244
|
+
stream: AsyncIterator[RichAgentStreamEvent[Any]],
|
|
245
|
+
) -> AsyncIterator[RichAgentStreamEvent[Any]]:
|
|
246
|
+
async for event in stream:
|
|
247
|
+
await handler(None, event)
|
|
248
|
+
yield event
|
|
249
|
+
|
|
250
|
+
return process
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
# Convenience alias for backwards compatibility with existing FileTracker usage
|
|
254
|
+
FileTracker = FileTrackingProcessor
|
agentpool/agents/interactions.py
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from collections.abc import Callable, Mapping
|
|
6
|
+
from contextlib import asynccontextmanager
|
|
6
7
|
from typing import TYPE_CHECKING, Any, Literal, cast, overload
|
|
7
8
|
|
|
8
9
|
from schemez import Schema
|
|
@@ -12,7 +13,7 @@ from agentpool.messaging import ChatMessage
|
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
if TYPE_CHECKING:
|
|
15
|
-
from collections.abc import Sequence
|
|
16
|
+
from collections.abc import AsyncIterator, Sequence
|
|
16
17
|
|
|
17
18
|
from toprompt import AnyPromptType
|
|
18
19
|
|
|
@@ -90,6 +91,33 @@ class Interactions:
|
|
|
90
91
|
def __init__(self, agent: SupportsStructuredOutput) -> None:
|
|
91
92
|
self.agent = agent
|
|
92
93
|
|
|
94
|
+
@asynccontextmanager
|
|
95
|
+
async def _with_structured_output[T](
|
|
96
|
+
self, output_type: type[T]
|
|
97
|
+
) -> AsyncIterator[SupportsStructuredOutput]:
|
|
98
|
+
"""Context manager to temporarily set structured output type and restore afterwards.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
output_type: The type to structure output as
|
|
102
|
+
|
|
103
|
+
Yields:
|
|
104
|
+
The agent configured for structured output
|
|
105
|
+
|
|
106
|
+
Example:
|
|
107
|
+
async with interactions._with_structured_output(MyType) as agent:
|
|
108
|
+
result = await agent.run(prompt)
|
|
109
|
+
"""
|
|
110
|
+
# Save original output type
|
|
111
|
+
old_output_type = getattr(self.agent, "_output_type", None)
|
|
112
|
+
try:
|
|
113
|
+
# Configure structured output
|
|
114
|
+
structured_agent = self.agent.to_structured(output_type)
|
|
115
|
+
yield structured_agent
|
|
116
|
+
finally:
|
|
117
|
+
# Restore original output type
|
|
118
|
+
if hasattr(self.agent, "_output_type"):
|
|
119
|
+
self.agent._output_type = old_output_type
|
|
120
|
+
|
|
93
121
|
# async def conversation(
|
|
94
122
|
# self,
|
|
95
123
|
# other: MessageNode[Any, Any],
|
|
@@ -218,8 +246,9 @@ Available options:
|
|
|
218
246
|
|
|
219
247
|
Select ONE option by its exact label."""
|
|
220
248
|
|
|
221
|
-
# Get LLM's string-based decision
|
|
222
|
-
|
|
249
|
+
# Get LLM's string-based decision using structured output
|
|
250
|
+
async with self._with_structured_output(LLMPick) as structured_agent:
|
|
251
|
+
result = await structured_agent.run(prompt or default_prompt)
|
|
223
252
|
|
|
224
253
|
# Convert to type-safe decision
|
|
225
254
|
if result.content.selection not in label_map:
|
|
@@ -328,7 +357,9 @@ Available options:
|
|
|
328
357
|
{picks_info} options by their exact labels.
|
|
329
358
|
List your selections, one per line, followed by your reasoning."""
|
|
330
359
|
|
|
331
|
-
|
|
360
|
+
# Get LLM's multi-selection using structured output
|
|
361
|
+
async with self._with_structured_output(LLMMultiPick) as structured_agent:
|
|
362
|
+
result = await structured_agent.run(prompt or default_prompt)
|
|
332
363
|
|
|
333
364
|
# Validate selections
|
|
334
365
|
invalid = [s for s in result.content.selections if s not in label_map]
|
|
@@ -368,7 +399,9 @@ List your selections, one per line, followed by your reasoning."""
|
|
|
368
399
|
instance: item_model # type: ignore
|
|
369
400
|
# explanation: str | None = None
|
|
370
401
|
|
|
371
|
-
|
|
402
|
+
# Use structured output via context manager
|
|
403
|
+
async with self._with_structured_output(Extraction) as structured_agent:
|
|
404
|
+
result = await structured_agent.run(final_prompt)
|
|
372
405
|
return as_type(**result.content.instance.model_dump())
|
|
373
406
|
|
|
374
407
|
async def extract_multiple[T](
|
|
@@ -404,7 +437,9 @@ List your selections, one per line, followed by your reasoning."""
|
|
|
404
437
|
instances: list[item_model] # type: ignore
|
|
405
438
|
# explanation: str | None = None
|
|
406
439
|
|
|
407
|
-
|
|
440
|
+
# Use structured output via context manager
|
|
441
|
+
async with self._with_structured_output(Extraction) as structured_agent:
|
|
442
|
+
result = await structured_agent.run(final_prompt)
|
|
408
443
|
num_instances = len(result.content.instances) # Validate counts
|
|
409
444
|
if len(result.content.instances) < min_items:
|
|
410
445
|
msg = f"Found only {num_instances} instances, need {min_items}"
|