Skip to content

fix(gateway): ignore tool-output blocks whose media type is not a string - #1871

Open
JasonBuildAI wants to merge 1 commit into
TencentCloud:developfrom
JasonBuildAI:fix/tool-media-nonstr-type
Open

JasonBuildAI wants to merge 1 commit into
TencentCloud:developfrom
JasonBuildAI:fix/tool-media-nonstr-type

Conversation

@JasonBuildAI

@JasonBuildAI JasonBuildAI commented Oct 10, 2026 •

Copy link
Copy Markdown

Summary

GET /api/agents/{agent_id}/threads/{thread_id}/history returns 500 when a tool output in the thread carries a JSON block whose type is not a string, e.g. [{"type": {"kind": "image"}}, {"text": "plain"}] (Fixes #1869).

tool_media.py tests block type values for membership in _MEDIA_BLOCK_TYPES (a frozenset). A dict or list value is unhashable, so the test itself raises:

File "src/octop/api/routers/chat/serialize.py", line 575, in _enrich_history_tool_media
    new_output = enrich_tool_output_string_sync(output, agent_id=media_agent)
File "src/octop/infra/gateway/media/tool_media.py", line 346, in enrich_media_block_preview
    if not isinstance(block, dict) or block.get("type") not in _MEDIA_BLOCK_TYPES:
TypeError: unhashable type: 'dict'

Media preview rewriting is presentational, but the exception escapes _enrich_history_tool_media, so one bad row makes the whole thread unreadable in the dashboard. Two of the four sites are on live paths, so the same payload can also break streaming tool-result enrichment (_enrich_block_with_backend) and IM media delivery (block_to_content_part).

Target branch

  • Base is develop (feature / fix - default)
  • Base is main (release/* or hotfix/* only)

Type of change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation
  • Refactor / chore
  • Release / hotfix

How it is fixed

  • Added _media_block_type(block): returns block["type"] only when it is a string present in _MEDIA_BLOCK_TYPES, otherwise "".
  • Replaced the four bare membership tests (iter_media_blocks, enrich_media_block_preview, _enrich_block_with_backend, block_to_content_part) with that helper, so a non-media block is skipped instead of raising.
  • _enrich_history_tool_media now falls back to the raw tool output and logs a warning when preview rewriting fails (the issue's second suggestion). That guard is not hypothetical: even with the membership test fixed, a media block whose URL holds a lone surrogate (file:///tmp/\ud800.png) still raises UnicodeEncodeError from urllib.parse.quote on the very same request path, so the endpoint stays 500-prone without it.

Test plan

Both failure modes reproduced first, then re-checked after the change.

  1. Non-string block type (the reported bug):
$ uv run python -c "from octop.infra.gateway.media.tool_media import enrich_media_block_preview as f; f({'type': {'kind': 'image'}}, agent_id='x')"
TypeError: unhashable type: 'dict'
  1. Unencodable media URL on the same request path - the new guard test fails without it:
$ uv run pytest tests/unit/api/test_chat_polish.py -q -k unencodable --tb=line
E   UnicodeEncodeError: 'utf-8' codec can't encode character '\ud800' in position 12: surrogates not allowed
...\urllib\parse.py:921: UnicodeEncodeError
FAILED tests/unit/api/test_chat_polish.py::test_load_projected_survives_unencodable_media_url
1 failed, 34 deselected

New tests (all failing on the unmodified tree, passing after):

  • uv run pytest tests/unit/gateway/test_tool_media.py tests/unit/api/test_chat_polish.py -q -> 71 passed
    • test_media_blocks_ignore_non_string_type (object / list / int / null type) covers iter_media_blocks + enrich_media_block_preview
    • test_enrich_sync_tolerates_non_string_block_type covers the history preview string path
    • test_enrich_tool_result_tolerates_non_string_block_type covers live streaming enrichment
    • test_block_to_content_part_ignores_non_string_type covers IM media delivery
    • test_load_projected_tolerates_non_string_tool_block_type and test_load_projected_survives_unencodable_media_url go through _load_projected_thread_messages, i.e. exactly the request path that 500s

Ship bar - this clone has no make on Windows, so the Makefile targets were run as their underlying commands:

  • uv run ruff check src tests -> All checks passed!
  • uv run ruff format --check src tests -> 1168 files already formatted
  • uv run mypy src/octop -> Success: no issues found in 565 source files
  • uv run pytest -n auto -q tests/unit/api tests/unit/gateway -> 767 passed, 12 skipped
  • uv run pytest -n auto -m "not live" -q -> 51 failed, 4333 passed, 157 skipped

Those 51 failures are pre-existing on develop in this environment, not caused by the change: a clean baseline run of the identical command with the change stashed gives the same failure set (52 failed, 4324 passed, 157 skipped), and the only delta is tests/unit/gateway/test_versioned_history.py::test_process_exit_after_commit_keeps_last_fragment, which fails under xdist in the baseline run, passes with the change, and also passes standalone here. Most of them are UnicodeDecodeError from tests reading files / subprocess output without an explicit encoding on this GBK-locale Windows host.

Checklist

  • make all passes locally (commands above; make is unavailable in this Windows clone)
  • Added/updated tests
  • Updated CHANGELOG.md
  • README / docs updated (not needed)

AI disclosure: this change was drafted with AI assistance at my request, under my direction and review.

A tool result can carry JSON where a block's "type" is an object or list.
Membership tests against _MEDIA_BLOCK_TYPES then raise
TypeError: unhashable type: 'dict', which 500s the thread history endpoint
and can also break live tool-result enrichment and IM media delivery.

Route every membership test through a helper that only accepts string types.
History preview rewriting also keeps the raw tool output and logs a warning
when it fails for any other reason (e.g. a media URL that cannot be quoted),
so cosmetic enrichment can no longer fail the whole history page.

Fixes TencentCloud#1869
@JasonBuildAI
JasonBuildAI force-pushed the fix/tool-media-nonstr-type branch from 696161d to 32322ea Compare October 15, 2026 17:38

This branch has not been deployed

No deployments
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.

1 participant