Skip to content

fix: 修复 MCP 服务器注册工具时的 Pydantic TypeAdapter 错误 - #39

Open
wensm77 wants to merge 837 commits into
phil65:mainfrom
wensm77:fix/mcp-server-pydantic-typeadapter-error
Open

fix: 修复 MCP 服务器注册工具时的 Pydantic TypeAdapter 错误#39
wensm77 wants to merge 837 commits into
phil65:mainfrom
wensm77:fix/mcp-server-pydantic-typeadapter-error

Conversation

@wensm77

@wensm77 wensm77 commented Jul 26, 2026

Copy link
Copy Markdown

问题描述

使用 agentpool serve-mcp 启动 MCP 服务器时失败,报错:

PydanticUserError: `TypeAdapter[<function SubagentCapability.spawn_subagent>]` is not fully defined; 
you should define `<function SubagentCapability.spawn_subagent>` and all referenced types, 
then call `.rebuild()` on the instance.

问题原因:SubagentCapability.spawn_subagentget_available_agents 使用了未绑定的泛型类型参数(RunContext[AgentDepsT]),FastMCP 的 TypeAdapter 在注册工具时无法处理这种类型。

解决方案

用具体的包装函数替代直接注册泛型静态方法:

  • spawn_subagent_wrapper(name: str, prompt: str) -> str 调用 self.pool.run_agent()
  • get_available_agents_wrapper() -> list[str] 返回 sorted(self.pool.agent_configs.keys())

这些包装函数具有具体的类型签名,Pydantic 可以正常处理。

测试验证

已验证:

  • MCP 服务器成功启动
  • Claude Code 通过 stdio 传输成功连接
  • 两个工具均正常工作(列出 agents、委派任务给子 agent)

变更内容

修改了 src/agentpool_server/mcp_server/server.py,用包装函数替代了直接的方法注册。

此修复使 agentpool 能够与 Claude Code 的 MCP 集成正常工作。

Leoyzen and others added 30 commits July 10, 2026 11:52
- test_scratchpad_skill_reference_redflag.py: Use mock SkillProvider instead of MCPCapability
- fsspec_toolset/toolset.py: Fix lazy tool creation — eagerly setup tools in __init__
- test_openai_api_server.py: Update assertion for TestModel response format
- uri_resolver.py: Add isinstance(provider, SkillProvider) guard before get_skills() calls
- test_workers.py: agent.tools.get_tools() → agent._get_all_tools()
- test_sys_prompts.py: agent.tools.register_tool() → agent._builtin_provider.register_tool()
- test_rfc0011_lineage.py: agent.tools.add_provider() → agent._add_capability()
- test_skill_autocomplete.py: Update for skill_resolver API
- sys_prompts.py: Pre-compute tools list for Jinja template
- system_prompt.jinja: Remove agent.tools.get_tools() call
… storage

Address review comment: MemoryFileSystem was local and scripts were lost.
Now checks if ctx.deps is AgentContext and uses host.internal_fs if available,
falling back to MemoryFileSystem otherwise.
- code_mode_capability.py: Revert to MemoryFileSystem — HostContext
  doesn't have internal_fs attribute. Script history is best-effort.
- workers.py: Cache tools in get_tools() to prevent duplicate
  registration on multiple calls (caused 'Tool name conflicts' error)
…d, update specs

- Delete src/agentpool/tools/factory.py (194 LOC, 6 classes, zero imports)
- Add 'source' field to Resource dataclass per spec requirement
- Update design.md with Implementation Notes section (8 notes)
- Update tasks.md with tools/factory.py deletion observation
- Update AGENTS.md: replace Resource Providers section with Capabilities,
  update Key Files, document ToolManager deletion and agent_pool status
* docs: add agent_pool backdoor cleanup design spec

Investigates the agent_pool backdoor problem and proposes a 5-phase
cleanup plan to achieve a clean pre-M4 baseline. Key architectural
changes: extract SkillService Protocol, extend HostContext, remove
pool back-reference, migrate ~64 call sites across 15 files.

Aligns with RFC Leoyzen#135 six-layer architecture.

* fix(openspec): correct M1/M2 task completion status

M1: T3.3 (recompile) deferred to M4, T3.5 partial (recompile tests
skipped), T6.1/T6.4-T6.6 verification deferred — all were marked [x]
but never completed.

M2: T11.2 partial (only Phase 1 core agents migrated, ~64 refs remain),
T11.3 not done (protocol servers never migrated), T11.4 not done
(DeprecationWarning still fires on unmigrated code), T12.9 partial
(warnings real, not residual) — all were marked [x].

openspec list now shows M1: 21/26, M2: 58/60 (was both ✓ Complete).

* feat(openspec): add m3-5-backdoor-cleanup change

Interim milestone between M3 and M4 to complete the agent_pool backdoor
cleanup. Covers:

- SkillService Protocol extraction (read-only skill orchestration)
- HostContext extended with skill_service + main_agent_name, pool removed
- ~64 .agent_pool refs migrated across 15 files (ACP, OpenCode, core)
- AgentFactory uses self._pool instead of host_context.pool
- Talk wiring via _bind_pool() instead of ctx.pool
- M1 T6.1/T6.4/T6.5/T6.6 deferred verification covered
- M2 T11.4/T12.9 DeprecationWarning clean check covered

7 task groups, 31 tasks (25 required + 6 optional property removal).
4 specs: skill-service (new), host-context/agent-pool/agent-factory (modified).

* refactor(openspec): remove SkillService from cleanup, defer to separate change

SkillService Protocol extraction moved to independent design change.
Cleanup spec now covers pure mechanical migration:

- ~64 .agent_pool refs → host_context (skill refs use host_context.pool.X
  as interim pattern, will become host_context.skill_service.X later)
- HostContext gains main_agent_name, pool field kept as temporary escape
- No new abstractions introduced
- 7 task groups, 30 tasks (24 required + 6 optional)

Skill-service design will be a separate openspec change covering:
SkillService Protocol, HostContext.skill_service field, pool field
removal, and skill ref migration from pool.X to skill_service.X.

* refactor(m3.5): migrate all agent_pool property reads to host_context

Complete the M3.5 backdoor cleanup — migrate all ~64 .agent_pool
property accesses across 15 source files to use .host_context instead.

Key changes:
- ACPProtocolHandler constructor: agent_pool param → host_context param
- ACPSession.agent_pool property → ACPSession.host_context property
- AgentPoolACPAgent.agent_pool property → AgentPoolACPAgent.host_context property
- _inject_pool_providers: added pool as explicit 3rd parameter
- MessageNode._bind_pool(): new internal method (no DeprecationWarning)
- HostContext.main_agent_name: new field
- AgentPool._safe_main_agent_name(): new helper method
- Talk wiring uses _bind_pool() instead of ctx.pool
- AgentFactory uses self._pool instead of host_context.pool

Skill-related refs (~6) migrate to host_context.pool.X as interim
pattern — will be replaced by host_context.skill_service.X in a
separate skill-service-extraction change.

Verification:
- 4836 tests passed, 0 failures
- ruff check: all checks passed
- 0 .agent_pool property reads in src/ (scope fidelity)
- No agentpool DeprecationWarnings triggered

Closes M2 T11.2/T11.3/T11.4/T12.9 (agent_pool migration).
T7.x (full property removal) deferred to M4.

* fix: address PR review comments and CI failures

Mypy fixes (12 → 0 errors):
- state.py: resolve _pool via _ctx.pool instead of assigning HostContext to
  AgentPool-typed field
- agent_routes.py: use ctx.pool.skill_resolver (escape hatch) and
  ctx.skills_registry instead of AgentPool-specific attrs on HostContext
- session.py:283,842: add None check on ctx.pool before .skill_commands
- session.py:601: use host_context.manifest.agents instead of pool.agent_configs
- utils.py:105: cast config_file_path to str for webbrowser.open()
- session_pool.py: widen run_stream/*prompts from str to Any (matches
  Protocol signature, fixes pre-existing PathReference type error)
- base_agent.py: remove now-unused type: ignore comment

Ruff format fix:
- test_ensure_session_durable.py: reformat long line

Review comment improvements (medium priority):
- agent.py: cache self.host_context in local variable
- base_team.py: cache self.host_context and ctx.pool in locals
- acp_agent.py: cache self.host_context in local variable
- server.py: cache agent.host_context in local variable

Test fixes:
- 16 test files: add pool.pool = pool self-reference for mock chain
  (state.py now resolves _pool via _ctx.pool)
- test_skill_autocomplete.py: use ctx.pool.skill_resolver path
- conftest.py: add mock_pool.pool = mock_pool

* chore(openspec): archive m3-5-backdoor-cleanup, sync specs to main

- Archive change to openspec/changes/archive/2026-07-10-m3-5-backdoor-cleanup/
- Sync 3 delta specs to main specs:
  - openspec/specs/agent-factory/spec.md (new)
  - openspec/specs/agent-pool/spec.md (new)
  - openspec/specs/host-context/spec.md (new)
- 4 incomplete tasks deferred (T6.6 manual QA, T7.1-T7.3 property removal → M4)
… format tool output (Leoyzen#141)

* fix(opencode): broadcast QuestionAskedEvent for durable elicitation + format tool output

Two fixes for the OpenCode server path (serve-opencode):

1. Elicitation broadcast (question_for_user timeout fix):
   - handle_elicitation Path 3 registered an elicitation future but never
     broadcast QuestionAskedEvent to the TUI, causing 5min timeouts.
   - Added broadcast_elicitation_question to OpenCodeInputProvider and
     InputProvider base class.
   - Path 3 now calls broadcast after registering the future, sharing
     the same future so resolve_question resolves both.
   - Payload handling supports both list[list[str]] (TUI REST endpoint)
     and ElicitationResumePayload (in-process resume / crash recovery).

2. Tool output formatting (request_comment display fix):
   - _process_tool_complete used str(result) which produced Python dict
     repr for MCP tool returns parsed as dict by pydantic-ai.
   - Added _format_tool_output with type-branch logic matching
     chat_message_to_opencode: dict/list -> anyenv.dump_json(indent=True).

* fix: address PR review — payload parsing, cleanup, mypy

- Fix multi-select array truncation in object schema payload parsing:
  array-type properties now return the full answer list instead of
  truncating to answer_list[0]
- Fix single-select enum formatting: non-object schemas now return
  a plain string for single-select and a list for multi-select
- Add cleanup_elicitation_question() to InputProvider base class
  (default no-op) and OpenCodeInputProvider (removes from
  _pending_questions_dict), called in finally block to prevent
  stale pending questions on timeout/cancel
- Remove unused type: ignore[assignment] causing mypy failure

* test: use TestModelConfig in capabilities test to fix CI

Cherry-pick from a515134. The test_from_config_capabilities_not_duplicated
test used model='openai:gpt-4o-mini' which requires OPENAI_API_KEY,
causing CI failures on fork PRs where secrets are unavailable.

* fix: address Gemini review round 2 — defensive isinstance checks, persistent fallback dict, try-except cleanup, JSON serialization resilience
Replace the resource provider hierarchy with domain-specific Protocol-based
capabilities and ExtensionRegistry for scope-aware capability management.

New architecture introduces 5 capability types backed by 4 Protocol
interfaces, replacing 8 deprecated components. Net reduction of ~12.7k lines.

New files:
- capabilities/resource_protocols.py — SkillResource, McpResource,
  CommandResource, ChangeObservable protocols + dataclasses
- capabilities/mcp_server_cap.py — McpServerCap replacing MCPCapability,
  lazy client init via SessionConnectionPool.get_client(), multi-listener
  change event broadcast, retry with exponential backoff
- capabilities/skill_manager_cap.py — SkillManagerCap replacing
  SkillCapability + SkillActivationCapability, aggregates local + remote
  skills, metadata-only instruction injection with optional matcher_fn
  and always_active bypass
- capabilities/extension_registry.py — 4-level scope (POOL/SESSION/AGENT/
  TURN), typed queries, URI routing (skill://, mcp://), change stream
  merging, cycle detection (CircularCompositionError), depth limit
- capabilities/skill_watcher.py — watchdog-based filesystem watcher with
  500ms debounce for skill hot-reload

Deleted components:
- MCPCapability, SkillCapability, SkillActivationCapability
- SkillMcpManager, SkillCommandRegistry, SkillProvider Protocol
- ResourceSource Protocol, AggregatedResourceSource

Other changes:
- SessionConnectionPool.get_client() wraps pooled transport with MCPClient
- AgentContext.resources field removed (was dead code, 0 read call sites)
- SkillsInstructionConfig.mode and SkillsToolsetConfig.injection_mode
  deleted (were dead code, stored but never read back)
- HostContext gains extension_registry field
- AgentFactory registers capabilities with ExtensionRegistry
- SkillURIResolver delegates to ExtensionRegistry.resolve_uri()
- MCPManager migrated from MCPCapability to McpServerCap
- OpenCode agent_routes migrated from get_skills() to list_skills()
- 7 delta specs synced to main openspec/specs/
- OpenSpec change archived to openspec/changes/archive/

Co-authored-by: Sisyphus <sisyphus@ohmyopencode.dev>
Interim milestone between M3 and M4 to complete the agent_pool backdoor
cleanup. Covers:

- SkillService Protocol extraction (read-only skill orchestration)
- HostContext extended with skill_service + main_agent_name, pool removed
- ~64 .agent_pool refs migrated across 15 files (ACP, OpenCode, core)
- AgentFactory uses self._pool instead of host_context.pool
- Talk wiring via _bind_pool() instead of ctx.pool
- M1 T6.1/T6.4/T6.5/T6.6 deferred verification covered
- M2 T11.4/T12.9 DeprecationWarning clean check covered

7 task groups, 31 tasks (25 required + 6 optional property removal).
4 specs: skill-service (new), host-context/agent-pool/agent-factory (modified).
…te change

SkillService Protocol extraction moved to independent design change.
Cleanup spec now covers pure mechanical migration:

- ~64 .agent_pool refs → host_context (skill refs use host_context.pool.X
  as interim pattern, will become host_context.skill_service.X later)
- HostContext gains main_agent_name, pool field kept as temporary escape
- No new abstractions introduced
- 7 task groups, 30 tasks (24 required + 6 optional)

Skill-service design will be a separate openspec change covering:
SkillService Protocol, HostContext.skill_service field, pool field
removal, and skill ref migration from pool.X to skill_service.X.
…e passing to FunctionToolset

McpServerCap.get_toolset() passed agentpool FunctionTool instances directly to pydantic-ai's FunctionToolset, which expects pydantic_ai.Tool objects. Since FunctionTool is not a subclass of pydantic_ai.Tool, pydantic-ai treated it as a plain callable and tried to access __name__ on it, causing AttributeError: 'FunctionTool' object has no attribute '__name__'. Fix: use wrap_tool_for_pydantic_ai() to convert FunctionTool to pydantic_ai.Tool before passing to FunctionToolset.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
- combined_toolset.py: add _background_tasks set for strong asyncio.Task
  references to prevent GC mid-execution
- factory.py: add empty-string guard in _sanitize_session_id to raise
  ValueError instead of silently producing a colliding key
- skill_manager_cap.py, skill_watcher.py, snapshot_store.py: replace
  direct logging imports with project-standard get_logger from
  agentpool.log
Resolved 9 conflicting files — all resolved in favor of HEAD (M3 architecture):
- agent.py: keep HEAD variable naming (ctx_mcp avoids shadowing)
- manager.py: keep HEAD version
- session_controller.py: keep HEAD AgentFactory delegation (already
  incorporates develop/agentic's child session MCP snapshot inheritance)
- acp_server/session.py: keep HEAD _mcp_snapshot direct assignment
- skills/capability.py: keep deletion (replaced by SkillManagerCap)
- test files: keep HEAD API naming (get_capabilities vs as_capability)
- deleted test for deleted code

Also fixed ruff import sorting in skill_watcher.py and snapshot_store.py.
Three critical regressions from the M3 capability refactor are fixed:

1. skill:// URI resolution: Register SkillManagerCap with ExtensionRegistry
   at POOL scope. Flatten URIs from skill://local/{name} to skill://{name}.
   Fix ResolvedSkillURI.parse() to handle flat URIs where urlparse puts
   name in netloc with empty path.

2. Per-skill tool registration: SkillManagerCap now accepts SkillToolManager,
   eagerly imports Python tools (PrefixedToolset {name}__tool__), creates
   per-skill McpServerCap instances (PrefixedToolset {name}__mcp__), and
   fully overrides get_toolset() (no super() call). Implements composite
   allowed_tools filtering via get_wrapper_toolset(). Updates for_run() to
   propagate tool_manager.

3. load_skill propagation: Restore skills_tools_provider injection in
   _inject_pool_providers() for non-SessionPool agent creation paths.

Additional changes:
- Migrate load_skill/list_skills MCP provider dispatch from getattr
  duck-typing to isinstance(SkillResource) protocol checks
- Remove dead code: deprecated pool fields (_skill_commands,
  _skill_mcp_manager, _skill_tool_manager, skill_commands property),
  unreachable agent.py branch, dead manager.py resource_provider
- Update stale comments referencing deleted classes (SkillCapability,
  SkillProvider, SkillCommandRegistry)
- Fix duplicate code block in pool.py _rebuild_skill_capabilities()
- Add tests for flat URI, load_skill propagation, and protocol migration

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
OpenSpec change with 9 design decisions (D1-D9), 5 spec files (2 new
capabilities + 3 modified), 64 tasks across 17 groups, and RFC-0052
for stakeholder review.

New specs:
- skill-tool-registration: per-skill Python/MCP tool registration,
  prefixing, allowed_tools filtering, lifecycle
- skill-tools: load_skill/list_skills tools resolution paths,
  argument substitution, protocol-based dispatch

Modified specs:
- skill-manager-cap: tool registration, ExtensionRegistry registration,
  composite filter, for_run propagation
- extension-registry: pool-level SkillManagerCap registration
- agent-factory: _inject_pool_providers skills_tools_provider injection

Oracle-verified across 4 review rounds.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
New test files:
- tests/capabilities/test_skill_tool_registration.py (20 tests):
  Python tool prefixing, MCP tool prefixing, allowed_tools filtering
  (including empty list edge case), composite multi-skill filter,
  for_run() propagation, pool rebuild, lifecycle, invalid import_path
  graceful error, McpServerCap creation failure isolation

- tests/integration/test_skill_e2e.py (10 tests):
  Flat URI resolution end-to-end, load_skill availability in standalone
  and child session agents, skill:// URI resolution, allowed_tools
  filtering e2e, tool prefix isolation, pool rebuild, for_run()
  tool_manager propagation, load_skill via TestModel

All 30 tests passing. Tasks 1.4, 2.7, 3.5, 4.2-4.4, 7.1-7.5, 8.11,
12.1-12.5, 13.1-13.14 marked complete. Remaining 3 tasks (14.8, 15.2,
15.3) deferred to CI.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
1. allowed_tools filter: replace rsplit('__', 1) with explicit category
   prefix matching ({skill_name}__tool__ / {skill_name}__mcp__) to
   correctly handle tool names containing double underscores.

2. Type safety: replace cast('list[Skill]', entries) with explicit
   Skill construction from SkillEntry objects in _available_skill_names
   and list_skills functions. Move Skill import to module level.

3. Flat URI: allow trailing slash (skill://my-skill/) to resolve
   correctly by checking parsed.path in ('', '/') instead of
   requiring completely empty path.

4. Remove redundant if/else branch in pool.py _rebuild_skill_capabilities
   — both branches created identical SkillManagerCap instances.

5. Remove redundant local imports of PurePosixPath and Skill in
   _load_visible_bare_skill (now imported at module level).

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
…nd tests

Remove all dead code blocks referencing the removed pool.skill_commands
property across 5 server files, 10 test files, and 3 diagnostic scripts.
The property was removed in the restore-skill-capabilities change but
these dead blocks remained — they would crash with AttributeError if
reached in production since the property no longer exists.

Server changes:
- agui_server/server.py: Remove skill_commands guard block and _skill_bridge
- acp_server/acp_agent.py: Remove _setup_skill_bridge(), get_skill_commands()
- acp_server/session.py: Remove _register_skill_commands() and cleanup in close()
- opencode_server/server.py: Remove skill_commands init block
- opencode_server/routes/session_routes.py: Remove 3 fallback blocks

Test changes:
- Remove pool.skill_commands = None Mock assignments (8 files)
- Update test_skill_autocomplete.py and test_command_execution.py to
  use CommandStore instead of removed skill_commands fallback

Script changes:
- diagnose_skills.py: Remove all skill_commands/resource_provider checks
- diagnostic_pool_skills.py: Remove skill_commands print
- qa_test_server.py: Remove dead skill_commands assignment

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
- agent.py: PERF401 — for+append → list.extend with generator
- manager.py: PIE790 — remove unnecessary pass in __aexit__
- skill_manager_cap.py: mypy — use AbstractCapability[Any] for
  heterogeneous children list (McpServerCap[None] vs AgentDepsT)
- uri_resolver.py: flat URI bug — skill://local/ was incorrectly
  caught by flat URI branch; restrict to parsed.path == '' only
- test_package_scoped_skills.py: update _FakeSkillProvider to
  implement SkillResource protocol (list_skills, read_skill,
  skill_exists) instead of old get_skills/get_skill_instructions

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
CI fix:
- test_list_skills_filters_provider_skills: provider skills are always
  visible since SkillEntry doesn't carry scope metadata

Gemini review (accepted):
- HIGH: _load_visible_bare_skill now applies _visible_model_skills filter
  to provider skills before selecting (defense-in-depth)
- MEDIUM: allowed_tools uses 'is not None' check to distinguish [] (block
  all) from None (no restrictions)
- MEDIUM: Move McpServerCap import to method level in _create_skill_mcp_children
- MEDIUM: Move DynamicToolset import to method level in get_toolset

Gemini review (rejected):
- Flat URI trailing slash: can't distinguish skill://local/ (error) from
  skill://my-skill/ (valid) — keeping parsed.path == '' check
- Any vs AgentDepsT: Any is correct for heterogeneous capability list,
  AgentDepsT would reintroduce mypy error

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
…t URIs)

BREAKING: skill:// URIs no longer support provider segments.
Use skill://skill-name instead of skill://provider/skill-name.

Changes:
- Remove provider field from ResolvedSkillURI (always None after D9)
- Simplify parse(): netloc is always skill_name, path is reference
- Remove _is_valid_provider_name, _validate_provider_name, and
  related constants (PROVIDER_NAME_PATTERN, MAX_PROVIDER_NAME_LENGTH)
- Simplify SkillURIResolver.resolve(): remove 70-line provider fallback
  chain, now searches all providers directly by skill_name
- Remove resolved.provider display blocks from _load_skill() in skills.py
- Fix external refs in session.py (local constant) and integration tests

Users can now write:
  skill://my-skill              (was: skill://local/my-skill)
  skill://my-skill/path/ref.md  (was: skill://local/my-skill/ref.md)

Tests: 50/50 pass across test_uri_resolver.py + test_flat_uri.py

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Previously, when ExtensionRegistry was configured, resolver.resolve() exclusively delegated to it and raised SkillNotFoundError immediately on failure. Now it falls through to _find_skill_with_alternatives(), enabling skill:// URIs to work even when ExtensionRegistry doesn't know the specific skill.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
…lve_uri()

ExtensionRegistry.resolve_uri() had its own ad-hoc URI parsing that
treated the first path segment as a provider name for URIs with
2+ segments. This broke flat skill:// URIs with reference paths
(e.g. skill://expert-knowledge/references/file.md → looked for
provider "expert-knowledge" instead of skill "expert-knowledge").

Fix: delegate to ResolvedSkillURI.parse() — the SINGLE canonical
parser — for all skill:// URI resolution. Remove dead _get_cap_name()
function and dead provider_name matching code.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
…o resolver

Problem: ExtensionRegistry.resolve_uri() returned only str | bytes | None,
discarding Skill metadata. The resolver fabricated a virtual PurePosixPath
which broke reference file loading for local filesystem skills.

Changes:
- SkillEntry: add skill_path field (UPath for local skills, None for virtual)
- SkillManagerCap.list_skills(): populate skill_path from real Skill objects
- ExtensionRegistry.resolve_uri(): return Skill | str | bytes | None.
  For skill:// URIs, return full Skill object with real skill_path, metadata.
- SkillURIResolver.resolve(): handle Skill return type — pass through directly
  for skill:// URIs; construct fallback Skill for str/bytes content.
- _load_skill(): simplify — instructions already populated by resolver,
  remove redundant isinstance(path, PurePosixPath) check. Reference file
  loading now uses real UPath, works without skill_provider fallback.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
…acts

- Move change from openspec/changes/ to archive/
- Fix ruff format indentation in extension_registry.py

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
… D9 flat URIs

- Update resolve_uri() assertions to check Skill.instructions instead of str
- Rework provider routing tests for D9 flat URI format (no provider segment)
- Update URI format in integration tests from skill://local/name to skill://name
- Remove resolved.provider assertions (attribute removed in D9)
- Update allowed_tools tests to expect FilteredToolset (parsed_allowed_tools
  returns list[str] never None, so filter always activates)
- Fix mypy: add from __future__ import annotations to extension_registry.py,
  cast skill_path type, handle str | None for instructions

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
…lash commands

The command_store field on ServerState was never initialized in production
because it was guarded by 'if pool.skill_commands is not None:' which always
returned None (property removed in M3). This caused all slash command
execution via the OpenCode server to return 404.

Fix: Build SkillCommand objects from pool.skills.list_skills() at server
startup, bridge them to slashed Commands via OpenCodeSkillBridge, and
initialize CommandStore. Also fix SkillCommand.resolved_skill_uri to use
flat skill://{name} format (D9) instead of old skill://local/{name}.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Leoyzen and others added 30 commits July 23, 2026 19:08
…n#277)

* fix: wire entrypoint registry into YAML capability resolution

Before this fix, capabilities registered via the agentpool.capabilities
entry-point group (e.g. mermaid_lint from mmdvet) could not be referenced
by their short name in YAML config. The handle_capabilities validator
only checked 6 hardcoded built-in names and fell back to treating the
type field as a Python import path, causing ValueError for entrypoint
names (which contain no dot).

Changes:
- Add EntryPointCapabilityConfig to agentpool_config/capabilities.py
  that lazily resolves entrypoint names via discover_entry_point_capabilities()
- Update handle_capabilities validator in models/agents.py to check
  the entrypoint registry before falling back to GenericCapabilityConfig
- Update get_agentlet() in native_agent/agent.py to handle
  EntryPointCapabilityConfig in the capability build loop
- Add EntryPointCapabilityConfig to the CapabilityConfig union and
  build_capability() dispatch
- Add 8 unit tests covering YAML parsing, build(), and build_capability()
  dispatch for entrypoint-based capabilities
- Update existing test_capabilities_yaml.py to include the new config type

* fix: use importlib.metadata directly in EntryPointCapabilityConfig.build()

The previous implementation imported from agentpool.capabilities.registry
inside build(), which violated the import-linter contract that
agentpool_config must not import from agentpool. Replace with direct
importlib.metadata.entry_points() usage (stdlib only, no agentpool dep).
Update tests to patch importlib.metadata.entry_points for build() tests.
…blackboard pagination and member work summary

- Delete old shutdown_request (soft shutdown that leaked max_members quota
  by keeping members in the dict with status='shutdown')
- Rename team_remove_member to shutdown_request (hard remove: closes
  session, removes from members dict, cleans up session metadata, writes
  audit to blackboard)
- Add line-based pagination to read_blackboard: limit (default 200),
  offset (0-indexed), context (center around a line number). Returns
  list[str] instead of joined string. Appends truncation hint when more
  lines exist.
- Inject work-status summary into team_add_member roster so new members
  know what existing members are working on (in_progress / completed /
  no active work)
- Update AGENTS.md tool table
- Update all affected tests (192 pass)
Add after_run hook in TeamCommCapability that checks for in_progress
tasks when a member agent's run completes. If unfinished tasks are
found, routes a reminder message to the member's own session via
session_pool.send_message (QUEUE mode). Limited to 1 reminder per
session to avoid infinite loops. Skipped for lead agents and during
session shutdown.

Modify shutdown_request to check for unfinished tasks before closing
a member's session. If any in_progress tasks are found, the ToolReturn
includes a warning telling the lead to update task status or reassign.

Add _get_unfinished_tasks static helper to avoid code duplication
between after_run and shutdown_request.

7 new unit tests covering: reminder sent for unfinished tasks, no
reminder for lead, no reminder when session closing, no duplicate
reminders, no reminder when all tasks completed, shutdown warning
with unfinished tasks, shutdown no warning when tasks completed.
…s watch

Both tools now accept an optional watch_task_ids parameter. When non-empty,
the watch loop monitors specific task file mtimes instead of general state
changes, returning as soon as any watched task is modified. When empty or
None, the existing behavior is preserved (any change ends the watch).

- Add _snapshot_task_mtimes static helper for task file mtime detection
- list_blackboard: watch_task_ids filters to specific task changes
- team_status: watch_task_ids filters to specific task changes
- 5 new tests covering timeout, detection, and unrelated-change filtering
task_create now accepts an optional owner parameter to assign a team
member as the task owner at creation time, eliminating the need for a
separate task_update call.
… semantics

- Add max_watch_timeout field to TeamModeConfig (default 120s)
- list_blackboard and team_status: timeout<=0 means no user limit, uses
  config max; timeout>0 is capped by config max via min(timeout, max)
- 3 new tests: timeout=0 uses config max, timeout capped by config,
  team_status timeout=0 uses config max
…base lock errors

Concurrent session writes to the shared SQLite database were causing
'sqlite3.OperationalError: database is locked' errors. Add PRAGMA
settings on every new SQLite connection:

- journal_mode=WAL: allows concurrent readers with a single writer
- busy_timeout=30000: writers wait up to 30s for the lock instead
  of failing immediately
- synchronous=NORMAL: safe with WAL, reduces fsync overhead
…olling

Replace 5-second polling loop with complete_event.wait() on the
RunHandle for event-driven completion detection. Write team state
file BEFORE closing session so team_status(watch=True) detects the
change immediately. Add broad exception handling to prevent silent
task death when close_session fails.
…routing

When a team member's run fails with an exception, the lead (parent
session) now receives a concise notification through _route_message
with source="team". This goes through the same unified message path
as member-initiated send_message, so it appears in the lead's
conversation history and the LLM can act on it in the next turn.

Previously, when a member crashed, the lead had no notification at
all — RunErrorEvent was published only to the member's own EventBus,
which the lead never subscribed to. The lead could only discover the
crash by manually polling team_status.

The notification is best-effort: if the lead session is unavailable
or closed, the notification is silently skipped. Normal member
completion (where the member calls send_message to the lead) is
unaffected — no duplicate notification.
Add idle_timeout (default 600s) and poll_interval (default 30s) fields
to TeamModeConfig so users can tune team member cleanup timing in YAML:

  team_mode:
    enabled: true
    idle_timeout: 600
    poll_interval: 15

Previously these were hardcoded class attributes (300s/30s) on
TeamCommCapability with no YAML config path. The _schedule_member_cleanup
method was converted from @staticmethod to instance method to access
self._config. Tests updated to pass values via manifest instead of
monkeypatching class attributes.
Add _notify_member helper to TeamCommCapability that sends a
best-effort system notification to a team member's session via
session_pool.send_message().

Three notification triggers:
1. task_create with owner set → notify the assigned member
2. task_update with new owner → notify the newly assigned member
   (skipped when task is already completed)
3. task_update with status=completed → find all downstream tasks
   whose blocked_by contains the completed task_id, check if they
   are now fully unblocked (is_unblocked), and notify their owners

Notifications use the existing notice_delivery_mode (steer/queue)
from TeamModeConfig and are wrapped as <team-message type=
task_notification>. Self-notifications are skipped. All failures
are logged as warnings and do not block the tool return.
MCP elicitation handlers in two code paths (MCPClient.call_tool and
MCPToolset via _make_elicitation_handler) were returning mcp.types.ElicitResult
(MCPElicitResult) directly to fastmcp's create_elicitation_callback wrapper.
Since MCPElicitResult is NOT a subclass of fastmcp's ElicitResult, the wrapper
wraps the entire object as content, producing {"content":{"_meta":null,"content":null}}
on the wire and causing Zod validation errors on MCP servers.

Fix both paths by extracting content from MCPElicitResult via match/case
before returning to the fastmcp wrapper. Also add normalize_elicit_content
to convert None values to empty strings per MCP ElicitationContentValue spec,
and add a safety net in _forwarding_elicitation_callback as defense in depth.
…sult-leak

fix(mcp): prevent MCPElicitResult leaking to fastmcp elicitation wrapper
The permission check in task_update compared the task owner against
self._agent_name (the YAML agent name, e.g. "translator"), but task
owners are set using team member names (e.g. "artisan_23830").  This
mismatch caused permission-denied errors when members tried to update
their own tasks.

Fix: extract team_member_name from session metadata (with _agent_name
fallback) and use it for both the ownership check and the updated_by
field.  Update test to assign tasks using the member name.
When a team is closed, member sessions' ProtocolChannel is closed.
But _consume_run could still pick up queued prompts from prompt_queue
and try to start a new turn, causing RuntimeError: ProtocolChannel is
closed; cannot publish.

Fix: check session.is_closing inside the _request_lock block before
creating a new RunHandle for chained prompts.
Remove task_create from _LEAD_ONLY_TOOLS so members can see and use
it.  The runtime permission check inside task_create already blocks
non-lead members from creating top-level tasks (parent_id=None),
while allowing subtask creation (parent_id set).  This enables more
autonomous collaboration — members can break down their work into
subtasks without lead involvement.
steer() fallback when active_agent_run is None was writing to
queued_steer_messages — a list that is never drained in production
code. Messages were permanently lost when RunHandle was destroyed.

Three fixes:
- Fix A: steer() fallback now writes to session.feedback_queue
  (survives across RunHandle boundaries) instead of queued_steer_messages
- Fix B: start() feedback_queue drain writes directly to
  queued_steer_messages (avoids infinite loop with Fix A)
- Fix C: new RunHandle.drain_queued_steer_messages() method drains
  queued_steer_messages into active_agent_run; called by
  NativeTurn.execute() after setting active_agent_run

Updated 5 existing tests to assert new behavior. Added 8 new TDD tests
covering all three message loss scenarios from PR Leoyzen#168 comment.
…down

When team_delete or shutdown_request closes member sessions, the
ConsumeRun exception handler would fire (ProtocolChannel closed,
RuntimeError) and notify the lead of a member crash.  This is
actually a normal shutdown, not a crash.

Fix: check session.is_closing before calling _notify_lead_of_member_crash.
If the session is being closed, the exception is expected and the
lead should not receive a crash notification.
…ervice to SessionPool

Replace agent_ctx.delegation.create_child_session() (Path A) with
direct SessionPool.create_child_session() calls (Path B) in
team_comm_capability.py for both team_create and team_add_member.

The new _create_member_session() helper:
- Uses SessionPool.create_child_session() which generates ses_ prefixed
  sortable IDs (same as session ID generation)
- Eagerly registers the agent via get_or_create_session_agent()
- Emits SpawnSessionStart for protocol server discovery

Remove create_child_session() from:
- RunLoopDelegationService (concrete implementation)
- DelegationService Protocol

This eliminates the dual-path problem where RunLoopDelegationService
created sessions with a different code path than AgentRunContext,
missing agent registration and done_event setup.
…nCode TUI

OpenCode's SessionID.create() uses descending() (bitwise NOT of
timestamp), so newer sessions have lexicographically smaller IDs.
The TUI's children() memo sorts by session.id ascending, and
moveChild(1)=Next moves toward index 0 (newest). With agentpool's
ascending IDs, this was reversed: Next went to oldest, Previous to
newest.

Changed generate_session_id() from ascending(session) to
descending(session), and updated session_routes.py's two direct
ascending(session) calls to descending(session) for consistency.

Updated test_generate_session_ids_are_sortable to assert descending
order. The ascending() function itself is unchanged — it's still used
for message, part, and other non-session ID types.
…ring

Session ID and created_at_ns were captured at different times
(generate_session_id() vs SessionState construction), causing
millisecond gaps that flip ordering for rapidly-created sessions.

Add extract_timestamp_ms() to decode the timestamp embedded in
ascending/descending IDs. After SessionState creation, override
created_at_ns and last_active_at_ns with the session ID's timestamp,
ensuring time.created order always matches session ID lexicographic
order.
…system prompt mutation (Leoyzen#282)

* fix: session ID ordering and child session sort

Two fixes for subagent session ordering issue:

1. identifiers.py: Handle backward clock jumps in _create() — when
   now_ms() returns a lower timestamp than _last_timestamp (NTP
   adjustment on macOS), keep the last timestamp and increment the
   counter instead of resetting. Ensures IDs are always monotonically
   increasing even when the wall clock goes backward.

2. session_routes.py: Sort GET /session/{id}/children by time.created
   (ascending) so OpenCode UI left/right navigation shows child
   sessions in creation order.

3. time_utils.py: Fix misleading docstring — now_ms() uses
   time.time_ns() (CLOCK_REALTIME) which is NOT monotonic.

* fix(capabilities): eliminate prefix cache invalidation from in-place system prompt mutation

MemoryCapability and SkillManagerCap modified SystemPromptPart.content
in-place every turn via before_model_request, causing prefix cache misses
on every model request (OpenAI auto-cache and Anthropic prompt caching).

- MemoryCapability: replace before_model_request with get_instructions
  returning an async callable (dynamic=True, no system prompt mutation)
- SkillManagerCap: replace before_model_request with get_instructions
  returning [static_metadata, dynamic_callable] — metadata is
  dynamic=False (cacheable), matched skill content is dynamic=True
- CombinedToolsetCapability: fix get_instructions to preserve callables
  and sequences from children (previously silently dropped non-str returns)
- Remove _inject_into_system_prompt utility (no longer used)
- DynamicContextCapability: no change (compaction is intentional)

Closes Leoyzen#281

* test: update TestMatcherFnBackwardCompat for get_instructions API

Replace before_model_request calls with get_instructions callable
invocation to match the prefix cache fix.
Enables EnqueuedMessagesEvent support (available since v2.12.0).
Fix ToolReturn import from pydantic_ai.messages instead of
pydantic_ai.tools (re-export removed in newer versions).
Move RunContext/ToolDefinition into TYPE_CHECKING block.
PydanticAI's UsageLimits defaults request_limit to 50, which is too
low for agents with many tool calls. When no usage_limits are
explicitly configured, default to request_limit=None (unlimited).
Ensures each member session gets a distinct time.created (millisecond
precision) when team_create creates multiple members in a tight loop.
SQLite WAL store operations complete in sub-millisecond time, so
without this delay all members get the same time.created, causing
non-deterministic sort order in OpenCode TUI subagent numbering.
… tools

The MCP server failed to start due to Pydantic TypeAdapter being unable to
process unbound generic types in SubagentCapability static methods.

Replace direct registration of generic methods with concrete wrapper functions
that have serializable type signatures. This enables Claude Code MCP integration
with agentpool.
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.

3 participants