agentpool 2.2.3__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 +0 -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/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 +18 -49
- 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/task/supervisor.py +2 -2
- acp/utils.py +2 -2
- agentpool/__init__.py +2 -0
- agentpool/agents/acp_agent/acp_agent.py +278 -263
- agentpool/agents/acp_agent/acp_converters.py +150 -17
- agentpool/agents/acp_agent/client_handler.py +35 -24
- agentpool/agents/acp_agent/session_state.py +14 -6
- agentpool/agents/agent.py +471 -643
- agentpool/agents/agui_agent/agui_agent.py +104 -107
- agentpool/agents/agui_agent/helpers.py +3 -4
- agentpool/agents/base_agent.py +485 -32
- 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 +654 -334
- agentpool/agents/claude_code_agent/converters.py +4 -141
- 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/events/__init__.py +22 -0
- agentpool/agents/events/builtin_handlers.py +65 -0
- agentpool/agents/events/event_emitter.py +3 -0
- agentpool/agents/events/events.py +84 -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 +13 -0
- agentpool/agents/slashed_agent.py +5 -4
- agentpool/agents/tool_wrapping.py +18 -6
- agentpool/common_types.py +35 -21
- 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 +9 -8
- agentpool/config_resources/external_acp_agents.yml +2 -1
- agentpool/delegation/base_team.py +4 -30
- agentpool/delegation/pool.py +104 -265
- agentpool/delegation/team.py +57 -57
- agentpool/delegation/teamrun.py +50 -55
- agentpool/functional/run.py +10 -4
- agentpool/mcp_server/client.py +73 -38
- agentpool/mcp_server/conversions.py +54 -13
- agentpool/mcp_server/manager.py +9 -23
- agentpool/mcp_server/registries/official_registry_client.py +10 -1
- agentpool/mcp_server/tool_bridge.py +114 -79
- 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 +87 -8
- agentpool/messaging/messagenode.py +52 -14
- agentpool/messaging/messages.py +2 -26
- agentpool/messaging/processing.py +10 -22
- agentpool/models/__init__.py +1 -1
- agentpool/models/acp_agents/base.py +6 -2
- agentpool/models/acp_agents/mcp_capable.py +124 -15
- agentpool/models/acp_agents/non_mcp.py +0 -23
- agentpool/models/agents.py +66 -66
- agentpool/models/agui_agents.py +1 -1
- agentpool/models/claude_code_agents.py +111 -17
- agentpool/models/file_parsing.py +0 -1
- agentpool/models/manifest.py +70 -50
- agentpool/prompts/conversion_manager.py +1 -1
- agentpool/prompts/prompts.py +5 -2
- agentpool/resource_providers/__init__.py +2 -0
- agentpool/resource_providers/aggregating.py +4 -2
- agentpool/resource_providers/base.py +13 -3
- 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 +66 -12
- agentpool/resource_providers/plan_provider.py +111 -18
- agentpool/resource_providers/pool.py +5 -3
- agentpool/resource_providers/resource_info.py +111 -0
- agentpool/resource_providers/static.py +2 -2
- agentpool/sessions/__init__.py +2 -0
- agentpool/sessions/manager.py +2 -3
- agentpool/sessions/models.py +9 -6
- agentpool/sessions/protocol.py +28 -0
- agentpool/sessions/session.py +11 -55
- agentpool/storage/manager.py +361 -54
- agentpool/talk/registry.py +4 -4
- agentpool/talk/talk.py +9 -10
- agentpool/testing.py +1 -1
- 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/streams.py +21 -96
- agentpool/vfs_registry.py +7 -2
- {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/METADATA +16 -22
- {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/RECORD +242 -195
- {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/WHEEL +1 -1
- agentpool_cli/__main__.py +20 -0
- agentpool_cli/create.py +1 -1
- agentpool_cli/serve_acp.py +59 -1
- agentpool_cli/serve_opencode.py +1 -1
- agentpool_cli/ui.py +557 -0
- agentpool_commands/__init__.py +12 -5
- agentpool_commands/agents.py +1 -1
- 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/utils.py +31 -32
- agentpool_config/__init__.py +30 -2
- agentpool_config/agentpool_tools.py +498 -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 +1 -5
- agentpool_config/nodes.py +1 -1
- agentpool_config/observability.py +44 -0
- agentpool_config/session.py +0 -3
- agentpool_config/storage.py +38 -39
- agentpool_config/task.py +3 -3
- agentpool_config/tools.py +11 -28
- agentpool_config/toolsets.py +22 -90
- agentpool_server/a2a_server/agent_worker.py +307 -0
- agentpool_server/a2a_server/server.py +23 -18
- agentpool_server/acp_server/acp_agent.py +125 -56
- agentpool_server/acp_server/commands/acp_commands.py +46 -216
- agentpool_server/acp_server/commands/docs_commands/fetch_repo.py +8 -7
- agentpool_server/acp_server/event_converter.py +651 -0
- agentpool_server/acp_server/input_provider.py +53 -10
- agentpool_server/acp_server/server.py +1 -11
- agentpool_server/acp_server/session.py +90 -410
- 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/ENDPOINTS.md +53 -14
- agentpool_server/opencode_server/OPENCODE_UI_TOOLS_COMPLETE.md +202 -0
- agentpool_server/opencode_server/__init__.py +0 -8
- agentpool_server/opencode_server/converters.py +132 -26
- agentpool_server/opencode_server/input_provider.py +160 -8
- agentpool_server/opencode_server/models/__init__.py +42 -20
- agentpool_server/opencode_server/models/app.py +12 -0
- agentpool_server/opencode_server/models/events.py +203 -29
- agentpool_server/opencode_server/models/mcp.py +19 -0
- agentpool_server/opencode_server/models/message.py +18 -1
- agentpool_server/opencode_server/models/parts.py +134 -1
- agentpool_server/opencode_server/models/question.py +56 -0
- agentpool_server/opencode_server/models/session.py +13 -1
- agentpool_server/opencode_server/routes/__init__.py +4 -0
- agentpool_server/opencode_server/routes/agent_routes.py +33 -2
- agentpool_server/opencode_server/routes/app_routes.py +66 -3
- agentpool_server/opencode_server/routes/config_routes.py +66 -5
- agentpool_server/opencode_server/routes/file_routes.py +184 -5
- agentpool_server/opencode_server/routes/global_routes.py +1 -1
- agentpool_server/opencode_server/routes/lsp_routes.py +1 -1
- agentpool_server/opencode_server/routes/message_routes.py +122 -66
- agentpool_server/opencode_server/routes/permission_routes.py +63 -0
- agentpool_server/opencode_server/routes/pty_routes.py +23 -22
- agentpool_server/opencode_server/routes/question_routes.py +128 -0
- agentpool_server/opencode_server/routes/session_routes.py +139 -68
- agentpool_server/opencode_server/routes/tui_routes.py +1 -1
- agentpool_server/opencode_server/server.py +47 -2
- agentpool_server/opencode_server/state.py +30 -0
- agentpool_storage/__init__.py +0 -4
- agentpool_storage/base.py +81 -2
- agentpool_storage/claude_provider/ARCHITECTURE.md +433 -0
- agentpool_storage/claude_provider/__init__.py +42 -0
- agentpool_storage/{claude_provider.py → claude_provider/provider.py} +190 -8
- agentpool_storage/file_provider.py +149 -15
- agentpool_storage/memory_provider.py +132 -12
- 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/session_store.py +20 -6
- agentpool_storage/sql_provider/sql_provider.py +135 -2
- agentpool_storage/sql_provider/utils.py +2 -12
- 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 -4
- agentpool_toolsets/builtin/code.py +4 -4
- agentpool_toolsets/builtin/debug.py +115 -40
- agentpool_toolsets/builtin/execution_environment.py +54 -165
- agentpool_toolsets/builtin/skills.py +0 -77
- 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/grep.py +25 -5
- agentpool_toolsets/fsspec_toolset/helpers.py +3 -2
- agentpool_toolsets/fsspec_toolset/toolset.py +350 -66
- agentpool_toolsets/mcp_discovery/data/mcp_servers.parquet +0 -0
- agentpool_toolsets/mcp_discovery/toolset.py +74 -17
- agentpool_toolsets/mcp_run_toolset.py +8 -11
- agentpool_toolsets/notifications.py +33 -33
- agentpool_toolsets/openapi.py +3 -1
- agentpool_toolsets/search_toolset.py +3 -1
- 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/opencode_provider.py +0 -730
- agentpool_storage/text_log_provider.py +0 -276
- agentpool_toolsets/builtin/chain.py +0 -288
- agentpool_toolsets/builtin/user_interaction.py +0 -52
- agentpool_toolsets/semantic_memory_toolset.py +0 -536
- {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/entry_points.txt +0 -0
- {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/licenses/LICENSE +0 -0
agentpool/storage/manager.py
CHANGED
|
@@ -3,37 +3,73 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
import os
|
|
6
8
|
from typing import TYPE_CHECKING, Any, Self
|
|
7
9
|
|
|
8
10
|
from anyenv import method_spawner
|
|
11
|
+
from anyenv.signals import Signal
|
|
12
|
+
from pydantic import BaseModel
|
|
9
13
|
from pydantic_ai import Agent
|
|
10
14
|
|
|
11
15
|
from agentpool.log import get_logger
|
|
12
16
|
from agentpool.messaging import ChatMessage
|
|
13
17
|
from agentpool.storage.serialization import serialize_messages
|
|
14
18
|
from agentpool.utils.tasks import TaskManager
|
|
19
|
+
from agentpool_config.session import SessionQuery
|
|
15
20
|
from agentpool_config.storage import (
|
|
21
|
+
ClaudeStorageConfig,
|
|
16
22
|
FileStorageConfig,
|
|
17
23
|
MemoryStorageConfig,
|
|
24
|
+
OpenCodeStorageConfig,
|
|
18
25
|
SQLStorageConfig,
|
|
19
|
-
|
|
26
|
+
ZedStorageConfig,
|
|
20
27
|
)
|
|
21
28
|
|
|
22
29
|
|
|
23
30
|
if TYPE_CHECKING:
|
|
24
|
-
from collections.abc import Sequence
|
|
31
|
+
from collections.abc import Callable, Sequence
|
|
25
32
|
from datetime import datetime
|
|
26
33
|
from types import TracebackType
|
|
27
34
|
|
|
28
35
|
from agentpool.common_types import JsonValue
|
|
29
36
|
from agentpool.sessions.models import ProjectData
|
|
30
|
-
from agentpool_config.session import SessionQuery
|
|
31
37
|
from agentpool_config.storage import BaseStorageProviderConfig, StorageConfig
|
|
32
38
|
from agentpool_storage.base import StorageProvider
|
|
33
39
|
|
|
34
40
|
logger = get_logger(__name__)
|
|
35
41
|
|
|
36
42
|
|
|
43
|
+
class ConversationMetadata(BaseModel):
|
|
44
|
+
"""Generated metadata for a conversation."""
|
|
45
|
+
|
|
46
|
+
title: str
|
|
47
|
+
"""Short descriptive title (3-7 words)."""
|
|
48
|
+
|
|
49
|
+
emoji: str
|
|
50
|
+
"""Single emoji representing the topic."""
|
|
51
|
+
|
|
52
|
+
icon: str
|
|
53
|
+
"""Iconify icon name (e.g., 'mdi:code-braces')."""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True, slots=True)
|
|
57
|
+
class TitleGeneratedEvent:
|
|
58
|
+
"""Event emitted when a conversation title is generated.
|
|
59
|
+
|
|
60
|
+
Attributes:
|
|
61
|
+
conversation_id: ID of the conversation
|
|
62
|
+
title: Generated title text
|
|
63
|
+
emoji: Generated emoji representing the topic
|
|
64
|
+
icon: Generated iconify icon name
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
conversation_id: str
|
|
68
|
+
title: str
|
|
69
|
+
emoji: str
|
|
70
|
+
icon: str
|
|
71
|
+
|
|
72
|
+
|
|
37
73
|
class StorageManager:
|
|
38
74
|
"""Manages multiple storage providers.
|
|
39
75
|
|
|
@@ -42,8 +78,19 @@ class StorageManager:
|
|
|
42
78
|
- Message distribution to providers
|
|
43
79
|
- History loading from capable providers
|
|
44
80
|
- Global logging filters
|
|
81
|
+
|
|
82
|
+
Signals:
|
|
83
|
+
- title_generated: Emitted when a conversation title is generated.
|
|
84
|
+
Subscribers receive TitleGeneratedEvent with conversation_id, title, emoji, icon.
|
|
85
|
+
|
|
86
|
+
Example:
|
|
87
|
+
manager.title_generated.connect(my_handler)
|
|
88
|
+
# Handler will be called with TitleGeneratedEvent when titles are generated
|
|
45
89
|
"""
|
|
46
90
|
|
|
91
|
+
# Signal emitted when a conversation title is generated
|
|
92
|
+
title_generated: Signal[TitleGeneratedEvent] = Signal()
|
|
93
|
+
|
|
47
94
|
def __init__(self, config: StorageConfig) -> None:
|
|
48
95
|
"""Initialize storage manager.
|
|
49
96
|
|
|
@@ -53,6 +100,7 @@ class StorageManager:
|
|
|
53
100
|
self.config = config
|
|
54
101
|
self.task_manager = TaskManager()
|
|
55
102
|
self.providers = [self._create_provider(cfg) for cfg in self.config.effective_providers]
|
|
103
|
+
self._conversation_logged: set[str] = set() # Track logged conversations for idempotency
|
|
56
104
|
|
|
57
105
|
async def __aenter__(self) -> Self:
|
|
58
106
|
"""Initialize all providers."""
|
|
@@ -124,15 +172,22 @@ class StorageManager:
|
|
|
124
172
|
from agentpool_storage.file_provider import FileProvider
|
|
125
173
|
|
|
126
174
|
return FileProvider(provider_config)
|
|
127
|
-
case TextLogConfig():
|
|
128
|
-
from agentpool_storage.text_log_provider import TextLogProvider
|
|
129
|
-
|
|
130
|
-
return TextLogProvider(provider_config)
|
|
131
|
-
|
|
132
175
|
case MemoryStorageConfig():
|
|
133
176
|
from agentpool_storage.memory_provider import MemoryStorageProvider
|
|
134
177
|
|
|
135
178
|
return MemoryStorageProvider(provider_config)
|
|
179
|
+
case ClaudeStorageConfig():
|
|
180
|
+
from agentpool_storage.claude_provider import ClaudeStorageProvider
|
|
181
|
+
|
|
182
|
+
return ClaudeStorageProvider(provider_config)
|
|
183
|
+
case OpenCodeStorageConfig():
|
|
184
|
+
from agentpool_storage.opencode_provider import OpenCodeStorageProvider
|
|
185
|
+
|
|
186
|
+
return OpenCodeStorageProvider(provider_config)
|
|
187
|
+
case ZedStorageConfig():
|
|
188
|
+
from agentpool_storage.zed_provider import ZedStorageProvider
|
|
189
|
+
|
|
190
|
+
return ZedStorageProvider(provider_config)
|
|
136
191
|
case _:
|
|
137
192
|
msg = f"Unknown provider type: {provider_config}"
|
|
138
193
|
raise ValueError(msg)
|
|
@@ -154,11 +209,7 @@ class StorageManager:
|
|
|
154
209
|
# Function to find capable provider by name
|
|
155
210
|
def find_provider(name: str) -> StorageProvider | None:
|
|
156
211
|
for p in self.providers:
|
|
157
|
-
if (
|
|
158
|
-
not getattr(p, "write_only", False)
|
|
159
|
-
and p.can_load_history
|
|
160
|
-
and p.__class__.__name__.lower() == name.lower()
|
|
161
|
-
):
|
|
212
|
+
if p.can_load_history and p.__class__.__name__.lower() == name.lower():
|
|
162
213
|
return p
|
|
163
214
|
return None
|
|
164
215
|
|
|
@@ -175,7 +226,7 @@ class StorageManager:
|
|
|
175
226
|
|
|
176
227
|
# Find first capable provider
|
|
177
228
|
for provider in self.providers:
|
|
178
|
-
if
|
|
229
|
+
if provider.can_load_history:
|
|
179
230
|
return provider
|
|
180
231
|
|
|
181
232
|
msg = "No capable provider found for loading history"
|
|
@@ -214,7 +265,6 @@ class StorageManager:
|
|
|
214
265
|
cost_info=message.cost_info,
|
|
215
266
|
model=message.model_name,
|
|
216
267
|
response_time=message.response_time,
|
|
217
|
-
forwarded_from=message.forwarded_from,
|
|
218
268
|
provider_name=message.provider_name,
|
|
219
269
|
provider_response_id=message.provider_response_id,
|
|
220
270
|
messages=serialize_messages(message.messages),
|
|
@@ -228,16 +278,68 @@ class StorageManager:
|
|
|
228
278
|
conversation_id: str,
|
|
229
279
|
node_name: str,
|
|
230
280
|
start_time: datetime | None = None,
|
|
281
|
+
initial_prompt: str | None = None,
|
|
282
|
+
on_title_generated: Callable[[str], None] | None = None,
|
|
231
283
|
) -> None:
|
|
232
|
-
"""Log conversation to all providers.
|
|
284
|
+
"""Log conversation to all providers (idempotent).
|
|
285
|
+
|
|
286
|
+
If conversation was already logged, skips provider calls but still
|
|
287
|
+
triggers title generation if initial_prompt is provided.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
conversation_id: Unique conversation identifier
|
|
291
|
+
node_name: Name of the node/agent
|
|
292
|
+
start_time: Optional start time
|
|
293
|
+
initial_prompt: Optional initial prompt to trigger title generation
|
|
294
|
+
on_title_generated: Optional callback invoked when title is generated
|
|
295
|
+
"""
|
|
233
296
|
if not self.config.log_conversations:
|
|
234
297
|
return
|
|
235
298
|
|
|
236
|
-
|
|
237
|
-
|
|
299
|
+
# Check if already logged (idempotent behavior)
|
|
300
|
+
if conversation_id not in self._conversation_logged:
|
|
301
|
+
# Mark as logged before calling providers
|
|
302
|
+
self._conversation_logged.add(conversation_id)
|
|
303
|
+
|
|
304
|
+
# Log to all providers
|
|
305
|
+
for provider in self.providers:
|
|
306
|
+
await provider.log_conversation(
|
|
307
|
+
conversation_id=conversation_id,
|
|
308
|
+
node_name=node_name,
|
|
309
|
+
start_time=start_time,
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
# Handle title generation based on prompt length
|
|
313
|
+
# Skip during tests to avoid external API calls
|
|
314
|
+
if not initial_prompt or os.environ.get("PYTEST_CURRENT_TEST"):
|
|
315
|
+
return
|
|
316
|
+
prompt_length = len(initial_prompt)
|
|
317
|
+
logger.info(
|
|
318
|
+
"log_conversation title decision",
|
|
319
|
+
conversation_id=conversation_id,
|
|
320
|
+
prompt_length=prompt_length,
|
|
321
|
+
has_model=bool(self.config.title_generation_model),
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
# For short prompts, use them directly as title (like Claude Code)
|
|
325
|
+
if prompt_length < 60: # noqa: PLR2004
|
|
326
|
+
logger.info(
|
|
327
|
+
"Using short prompt directly as title",
|
|
328
|
+
conversation_id=conversation_id,
|
|
329
|
+
title=initial_prompt,
|
|
330
|
+
)
|
|
331
|
+
await self.update_conversation_title(conversation_id, initial_prompt)
|
|
332
|
+
# For longer prompts, generate semantic title if model configured
|
|
333
|
+
elif self.config.title_generation_model:
|
|
334
|
+
logger.info(
|
|
335
|
+
"Creating title generation task for long prompt",
|
|
238
336
|
conversation_id=conversation_id,
|
|
239
|
-
|
|
240
|
-
|
|
337
|
+
)
|
|
338
|
+
self.task_manager.create_task(
|
|
339
|
+
self._generate_title_from_prompt(
|
|
340
|
+
conversation_id, initial_prompt, on_title_generated
|
|
341
|
+
),
|
|
342
|
+
name=f"title_gen_{conversation_id[:8]}",
|
|
241
343
|
)
|
|
242
344
|
|
|
243
345
|
@method_spawner
|
|
@@ -365,6 +467,140 @@ class StorageManager:
|
|
|
365
467
|
provider = self.get_history_provider()
|
|
366
468
|
return await provider.get_conversation_title(conversation_id)
|
|
367
469
|
|
|
470
|
+
async def get_conversation_titles(
|
|
471
|
+
self,
|
|
472
|
+
conversation_ids: list[str],
|
|
473
|
+
) -> dict[str, str | None]:
|
|
474
|
+
"""Get titles for multiple conversations.
|
|
475
|
+
|
|
476
|
+
Args:
|
|
477
|
+
conversation_ids: List of conversation IDs
|
|
478
|
+
|
|
479
|
+
Returns:
|
|
480
|
+
Dict mapping conversation_id to title (or None if not set)
|
|
481
|
+
"""
|
|
482
|
+
if not conversation_ids:
|
|
483
|
+
return {}
|
|
484
|
+
|
|
485
|
+
provider = self.get_history_provider()
|
|
486
|
+
titles: dict[str, str | None] = {}
|
|
487
|
+
for conv_id in conversation_ids:
|
|
488
|
+
try:
|
|
489
|
+
titles[conv_id] = await provider.get_conversation_title(conv_id)
|
|
490
|
+
except Exception: # noqa: BLE001
|
|
491
|
+
titles[conv_id] = None
|
|
492
|
+
return titles
|
|
493
|
+
|
|
494
|
+
async def get_message_counts(
|
|
495
|
+
self,
|
|
496
|
+
conversation_ids: list[str],
|
|
497
|
+
) -> dict[str, int]:
|
|
498
|
+
"""Get message counts for multiple conversations.
|
|
499
|
+
|
|
500
|
+
Args:
|
|
501
|
+
conversation_ids: List of conversation IDs
|
|
502
|
+
|
|
503
|
+
Returns:
|
|
504
|
+
Dict mapping conversation_id to message count
|
|
505
|
+
"""
|
|
506
|
+
if not conversation_ids:
|
|
507
|
+
return {}
|
|
508
|
+
|
|
509
|
+
counts: dict[str, int] = {}
|
|
510
|
+
for conv_id in conversation_ids:
|
|
511
|
+
try:
|
|
512
|
+
query = SessionQuery(name=conv_id)
|
|
513
|
+
messages = await self.filter_messages(query)
|
|
514
|
+
counts[conv_id] = len(messages) if messages else 0
|
|
515
|
+
except Exception: # noqa: BLE001
|
|
516
|
+
counts[conv_id] = 0
|
|
517
|
+
return counts
|
|
518
|
+
|
|
519
|
+
@method_spawner
|
|
520
|
+
async def get_conversation_messages(
|
|
521
|
+
self,
|
|
522
|
+
conversation_id: str,
|
|
523
|
+
*,
|
|
524
|
+
include_ancestors: bool = False,
|
|
525
|
+
) -> list[ChatMessage[str]]:
|
|
526
|
+
"""Get all messages for a conversation.
|
|
527
|
+
|
|
528
|
+
Args:
|
|
529
|
+
conversation_id: ID of the conversation
|
|
530
|
+
include_ancestors: If True, also include messages from ancestor
|
|
531
|
+
conversations by following the parent_id chain. Useful for
|
|
532
|
+
forked conversations.
|
|
533
|
+
|
|
534
|
+
Returns:
|
|
535
|
+
List of messages ordered by timestamp.
|
|
536
|
+
"""
|
|
537
|
+
provider = self.get_history_provider()
|
|
538
|
+
return await provider.get_conversation_messages(
|
|
539
|
+
conversation_id, include_ancestors=include_ancestors
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
@method_spawner
|
|
543
|
+
async def get_message(self, message_id: str) -> ChatMessage[str] | None:
|
|
544
|
+
"""Get a single message by ID.
|
|
545
|
+
|
|
546
|
+
Args:
|
|
547
|
+
message_id: ID of the message
|
|
548
|
+
|
|
549
|
+
Returns:
|
|
550
|
+
The message if found, None otherwise.
|
|
551
|
+
"""
|
|
552
|
+
provider = self.get_history_provider()
|
|
553
|
+
return await provider.get_message(message_id)
|
|
554
|
+
|
|
555
|
+
@method_spawner
|
|
556
|
+
async def get_message_ancestry(self, message_id: str) -> list[ChatMessage[str]]:
|
|
557
|
+
"""Get the ancestry chain of a message.
|
|
558
|
+
|
|
559
|
+
Traverses the parent_id chain to build full history leading to this message.
|
|
560
|
+
|
|
561
|
+
Args:
|
|
562
|
+
message_id: ID of the message
|
|
563
|
+
|
|
564
|
+
Returns:
|
|
565
|
+
List of messages from oldest ancestor to the specified message.
|
|
566
|
+
"""
|
|
567
|
+
provider = self.get_history_provider()
|
|
568
|
+
return await provider.get_message_ancestry(message_id)
|
|
569
|
+
|
|
570
|
+
@method_spawner
|
|
571
|
+
async def fork_conversation(
|
|
572
|
+
self,
|
|
573
|
+
*,
|
|
574
|
+
source_conversation_id: str,
|
|
575
|
+
new_conversation_id: str,
|
|
576
|
+
fork_from_message_id: str | None = None,
|
|
577
|
+
new_agent_name: str | None = None,
|
|
578
|
+
) -> str | None:
|
|
579
|
+
"""Fork a conversation at a specific point.
|
|
580
|
+
|
|
581
|
+
Creates a new conversation that branches from the source. New messages
|
|
582
|
+
in the forked conversation should use the returned fork_point_id as
|
|
583
|
+
their parent_id to maintain the history chain.
|
|
584
|
+
|
|
585
|
+
Args:
|
|
586
|
+
source_conversation_id: ID of the conversation to fork from
|
|
587
|
+
new_conversation_id: ID for the new forked conversation
|
|
588
|
+
fork_from_message_id: Message ID to fork from. If None, forks from
|
|
589
|
+
the last message.
|
|
590
|
+
new_agent_name: Agent name for the new conversation.
|
|
591
|
+
|
|
592
|
+
Returns:
|
|
593
|
+
The message_id of the fork point (use as parent_id for new messages),
|
|
594
|
+
or None if the source conversation is empty.
|
|
595
|
+
"""
|
|
596
|
+
provider = self.get_history_provider()
|
|
597
|
+
return await provider.fork_conversation(
|
|
598
|
+
source_conversation_id=source_conversation_id,
|
|
599
|
+
new_conversation_id=new_conversation_id,
|
|
600
|
+
fork_from_message_id=fork_from_message_id,
|
|
601
|
+
new_agent_name=new_agent_name,
|
|
602
|
+
)
|
|
603
|
+
|
|
368
604
|
@method_spawner
|
|
369
605
|
async def delete_conversation_messages(
|
|
370
606
|
self,
|
|
@@ -434,7 +670,6 @@ class StorageManager:
|
|
|
434
670
|
model_name=message.model_name,
|
|
435
671
|
cost_info=message.cost_info,
|
|
436
672
|
response_time=message.response_time,
|
|
437
|
-
forwarded_from=message.forwarded_from,
|
|
438
673
|
timestamp=message.timestamp,
|
|
439
674
|
provider_name=message.provider_name,
|
|
440
675
|
provider_response_id=message.provider_response_id,
|
|
@@ -446,6 +681,105 @@ class StorageManager:
|
|
|
446
681
|
|
|
447
682
|
return deleted, added
|
|
448
683
|
|
|
684
|
+
async def _generate_title_core(
|
|
685
|
+
self,
|
|
686
|
+
conversation_id: str,
|
|
687
|
+
prompt_text: str,
|
|
688
|
+
) -> ConversationMetadata | None:
|
|
689
|
+
"""Core title generation logic using LLM with structured output.
|
|
690
|
+
|
|
691
|
+
Args:
|
|
692
|
+
conversation_id: ID of the conversation to title
|
|
693
|
+
prompt_text: Formatted prompt text to send to the LLM
|
|
694
|
+
|
|
695
|
+
Returns:
|
|
696
|
+
ConversationMetadata with title, emoji, and icon, or None if generation fails.
|
|
697
|
+
"""
|
|
698
|
+
logger.info("_generate_title_core called", conversation_id=conversation_id)
|
|
699
|
+
if not self.config.title_generation_model:
|
|
700
|
+
logger.info("No title_generation_model configured, skipping")
|
|
701
|
+
return None
|
|
702
|
+
|
|
703
|
+
try:
|
|
704
|
+
from llmling_models.models.helpers import infer_model
|
|
705
|
+
|
|
706
|
+
model = infer_model(self.config.title_generation_model)
|
|
707
|
+
agent: Agent[None, ConversationMetadata] = Agent(
|
|
708
|
+
model=model,
|
|
709
|
+
instructions=self.config.title_generation_prompt,
|
|
710
|
+
output_type=ConversationMetadata,
|
|
711
|
+
)
|
|
712
|
+
logger.debug("Title generation prompt", prompt_text=prompt_text)
|
|
713
|
+
result = await agent.run(prompt_text)
|
|
714
|
+
metadata = result.output
|
|
715
|
+
|
|
716
|
+
# Store the title
|
|
717
|
+
await self.update_conversation_title(conversation_id, metadata.title)
|
|
718
|
+
logger.debug(
|
|
719
|
+
"Generated conversation metadata",
|
|
720
|
+
conversation_id=conversation_id,
|
|
721
|
+
title=metadata.title,
|
|
722
|
+
emoji=metadata.emoji,
|
|
723
|
+
icon=metadata.icon,
|
|
724
|
+
)
|
|
725
|
+
|
|
726
|
+
# Emit signal for subscribers (e.g., OpenCode UI updates)
|
|
727
|
+
event = TitleGeneratedEvent(
|
|
728
|
+
conversation_id=conversation_id,
|
|
729
|
+
title=metadata.title,
|
|
730
|
+
emoji=metadata.emoji,
|
|
731
|
+
icon=metadata.icon,
|
|
732
|
+
)
|
|
733
|
+
logger.info(
|
|
734
|
+
"Emitting title_generated signal",
|
|
735
|
+
conversation_id=conversation_id,
|
|
736
|
+
title=metadata.title,
|
|
737
|
+
)
|
|
738
|
+
await self.title_generated.emit(event)
|
|
739
|
+
except Exception:
|
|
740
|
+
logger.exception("Failed to generate session title", conversation_id=conversation_id)
|
|
741
|
+
return None
|
|
742
|
+
else:
|
|
743
|
+
return metadata
|
|
744
|
+
|
|
745
|
+
async def _generate_title_from_prompt(
|
|
746
|
+
self,
|
|
747
|
+
conversation_id: str,
|
|
748
|
+
prompt: str,
|
|
749
|
+
on_title_generated: Callable[[str], None] | None = None,
|
|
750
|
+
) -> str | None:
|
|
751
|
+
"""Generate title from initial prompt (internal, fire-and-forget).
|
|
752
|
+
|
|
753
|
+
Called automatically by log_conversation when initial_prompt is provided.
|
|
754
|
+
|
|
755
|
+
Args:
|
|
756
|
+
conversation_id: ID of the conversation to title
|
|
757
|
+
prompt: The initial user prompt
|
|
758
|
+
on_title_generated: Optional callback invoked with the generated title
|
|
759
|
+
|
|
760
|
+
Returns:
|
|
761
|
+
The generated title, or None if generation fails/disabled.
|
|
762
|
+
"""
|
|
763
|
+
# Check if title already exists
|
|
764
|
+
existing = await self.get_conversation_title(conversation_id)
|
|
765
|
+
if existing:
|
|
766
|
+
if on_title_generated:
|
|
767
|
+
on_title_generated(existing)
|
|
768
|
+
return existing
|
|
769
|
+
|
|
770
|
+
# Generate using core logic
|
|
771
|
+
metadata = await self._generate_title_core(
|
|
772
|
+
conversation_id,
|
|
773
|
+
f"user: {prompt[:500]}",
|
|
774
|
+
)
|
|
775
|
+
|
|
776
|
+
if metadata:
|
|
777
|
+
title = metadata.title
|
|
778
|
+
if on_title_generated:
|
|
779
|
+
on_title_generated(title)
|
|
780
|
+
return title
|
|
781
|
+
return None
|
|
782
|
+
|
|
449
783
|
async def generate_conversation_title(
|
|
450
784
|
self,
|
|
451
785
|
conversation_id: str,
|
|
@@ -463,43 +797,18 @@ class StorageManager:
|
|
|
463
797
|
Returns:
|
|
464
798
|
The generated title, or None if title generation is disabled.
|
|
465
799
|
"""
|
|
466
|
-
if not self.config.title_generation_model:
|
|
467
|
-
return None
|
|
468
|
-
|
|
469
800
|
# Check if title already exists
|
|
470
801
|
existing = await self.get_conversation_title(conversation_id)
|
|
471
802
|
if existing:
|
|
472
803
|
return existing
|
|
473
804
|
|
|
474
805
|
# Format messages for the prompt
|
|
475
|
-
formatted = "\n".join(
|
|
476
|
-
f"{msg.role}: {msg.content[:500]}"
|
|
477
|
-
for msg in messages[:4] # Limit context
|
|
478
|
-
)
|
|
806
|
+
formatted = "\n".join(f"{i.role}: {i.content[:500]}" for i in messages[:4])
|
|
479
807
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
model=self.config.title_generation_model,
|
|
483
|
-
instructions=self.config.title_generation_prompt,
|
|
484
|
-
)
|
|
485
|
-
result = await agent.run(formatted)
|
|
486
|
-
title = result.output.strip().strip("\"'") # Remove quotes if present
|
|
808
|
+
# Generate using core logic
|
|
809
|
+
metadata = await self._generate_title_core(conversation_id, formatted)
|
|
487
810
|
|
|
488
|
-
|
|
489
|
-
await self.update_conversation_title(conversation_id, title)
|
|
490
|
-
logger.debug(
|
|
491
|
-
"Generated conversation title",
|
|
492
|
-
conversation_id=conversation_id,
|
|
493
|
-
title=title,
|
|
494
|
-
)
|
|
495
|
-
except Exception:
|
|
496
|
-
logger.exception(
|
|
497
|
-
"Failed to generate conversation title",
|
|
498
|
-
conversation_id=conversation_id,
|
|
499
|
-
)
|
|
500
|
-
return None
|
|
501
|
-
else:
|
|
502
|
-
return title
|
|
811
|
+
return metadata.title if metadata else None
|
|
503
812
|
|
|
504
813
|
# Project methods
|
|
505
814
|
|
|
@@ -512,10 +821,8 @@ class StorageManager:
|
|
|
512
821
|
Raises:
|
|
513
822
|
RuntimeError: If no capable provider found.
|
|
514
823
|
"""
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
if hasattr(provider, "save_project") and not getattr(provider, "write_only", False):
|
|
518
|
-
return provider
|
|
824
|
+
if self.providers:
|
|
825
|
+
return self.providers[0]
|
|
519
826
|
msg = "No provider found that supports project storage"
|
|
520
827
|
raise RuntimeError(msg)
|
|
521
828
|
|
agentpool/talk/registry.py
CHANGED
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
from dataclasses import dataclass
|
|
6
6
|
from typing import TYPE_CHECKING, Any, Literal
|
|
7
7
|
|
|
8
|
-
from
|
|
8
|
+
from anyenv.signals import Signal
|
|
9
9
|
|
|
10
10
|
from agentpool.log import get_logger
|
|
11
11
|
from agentpool.talk.talk import Talk
|
|
@@ -64,7 +64,7 @@ class ConnectionRegistry(BaseRegistry[str, Talk]):
|
|
|
64
64
|
connections get registered.
|
|
65
65
|
"""
|
|
66
66
|
|
|
67
|
-
message_flow = Signal
|
|
67
|
+
message_flow = Signal[Talk.ConnectionProcessed]()
|
|
68
68
|
|
|
69
69
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
70
70
|
"""Initialize registry and connect event handlers."""
|
|
@@ -90,9 +90,9 @@ class ConnectionRegistry(BaseRegistry[str, Talk]):
|
|
|
90
90
|
new_talk.connection_processed.connect(self._handle_message_flow)
|
|
91
91
|
logger.debug("Reconnected signal for talk", name=name)
|
|
92
92
|
|
|
93
|
-
def _handle_message_flow(self, event: Talk.ConnectionProcessed) -> None:
|
|
93
|
+
async def _handle_message_flow(self, event: Talk.ConnectionProcessed) -> None:
|
|
94
94
|
"""Forward message flow to global stream."""
|
|
95
|
-
self.message_flow.emit(event)
|
|
95
|
+
await self.message_flow.emit(event)
|
|
96
96
|
|
|
97
97
|
@property
|
|
98
98
|
def _error_class(self) -> type[ConnectionRegistryError]:
|
agentpool/talk/talk.py
CHANGED
|
@@ -8,7 +8,7 @@ from contextlib import asynccontextmanager
|
|
|
8
8
|
from dataclasses import dataclass, field, replace
|
|
9
9
|
from typing import TYPE_CHECKING, Any, Self, overload
|
|
10
10
|
|
|
11
|
-
from
|
|
11
|
+
from anyenv.signals import Signal
|
|
12
12
|
|
|
13
13
|
from agentpool.log import get_logger
|
|
14
14
|
from agentpool.messaging import ChatMessage
|
|
@@ -53,11 +53,11 @@ class Talk[TTransmittedData = Any]:
|
|
|
53
53
|
timestamp: datetime = field(default_factory=get_now)
|
|
54
54
|
|
|
55
55
|
# Original message "coming in"
|
|
56
|
-
message_received = Signal(
|
|
56
|
+
message_received = Signal[ChatMessage[Any]]()
|
|
57
57
|
# After any transformation (one for each message, not per target)
|
|
58
|
-
message_forwarded = Signal(
|
|
58
|
+
message_forwarded = Signal[ChatMessage[Any]]()
|
|
59
59
|
# Comprehensive signal capturing all information about one "message handling process"
|
|
60
|
-
connection_processed = Signal(
|
|
60
|
+
connection_processed = Signal[ConnectionProcessed]()
|
|
61
61
|
|
|
62
62
|
def __init__(
|
|
63
63
|
self,
|
|
@@ -270,7 +270,7 @@ class Talk[TTransmittedData = Any]:
|
|
|
270
270
|
)
|
|
271
271
|
]
|
|
272
272
|
# 7. emit connection processed event
|
|
273
|
-
self.connection_processed.emit(
|
|
273
|
+
await self.connection_processed.emit(
|
|
274
274
|
self.ConnectionProcessed(
|
|
275
275
|
message=processed_message,
|
|
276
276
|
source=self.source,
|
|
@@ -283,7 +283,7 @@ class Talk[TTransmittedData = Any]:
|
|
|
283
283
|
if target_list:
|
|
284
284
|
messages = [*self._stats.messages, processed_message]
|
|
285
285
|
self._stats = replace(self._stats, messages=messages)
|
|
286
|
-
self.message_forwarded.emit(processed_message)
|
|
286
|
+
await self.message_forwarded.emit(processed_message)
|
|
287
287
|
|
|
288
288
|
# 9. Second pass: Actually process for each target
|
|
289
289
|
responses: list[ChatMessage[Any]] = []
|
|
@@ -308,10 +308,9 @@ class Talk[TTransmittedData = Any]:
|
|
|
308
308
|
|
|
309
309
|
match self.connection_type:
|
|
310
310
|
case "run":
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
return await target.run(*prompts)
|
|
311
|
+
# Use run_message to handle ChatMessage routing
|
|
312
|
+
# It extracts content, preserves conversation_id, and applies forwarding
|
|
313
|
+
return await target.run_message(message)
|
|
315
314
|
|
|
316
315
|
case "context":
|
|
317
316
|
meta = {
|
agentpool/testing.py
CHANGED
|
@@ -35,7 +35,7 @@ from typing import TYPE_CHECKING, Any, Literal
|
|
|
35
35
|
if TYPE_CHECKING:
|
|
36
36
|
from collections.abc import AsyncIterator, Sequence
|
|
37
37
|
|
|
38
|
-
from
|
|
38
|
+
from evented_config import EventConfig
|
|
39
39
|
|
|
40
40
|
from agentpool.agents.acp_agent import ACPAgent
|
|
41
41
|
from agentpool.common_types import BuiltinEventHandlerType, IndividualEventHandler
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Agent CLI tool for executing internal commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
from agentpool.tool_impls.agent_cli.tool import AgentCliTool
|
|
8
|
+
from agentpool_config.tools import ToolHints
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
__all__ = ["AgentCliTool", "create_agent_cli_tool"]
|
|
12
|
+
|
|
13
|
+
# Tool metadata defaults
|
|
14
|
+
NAME = "agent_cli"
|
|
15
|
+
DESCRIPTION = "Execute an internal agent management command."
|
|
16
|
+
CATEGORY: Literal["other"] = "other"
|
|
17
|
+
HINTS = ToolHints()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def create_agent_cli_tool(
|
|
21
|
+
*,
|
|
22
|
+
name: str = NAME,
|
|
23
|
+
description: str = DESCRIPTION,
|
|
24
|
+
requires_confirmation: bool = False,
|
|
25
|
+
) -> AgentCliTool:
|
|
26
|
+
"""Create a configured AgentCliTool instance.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
name: Tool name override.
|
|
30
|
+
description: Tool description override.
|
|
31
|
+
requires_confirmation: Whether tool execution needs confirmation.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Configured AgentCliTool instance.
|
|
35
|
+
"""
|
|
36
|
+
return AgentCliTool(
|
|
37
|
+
name=name,
|
|
38
|
+
description=description,
|
|
39
|
+
category=CATEGORY,
|
|
40
|
+
hints=HINTS,
|
|
41
|
+
requires_confirmation=requires_confirmation,
|
|
42
|
+
)
|