Skip to content

feat(capabilities): add QuestionCapability and migrate BackgroundTaskCapability from xeno-agent - #345

Closed
Million-mo wants to merge 7 commits into
wolf1069b:mainfrom
Million-mo:feat/question-capability
Closed

feat(capabilities): add QuestionCapability and migrate BackgroundTaskCapability from xeno-agent#345
Million-mo wants to merge 7 commits into
wolf1069b:mainfrom
Million-mo:feat/question-capability

Conversation

@Million-mo

@Million-mo Million-mo commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

Summary

Migrate question and background_task capabilities from xeno-agent into agentpool as built-in AbstractCapability subclasses with YAML schema override support.

QuestionCapability (src/agentpool/capabilities/question.py)

  • Provides question_for_user, ask_followup_question, and question tools
  • Accepts schemas: dict[str, str] for YAML schema file paths (resolved relative to CONFIG_DIR)
  • Accepts enabled_tools: list[str] to control which tools are exposed
  • Tool implementations delegate to agentpool_toolsets.builtin.question_tools.QuestionTools
  • question tool replaces the legacy QuestionTool from tool_impls/question/
  • Declared via the question entry point in pyproject.toml

BackgroundTaskCapability (src/agentpool/capabilities/background_task/)

  • Migrated from xeno-agent (32 files, 277 tests)
  • Four tools: task, background_output, background_cancel, steer_task
  • BackgroundTaskManager — concurrency gating, timeout, cleanup, blocking-waiter tracking
  • NotificationBatcher — debounced batched completion notifications via followup()
  • YAML schema overrides via config/tools/ (task.yaml, background_output.yaml, etc.)
  • Declared via the background_task entry point in pyproject.toml

Utility: src/agentpool/utils/tool_schema.py

  • load_tool_schema() — load OpenAIFunctionDefinition from YAML/JSON files
  • apply_params_schema() — override tool function_schema with YAML parameters

Bug fixes included

  • NotificationBatcher: replaced anyio.create_task_group / start_soon with asyncio.ensure_future because loop.call_later callbacks run outside anyio sniffio context, causing AsyncLibraryNotFoundError on Ubuntu CI
  • mypy: 16 type errors fixed across 4 files (cast, return annotations, non-overlapping comparisons, dead code removal)
  • Flaky tests: replaced fixed asyncio.sleep(0.8) with _wait_until_called() polling helper in 3 test files

Commits

  1. feat(capabilities): add QuestionCapability with YAML schema override support
  2. test(question): migrate question tool tests from xeno-agent — 51 tests
  3. feat(question): merge simple question tool into QuestionCapability
  4. feat(capabilities): add BackgroundTaskCapability migrated from xeno-agent — 277 tests
  5. fix(ci): resolve ruff format, mypy, and flaky test failures
  6. fix(ci): replace anyio TaskGroup with asyncio.ensure_future in NotificationBatcher

Test Plan

  • uv run ruff check passes
  • uv run ruff format --check passes
  • uv run --no-group docs mypy src/ — 0 errors
  • uv run pytest -m unit — 4867 passed (4 pre-existing failures unrelated)
  • uv run pytest -m "not unit and not integration..." — 324 passed
  • All 277 background_task tests pass
  • All 64 question tests pass
  • CI: all 12 checks pass (Format, Lint, Import Linter, Smoke, Mypy, Unit, Integration, Core, VCR, Cassette hygiene, E2E smoke, CI Report)

…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).
@Million-mo Million-mo changed the title feat(capabilities): add QuestionCapability with YAML schema override support feat(capabilities): add QuestionCapability and migrate BackgroundTaskCapability from xeno-agent Aug 3, 2026
@Million-mo
Million-mo requested a review from Leoyzen August 3, 2026 09:23
@Million-mo

Copy link
Copy Markdown
Collaborator Author

/oc

@Leoyzen

Leoyzen commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

@Million-mo 保留一个 question 就行了吧。ask_followup_question实际是为了对齐以前版本(不怎么用了);question 是 agentpool 的能力吧,没有适配 multi-question 的情况。其中相当于把 question 升级为 question_for_user,统一改为 question?

@Leoyzen

Leoyzen commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

是不是留一个 task 就行,(实际实现是 background task 的功能)?

@Million-mo

Copy link
Copy Markdown
Collaborator Author

分析了一下 SubagentTools.taskBackgroundTaskCapability._task 的差异,结论是不能简单合并成一个 task,原因是两者的基类不同导致能力边界完全不一样。

核心重叠:创建子会话、delegation depth 守卫、sync 流式收集、async fire-and-forget 写文件 — 这些路径结构上几乎一样。

BackgroundTaskCapability 独有(无法塞进 SubagentTools)

  • 三个独立工具:background_output(阻塞等待)、background_cancelsteer_task(运行中转向)
  • NotificationBatcher — 任务完成后去重批量通知父 agent
  • after_node_run + force_retrieval — agent 结束前强制检索未完成任务结果
  • get_model_settings — 动态 tool_choice 强制调用 background_output
  • load_skills / expected_output / title 参数
  • SessionTaskState — 并发信号量、超时、清理的完整生命周期管理

这些功能依赖 AbstractCapabilitybefore_run/after_node_run/get_model_settings/get_ordering 生命周期钩子,而 SubagentTools 继承的 FunctionToolsetCapability 没有这些钩子。反方向也不行 — 把生命周期机制硬塞进 toolset 会破坏 pydantic-ai 的 capability 架构。

SubagentTools 独有

  • list_available_nodes 发现工具
  • 真正的 team 支持(parallel/sequential),BackgroundTaskCapability 的 team 处理实际是不完整的
  • runtime registry 注册、model_id 传播

如果要合并,正确方向是把 SubagentTools 的独有功能(team 执行、list_available_nodes、runtime registry)移植到 BackgroundTaskCapability 里,然后退役 SubagentTools。这是一个较大的重构,建议作为单独的 PR。

这个 PR 先保持两者分离,question 合并已经按你的建议做了(删掉 ask_followup_question,统一为一个 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 Million-mo closed this Aug 3, 2026
@Leoyzen

Leoyzen commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

@Million-mo list_available_node 没有意义,这是历史遗留代码,在最早的 agentpool 中没有办法注入 agent list,所以提供个工具。在最新的一些实践中,直接就注入到 system prompt 中了,所以这个工具并没有太大存在必要。

Million-mo added a commit that referenced this pull request Aug 4, 2026
- 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.
Million-mo added a commit that referenced this pull request Aug 4, 2026
…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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants