fix: 修复 MCP 服务器注册工具时的 Pydantic TypeAdapter 错误 - #39
Open
wensm77 wants to merge 837 commits into
Open
Conversation
- 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>
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
问题描述
使用
agentpool serve-mcp启动 MCP 服务器时失败,报错:问题原因:
SubagentCapability.spawn_subagent和get_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 可以正常处理。
测试验证
已验证:
变更内容
修改了
src/agentpool_server/mcp_server/server.py,用包装函数替代了直接的方法注册。此修复使 agentpool 能够与 Claude Code 的 MCP 集成正常工作。