agentpool 2.1.9__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.
Potentially problematic release.
This version of agentpool might be problematic. Click here for more details.
- acp/README.md +64 -0
- acp/__init__.py +172 -0
- acp/__main__.py +10 -0
- acp/acp_requests.py +285 -0
- acp/agent/__init__.py +6 -0
- acp/agent/connection.py +256 -0
- acp/agent/implementations/__init__.py +6 -0
- acp/agent/implementations/debug_server/__init__.py +1 -0
- acp/agent/implementations/debug_server/cli.py +79 -0
- acp/agent/implementations/debug_server/debug.html +234 -0
- acp/agent/implementations/debug_server/debug_server.py +496 -0
- acp/agent/implementations/testing.py +91 -0
- acp/agent/protocol.py +65 -0
- acp/bridge/README.md +162 -0
- acp/bridge/__init__.py +6 -0
- acp/bridge/__main__.py +91 -0
- acp/bridge/bridge.py +246 -0
- acp/bridge/py.typed +0 -0
- acp/bridge/settings.py +15 -0
- acp/client/__init__.py +7 -0
- acp/client/connection.py +251 -0
- acp/client/implementations/__init__.py +7 -0
- acp/client/implementations/default_client.py +185 -0
- acp/client/implementations/headless_client.py +266 -0
- acp/client/implementations/noop_client.py +110 -0
- acp/client/protocol.py +61 -0
- acp/connection.py +280 -0
- acp/exceptions.py +46 -0
- acp/filesystem.py +524 -0
- acp/notifications.py +832 -0
- acp/py.typed +0 -0
- acp/schema/__init__.py +265 -0
- acp/schema/agent_plan.py +30 -0
- acp/schema/agent_requests.py +126 -0
- acp/schema/agent_responses.py +256 -0
- acp/schema/base.py +39 -0
- acp/schema/capabilities.py +230 -0
- acp/schema/client_requests.py +247 -0
- acp/schema/client_responses.py +96 -0
- acp/schema/common.py +81 -0
- acp/schema/content_blocks.py +188 -0
- acp/schema/mcp.py +82 -0
- acp/schema/messages.py +171 -0
- acp/schema/notifications.py +82 -0
- acp/schema/protocol_stuff.md +3 -0
- acp/schema/session_state.py +160 -0
- acp/schema/session_updates.py +419 -0
- acp/schema/slash_commands.py +51 -0
- acp/schema/terminal.py +15 -0
- acp/schema/tool_call.py +347 -0
- acp/stdio.py +250 -0
- acp/task/__init__.py +53 -0
- acp/task/debug.py +197 -0
- acp/task/dispatcher.py +93 -0
- acp/task/queue.py +69 -0
- acp/task/sender.py +82 -0
- acp/task/state.py +87 -0
- acp/task/supervisor.py +93 -0
- acp/terminal_handle.py +30 -0
- acp/tool_call_reporter.py +199 -0
- acp/tool_call_state.py +178 -0
- acp/transports.py +104 -0
- acp/utils.py +240 -0
- agentpool/__init__.py +63 -0
- agentpool/__main__.py +7 -0
- agentpool/agents/__init__.py +30 -0
- agentpool/agents/acp_agent/__init__.py +5 -0
- agentpool/agents/acp_agent/acp_agent.py +837 -0
- agentpool/agents/acp_agent/acp_converters.py +294 -0
- agentpool/agents/acp_agent/client_handler.py +317 -0
- agentpool/agents/acp_agent/session_state.py +44 -0
- agentpool/agents/agent.py +1264 -0
- agentpool/agents/agui_agent/__init__.py +19 -0
- agentpool/agents/agui_agent/agui_agent.py +677 -0
- agentpool/agents/agui_agent/agui_converters.py +423 -0
- agentpool/agents/agui_agent/chunk_transformer.py +204 -0
- agentpool/agents/agui_agent/event_types.py +83 -0
- agentpool/agents/agui_agent/helpers.py +192 -0
- agentpool/agents/architect.py +71 -0
- agentpool/agents/base_agent.py +177 -0
- agentpool/agents/claude_code_agent/__init__.py +11 -0
- agentpool/agents/claude_code_agent/claude_code_agent.py +1021 -0
- agentpool/agents/claude_code_agent/converters.py +243 -0
- agentpool/agents/context.py +105 -0
- agentpool/agents/events/__init__.py +61 -0
- agentpool/agents/events/builtin_handlers.py +129 -0
- agentpool/agents/events/event_emitter.py +320 -0
- agentpool/agents/events/events.py +561 -0
- agentpool/agents/events/tts_handlers.py +186 -0
- agentpool/agents/interactions.py +419 -0
- agentpool/agents/slashed_agent.py +244 -0
- agentpool/agents/sys_prompts.py +178 -0
- agentpool/agents/tool_wrapping.py +184 -0
- agentpool/base_provider.py +28 -0
- agentpool/common_types.py +226 -0
- agentpool/config_resources/__init__.py +16 -0
- agentpool/config_resources/acp_assistant.yml +24 -0
- agentpool/config_resources/agents.yml +109 -0
- agentpool/config_resources/agents_template.yml +18 -0
- agentpool/config_resources/agui_test.yml +18 -0
- agentpool/config_resources/claude_code_agent.yml +16 -0
- agentpool/config_resources/claude_style_subagent.md +30 -0
- agentpool/config_resources/external_acp_agents.yml +77 -0
- agentpool/config_resources/opencode_style_subagent.md +19 -0
- agentpool/config_resources/tts_test_agents.yml +78 -0
- agentpool/delegation/__init__.py +8 -0
- agentpool/delegation/base_team.py +504 -0
- agentpool/delegation/message_flow_tracker.py +39 -0
- agentpool/delegation/pool.py +1129 -0
- agentpool/delegation/team.py +325 -0
- agentpool/delegation/teamrun.py +343 -0
- agentpool/docs/__init__.py +5 -0
- agentpool/docs/gen_examples.py +42 -0
- agentpool/docs/utils.py +370 -0
- agentpool/functional/__init__.py +20 -0
- agentpool/functional/py.typed +0 -0
- agentpool/functional/run.py +80 -0
- agentpool/functional/structure.py +136 -0
- agentpool/hooks/__init__.py +20 -0
- agentpool/hooks/agent_hooks.py +247 -0
- agentpool/hooks/base.py +119 -0
- agentpool/hooks/callable.py +140 -0
- agentpool/hooks/command.py +180 -0
- agentpool/hooks/prompt.py +122 -0
- agentpool/jinja_filters.py +132 -0
- agentpool/log.py +224 -0
- agentpool/mcp_server/__init__.py +17 -0
- agentpool/mcp_server/client.py +429 -0
- agentpool/mcp_server/constants.py +32 -0
- agentpool/mcp_server/conversions.py +172 -0
- agentpool/mcp_server/helpers.py +47 -0
- agentpool/mcp_server/manager.py +232 -0
- agentpool/mcp_server/message_handler.py +164 -0
- agentpool/mcp_server/registries/__init__.py +1 -0
- agentpool/mcp_server/registries/official_registry_client.py +345 -0
- agentpool/mcp_server/registries/pulsemcp_client.py +88 -0
- agentpool/mcp_server/tool_bridge.py +548 -0
- agentpool/messaging/__init__.py +58 -0
- agentpool/messaging/compaction.py +928 -0
- agentpool/messaging/connection_manager.py +319 -0
- agentpool/messaging/context.py +66 -0
- agentpool/messaging/event_manager.py +426 -0
- agentpool/messaging/events.py +39 -0
- agentpool/messaging/message_container.py +209 -0
- agentpool/messaging/message_history.py +491 -0
- agentpool/messaging/messagenode.py +377 -0
- agentpool/messaging/messages.py +655 -0
- agentpool/messaging/processing.py +76 -0
- agentpool/mime_utils.py +95 -0
- agentpool/models/__init__.py +21 -0
- agentpool/models/acp_agents/__init__.py +22 -0
- agentpool/models/acp_agents/base.py +308 -0
- agentpool/models/acp_agents/mcp_capable.py +790 -0
- agentpool/models/acp_agents/non_mcp.py +842 -0
- agentpool/models/agents.py +450 -0
- agentpool/models/agui_agents.py +89 -0
- agentpool/models/claude_code_agents.py +238 -0
- agentpool/models/file_agents.py +116 -0
- agentpool/models/file_parsing.py +367 -0
- agentpool/models/manifest.py +658 -0
- agentpool/observability/__init__.py +9 -0
- agentpool/observability/observability_registry.py +97 -0
- agentpool/prompts/__init__.py +1 -0
- agentpool/prompts/base.py +27 -0
- agentpool/prompts/builtin_provider.py +75 -0
- agentpool/prompts/conversion_manager.py +95 -0
- agentpool/prompts/convert.py +96 -0
- agentpool/prompts/manager.py +204 -0
- agentpool/prompts/parts/zed.md +33 -0
- agentpool/prompts/prompts.py +581 -0
- agentpool/py.typed +0 -0
- agentpool/queries/tree-sitter-language-pack/README.md +7 -0
- agentpool/queries/tree-sitter-language-pack/arduino-tags.scm +5 -0
- agentpool/queries/tree-sitter-language-pack/c-tags.scm +9 -0
- agentpool/queries/tree-sitter-language-pack/chatito-tags.scm +16 -0
- agentpool/queries/tree-sitter-language-pack/clojure-tags.scm +7 -0
- agentpool/queries/tree-sitter-language-pack/commonlisp-tags.scm +122 -0
- agentpool/queries/tree-sitter-language-pack/cpp-tags.scm +15 -0
- agentpool/queries/tree-sitter-language-pack/csharp-tags.scm +26 -0
- agentpool/queries/tree-sitter-language-pack/d-tags.scm +26 -0
- agentpool/queries/tree-sitter-language-pack/dart-tags.scm +92 -0
- agentpool/queries/tree-sitter-language-pack/elisp-tags.scm +5 -0
- agentpool/queries/tree-sitter-language-pack/elixir-tags.scm +54 -0
- agentpool/queries/tree-sitter-language-pack/elm-tags.scm +19 -0
- agentpool/queries/tree-sitter-language-pack/gleam-tags.scm +41 -0
- agentpool/queries/tree-sitter-language-pack/go-tags.scm +42 -0
- agentpool/queries/tree-sitter-language-pack/java-tags.scm +20 -0
- agentpool/queries/tree-sitter-language-pack/javascript-tags.scm +88 -0
- agentpool/queries/tree-sitter-language-pack/lua-tags.scm +34 -0
- agentpool/queries/tree-sitter-language-pack/matlab-tags.scm +10 -0
- agentpool/queries/tree-sitter-language-pack/ocaml-tags.scm +115 -0
- agentpool/queries/tree-sitter-language-pack/ocaml_interface-tags.scm +98 -0
- agentpool/queries/tree-sitter-language-pack/pony-tags.scm +39 -0
- agentpool/queries/tree-sitter-language-pack/properties-tags.scm +5 -0
- agentpool/queries/tree-sitter-language-pack/python-tags.scm +14 -0
- agentpool/queries/tree-sitter-language-pack/r-tags.scm +21 -0
- agentpool/queries/tree-sitter-language-pack/racket-tags.scm +12 -0
- agentpool/queries/tree-sitter-language-pack/ruby-tags.scm +64 -0
- agentpool/queries/tree-sitter-language-pack/rust-tags.scm +60 -0
- agentpool/queries/tree-sitter-language-pack/solidity-tags.scm +43 -0
- agentpool/queries/tree-sitter-language-pack/swift-tags.scm +51 -0
- agentpool/queries/tree-sitter-language-pack/udev-tags.scm +20 -0
- agentpool/queries/tree-sitter-languages/README.md +24 -0
- agentpool/queries/tree-sitter-languages/c-tags.scm +9 -0
- agentpool/queries/tree-sitter-languages/c_sharp-tags.scm +46 -0
- agentpool/queries/tree-sitter-languages/cpp-tags.scm +15 -0
- agentpool/queries/tree-sitter-languages/dart-tags.scm +91 -0
- agentpool/queries/tree-sitter-languages/elisp-tags.scm +8 -0
- agentpool/queries/tree-sitter-languages/elixir-tags.scm +54 -0
- agentpool/queries/tree-sitter-languages/elm-tags.scm +19 -0
- agentpool/queries/tree-sitter-languages/fortran-tags.scm +15 -0
- agentpool/queries/tree-sitter-languages/go-tags.scm +30 -0
- agentpool/queries/tree-sitter-languages/haskell-tags.scm +3 -0
- agentpool/queries/tree-sitter-languages/hcl-tags.scm +77 -0
- agentpool/queries/tree-sitter-languages/java-tags.scm +20 -0
- agentpool/queries/tree-sitter-languages/javascript-tags.scm +88 -0
- agentpool/queries/tree-sitter-languages/julia-tags.scm +60 -0
- agentpool/queries/tree-sitter-languages/kotlin-tags.scm +27 -0
- agentpool/queries/tree-sitter-languages/matlab-tags.scm +10 -0
- agentpool/queries/tree-sitter-languages/ocaml-tags.scm +115 -0
- agentpool/queries/tree-sitter-languages/ocaml_interface-tags.scm +98 -0
- agentpool/queries/tree-sitter-languages/php-tags.scm +26 -0
- agentpool/queries/tree-sitter-languages/python-tags.scm +12 -0
- agentpool/queries/tree-sitter-languages/ql-tags.scm +26 -0
- agentpool/queries/tree-sitter-languages/ruby-tags.scm +64 -0
- agentpool/queries/tree-sitter-languages/rust-tags.scm +60 -0
- agentpool/queries/tree-sitter-languages/scala-tags.scm +65 -0
- agentpool/queries/tree-sitter-languages/typescript-tags.scm +41 -0
- agentpool/queries/tree-sitter-languages/zig-tags.scm +3 -0
- agentpool/repomap.py +1231 -0
- agentpool/resource_providers/__init__.py +17 -0
- agentpool/resource_providers/aggregating.py +54 -0
- agentpool/resource_providers/base.py +172 -0
- agentpool/resource_providers/codemode/__init__.py +9 -0
- agentpool/resource_providers/codemode/code_executor.py +215 -0
- agentpool/resource_providers/codemode/default_prompt.py +19 -0
- agentpool/resource_providers/codemode/helpers.py +83 -0
- agentpool/resource_providers/codemode/progress_executor.py +212 -0
- agentpool/resource_providers/codemode/provider.py +150 -0
- agentpool/resource_providers/codemode/remote_mcp_execution.py +143 -0
- agentpool/resource_providers/codemode/remote_provider.py +171 -0
- agentpool/resource_providers/filtering.py +42 -0
- agentpool/resource_providers/mcp_provider.py +246 -0
- agentpool/resource_providers/plan_provider.py +196 -0
- agentpool/resource_providers/pool.py +69 -0
- agentpool/resource_providers/static.py +289 -0
- agentpool/running/__init__.py +20 -0
- agentpool/running/decorators.py +56 -0
- agentpool/running/discovery.py +101 -0
- agentpool/running/executor.py +284 -0
- agentpool/running/injection.py +111 -0
- agentpool/running/py.typed +0 -0
- agentpool/running/run_nodes.py +87 -0
- agentpool/server.py +122 -0
- agentpool/sessions/__init__.py +13 -0
- agentpool/sessions/manager.py +302 -0
- agentpool/sessions/models.py +71 -0
- agentpool/sessions/session.py +239 -0
- agentpool/sessions/store.py +163 -0
- agentpool/skills/__init__.py +5 -0
- agentpool/skills/manager.py +120 -0
- agentpool/skills/registry.py +210 -0
- agentpool/skills/skill.py +36 -0
- agentpool/storage/__init__.py +17 -0
- agentpool/storage/manager.py +419 -0
- agentpool/storage/serialization.py +136 -0
- agentpool/talk/__init__.py +13 -0
- agentpool/talk/registry.py +128 -0
- agentpool/talk/stats.py +159 -0
- agentpool/talk/talk.py +604 -0
- agentpool/tasks/__init__.py +20 -0
- agentpool/tasks/exceptions.py +25 -0
- agentpool/tasks/registry.py +33 -0
- agentpool/testing.py +129 -0
- agentpool/text_templates/__init__.py +39 -0
- agentpool/text_templates/system_prompt.jinja +30 -0
- agentpool/text_templates/tool_call_default.jinja +13 -0
- agentpool/text_templates/tool_call_markdown.jinja +25 -0
- agentpool/text_templates/tool_call_simple.jinja +5 -0
- agentpool/tools/__init__.py +16 -0
- agentpool/tools/base.py +269 -0
- agentpool/tools/exceptions.py +9 -0
- agentpool/tools/manager.py +255 -0
- agentpool/tools/tool_call_info.py +87 -0
- agentpool/ui/__init__.py +2 -0
- agentpool/ui/base.py +89 -0
- agentpool/ui/mock_provider.py +81 -0
- agentpool/ui/stdlib_provider.py +150 -0
- agentpool/utils/__init__.py +44 -0
- agentpool/utils/baseregistry.py +185 -0
- agentpool/utils/count_tokens.py +62 -0
- agentpool/utils/dag.py +184 -0
- agentpool/utils/importing.py +206 -0
- agentpool/utils/inspection.py +334 -0
- agentpool/utils/model_capabilities.py +25 -0
- agentpool/utils/network.py +28 -0
- agentpool/utils/now.py +22 -0
- agentpool/utils/parse_time.py +87 -0
- agentpool/utils/result_utils.py +35 -0
- agentpool/utils/signatures.py +305 -0
- agentpool/utils/streams.py +112 -0
- agentpool/utils/tasks.py +186 -0
- agentpool/vfs_registry.py +250 -0
- agentpool-2.1.9.dist-info/METADATA +336 -0
- agentpool-2.1.9.dist-info/RECORD +474 -0
- agentpool-2.1.9.dist-info/WHEEL +4 -0
- agentpool-2.1.9.dist-info/entry_points.txt +14 -0
- agentpool-2.1.9.dist-info/licenses/LICENSE +22 -0
- agentpool_cli/__init__.py +34 -0
- agentpool_cli/__main__.py +66 -0
- agentpool_cli/agent.py +175 -0
- agentpool_cli/cli_types.py +23 -0
- agentpool_cli/common.py +163 -0
- agentpool_cli/create.py +175 -0
- agentpool_cli/history.py +217 -0
- agentpool_cli/log.py +78 -0
- agentpool_cli/py.typed +0 -0
- agentpool_cli/run.py +84 -0
- agentpool_cli/serve_acp.py +177 -0
- agentpool_cli/serve_api.py +69 -0
- agentpool_cli/serve_mcp.py +74 -0
- agentpool_cli/serve_vercel.py +233 -0
- agentpool_cli/store.py +171 -0
- agentpool_cli/task.py +84 -0
- agentpool_cli/utils.py +104 -0
- agentpool_cli/watch.py +54 -0
- agentpool_commands/__init__.py +180 -0
- agentpool_commands/agents.py +199 -0
- agentpool_commands/base.py +45 -0
- agentpool_commands/commands.py +58 -0
- agentpool_commands/completers.py +110 -0
- agentpool_commands/connections.py +175 -0
- agentpool_commands/markdown_utils.py +31 -0
- agentpool_commands/models.py +62 -0
- agentpool_commands/prompts.py +78 -0
- agentpool_commands/py.typed +0 -0
- agentpool_commands/read.py +77 -0
- agentpool_commands/resources.py +210 -0
- agentpool_commands/session.py +48 -0
- agentpool_commands/tools.py +269 -0
- agentpool_commands/utils.py +189 -0
- agentpool_commands/workers.py +163 -0
- agentpool_config/__init__.py +53 -0
- agentpool_config/builtin_tools.py +265 -0
- agentpool_config/commands.py +237 -0
- agentpool_config/conditions.py +301 -0
- agentpool_config/converters.py +30 -0
- agentpool_config/durable.py +331 -0
- agentpool_config/event_handlers.py +600 -0
- agentpool_config/events.py +153 -0
- agentpool_config/forward_targets.py +251 -0
- agentpool_config/hook_conditions.py +331 -0
- agentpool_config/hooks.py +241 -0
- agentpool_config/jinja.py +206 -0
- agentpool_config/knowledge.py +41 -0
- agentpool_config/loaders.py +350 -0
- agentpool_config/mcp_server.py +243 -0
- agentpool_config/nodes.py +202 -0
- agentpool_config/observability.py +191 -0
- agentpool_config/output_types.py +55 -0
- agentpool_config/pool_server.py +267 -0
- agentpool_config/prompt_hubs.py +105 -0
- agentpool_config/prompts.py +185 -0
- agentpool_config/py.typed +0 -0
- agentpool_config/resources.py +33 -0
- agentpool_config/session.py +119 -0
- agentpool_config/skills.py +17 -0
- agentpool_config/storage.py +288 -0
- agentpool_config/system_prompts.py +190 -0
- agentpool_config/task.py +162 -0
- agentpool_config/teams.py +52 -0
- agentpool_config/tools.py +112 -0
- agentpool_config/toolsets.py +1033 -0
- agentpool_config/workers.py +86 -0
- agentpool_prompts/__init__.py +1 -0
- agentpool_prompts/braintrust_hub.py +235 -0
- agentpool_prompts/fabric.py +75 -0
- agentpool_prompts/langfuse_hub.py +79 -0
- agentpool_prompts/promptlayer_provider.py +59 -0
- agentpool_prompts/py.typed +0 -0
- agentpool_server/__init__.py +9 -0
- agentpool_server/a2a_server/__init__.py +5 -0
- agentpool_server/a2a_server/a2a_types.py +41 -0
- agentpool_server/a2a_server/server.py +190 -0
- agentpool_server/a2a_server/storage.py +81 -0
- agentpool_server/acp_server/__init__.py +22 -0
- agentpool_server/acp_server/acp_agent.py +786 -0
- agentpool_server/acp_server/acp_tools.py +43 -0
- agentpool_server/acp_server/commands/__init__.py +18 -0
- agentpool_server/acp_server/commands/acp_commands.py +594 -0
- agentpool_server/acp_server/commands/debug_commands.py +376 -0
- agentpool_server/acp_server/commands/docs_commands/__init__.py +39 -0
- agentpool_server/acp_server/commands/docs_commands/fetch_repo.py +169 -0
- agentpool_server/acp_server/commands/docs_commands/get_schema.py +176 -0
- agentpool_server/acp_server/commands/docs_commands/get_source.py +110 -0
- agentpool_server/acp_server/commands/docs_commands/git_diff.py +111 -0
- agentpool_server/acp_server/commands/docs_commands/helpers.py +33 -0
- agentpool_server/acp_server/commands/docs_commands/url_to_markdown.py +90 -0
- agentpool_server/acp_server/commands/spawn.py +210 -0
- agentpool_server/acp_server/converters.py +235 -0
- agentpool_server/acp_server/input_provider.py +338 -0
- agentpool_server/acp_server/server.py +288 -0
- agentpool_server/acp_server/session.py +969 -0
- agentpool_server/acp_server/session_manager.py +313 -0
- agentpool_server/acp_server/syntax_detection.py +250 -0
- agentpool_server/acp_server/zed_tools.md +90 -0
- agentpool_server/aggregating_server.py +309 -0
- agentpool_server/agui_server/__init__.py +11 -0
- agentpool_server/agui_server/server.py +128 -0
- agentpool_server/base.py +189 -0
- agentpool_server/http_server.py +164 -0
- agentpool_server/mcp_server/__init__.py +6 -0
- agentpool_server/mcp_server/server.py +314 -0
- agentpool_server/mcp_server/zed_wrapper.py +110 -0
- agentpool_server/openai_api_server/__init__.py +5 -0
- agentpool_server/openai_api_server/completions/__init__.py +1 -0
- agentpool_server/openai_api_server/completions/helpers.py +81 -0
- agentpool_server/openai_api_server/completions/models.py +98 -0
- agentpool_server/openai_api_server/responses/__init__.py +1 -0
- agentpool_server/openai_api_server/responses/helpers.py +74 -0
- agentpool_server/openai_api_server/responses/models.py +96 -0
- agentpool_server/openai_api_server/server.py +242 -0
- agentpool_server/py.typed +0 -0
- agentpool_storage/__init__.py +9 -0
- agentpool_storage/base.py +310 -0
- agentpool_storage/file_provider.py +378 -0
- agentpool_storage/formatters.py +129 -0
- agentpool_storage/memory_provider.py +396 -0
- agentpool_storage/models.py +108 -0
- agentpool_storage/py.typed +0 -0
- agentpool_storage/session_store.py +262 -0
- agentpool_storage/sql_provider/__init__.py +21 -0
- agentpool_storage/sql_provider/cli.py +146 -0
- agentpool_storage/sql_provider/models.py +249 -0
- agentpool_storage/sql_provider/queries.py +15 -0
- agentpool_storage/sql_provider/sql_provider.py +444 -0
- agentpool_storage/sql_provider/utils.py +234 -0
- agentpool_storage/text_log_provider.py +275 -0
- agentpool_toolsets/__init__.py +15 -0
- agentpool_toolsets/builtin/__init__.py +33 -0
- agentpool_toolsets/builtin/agent_management.py +239 -0
- agentpool_toolsets/builtin/chain.py +288 -0
- agentpool_toolsets/builtin/code.py +398 -0
- agentpool_toolsets/builtin/debug.py +291 -0
- agentpool_toolsets/builtin/execution_environment.py +381 -0
- agentpool_toolsets/builtin/file_edit/__init__.py +11 -0
- agentpool_toolsets/builtin/file_edit/file_edit.py +747 -0
- agentpool_toolsets/builtin/file_edit/fuzzy_matcher/__init__.py +5 -0
- agentpool_toolsets/builtin/file_edit/fuzzy_matcher/example_usage.py +311 -0
- agentpool_toolsets/builtin/file_edit/fuzzy_matcher/streaming_fuzzy_matcher.py +443 -0
- agentpool_toolsets/builtin/history.py +36 -0
- agentpool_toolsets/builtin/integration.py +85 -0
- agentpool_toolsets/builtin/skills.py +77 -0
- agentpool_toolsets/builtin/subagent_tools.py +324 -0
- agentpool_toolsets/builtin/tool_management.py +90 -0
- agentpool_toolsets/builtin/user_interaction.py +52 -0
- agentpool_toolsets/builtin/workers.py +128 -0
- agentpool_toolsets/composio_toolset.py +96 -0
- agentpool_toolsets/config_creation.py +192 -0
- agentpool_toolsets/entry_points.py +47 -0
- agentpool_toolsets/fsspec_toolset/__init__.py +7 -0
- agentpool_toolsets/fsspec_toolset/diagnostics.py +115 -0
- agentpool_toolsets/fsspec_toolset/grep.py +450 -0
- agentpool_toolsets/fsspec_toolset/helpers.py +631 -0
- agentpool_toolsets/fsspec_toolset/streaming_diff_parser.py +249 -0
- agentpool_toolsets/fsspec_toolset/toolset.py +1384 -0
- agentpool_toolsets/mcp_run_toolset.py +61 -0
- agentpool_toolsets/notifications.py +146 -0
- agentpool_toolsets/openapi.py +118 -0
- agentpool_toolsets/py.typed +0 -0
- agentpool_toolsets/search_toolset.py +202 -0
- agentpool_toolsets/semantic_memory_toolset.py +536 -0
- agentpool_toolsets/streaming_tools.py +265 -0
- agentpool_toolsets/vfs_toolset.py +124 -0
|
@@ -0,0 +1,928 @@
|
|
|
1
|
+
"""Composable message compaction pipeline for managing conversation history.
|
|
2
|
+
|
|
3
|
+
This module provides a pipeline-based approach to compacting and transforming
|
|
4
|
+
pydantic-ai message history. Each step in the pipeline operates on the message
|
|
5
|
+
sequence and can filter, truncate, summarize, or transform messages.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
```python
|
|
9
|
+
from agentpool.messaging.compaction import (
|
|
10
|
+
CompactionPipeline,
|
|
11
|
+
FilterThinking,
|
|
12
|
+
TruncateToolOutputs,
|
|
13
|
+
KeepLastMessages,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
# Programmatic usage
|
|
17
|
+
pipeline = CompactionPipeline(steps=[
|
|
18
|
+
FilterThinking(),
|
|
19
|
+
TruncateToolOutputs(max_length=1000),
|
|
20
|
+
KeepLastMessages(count=10),
|
|
21
|
+
])
|
|
22
|
+
compacted = await pipeline.apply(messages)
|
|
23
|
+
|
|
24
|
+
# Or via config (for YAML)
|
|
25
|
+
config = CompactionPipelineConfig(steps=[
|
|
26
|
+
FilterThinkingConfig(),
|
|
27
|
+
TruncateToolOutputsConfig(max_length=1000),
|
|
28
|
+
KeepLastMessagesConfig(count=10),
|
|
29
|
+
])
|
|
30
|
+
pipeline = config.build()
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
YAML configuration example:
|
|
34
|
+
```yaml
|
|
35
|
+
compaction:
|
|
36
|
+
steps:
|
|
37
|
+
- type: filter_thinking
|
|
38
|
+
- type: truncate_tool_outputs
|
|
39
|
+
max_length: 1000
|
|
40
|
+
- type: keep_last
|
|
41
|
+
count: 10
|
|
42
|
+
- type: summarize
|
|
43
|
+
model: openai:gpt-4o-mini
|
|
44
|
+
threshold: 20
|
|
45
|
+
```
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
from __future__ import annotations
|
|
49
|
+
|
|
50
|
+
from abc import ABC, abstractmethod
|
|
51
|
+
from collections.abc import Sequence
|
|
52
|
+
from dataclasses import dataclass, field, replace
|
|
53
|
+
from typing import TYPE_CHECKING, Annotated, Any, Literal, Self, cast
|
|
54
|
+
|
|
55
|
+
from pydantic import BaseModel, Field
|
|
56
|
+
from pydantic_ai import (
|
|
57
|
+
Agent,
|
|
58
|
+
BinaryContent,
|
|
59
|
+
ModelRequest,
|
|
60
|
+
ModelResponse,
|
|
61
|
+
RetryPromptPart,
|
|
62
|
+
TextPart,
|
|
63
|
+
ThinkingPart,
|
|
64
|
+
ToolCallPart,
|
|
65
|
+
ToolReturnPart,
|
|
66
|
+
UserPromptPart,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
if TYPE_CHECKING:
|
|
71
|
+
from collections.abc import Callable
|
|
72
|
+
|
|
73
|
+
from pydantic_ai import ModelRequestPart, ModelResponsePart
|
|
74
|
+
from tokonomics.model_names import ModelId
|
|
75
|
+
|
|
76
|
+
# Type aliases
|
|
77
|
+
ModelMessage = ModelRequest | ModelResponse
|
|
78
|
+
MessageSequence = Sequence[ModelMessage]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class CompactionStep(ABC):
|
|
82
|
+
"""Base class for message compaction steps.
|
|
83
|
+
|
|
84
|
+
Each step transforms a sequence of messages into a (potentially) smaller
|
|
85
|
+
or modified sequence. Steps can be composed into a pipeline.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
@abstractmethod
|
|
89
|
+
async def apply(self, messages: MessageSequence) -> list[ModelMessage]:
|
|
90
|
+
"""Apply this compaction step to the message sequence.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
messages: The input message sequence to transform.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
The transformed message sequence.
|
|
97
|
+
"""
|
|
98
|
+
...
|
|
99
|
+
|
|
100
|
+
def __or__(self, other: CompactionStep) -> CompactionPipeline:
|
|
101
|
+
"""Compose two steps into a pipeline using the | operator."""
|
|
102
|
+
return CompactionPipeline(steps=[self, other])
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@dataclass
|
|
106
|
+
class CompactionPipeline(CompactionStep):
|
|
107
|
+
"""A pipeline of compaction steps applied in sequence.
|
|
108
|
+
|
|
109
|
+
Steps are applied left-to-right, with each step receiving the output
|
|
110
|
+
of the previous step.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
steps: list[CompactionStep] = field(default_factory=list)
|
|
114
|
+
|
|
115
|
+
async def apply(self, messages: MessageSequence) -> list[ModelMessage]:
|
|
116
|
+
"""Apply all steps in sequence."""
|
|
117
|
+
result: list[ModelMessage] = list(messages)
|
|
118
|
+
for step in self.steps:
|
|
119
|
+
result = await step.apply(result)
|
|
120
|
+
return result
|
|
121
|
+
|
|
122
|
+
def __or__(self, other: CompactionStep) -> CompactionPipeline:
|
|
123
|
+
"""Add another step to the pipeline."""
|
|
124
|
+
if isinstance(other, CompactionPipeline):
|
|
125
|
+
return CompactionPipeline(steps=[*self.steps, *other.steps])
|
|
126
|
+
return CompactionPipeline(steps=[*self.steps, other])
|
|
127
|
+
|
|
128
|
+
def __ior__(self, other: CompactionStep) -> Self:
|
|
129
|
+
"""Add a step in place."""
|
|
130
|
+
if isinstance(other, CompactionPipeline):
|
|
131
|
+
self.steps.extend(other.steps)
|
|
132
|
+
else:
|
|
133
|
+
self.steps.append(other)
|
|
134
|
+
return self
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# =============================================================================
|
|
138
|
+
# Filter Steps - Remove or filter parts of messages
|
|
139
|
+
# =============================================================================
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@dataclass
|
|
143
|
+
class FilterThinking(CompactionStep):
|
|
144
|
+
"""Remove all thinking parts from model responses.
|
|
145
|
+
|
|
146
|
+
Thinking parts can consume significant context space without providing
|
|
147
|
+
value in subsequent interactions.
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
async def apply(self, messages: MessageSequence) -> list[ModelMessage]:
|
|
151
|
+
result: list[ModelMessage] = []
|
|
152
|
+
for msg in messages:
|
|
153
|
+
match msg:
|
|
154
|
+
case ModelResponse(parts=parts) if any(isinstance(p, ThinkingPart) for p in parts):
|
|
155
|
+
filtered_parts = [p for p in parts if not isinstance(p, ThinkingPart)]
|
|
156
|
+
if filtered_parts: # Only include if there are remaining parts
|
|
157
|
+
result.append(replace(msg, parts=filtered_parts))
|
|
158
|
+
case _:
|
|
159
|
+
result.append(msg)
|
|
160
|
+
return result
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@dataclass
|
|
164
|
+
class FilterRetryPrompts(CompactionStep):
|
|
165
|
+
"""Remove retry prompt parts from requests.
|
|
166
|
+
|
|
167
|
+
Retry prompts are typically not needed after the conversation has moved on.
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
async def apply(self, messages: MessageSequence) -> list[ModelMessage]:
|
|
171
|
+
result: list[ModelMessage] = []
|
|
172
|
+
for msg in messages:
|
|
173
|
+
match msg:
|
|
174
|
+
case ModelRequest(parts=parts) if any(
|
|
175
|
+
isinstance(p, RetryPromptPart) for p in parts
|
|
176
|
+
):
|
|
177
|
+
filtered_parts = [p for p in parts if not isinstance(p, RetryPromptPart)]
|
|
178
|
+
if filtered_parts:
|
|
179
|
+
result.append(replace(msg, parts=filtered_parts))
|
|
180
|
+
case _:
|
|
181
|
+
result.append(msg)
|
|
182
|
+
return result
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@dataclass
|
|
186
|
+
class FilterBinaryContent(CompactionStep):
|
|
187
|
+
"""Remove binary content (images, audio, etc.) from messages.
|
|
188
|
+
|
|
189
|
+
Useful when you want to keep only text content for context efficiency.
|
|
190
|
+
"""
|
|
191
|
+
|
|
192
|
+
keep_references: bool = False
|
|
193
|
+
"""If True, replace binary with a placeholder text describing what was there."""
|
|
194
|
+
|
|
195
|
+
async def apply(self, messages: MessageSequence) -> list[ModelMessage]:
|
|
196
|
+
result: list[ModelMessage] = []
|
|
197
|
+
for msg in messages:
|
|
198
|
+
match msg:
|
|
199
|
+
case ModelRequest(parts=parts):
|
|
200
|
+
filtered_parts: list[ModelRequestPart | ModelResponsePart] = []
|
|
201
|
+
for part in parts:
|
|
202
|
+
if isinstance(part, UserPromptPart):
|
|
203
|
+
if isinstance(part.content, list):
|
|
204
|
+
new_content: list[Any] = []
|
|
205
|
+
for item in part.content:
|
|
206
|
+
if isinstance(item, BinaryContent):
|
|
207
|
+
if self.keep_references:
|
|
208
|
+
new_content.append(f"[Binary: {item.media_type}]")
|
|
209
|
+
else:
|
|
210
|
+
new_content.append(item)
|
|
211
|
+
if new_content:
|
|
212
|
+
filtered_parts.append(replace(part, content=new_content))
|
|
213
|
+
else:
|
|
214
|
+
filtered_parts.append(part)
|
|
215
|
+
else:
|
|
216
|
+
filtered_parts.append(part)
|
|
217
|
+
if filtered_parts:
|
|
218
|
+
result.append(replace(msg, parts=cast(Sequence[Any], filtered_parts)))
|
|
219
|
+
case _:
|
|
220
|
+
result.append(msg)
|
|
221
|
+
return result
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
@dataclass
|
|
225
|
+
class FilterToolCalls(CompactionStep):
|
|
226
|
+
"""Filter tool calls by name.
|
|
227
|
+
|
|
228
|
+
Can be used to remove specific tool calls that are not relevant
|
|
229
|
+
for future context (e.g., debugging tools, one-time lookups).
|
|
230
|
+
"""
|
|
231
|
+
|
|
232
|
+
exclude_tools: list[str] = field(default_factory=list)
|
|
233
|
+
"""Tool names to exclude from the history."""
|
|
234
|
+
|
|
235
|
+
include_only: list[str] | None = None
|
|
236
|
+
"""If set, only keep these tools (overrides exclude_tools)."""
|
|
237
|
+
|
|
238
|
+
async def apply(self, messages: MessageSequence) -> list[ModelMessage]:
|
|
239
|
+
|
|
240
|
+
def should_keep(tool_name: str) -> bool:
|
|
241
|
+
if self.include_only is not None:
|
|
242
|
+
return tool_name in self.include_only
|
|
243
|
+
return tool_name not in self.exclude_tools
|
|
244
|
+
|
|
245
|
+
result: list[ModelMessage] = []
|
|
246
|
+
excluded_call_ids: set[str] = set()
|
|
247
|
+
|
|
248
|
+
for msg in messages:
|
|
249
|
+
match msg:
|
|
250
|
+
case ModelResponse(parts=parts):
|
|
251
|
+
filtered_parts: list[ModelRequestPart | ModelResponsePart] = []
|
|
252
|
+
for part in parts:
|
|
253
|
+
if isinstance(part, ToolCallPart):
|
|
254
|
+
if should_keep(part.tool_name):
|
|
255
|
+
filtered_parts.append(part)
|
|
256
|
+
else:
|
|
257
|
+
excluded_call_ids.add(part.tool_call_id)
|
|
258
|
+
else:
|
|
259
|
+
filtered_parts.append(part)
|
|
260
|
+
if filtered_parts:
|
|
261
|
+
result.append(replace(msg, parts=cast(Sequence[Any], filtered_parts)))
|
|
262
|
+
|
|
263
|
+
case ModelRequest(parts=parts):
|
|
264
|
+
# Also filter corresponding tool returns
|
|
265
|
+
filtered_parts = [
|
|
266
|
+
p
|
|
267
|
+
for p in parts
|
|
268
|
+
if not (
|
|
269
|
+
isinstance(p, ToolReturnPart) and p.tool_call_id in excluded_call_ids
|
|
270
|
+
)
|
|
271
|
+
]
|
|
272
|
+
if filtered_parts:
|
|
273
|
+
result.append(replace(msg, parts=cast(Sequence[Any], filtered_parts)))
|
|
274
|
+
|
|
275
|
+
case _:
|
|
276
|
+
result.append(msg)
|
|
277
|
+
|
|
278
|
+
return result
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
@dataclass
|
|
282
|
+
class FilterEmptyMessages(CompactionStep):
|
|
283
|
+
"""Remove messages that have no meaningful content.
|
|
284
|
+
|
|
285
|
+
Cleans up the history by removing empty or near-empty messages.
|
|
286
|
+
"""
|
|
287
|
+
|
|
288
|
+
async def apply(self, messages: MessageSequence) -> list[ModelMessage]:
|
|
289
|
+
result: list[ModelMessage] = []
|
|
290
|
+
for msg in messages:
|
|
291
|
+
match msg:
|
|
292
|
+
case ModelRequest(parts=parts) | ModelResponse(parts=parts):
|
|
293
|
+
if any(_part_has_content(p) for p in parts):
|
|
294
|
+
result.append(msg)
|
|
295
|
+
case _:
|
|
296
|
+
result.append(msg)
|
|
297
|
+
return result
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _part_has_content(part: Any) -> bool:
|
|
301
|
+
"""Check if a message part has meaningful content."""
|
|
302
|
+
# Use has_content if available (TextPart, ThinkingPart, etc.)
|
|
303
|
+
if hasattr(part, "has_content"):
|
|
304
|
+
return part.has_content() # type: ignore[no-any-return]
|
|
305
|
+
# For UserPromptPart, check content directly
|
|
306
|
+
if isinstance(part, UserPromptPart):
|
|
307
|
+
content = part.content
|
|
308
|
+
if isinstance(content, str):
|
|
309
|
+
return bool(content.strip())
|
|
310
|
+
if isinstance(content, list):
|
|
311
|
+
return bool(content)
|
|
312
|
+
return content is not None
|
|
313
|
+
# For ToolReturnPart, check content
|
|
314
|
+
if isinstance(part, ToolReturnPart):
|
|
315
|
+
return part.content is not None
|
|
316
|
+
# Default: assume has content
|
|
317
|
+
return True
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
# =============================================================================
|
|
321
|
+
# Truncation Steps - Shorten content while preserving structure
|
|
322
|
+
# =============================================================================
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
@dataclass
|
|
326
|
+
class TruncateToolOutputs(CompactionStep):
|
|
327
|
+
"""Truncate large tool outputs to a maximum length.
|
|
328
|
+
|
|
329
|
+
Tool outputs can sometimes be very large (e.g., file contents, API responses).
|
|
330
|
+
This step truncates them while preserving the beginning of the content.
|
|
331
|
+
"""
|
|
332
|
+
|
|
333
|
+
max_length: int = 2000
|
|
334
|
+
"""Maximum length for tool output content."""
|
|
335
|
+
|
|
336
|
+
suffix: str = "\n... [truncated]"
|
|
337
|
+
"""Suffix to append when content is truncated."""
|
|
338
|
+
|
|
339
|
+
async def apply(self, messages: MessageSequence) -> list[ModelMessage]:
|
|
340
|
+
result: list[ModelMessage] = []
|
|
341
|
+
for msg in messages:
|
|
342
|
+
match msg:
|
|
343
|
+
case ModelRequest(parts=parts):
|
|
344
|
+
new_parts: list[ModelRequestPart | ModelResponsePart] = []
|
|
345
|
+
for part in parts:
|
|
346
|
+
if isinstance(part, ToolReturnPart):
|
|
347
|
+
content = part.content
|
|
348
|
+
if isinstance(content, str) and len(content) > self.max_length:
|
|
349
|
+
truncated = content[: self.max_length - len(self.suffix)]
|
|
350
|
+
new_parts.append(replace(part, content=truncated + self.suffix))
|
|
351
|
+
else:
|
|
352
|
+
new_parts.append(part)
|
|
353
|
+
else:
|
|
354
|
+
new_parts.append(part)
|
|
355
|
+
result.append(replace(msg, parts=cast(Sequence[Any], new_parts)))
|
|
356
|
+
case _:
|
|
357
|
+
result.append(msg)
|
|
358
|
+
return result
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
@dataclass
|
|
362
|
+
class TruncateTextParts(CompactionStep):
|
|
363
|
+
"""Truncate long text parts in responses.
|
|
364
|
+
|
|
365
|
+
Useful for limiting very long model responses in the context.
|
|
366
|
+
"""
|
|
367
|
+
|
|
368
|
+
max_length: int = 5000
|
|
369
|
+
"""Maximum length for text content."""
|
|
370
|
+
|
|
371
|
+
suffix: str = "\n... [truncated]"
|
|
372
|
+
"""Suffix to append when content is truncated."""
|
|
373
|
+
|
|
374
|
+
async def apply(self, messages: MessageSequence) -> list[ModelMessage]:
|
|
375
|
+
result: list[ModelMessage] = []
|
|
376
|
+
for msg in messages:
|
|
377
|
+
match msg:
|
|
378
|
+
case ModelResponse(parts=parts):
|
|
379
|
+
new_parts: list[ModelRequestPart | ModelResponsePart] = []
|
|
380
|
+
for part in parts:
|
|
381
|
+
if isinstance(part, TextPart) and len(part.content) > self.max_length:
|
|
382
|
+
truncated = part.content[: self.max_length - len(self.suffix)]
|
|
383
|
+
new_parts.append(replace(part, content=truncated + self.suffix))
|
|
384
|
+
else:
|
|
385
|
+
new_parts.append(part)
|
|
386
|
+
result.append(replace(msg, parts=cast(Sequence[Any], new_parts)))
|
|
387
|
+
case _:
|
|
388
|
+
result.append(msg)
|
|
389
|
+
return result
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
# =============================================================================
|
|
393
|
+
# Selection Steps - Keep subsets of messages
|
|
394
|
+
# =============================================================================
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
@dataclass
|
|
398
|
+
class KeepLastMessages(CompactionStep):
|
|
399
|
+
"""Keep only the last N messages.
|
|
400
|
+
|
|
401
|
+
A simple sliding window approach to context management.
|
|
402
|
+
Messages are counted as request/response pairs when `count_pairs` is True.
|
|
403
|
+
"""
|
|
404
|
+
|
|
405
|
+
count: int = 10
|
|
406
|
+
"""Number of messages (or pairs) to keep."""
|
|
407
|
+
|
|
408
|
+
count_pairs: bool = True
|
|
409
|
+
"""If True, count request/response pairs instead of individual messages."""
|
|
410
|
+
|
|
411
|
+
async def apply(self, messages: MessageSequence) -> list[ModelMessage]:
|
|
412
|
+
if not messages:
|
|
413
|
+
return []
|
|
414
|
+
|
|
415
|
+
if not self.count_pairs:
|
|
416
|
+
return list(messages[-self.count :])
|
|
417
|
+
|
|
418
|
+
# Count pairs (each request+response = 1 pair)
|
|
419
|
+
pairs: list[list[ModelMessage]] = []
|
|
420
|
+
current_pair: list[ModelMessage] = []
|
|
421
|
+
|
|
422
|
+
for msg in messages:
|
|
423
|
+
current_pair.append(msg)
|
|
424
|
+
if isinstance(msg, ModelResponse):
|
|
425
|
+
pairs.append(current_pair)
|
|
426
|
+
current_pair = []
|
|
427
|
+
|
|
428
|
+
# Don't forget incomplete pair at the end
|
|
429
|
+
if current_pair:
|
|
430
|
+
pairs.append(current_pair)
|
|
431
|
+
|
|
432
|
+
# Keep last N pairs
|
|
433
|
+
kept_pairs = pairs[-self.count :]
|
|
434
|
+
return [msg for pair in kept_pairs for msg in pair]
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
@dataclass
|
|
438
|
+
class KeepFirstMessages(CompactionStep):
|
|
439
|
+
"""Keep only the first N messages.
|
|
440
|
+
|
|
441
|
+
Useful for keeping initial context/instructions while discarding
|
|
442
|
+
middle conversation.
|
|
443
|
+
"""
|
|
444
|
+
|
|
445
|
+
count: int = 2
|
|
446
|
+
"""Number of messages to keep from the beginning."""
|
|
447
|
+
|
|
448
|
+
async def apply(self, messages: MessageSequence) -> list[ModelMessage]:
|
|
449
|
+
return list(messages[: self.count])
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
@dataclass
|
|
453
|
+
class KeepFirstAndLast(CompactionStep):
|
|
454
|
+
"""Keep first N and last M messages, discarding the middle.
|
|
455
|
+
|
|
456
|
+
Useful for preserving initial context while maintaining recent history.
|
|
457
|
+
"""
|
|
458
|
+
|
|
459
|
+
first_count: int = 2
|
|
460
|
+
"""Number of messages to keep from the beginning."""
|
|
461
|
+
|
|
462
|
+
last_count: int = 5
|
|
463
|
+
"""Number of messages to keep from the end."""
|
|
464
|
+
|
|
465
|
+
async def apply(self, messages: MessageSequence) -> list[ModelMessage]:
|
|
466
|
+
msg_list = list(messages)
|
|
467
|
+
if len(msg_list) <= self.first_count + self.last_count:
|
|
468
|
+
return msg_list
|
|
469
|
+
|
|
470
|
+
first = msg_list[: self.first_count]
|
|
471
|
+
last = msg_list[-self.last_count :]
|
|
472
|
+
return first + last
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
# =============================================================================
|
|
476
|
+
# Advanced Steps - Token-aware and LLM-based
|
|
477
|
+
# =============================================================================
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
@dataclass
|
|
481
|
+
class TokenBudget(CompactionStep):
|
|
482
|
+
"""Keep messages that fit within a token budget.
|
|
483
|
+
|
|
484
|
+
Works backwards from most recent, adding messages until the budget
|
|
485
|
+
is exhausted. Requires tokonomics for token counting.
|
|
486
|
+
"""
|
|
487
|
+
|
|
488
|
+
max_tokens: int = 4000
|
|
489
|
+
"""Maximum number of tokens to allow."""
|
|
490
|
+
|
|
491
|
+
model: str = "gpt-4o"
|
|
492
|
+
"""Model to use for token counting."""
|
|
493
|
+
|
|
494
|
+
async def apply(self, messages: MessageSequence) -> list[ModelMessage]:
|
|
495
|
+
try:
|
|
496
|
+
import tokonomics
|
|
497
|
+
except ImportError:
|
|
498
|
+
# Fall back to character-based estimation
|
|
499
|
+
return await self._apply_char_estimate(messages)
|
|
500
|
+
|
|
501
|
+
result: list[ModelMessage] = []
|
|
502
|
+
total_tokens = 0
|
|
503
|
+
|
|
504
|
+
# Process from most recent to oldest
|
|
505
|
+
for msg in reversed(messages):
|
|
506
|
+
# Estimate tokens for this message
|
|
507
|
+
text = _extract_text_content(msg)
|
|
508
|
+
token_count = tokonomics.count_tokens(text, self.model)
|
|
509
|
+
|
|
510
|
+
if total_tokens + token_count > self.max_tokens:
|
|
511
|
+
break
|
|
512
|
+
|
|
513
|
+
result.insert(0, msg)
|
|
514
|
+
total_tokens += token_count
|
|
515
|
+
|
|
516
|
+
return result
|
|
517
|
+
|
|
518
|
+
async def _apply_char_estimate(self, messages: MessageSequence) -> list[ModelMessage]:
|
|
519
|
+
"""Fallback using character-based token estimation (4 chars ≈ 1 token)."""
|
|
520
|
+
result: list[ModelMessage] = []
|
|
521
|
+
total_chars = 0
|
|
522
|
+
max_chars = self.max_tokens * 4
|
|
523
|
+
|
|
524
|
+
for msg in reversed(messages):
|
|
525
|
+
text = _extract_text_content(msg)
|
|
526
|
+
char_count = len(text)
|
|
527
|
+
|
|
528
|
+
if total_chars + char_count > max_chars:
|
|
529
|
+
break
|
|
530
|
+
|
|
531
|
+
result.insert(0, msg)
|
|
532
|
+
total_chars += char_count
|
|
533
|
+
|
|
534
|
+
return result
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
@dataclass
|
|
538
|
+
class Summarize(CompactionStep):
|
|
539
|
+
"""Summarize older messages using an LLM.
|
|
540
|
+
|
|
541
|
+
When the message count exceeds the threshold, older messages are
|
|
542
|
+
summarized into a single message while recent ones are kept intact.
|
|
543
|
+
"""
|
|
544
|
+
|
|
545
|
+
model: str = "openai:gpt-4o-mini"
|
|
546
|
+
"""Model to use for summarization."""
|
|
547
|
+
|
|
548
|
+
threshold: int = 15
|
|
549
|
+
"""Minimum message count before summarization kicks in."""
|
|
550
|
+
|
|
551
|
+
keep_recent: int = 5
|
|
552
|
+
"""Number of recent messages to keep unsummarized."""
|
|
553
|
+
|
|
554
|
+
summary_prompt: str = (
|
|
555
|
+
"Summarize the following conversation history concisely, "
|
|
556
|
+
"preserving key information, decisions, and context that may be "
|
|
557
|
+
"relevant for continuing the conversation:\n\n{conversation}"
|
|
558
|
+
)
|
|
559
|
+
"""Prompt template for summarization. Use {conversation} placeholder."""
|
|
560
|
+
|
|
561
|
+
_agent: Agent[None, str] | None = field(default=None, repr=False)
|
|
562
|
+
|
|
563
|
+
async def apply(self, messages: MessageSequence) -> list[ModelMessage]:
|
|
564
|
+
if len(messages) <= self.threshold:
|
|
565
|
+
return list(messages)
|
|
566
|
+
|
|
567
|
+
# Split into messages to summarize and messages to keep
|
|
568
|
+
to_summarize = list(messages[: -self.keep_recent])
|
|
569
|
+
to_keep = list(messages[-self.keep_recent :])
|
|
570
|
+
|
|
571
|
+
# Format conversation for summarization
|
|
572
|
+
conversation_text = _format_conversation(to_summarize)
|
|
573
|
+
|
|
574
|
+
# Get or create summarization agent
|
|
575
|
+
agent = await self._get_agent()
|
|
576
|
+
|
|
577
|
+
# Generate summary
|
|
578
|
+
prompt = self.summary_prompt.format(conversation=conversation_text)
|
|
579
|
+
result = await agent.run(prompt)
|
|
580
|
+
|
|
581
|
+
# Create summary message
|
|
582
|
+
summary_request = ModelRequest(
|
|
583
|
+
parts=[UserPromptPart(content=f"[Conversation Summary]\n{result.output}")]
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
return [summary_request, *to_keep]
|
|
587
|
+
|
|
588
|
+
async def _get_agent(self) -> Agent[None, str]:
|
|
589
|
+
"""Get or create the summarization agent."""
|
|
590
|
+
if self._agent is None:
|
|
591
|
+
self._agent = Agent(model=self.model, output_type=str)
|
|
592
|
+
return self._agent
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
# =============================================================================
|
|
596
|
+
# Conditional Steps - Apply steps based on conditions
|
|
597
|
+
# =============================================================================
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
@dataclass
|
|
601
|
+
class ConditionalStep(CompactionStep):
|
|
602
|
+
"""Apply a step only when a condition is met."""
|
|
603
|
+
|
|
604
|
+
step: CompactionStep
|
|
605
|
+
"""The step to conditionally apply."""
|
|
606
|
+
|
|
607
|
+
condition: Callable[[MessageSequence], bool]
|
|
608
|
+
"""Function that returns True if the step should be applied."""
|
|
609
|
+
|
|
610
|
+
async def apply(self, messages: MessageSequence) -> list[ModelMessage]:
|
|
611
|
+
if self.condition(messages):
|
|
612
|
+
return await self.step.apply(messages)
|
|
613
|
+
return list(messages)
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
@dataclass
|
|
617
|
+
class WhenMessageCountExceeds(CompactionStep):
|
|
618
|
+
"""Apply a step only when message count exceeds a threshold."""
|
|
619
|
+
|
|
620
|
+
step: CompactionStep
|
|
621
|
+
"""The step to conditionally apply."""
|
|
622
|
+
|
|
623
|
+
threshold: int = 20
|
|
624
|
+
"""Message count threshold."""
|
|
625
|
+
|
|
626
|
+
async def apply(self, messages: MessageSequence) -> list[ModelMessage]:
|
|
627
|
+
if len(messages) > self.threshold:
|
|
628
|
+
return await self.step.apply(messages)
|
|
629
|
+
return list(messages)
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
# =============================================================================
|
|
633
|
+
# Configuration Models - For YAML/JSON configuration
|
|
634
|
+
# =============================================================================
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
class FilterThinkingConfig(BaseModel):
|
|
638
|
+
"""Configuration for FilterThinking step."""
|
|
639
|
+
|
|
640
|
+
type: Literal["filter_thinking"] = "filter_thinking"
|
|
641
|
+
|
|
642
|
+
def build(self) -> FilterThinking:
|
|
643
|
+
return FilterThinking()
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
class FilterRetryPromptsConfig(BaseModel):
|
|
647
|
+
"""Configuration for FilterRetryPrompts step."""
|
|
648
|
+
|
|
649
|
+
type: Literal["filter_retry_prompts"] = "filter_retry_prompts"
|
|
650
|
+
|
|
651
|
+
def build(self) -> FilterRetryPrompts:
|
|
652
|
+
return FilterRetryPrompts()
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
class FilterBinaryContentConfig(BaseModel):
|
|
656
|
+
"""Configuration for FilterBinaryContent step."""
|
|
657
|
+
|
|
658
|
+
type: Literal["filter_binary"] = "filter_binary"
|
|
659
|
+
keep_references: bool = False
|
|
660
|
+
|
|
661
|
+
def build(self) -> FilterBinaryContent:
|
|
662
|
+
return FilterBinaryContent(keep_references=self.keep_references)
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
class FilterToolCallsConfig(BaseModel):
|
|
666
|
+
"""Configuration for FilterToolCalls step."""
|
|
667
|
+
|
|
668
|
+
type: Literal["filter_tools"] = "filter_tools"
|
|
669
|
+
exclude_tools: list[str] = Field(default_factory=list)
|
|
670
|
+
include_only: list[str] | None = None
|
|
671
|
+
|
|
672
|
+
def build(self) -> FilterToolCalls:
|
|
673
|
+
return FilterToolCalls(exclude_tools=self.exclude_tools, include_only=self.include_only)
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
class FilterEmptyMessagesConfig(BaseModel):
|
|
677
|
+
"""Configuration for FilterEmptyMessages step."""
|
|
678
|
+
|
|
679
|
+
type: Literal["filter_empty"] = "filter_empty"
|
|
680
|
+
|
|
681
|
+
def build(self) -> FilterEmptyMessages:
|
|
682
|
+
return FilterEmptyMessages()
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
class TruncateToolOutputsConfig(BaseModel):
|
|
686
|
+
"""Configuration for TruncateToolOutputs step."""
|
|
687
|
+
|
|
688
|
+
type: Literal["truncate_tool_outputs"] = "truncate_tool_outputs"
|
|
689
|
+
max_length: int = 2000
|
|
690
|
+
suffix: str = "\n... [truncated]"
|
|
691
|
+
|
|
692
|
+
def build(self) -> TruncateToolOutputs:
|
|
693
|
+
return TruncateToolOutputs(max_length=self.max_length, suffix=self.suffix)
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
class TruncateTextPartsConfig(BaseModel):
|
|
697
|
+
"""Configuration for TruncateTextParts step."""
|
|
698
|
+
|
|
699
|
+
type: Literal["truncate_text"] = "truncate_text"
|
|
700
|
+
max_length: int = 5000
|
|
701
|
+
suffix: str = "\n... [truncated]"
|
|
702
|
+
|
|
703
|
+
def build(self) -> TruncateTextParts:
|
|
704
|
+
return TruncateTextParts(max_length=self.max_length, suffix=self.suffix)
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
class KeepLastMessagesConfig(BaseModel):
|
|
708
|
+
"""Configuration for KeepLastMessages step."""
|
|
709
|
+
|
|
710
|
+
type: Literal["keep_last"] = "keep_last"
|
|
711
|
+
count: int = 10
|
|
712
|
+
count_pairs: bool = True
|
|
713
|
+
|
|
714
|
+
def build(self) -> KeepLastMessages:
|
|
715
|
+
return KeepLastMessages(count=self.count, count_pairs=self.count_pairs)
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
class KeepFirstMessagesConfig(BaseModel):
|
|
719
|
+
"""Configuration for KeepFirstMessages step."""
|
|
720
|
+
|
|
721
|
+
type: Literal["keep_first"] = "keep_first"
|
|
722
|
+
count: int = 2
|
|
723
|
+
|
|
724
|
+
def build(self) -> KeepFirstMessages:
|
|
725
|
+
return KeepFirstMessages(count=self.count)
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
class KeepFirstAndLastConfig(BaseModel):
|
|
729
|
+
"""Configuration for KeepFirstAndLast step."""
|
|
730
|
+
|
|
731
|
+
type: Literal["keep_first_last"] = "keep_first_last"
|
|
732
|
+
first_count: int = 2
|
|
733
|
+
last_count: int = 5
|
|
734
|
+
|
|
735
|
+
def build(self) -> KeepFirstAndLast:
|
|
736
|
+
return KeepFirstAndLast(first_count=self.first_count, last_count=self.last_count)
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
class TokenBudgetConfig(BaseModel):
|
|
740
|
+
"""Configuration for TokenBudget step."""
|
|
741
|
+
|
|
742
|
+
type: Literal["token_budget"] = "token_budget"
|
|
743
|
+
max_tokens: int = 4000
|
|
744
|
+
model: str = "gpt-4o"
|
|
745
|
+
|
|
746
|
+
def build(self) -> TokenBudget:
|
|
747
|
+
return TokenBudget(max_tokens=self.max_tokens, model=self.model)
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
class SummarizeConfig(BaseModel):
|
|
751
|
+
"""Configuration for Summarize step."""
|
|
752
|
+
|
|
753
|
+
type: Literal["summarize"] = "summarize"
|
|
754
|
+
model: str = "openai:gpt-4o-mini"
|
|
755
|
+
threshold: int = 15
|
|
756
|
+
keep_recent: int = 5
|
|
757
|
+
summary_prompt: str | None = None
|
|
758
|
+
|
|
759
|
+
def build(self) -> Summarize:
|
|
760
|
+
kwargs: dict[str, Any] = {
|
|
761
|
+
"model": self.model,
|
|
762
|
+
"threshold": self.threshold,
|
|
763
|
+
"keep_recent": self.keep_recent,
|
|
764
|
+
}
|
|
765
|
+
if self.summary_prompt:
|
|
766
|
+
kwargs["summary_prompt"] = self.summary_prompt
|
|
767
|
+
return Summarize(**kwargs)
|
|
768
|
+
|
|
769
|
+
|
|
770
|
+
class WhenMessageCountExceedsConfig(BaseModel):
|
|
771
|
+
"""Configuration for WhenMessageCountExceeds wrapper."""
|
|
772
|
+
|
|
773
|
+
type: Literal["when_count_exceeds"] = "when_count_exceeds"
|
|
774
|
+
threshold: int = 20
|
|
775
|
+
step: "CompactionStepConfig"
|
|
776
|
+
|
|
777
|
+
def build(self) -> WhenMessageCountExceeds:
|
|
778
|
+
return WhenMessageCountExceeds(step=self.step.build(), threshold=self.threshold)
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
# Union of all config types with discriminator
|
|
782
|
+
CompactionStepConfig = Annotated[
|
|
783
|
+
FilterThinkingConfig
|
|
784
|
+
| FilterRetryPromptsConfig
|
|
785
|
+
| FilterBinaryContentConfig
|
|
786
|
+
| FilterToolCallsConfig
|
|
787
|
+
| FilterEmptyMessagesConfig
|
|
788
|
+
| TruncateToolOutputsConfig
|
|
789
|
+
| TruncateTextPartsConfig
|
|
790
|
+
| KeepLastMessagesConfig
|
|
791
|
+
| KeepFirstMessagesConfig
|
|
792
|
+
| KeepFirstAndLastConfig
|
|
793
|
+
| TokenBudgetConfig
|
|
794
|
+
| SummarizeConfig
|
|
795
|
+
| WhenMessageCountExceedsConfig,
|
|
796
|
+
Field(discriminator="type"),
|
|
797
|
+
]
|
|
798
|
+
|
|
799
|
+
# Update forward reference
|
|
800
|
+
WhenMessageCountExceedsConfig.model_rebuild()
|
|
801
|
+
|
|
802
|
+
|
|
803
|
+
class CompactionPipelineConfig(BaseModel):
|
|
804
|
+
"""Configuration for a complete compaction pipeline.
|
|
805
|
+
|
|
806
|
+
Example YAML:
|
|
807
|
+
```yaml
|
|
808
|
+
compaction:
|
|
809
|
+
steps:
|
|
810
|
+
- type: filter_thinking
|
|
811
|
+
- type: truncate_tool_outputs
|
|
812
|
+
max_length: 1000
|
|
813
|
+
- type: keep_last
|
|
814
|
+
count: 10
|
|
815
|
+
```
|
|
816
|
+
"""
|
|
817
|
+
|
|
818
|
+
steps: list[CompactionStepConfig] = Field(default_factory=list)
|
|
819
|
+
"""Ordered list of compaction steps to apply."""
|
|
820
|
+
|
|
821
|
+
def build(self) -> CompactionPipeline:
|
|
822
|
+
"""Build a CompactionPipeline from this configuration."""
|
|
823
|
+
return CompactionPipeline(steps=[step.build() for step in self.steps])
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
# =============================================================================
|
|
827
|
+
# Preset Pipelines - Common configurations
|
|
828
|
+
# =============================================================================
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
def minimal_context() -> CompactionPipeline:
|
|
832
|
+
"""Create a pipeline that aggressively minimizes context.
|
|
833
|
+
|
|
834
|
+
Removes thinking, truncates outputs, and keeps only recent messages.
|
|
835
|
+
"""
|
|
836
|
+
return CompactionPipeline(
|
|
837
|
+
steps=[
|
|
838
|
+
FilterThinking(),
|
|
839
|
+
FilterRetryPrompts(),
|
|
840
|
+
TruncateToolOutputs(max_length=500),
|
|
841
|
+
KeepLastMessages(count=5),
|
|
842
|
+
]
|
|
843
|
+
)
|
|
844
|
+
|
|
845
|
+
|
|
846
|
+
def balanced_context() -> CompactionPipeline:
|
|
847
|
+
"""Create a balanced pipeline for general use.
|
|
848
|
+
|
|
849
|
+
Removes thinking, moderately truncates, keeps reasonable history.
|
|
850
|
+
"""
|
|
851
|
+
return CompactionPipeline(
|
|
852
|
+
steps=[
|
|
853
|
+
FilterThinking(),
|
|
854
|
+
TruncateToolOutputs(max_length=2000),
|
|
855
|
+
TruncateTextParts(max_length=5000),
|
|
856
|
+
KeepLastMessages(count=15),
|
|
857
|
+
]
|
|
858
|
+
)
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
def summarizing_context(model: ModelId | str = "openai:gpt-4o-mini") -> CompactionPipeline:
|
|
862
|
+
"""Create a pipeline that summarizes older messages.
|
|
863
|
+
|
|
864
|
+
Best for long conversations where context needs to be preserved.
|
|
865
|
+
"""
|
|
866
|
+
return CompactionPipeline(
|
|
867
|
+
steps=[
|
|
868
|
+
FilterThinking(),
|
|
869
|
+
TruncateToolOutputs(max_length=1000),
|
|
870
|
+
Summarize(model=model, threshold=20, keep_recent=8),
|
|
871
|
+
]
|
|
872
|
+
)
|
|
873
|
+
|
|
874
|
+
|
|
875
|
+
# =============================================================================
|
|
876
|
+
# Helper Functions
|
|
877
|
+
# =============================================================================
|
|
878
|
+
|
|
879
|
+
|
|
880
|
+
def _extract_text_content(msg: ModelMessage) -> str:
|
|
881
|
+
"""Extract text content from a message for token counting."""
|
|
882
|
+
parts_text: list[str] = []
|
|
883
|
+
|
|
884
|
+
match msg:
|
|
885
|
+
case ModelRequest(parts=parts) | ModelResponse(parts=parts):
|
|
886
|
+
for part in parts:
|
|
887
|
+
match part:
|
|
888
|
+
case TextPart(content=content):
|
|
889
|
+
parts_text.append(content)
|
|
890
|
+
case ThinkingPart(content=content):
|
|
891
|
+
parts_text.append(content)
|
|
892
|
+
case UserPromptPart(content=content):
|
|
893
|
+
if isinstance(content, str):
|
|
894
|
+
parts_text.append(content)
|
|
895
|
+
elif isinstance(content, list):
|
|
896
|
+
for item in content:
|
|
897
|
+
if isinstance(item, str):
|
|
898
|
+
parts_text.append(item) # noqa: PERF401
|
|
899
|
+
case ToolReturnPart(content=content):
|
|
900
|
+
if isinstance(content, str):
|
|
901
|
+
parts_text.append(content)
|
|
902
|
+
elif content is not None:
|
|
903
|
+
parts_text.append(str(content))
|
|
904
|
+
|
|
905
|
+
return "\n".join(parts_text)
|
|
906
|
+
|
|
907
|
+
|
|
908
|
+
def _format_conversation(messages: Sequence[ModelMessage]) -> str:
|
|
909
|
+
"""Format messages as a readable conversation for summarization."""
|
|
910
|
+
lines: list[str] = []
|
|
911
|
+
|
|
912
|
+
for msg in messages:
|
|
913
|
+
match msg:
|
|
914
|
+
case ModelRequest(parts=parts):
|
|
915
|
+
for request_part in parts:
|
|
916
|
+
match request_part:
|
|
917
|
+
case UserPromptPart(content=content):
|
|
918
|
+
text = content if isinstance(content, str) else str(content)
|
|
919
|
+
lines.append(f"User: {text}")
|
|
920
|
+
case ToolReturnPart(tool_name=name, content=content):
|
|
921
|
+
lines.append(f"Tool Result ({name}): {content}")
|
|
922
|
+
case ModelResponse(parts=parts):
|
|
923
|
+
for response_part in parts:
|
|
924
|
+
match response_part:
|
|
925
|
+
case TextPart(content=content):
|
|
926
|
+
lines.append(f"Assistant: {content}")
|
|
927
|
+
|
|
928
|
+
return "\n\n".join(lines)
|