feat(resource): configurable max_text_chars for resource reads - #393
Closed
Million-mo wants to merge 2 commits into
Closed
feat(resource): configurable max_text_chars for resource reads#393Million-mo wants to merge 2 commits into
Million-mo wants to merge 2 commits into
Conversation
ResourceCapability now accepts a max_text_chars parameter (default 10000) controlling the maximum text length per read_resource call before truncation. Previously hardcoded in resolve_resource_content() with the tail silently discarded — a problem for knowledge-base sources whose chapter resources can exceed 10k characters. The truncation suffix now guides the model to use a narrower resource URI or a paginated read tool for full content, instead of an unexplained cut. Backward-compatible: ResourceCapability() with no args preserves the default.
Collaborator
Author
|
Superseded by PR #394 which includes all changes from this PR plus per-agent max_text_chars config wiring and kb_diag_agent.yaml alignment. |
Million-mo
added a commit
that referenced
this pull request
Aug 26, 2026
…align (#394) * feat(resource): per-agent max_text_chars config + kb_diag_agent YAML align ResourceConfig now accepts max_text_chars (default 10000, min 100) in agent YAML config. NativeAgent creates a per-agent ResourceCapability with the agent's max_text_chars instead of sharing the pool-level instance. ResourceCapability.__init__ gains max_text_chars parameter (backward compatible). Truncation suffix improved with guidance directing the model to use narrower URIs or paginated read tools. kb_diag_agent.yaml aligned with live knowledge_diag server v3.4.4: - Enabled search_kb (removed from disabled_tools) - Added get_doc_toc and read_chapter_page tool-schema-overlap rewrites - Added search_kb rewrite with methods (FULL/FAST/WIKI) param docs - Updated existing tool descriptions to reference page-based workflow Supersedes PR #393. * feat(mcp): resource subscribe-on-read wiring in McpServerCap Best-effort subscribe to resource URIs after successful read_resource() calls, enabling notifications/resources/updated for resources the agent has read. Tracked subscriptions are re-established on reconnect and cleaned up on disconnect. No-op for servers with subscribe:false (like knowledge_diag v3.4.4) — subscribe fails silently, read proceeds normally. Activates automatically when server enables subscription support. * fix(mcp): address PR #394 review — wire max_text_chars, fix broken test, consolidate truncation - Wire self._max_text_chars into read_mcp_resource (was hardcoded _DEFAULT_READ_TEXT_LIMIT) and use _truncate_text helper with guidance suffix - Remove dead _truncate_text static method from ResourceCapability (zero callers, old suffix format) - Add constructor validation: max_text_chars < 100 raises ValueError - Consolidate default constant: _DEFAULT_MAX_TEXT_CHARS in resource_resolver.py, aliased in resource_capability.py - Fix broken test assertion in test_resource_resolution.py to match new guidance suffix format - Add tests: max_text_chars validation, read_mcp_resource truncation with per-agent limit, suffix guidance text - Correct changelog: limit was previously hardcoded, not a pre-existing constructor param - Update capabilities/AGENTS.md: ResourceCapability is per-agent constructed, not registered at SESSION scope
Million-mo
added a commit
that referenced
this pull request
Aug 28, 2026
…align (#400) * fix(opencode): eliminate EventBus loopback causing duplicate SSE projections (#391) * fix(opencode): eliminate EventBus loopback causing duplicate SSE projections The OpenCodeEventBridge republished protocol projections (MessageUpdatedEvent, PartUpdatedEvent, etc.) back into the same EventBus that carries native agent events. This created a feedback loop: native events → event bridge → broadcast → EventBus republish → SSE delivery, causing duplicate renders in attached OpenCode TUI clients (issue #380). Architecture change — direct-wire SSE: - state.broadcast_event() now fans projections directly to per-connection SSE subscriber queues instead of republishing to EventBus - global_routes._event_generator reads from state.event_subscribers queues (no EventBus subscription, no CustomEvent unwrapping) - Reconnect replay via state.replay_projections() using Last-Event-ID - Deleted event_bridge.py (the loopback republisher) EventBus source isolation (defense-in-depth): - EventEnvelope gains source_hint field; publish() accepts source_hint - subscribe() accepts exclude_source param (filters both live fanout and replay) - ProtocolEventConsumerMixin hooks: _get_subscription_replay() and _get_subscription_exclude_source() (defaults: replay=True, exclude=None) - OpenCode overrides: replay=False, exclude_source={"opencode_event_bridge"} Session consumer replay alignment: - OpenCode session-level consumers now use replay=False (matching the global SSE endpoint's first-connect policy), preventing stale events from being redelivered on consumer startup Testing: - 7082 tests passed (full suite), ruff/mypy clean - New e2e test: test_attach_existing_session_first_prompt_renders_once - New unit tests: EventBus source_hint/exclude_source (4 tests) - Rewritten integration tests for direct-wire SSE model Note: A residual TUI-side duplication may still be visible in opencode attach mode due to the TUI's local echo (createUserMessage) not matching the server-generated message ID in the SSE event. This is tracked as an opencode TUI bug (anomalyco/opencode#14372, #24773, #29478) with upstream fix PR #31945 still unmerged. The server-side fix in this commit eliminates the EventBus loopback path; the remaining duplication is purely client-side. * fix(opencode): address review — QueueFull policy, typed session-id extraction, mock alignment Review-driven fixes on PR #391 (direct-wire SSE loopback elimination): BLOCKER: broadcast_event now handles asyncio.QueueFull per-subscriber with the same drop-oldest policy as EventBus._enqueue, so a stalled SSE client can no longer abort fanout to every other subscriber. Adds structured warning logging on overflow plus a debug fanout log (telemetry on the delivery critical path). New regression test: test_broadcast_event_drop_oldest_on_queue_full. MAJOR: ServerState.extract_session_id now delegates to the typed global_routes._extract_session_id (match-based, no getattr) instead of the getattr probe that read info.id for MessageUpdatedEvent — buffering message.updated under per-message keys and leaking memory. Typed variant reads props.info.session_id as documented. Test mocks in test_global_event.py / test_sse_compliance.py aligned with production: deque(maxlen=100) buffers, drop-oldest overflow, typed extractor — the queue-full behavior is no longer suppressed out of the green suite. MINOR: replay_projections replays merged buffers in global event_id order (monotonic SSE ids for reconnecting clients) and stops with a warning on QueueFull instead of silently dropping a suffix. MINOR: _get_subscription_exclude_source documented as currently inert (its only producer was deleted with the loopback bridge; kept as defense-in-depth, exercised by unit tests). Nits: stale event_bridge docstrings/comment/names updated across test_event_pipeline_e2e.py and conftest.py; corrected the pre-existing dedup-set claim in src/wolfharness/AGENTS.md (the set is a private ACPEventConverter field, not on SessionController); changelog trailing newline; ADR eventbus-replay.md annotated as superseded by the direct-wire design (PR #391). Verified: ruff clean, mypy strict clean (686 files), 326 affected tests pass. * feat(resource): per-agent max_text_chars config + kb_diag_agent YAML align (#394) * feat(resource): per-agent max_text_chars config + kb_diag_agent YAML align ResourceConfig now accepts max_text_chars (default 10000, min 100) in agent YAML config. NativeAgent creates a per-agent ResourceCapability with the agent's max_text_chars instead of sharing the pool-level instance. ResourceCapability.__init__ gains max_text_chars parameter (backward compatible). Truncation suffix improved with guidance directing the model to use narrower URIs or paginated read tools. kb_diag_agent.yaml aligned with live knowledge_diag server v3.4.4: - Enabled search_kb (removed from disabled_tools) - Added get_doc_toc and read_chapter_page tool-schema-overlap rewrites - Added search_kb rewrite with methods (FULL/FAST/WIKI) param docs - Updated existing tool descriptions to reference page-based workflow Supersedes PR #393. * feat(mcp): resource subscribe-on-read wiring in McpServerCap Best-effort subscribe to resource URIs after successful read_resource() calls, enabling notifications/resources/updated for resources the agent has read. Tracked subscriptions are re-established on reconnect and cleaned up on disconnect. No-op for servers with subscribe:false (like knowledge_diag v3.4.4) — subscribe fails silently, read proceeds normally. Activates automatically when server enables subscription support. * fix(mcp): address PR #394 review — wire max_text_chars, fix broken test, consolidate truncation - Wire self._max_text_chars into read_mcp_resource (was hardcoded _DEFAULT_READ_TEXT_LIMIT) and use _truncate_text helper with guidance suffix - Remove dead _truncate_text static method from ResourceCapability (zero callers, old suffix format) - Add constructor validation: max_text_chars < 100 raises ValueError - Consolidate default constant: _DEFAULT_MAX_TEXT_CHARS in resource_resolver.py, aliased in resource_capability.py - Fix broken test assertion in test_resource_resolution.py to match new guidance suffix format - Add tests: max_text_chars validation, read_mcp_resource truncation with per-agent limit, suffix guidance text - Correct changelog: limit was previously hardcoded, not a pre-existing constructor param - Update capabilities/AGENTS.md: ResourceCapability is per-agent constructed, not registered at SESSION scope * fix: re-raise serve() exceptions in ACPServer._start_async The except Exception block logged but swallowed startup errors (e.g. OSError: Address already in use), causing silent exit 0. Add raise after log.exception to propagate to the CLI. Fixes CI failure in test_start_async_propagates_serve_oserror.
Million-mo
added a commit
that referenced
this pull request
Aug 29, 2026
…on (#405) * fix(opencode): eliminate EventBus loopback causing duplicate SSE projections (#391) * fix(opencode): eliminate EventBus loopback causing duplicate SSE projections The OpenCodeEventBridge republished protocol projections (MessageUpdatedEvent, PartUpdatedEvent, etc.) back into the same EventBus that carries native agent events. This created a feedback loop: native events → event bridge → broadcast → EventBus republish → SSE delivery, causing duplicate renders in attached OpenCode TUI clients (issue #380). Architecture change — direct-wire SSE: - state.broadcast_event() now fans projections directly to per-connection SSE subscriber queues instead of republishing to EventBus - global_routes._event_generator reads from state.event_subscribers queues (no EventBus subscription, no CustomEvent unwrapping) - Reconnect replay via state.replay_projections() using Last-Event-ID - Deleted event_bridge.py (the loopback republisher) EventBus source isolation (defense-in-depth): - EventEnvelope gains source_hint field; publish() accepts source_hint - subscribe() accepts exclude_source param (filters both live fanout and replay) - ProtocolEventConsumerMixin hooks: _get_subscription_replay() and _get_subscription_exclude_source() (defaults: replay=True, exclude=None) - OpenCode overrides: replay=False, exclude_source={"opencode_event_bridge"} Session consumer replay alignment: - OpenCode session-level consumers now use replay=False (matching the global SSE endpoint's first-connect policy), preventing stale events from being redelivered on consumer startup Testing: - 7082 tests passed (full suite), ruff/mypy clean - New e2e test: test_attach_existing_session_first_prompt_renders_once - New unit tests: EventBus source_hint/exclude_source (4 tests) - Rewritten integration tests for direct-wire SSE model Note: A residual TUI-side duplication may still be visible in opencode attach mode due to the TUI's local echo (createUserMessage) not matching the server-generated message ID in the SSE event. This is tracked as an opencode TUI bug (anomalyco/opencode#14372, #24773, #29478) with upstream fix PR #31945 still unmerged. The server-side fix in this commit eliminates the EventBus loopback path; the remaining duplication is purely client-side. * fix(opencode): address review — QueueFull policy, typed session-id extraction, mock alignment Review-driven fixes on PR #391 (direct-wire SSE loopback elimination): BLOCKER: broadcast_event now handles asyncio.QueueFull per-subscriber with the same drop-oldest policy as EventBus._enqueue, so a stalled SSE client can no longer abort fanout to every other subscriber. Adds structured warning logging on overflow plus a debug fanout log (telemetry on the delivery critical path). New regression test: test_broadcast_event_drop_oldest_on_queue_full. MAJOR: ServerState.extract_session_id now delegates to the typed global_routes._extract_session_id (match-based, no getattr) instead of the getattr probe that read info.id for MessageUpdatedEvent — buffering message.updated under per-message keys and leaking memory. Typed variant reads props.info.session_id as documented. Test mocks in test_global_event.py / test_sse_compliance.py aligned with production: deque(maxlen=100) buffers, drop-oldest overflow, typed extractor — the queue-full behavior is no longer suppressed out of the green suite. MINOR: replay_projections replays merged buffers in global event_id order (monotonic SSE ids for reconnecting clients) and stops with a warning on QueueFull instead of silently dropping a suffix. MINOR: _get_subscription_exclude_source documented as currently inert (its only producer was deleted with the loopback bridge; kept as defense-in-depth, exercised by unit tests). Nits: stale event_bridge docstrings/comment/names updated across test_event_pipeline_e2e.py and conftest.py; corrected the pre-existing dedup-set claim in src/wolfharness/AGENTS.md (the set is a private ACPEventConverter field, not on SessionController); changelog trailing newline; ADR eventbus-replay.md annotated as superseded by the direct-wire design (PR #391). Verified: ruff clean, mypy strict clean (686 files), 326 affected tests pass. * feat(resource): per-agent max_text_chars config + kb_diag_agent YAML align (#394) * feat(resource): per-agent max_text_chars config + kb_diag_agent YAML align ResourceConfig now accepts max_text_chars (default 10000, min 100) in agent YAML config. NativeAgent creates a per-agent ResourceCapability with the agent's max_text_chars instead of sharing the pool-level instance. ResourceCapability.__init__ gains max_text_chars parameter (backward compatible). Truncation suffix improved with guidance directing the model to use narrower URIs or paginated read tools. kb_diag_agent.yaml aligned with live knowledge_diag server v3.4.4: - Enabled search_kb (removed from disabled_tools) - Added get_doc_toc and read_chapter_page tool-schema-overlap rewrites - Added search_kb rewrite with methods (FULL/FAST/WIKI) param docs - Updated existing tool descriptions to reference page-based workflow Supersedes PR #393. * feat(mcp): resource subscribe-on-read wiring in McpServerCap Best-effort subscribe to resource URIs after successful read_resource() calls, enabling notifications/resources/updated for resources the agent has read. Tracked subscriptions are re-established on reconnect and cleaned up on disconnect. No-op for servers with subscribe:false (like knowledge_diag v3.4.4) — subscribe fails silently, read proceeds normally. Activates automatically when server enables subscription support. * fix(mcp): address PR #394 review — wire max_text_chars, fix broken test, consolidate truncation - Wire self._max_text_chars into read_mcp_resource (was hardcoded _DEFAULT_READ_TEXT_LIMIT) and use _truncate_text helper with guidance suffix - Remove dead _truncate_text static method from ResourceCapability (zero callers, old suffix format) - Add constructor validation: max_text_chars < 100 raises ValueError - Consolidate default constant: _DEFAULT_MAX_TEXT_CHARS in resource_resolver.py, aliased in resource_capability.py - Fix broken test assertion in test_resource_resolution.py to match new guidance suffix format - Add tests: max_text_chars validation, read_mcp_resource truncation with per-agent limit, suffix guidance text - Correct changelog: limit was previously hardcoded, not a pre-existing constructor param - Update capabilities/AGENTS.md: ResourceCapability is per-agent constructed, not registered at SESSION scope * ci: retrigger CI after transient setup-uv fetch failure
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.
问题
ResourceCapability.read_resource的文本截断上限 (max_text_chars=10 000) 硬编码在resolve_resource_content()中,超出部分尾部静默丢弃且模型无法取回。对知识库类 MCP 服务(如 knowledge_diag,章节资源可能超 10k 字符)会导致模型拿到残缺内容。改动
ResourceCapability.__init__新增max_text_chars参数(默认 10 000,向后兼容),存储为self._max_text_chars。read_resource工具将self._max_text_chars透传给resolve_resource_content()。[truncated: N chars total, showing first M]改为包含指引信息,引导模型使用更细粒度的资源 URI(章节/分片)或分页读取工具获取完整内容。验证
uv run ruff check✓uv run ruff format --check✓uv run --no-group docs mypy✓ (no issues)uv run pytest tests/capabilities/✓ (1434 passed, 1 skipped)关于 A1(cursor 分页)
调查中发现 FastMCP 的
Client.list_resources()/list_resource_templates()已内置 cursor 自动翻页(最多 250 页),AgentPool 调用的是 FastMCP Client 而非原始 SDK ClientSession,因此列表分页不存在 bug,无需修改。