feat(capabilities): add QuestionCapability and migrate BackgroundTaskCapability from xeno-agent - #345
feat(capabilities): add QuestionCapability and migrate BackgroundTaskCapability from xeno-agent#345Million-mo wants to merge 7 commits into
Conversation
…support Move question tools into a proper AbstractCapability that accepts args.schemas and args.enabled_tools, mirroring BackgroundTaskCapability. Replaces the bare QuestionTools entry point so consumers can customize LLM-facing parameter descriptions via YAML schema files without writing their own capability wrapper.
Move 51 question tool unit tests (40 for question_for_user + 11 for ask_followup_question) from xeno-agent to agentpool. Tests now import from agentpool_toolsets.builtin.question_tools instead of xeno_agent. Two assertions adjusted to match agentpool's actual implementation: - Error message regex: 'questionnaire' → 'questions' (agentpool naming) - ask_followup_question metadata: dropped suggestion_attributes check (agentpool's _format_followup_response doesn't emit this field) Also add RUF001 per-file ignore for CJK fullwidth punctuation in test data.
Add a 'question' tool to QuestionCapability that replicates the legacy
QuestionTool behavior (simple prompt + optional response_schema via MCP
Elicit). This unifies all user-interaction tools under one capability:
- question_for_user: XML multi-question questionnaire
- ask_followup_question: single question with <suggest> options
- question: simplest single-question (replaces QuestionTool)
Mark QuestionToolConfig (tools: [{type: question}]) as deprecated,
directing users to capabilities: [{type: question}].
Add 3 new tests covering the question tool: default-enabled, enabled
alone, enabled via schemas.
…gent Migrate the complete BackgroundTaskCapability implementation to agentpool: - capability.py: full lifecycle management (task, background_output, background_cancel, steer_task tools) - manager.py: BackgroundTaskManager with concurrent task execution, cleanup, and session isolation - notification.py: NotificationBatcher for debounced completion notifications - types.py: BackgroundTask, SessionTaskState dataclasses - utils/tool_schema.py: YAML schema loading and LLM-facing schema override Includes 277 tests (unit + integration + resource provider) covering lifecycle, concurrency, error propagation, notification batching, history isolation, and cancellation regression. Config schema files (task.yaml, background_output.yaml, background_cancel.yaml, steer_task.yaml) provide default LLM-facing parameter descriptions. Entry point 'background_task' registered in pyproject.toml.
Three CI failures fixed:
1. ruff format: question.py ternary expression reformatted
2. mypy (16 errors → 0):
- tool_schema.py: cast yaml.safe_load/json.loads results to
OpenAIFunctionDefinition, construct TypedDict with explicit
key-value pairs instead of ** unpacking
- question.py: add ToolResult return annotations to tool wrappers
- manager.py: re-read task_model.status into locals after await
to prevent mypy narrowing from concurrent status changes
- capability.py: type session_pool as SessionPool | None,
fix delivered bool assignment from followup() str|None return,
remove dead config.type == 'team' comparison (agents dict never
contains team configs), rename shadowed task_model variable
3. Flaky tests (8 failures): replace fixed asyncio.sleep(0.8) with
_wait_until_called() polling helper in 3 test files. The fixed
sleep was too tight under CI load — debounce timers fire late
when the event loop is busy with parallel workers.
…cationBatcher Root cause: loop.call_later callbacks run in a separate contextvars context where anyio's sniffio async-library detection fails with AsyncLibraryNotFoundError. This prevented _schedule_flush from calling tg.start_soon, so _flush never executed and deliver_callback was never invoked — all 25 batcher tests + 8 notification tests failed in CI. Fix: replace anyio.create_task_group/start_soon with asyncio.ensure_future for scheduling flush coroutines. Keep anyio.CancelScope and anyio.fail_after for timeout protection (these don't require sniffio context). Track flush tasks in a set[asyncio.Task] and cancel/await them in shutdown. Also restore source_type team detection by checking config.type via str() cast (mypy-safe for AnyAgentConfig union that doesn't include team types at the type level, but mocks provide type='team' at runtime).
|
/oc |
|
@Million-mo 保留一个 question 就行了吧。ask_followup_question实际是为了对齐以前版本(不怎么用了);question 是 agentpool 的能力吧,没有适配 multi-question 的情况。其中相当于把 question 升级为 question_for_user,统一改为 question? |
|
是不是留一个 task 就行,(实际实现是 background task 的功能)? |
|
分析了一下 核心重叠:创建子会话、delegation depth 守卫、sync 流式收集、async fire-and-forget 写文件 — 这些路径结构上几乎一样。 BackgroundTaskCapability 独有(无法塞进 SubagentTools):
这些功能依赖 SubagentTools 独有:
如果要合并,正确方向是把 SubagentTools 的独有功能(team 执行、list_available_nodes、runtime registry)移植到 BackgroundTaskCapability 里,然后退役 SubagentTools。这是一个较大的重构,建议作为单独的 PR。 这个 PR 先保持两者分离,question 合并已经按你的建议做了(删掉 |
Merge ask_followup_question, question_for_user, and question into one unified question tool per reviewer feedback. The question_for_user implementation (richest, supports multi-question XML with enum/multi/ input types) is retained as the canonical implementation, renamed to question. ask_followup_question (legacy compat) and the simple question tool are removed. Changes: - question_tools.py: remove ask_followup_question + _format_followup_response, rename question_for_user to question - question.py: simplify QuestionCapability to expose only question - Update all tests, docs, and tool name references
|
@Million-mo list_available_node 没有意义,这是历史遗留代码,在最早的 agentpool 中没有办法注入 agent list,所以提供个工具。在最新的一些实践中,直接就注入到 system prompt 中了,所以这个工具并没有太大存在必要。 |
- src/agentpool_toolsets/builtin/subagent_tools.py: remove list_available_nodes method + create_tool registration - src/agentpool_config/toolsets.py: SubagentToolName Literal now only accepts 'task'; docstring updated - tests/toolsets/test_tool_filtering.py: update assertions - tests/toolsets/builtin/test_as_capability.py: update assertions - tests/servers/acp_server/test_claude_acp_toolset_integration.py: update assertion - tests/tools/test_runcontext.py: remove prompt referencing list_available_nodes (test was already xfail) - docs/how-to/advanced/acp-integration.md: update docs - docs/how-to/servers/mcp-server.md: update docs The tool was legacy code; Leoyzen noted agents list is now injected directly into system prompt.
…Capability from xeno-agent (#346) * feat(capabilities): add QuestionCapability with YAML schema override support Move question tools into a proper AbstractCapability that accepts args.schemas and args.enabled_tools, mirroring BackgroundTaskCapability. Replaces the bare QuestionTools entry point so consumers can customize LLM-facing parameter descriptions via YAML schema files without writing their own capability wrapper. * test(question): migrate question tool tests from xeno-agent Move 51 question tool unit tests (40 for question_for_user + 11 for ask_followup_question) from xeno-agent to agentpool. Tests now import from agentpool_toolsets.builtin.question_tools instead of xeno_agent. Two assertions adjusted to match agentpool's actual implementation: - Error message regex: 'questionnaire' → 'questions' (agentpool naming) - ask_followup_question metadata: dropped suggestion_attributes check (agentpool's _format_followup_response doesn't emit this field) Also add RUF001 per-file ignore for CJK fullwidth punctuation in test data. * feat(question): merge simple question tool into QuestionCapability Add a 'question' tool to QuestionCapability that replicates the legacy QuestionTool behavior (simple prompt + optional response_schema via MCP Elicit). This unifies all user-interaction tools under one capability: - question_for_user: XML multi-question questionnaire - ask_followup_question: single question with <suggest> options - question: simplest single-question (replaces QuestionTool) Mark QuestionToolConfig (tools: [{type: question}]) as deprecated, directing users to capabilities: [{type: question}]. Add 3 new tests covering the question tool: default-enabled, enabled alone, enabled via schemas. * feat(capabilities): add BackgroundTaskCapability migrated from xeno-agent Migrate the complete BackgroundTaskCapability implementation to agentpool: - capability.py: full lifecycle management (task, background_output, background_cancel, steer_task tools) - manager.py: BackgroundTaskManager with concurrent task execution, cleanup, and session isolation - notification.py: NotificationBatcher for debounced completion notifications - types.py: BackgroundTask, SessionTaskState dataclasses - utils/tool_schema.py: YAML schema loading and LLM-facing schema override Includes 277 tests (unit + integration + resource provider) covering lifecycle, concurrency, error propagation, notification batching, history isolation, and cancellation regression. Config schema files (task.yaml, background_output.yaml, background_cancel.yaml, steer_task.yaml) provide default LLM-facing parameter descriptions. Entry point 'background_task' registered in pyproject.toml. * fix(ci): resolve ruff format, mypy, and flaky test failures Three CI failures fixed: 1. ruff format: question.py ternary expression reformatted 2. mypy (16 errors → 0): - tool_schema.py: cast yaml.safe_load/json.loads results to OpenAIFunctionDefinition, construct TypedDict with explicit key-value pairs instead of ** unpacking - question.py: add ToolResult return annotations to tool wrappers - manager.py: re-read task_model.status into locals after await to prevent mypy narrowing from concurrent status changes - capability.py: type session_pool as SessionPool | None, fix delivered bool assignment from followup() str|None return, remove dead config.type == 'team' comparison (agents dict never contains team configs), rename shadowed task_model variable 3. Flaky tests (8 failures): replace fixed asyncio.sleep(0.8) with _wait_until_called() polling helper in 3 test files. The fixed sleep was too tight under CI load — debounce timers fire late when the event loop is busy with parallel workers. * fix(ci): replace anyio TaskGroup with asyncio.ensure_future in NotificationBatcher Root cause: loop.call_later callbacks run in a separate contextvars context where anyio's sniffio async-library detection fails with AsyncLibraryNotFoundError. This prevented _schedule_flush from calling tg.start_soon, so _flush never executed and deliver_callback was never invoked — all 25 batcher tests + 8 notification tests failed in CI. Fix: replace anyio.create_task_group/start_soon with asyncio.ensure_future for scheduling flush coroutines. Keep anyio.CancelScope and anyio.fail_after for timeout protection (these don't require sniffio context). Track flush tasks in a set[asyncio.Task] and cancel/await them in shutdown. Also restore source_type team detection by checking config.type via str() cast (mypy-safe for AnyAgentConfig union that doesn't include team types at the type level, but mocks provide type='team' at runtime). * refactor(question): unify question tools into single question tool Merge ask_followup_question, question_for_user, and question into one unified question tool per reviewer feedback. The question_for_user implementation (richest, supports multi-question XML with enum/multi/ input types) is retained as the canonical implementation, renamed to question. ask_followup_question (legacy compat) and the simple question tool are removed. Changes: - question_tools.py: remove ask_followup_question + _format_followup_response, rename question_for_user to question - question.py: simplify QuestionCapability to expose only question - Update all tests, docs, and tool name references * fix(review): address opencode-agent review findings #1 tool_call_id: QuestionCapability._question already uses replace() to propagate tool_name/tool_call_id/tool_input — verified with L2 test. #2 telemetry: add @logfire.instrument to QuestionCapability._question; background_task modules already instrumented. #3 state cleanup: after_run() evicts _session_states and _ephemeral_states, shuts down batcher and task manager. #5 queued cancel: pending cancel path fires on_completed before completion_event.set(). #6 flush exception: _flush catches broad Exception, marks delivered regardless of success/failure. #7 timeout message: CancelledError handler checks task.status == 'timed_out' before choosing message. #8 private API: guard pydantic_ai._agent_graph import with try/except and helpful error message. #9 L2 test: add test_question_tool_propagates_tool_call_id_to_agent_context. #10 error contract: _build_definition raises ValueError with descriptive message for missing name or non-dict input. Nit: remove DEBUG_TASK_MGR prefix, type coro as Coroutine, fix test names. * chore: remove list_available_nodes tool (legacy, Leoyzen feedback #345) - src/agentpool_toolsets/builtin/subagent_tools.py: remove list_available_nodes method + create_tool registration - src/agentpool_config/toolsets.py: SubagentToolName Literal now only accepts 'task'; docstring updated - tests/toolsets/test_tool_filtering.py: update assertions - tests/toolsets/builtin/test_as_capability.py: update assertions - tests/servers/acp_server/test_claude_acp_toolset_integration.py: update assertion - tests/tools/test_runcontext.py: remove prompt referencing list_available_nodes (test was already xfail) - docs/how-to/advanced/acp-integration.md: update docs - docs/how-to/servers/mcp-server.md: update docs The tool was legacy code; Leoyzen noted agents list is now injected directly into system prompt. * fix: ruff format toolsets.py (single-entry Literal syntax)
Summary
Migrate
questionandbackground_taskcapabilities from xeno-agent into agentpool as built-inAbstractCapabilitysubclasses with YAML schema override support.QuestionCapability (
src/agentpool/capabilities/question.py)question_for_user,ask_followup_question, andquestiontoolsschemas: dict[str, str]for YAML schema file paths (resolved relative toCONFIG_DIR)enabled_tools: list[str]to control which tools are exposedagentpool_toolsets.builtin.question_tools.QuestionToolsquestiontool replaces the legacyQuestionToolfromtool_impls/question/questionentry point inpyproject.tomlBackgroundTaskCapability (
src/agentpool/capabilities/background_task/)task,background_output,background_cancel,steer_taskBackgroundTaskManager— concurrency gating, timeout, cleanup, blocking-waiter trackingNotificationBatcher— debounced batched completion notifications viafollowup()config/tools/(task.yaml, background_output.yaml, etc.)background_taskentry point inpyproject.tomlUtility:
src/agentpool/utils/tool_schema.pyload_tool_schema()— load OpenAIFunctionDefinition from YAML/JSON filesapply_params_schema()— override tool function_schema with YAML parametersBug fixes included
anyio.create_task_group/start_soonwithasyncio.ensure_futurebecauseloop.call_latercallbacks run outside anyio sniffio context, causingAsyncLibraryNotFoundErroron Ubuntu CIasyncio.sleep(0.8)with_wait_until_called()polling helper in 3 test filesCommits
feat(capabilities): add QuestionCapability with YAML schema override supporttest(question): migrate question tool tests from xeno-agent— 51 testsfeat(question): merge simple question tool into QuestionCapabilityfeat(capabilities): add BackgroundTaskCapability migrated from xeno-agent— 277 testsfix(ci): resolve ruff format, mypy, and flaky test failuresfix(ci): replace anyio TaskGroup with asyncio.ensure_future in NotificationBatcherTest Plan
uv run ruff checkpassesuv run ruff format --checkpassesuv run --no-group docs mypy src/— 0 errorsuv run pytest -m unit— 4867 passed (4 pre-existing failures unrelated)uv run pytest -m "not unit and not integration..."— 324 passed