Skip to content

fix(core): correlate streamed tool calls safely - #205

Merged
elkaix merged 10 commits into
mainfrom
fix/streamed-tool-call-correlation
Jul 15, 2026
Merged

fix(core): correlate streamed tool calls safely#205
elkaix merged 10 commits into
mainfrom
fix/streamed-tool-call-correlation

Conversation

@elkaix

@elkaix elkaix commented Jul 15, 2026

Copy link
Copy Markdown
Member

Related Issue

None — maintainer-directed implementation from the approved provider stream/tool-execution design.

Description

  • correlate interleaved streamed tool-call fragments by provider index/identity with deterministic missing-ID fallback
  • validate terminal stream state before any tool dispatch and roll back earlier futures if later dispatch fails
  • preserve OpenAI Responses response/item/call identity boundaries, normalize terminal statuses, and reject ResponseErrorEvent before tool callbacks
  • keep transport-only correlation metadata out of Message, Wire, and session serialization
  • retry stream protocol failures only when no observable output was published
  • replace payload-bearing trace logs with structural counts/types
  • document the approved architecture and remove the unavailable guard requirement

Verification

  • focused core stream suite: 160 passed before review follow-ups
  • focused root compatibility suite: 54 passed
  • make check-pythinker-core
  • make test-pythinker-core: 407 passed on the final review-fix commit
  • make check-pythinker-code
  • make test-pythinker-code: 6,938 unit tests passed; 65 e2e tests passed
  • git diff --check

Notes

  • structurally complete invalid JSON remains an execution-boundary ToolParseError
  • complete legacy tool streams without a provider finish reason remain supported intentionally
  • non-streamed missing-ID behavior is unchanged
  • no credentialed live-provider smoke test was run; the original intermittent GLM symptom should not be claimed fully resolved without a redacted transcript or live confirmation

Checklist

  • I have read the CONTRIBUTING document.
  • I have linked the related issue, if any (none for this maintainer-directed change).
  • I have added tests that prove the fix is effective.
  • I updated the canonical root changelog and regenerated the docs changelog with npm run sync from docs/.
  • I updated the applicable architecture documentation.

Summary by CodeRabbit

  • New Features
    • Improved streamed tool-call correlation for interleaved calls, including correct reassembly of fragmented arguments.
    • Added a unified stream-protocol error with clearer terminal-state details.
  • Bug Fixes
    • Malformed/truncated streams now stop before any tool execution and avoid unsafe retries after output is shown.
    • Prevented duplicate tool-call emissions; tool ordering and streaming metadata are preserved correctly while keeping transport metadata out of saved context.
  • Tests
    • Expanded async coverage for streaming behavior, provider compatibility, protocol errors, serialization/persistence, and retry recovery.

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1f9c799c-6c8d-41e7-a8be-ab791c71e1ec

📥 Commits

Reviewing files that changed from the base of the PR and between 667900e and 87c4c1d.

📒 Files selected for processing (2)
  • packages/pythinker-core/src/pythinker_core/contrib/chat_provider/openai_responses.py
  • packages/pythinker-core/tests/test_stream_tool_call_metadata.py

📝 Walkthrough

Walkthrough

Parallel streamed tool calls now preserve correlation metadata, assemble through validation, defer tool callbacks until successful completion, prevent execution after malformed streams, and retry only unpublished protocol errors. Documentation and changelog entries were updated.

Changes

Stream correlation and failure handling

Layer / File(s) Summary
Correlation contracts and message metadata
packages/pythinker-core/src/pythinker_core/chat_provider/__init__.py, message.py, stream_message_assembler.py
Adds protocol error metadata and hidden stream correlation fields for streamed tool calls.
Provider stream correlation
packages/pythinker-core/src/pythinker_core/contrib/chat_provider/*, packages/pythinker-core/src/pythinker_core/chat_provider/*
Preserves tool-call indexes and identifiers across fragments and normalizes typed terminal events.
Stream assembly and generation flow
packages/pythinker-core/src/pythinker_core/stream_message_assembler.py, _generate.py, packages/pythinker-core/tests/*
Validates and assembles streamed content and tool calls, then invokes callbacks after successful assembly.
Execution rollback and retry policy
packages/pythinker-core/src/pythinker_core/__init__.py, src/pythinker_code/soul/pythinkersoul.py, packages/pythinker-core/tests/test_step.py, tests/core/*
Prevents execution after malformed streams, cancels pending dispatches, suppresses stale callbacks, and retries only unpublished protocol errors.
Semantic persistence and wire validation
tests/core/test_context.py, tests/core/test_wire_message.py, packages/pythinker-core/tests/test_message.py
Verifies transport correlation metadata is omitted from persisted and serialized semantic tool-call data.

Process documentation updates

Layer / File(s) Summary
Guardrail and release documentation
AGENTS.md, plips/plip-10-lsp-system.md, CHANGELOG.md
Removes guardrail skill instructions, replaces the checklist requirement with a C01–C15 review, and records the streaming changes.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Provider as Stream provider
  participant Generate as generate
  participant Assembler as StreamMessageAssembler
  participant Step as step
  participant Soul as PythinkerSoul
  Provider->>Generate: stream correlated tool-call fragments
  Generate->>Assembler: assemble and validate fragments
  Assembler-->>Generate: finalized message or protocol error
  Generate->>Step: return result or error
  Step->>Step: dispatch tools only after successful assembly
  Soul->>Soul: retry only when output_published is false
Loading

Suggested labels: bug

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.86% which is insufficient. The required threshold is 70.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title follows conventional commits and accurately summarizes the streamed tool-call correlation change.
Description check ✅ Passed The description includes the required Related Issue, Description, and Checklist sections with the needed items covered.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/streamed-tool-call-correlation

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Jul 15, 2026

Copy link
Copy Markdown

Comment thread packages/pythinker-core/tests/test_step.py Fixed
Comment thread packages/pythinker-core/tests/test_step.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/pythinker-core/src/pythinker_core/_generate.py`:
- Around line 54-57: Update the tracing around chat_provider.generate and the
async stream loop to stop logging raw history, prompts, tool arguments, or
streamed part contents. Replace the history trace with safe metadata such as
item counts, and log only each received part’s type or other non-sensitive
structural metadata.

In
`@packages/pythinker-core/src/pythinker_core/contrib/chat_provider/openai_responses.py`:
- Around line 553-563: Update both function_call branches in openai_responses.py
to pass blank call_id values through instead of falling back to uuid.uuid4(),
allowing StreamMessageAssembler to assign the stable ID. Add a regression
covering an empty call_id in
packages/pythinker-core/tests/test_stream_tool_call_metadata.py at lines
315-352.

In `@packages/pythinker-core/src/pythinker_core/stream_message_assembler.py`:
- Around line 14-23: Remove None from _ALLOWED_TOOL_FINISH_REASONS so a
tool-call stream ending at clean EOF without a terminal reason fails closed.
Update packages/pythinker-core/tests/test_stream_message_assembler.py lines
158-168 to assert terminal_failure rather than successful finalization.

In `@packages/pythinker-core/tests/test_stream_tool_call_metadata.py`:
- Around line 315-352: Extend
test_openai_responses_uses_output_index_and_semantic_call_id with an empty
call_id scenario using two otherwise identical Responses streams. Collect both
streams and assert their final ToolCall IDs are identical, while preserving the
existing output_index and argument assertions.
- Around line 355-387: Extend the OpenAI Responses stream regression coverage
around OpenAIResponsesStreamedMessage to emit a ResponseErrorEvent after a
tool-call start, then assert generate() terminates before invoking on_tool_call.
Preserve the existing response/item ID assertions and use the stream’s
established terminal-error handling path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: f03f2947-c25e-46c4-90f4-f04d3d2790ce

📥 Commits

Reviewing files that changed from the base of the PR and between bdbe55b and 87d6ca7.

⛔ Files ignored due to path filters (2)
  • docs/en/release-notes/changelog.md is excluded by !docs/**
  • docs/superpowers/specs/2026-07-14-provider-stream-and-tool-execution-design.md is excluded by !docs/**
📒 Files selected for processing (21)
  • AGENTS.md
  • CHANGELOG.md
  • packages/pythinker-core/src/pythinker_core/__init__.py
  • packages/pythinker-core/src/pythinker_core/_generate.py
  • packages/pythinker-core/src/pythinker_core/chat_provider/__init__.py
  • packages/pythinker-core/src/pythinker_core/chat_provider/pythinker.py
  • packages/pythinker-core/src/pythinker_core/contrib/chat_provider/anthropic.py
  • packages/pythinker-core/src/pythinker_core/contrib/chat_provider/openai_legacy.py
  • packages/pythinker-core/src/pythinker_core/contrib/chat_provider/openai_responses.py
  • packages/pythinker-core/src/pythinker_core/message.py
  • packages/pythinker-core/src/pythinker_core/stream_message_assembler.py
  • packages/pythinker-core/tests/test_generate.py
  • packages/pythinker-core/tests/test_message.py
  • packages/pythinker-core/tests/test_step.py
  • packages/pythinker-core/tests/test_stream_message_assembler.py
  • packages/pythinker-core/tests/test_stream_tool_call_metadata.py
  • plips/plip-10-lsp-system.md
  • src/pythinker_code/soul/pythinkersoul.py
  • tests/core/test_context.py
  • tests/core/test_pythinkersoul_retry_recovery.py
  • tests/core/test_wire_message.py
💤 Files with no reviewable changes (1)
  • AGENTS.md

Comment thread packages/pythinker-core/src/pythinker_core/_generate.py Outdated
Comment thread packages/pythinker-core/tests/test_stream_tool_call_metadata.py
Comment thread packages/pythinker-core/tests/test_stream_tool_call_metadata.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/pythinker-core/src/pythinker_core/contrib/chat_provider/openai_responses.py (1)

578-586: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Make ResponseErrorEvent terminal.

ResponseErrorEvent can still be followed by a later terminal event, which overwrites "failed" and can let tool dispatch proceed. Stop consuming the stream after the error, and add a regression with a trailing completed event to keep the callback list empty.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/pythinker-core/src/pythinker_core/contrib/chat_provider/openai_responses.py`
around lines 578 - 586, Make ResponseErrorEvent terminal in the stream handling
logic by stopping consumption immediately after setting _finish_reason to
"failed", preventing later terminal events from overwriting the failure or
dispatching tools. Add a regression in
packages/pythinker-core/tests/test_stream_tool_call_metadata.py around the
affected test to include a trailing completed event and assert the callback list
remains empty.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In
`@packages/pythinker-core/src/pythinker_core/contrib/chat_provider/openai_responses.py`:
- Around line 578-586: Make ResponseErrorEvent terminal in the stream handling
logic by stopping consumption immediately after setting _finish_reason to
"failed", preventing later terminal events from overwriting the failure or
dispatching tools. Add a regression in
packages/pythinker-core/tests/test_stream_tool_call_metadata.py around the
affected test to include a trailing completed event and assert the callback list
remains empty.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 64e85701-1da1-46ba-9736-2a98cee672ed

📥 Commits

Reviewing files that changed from the base of the PR and between 87d6ca7 and 667900e.

📒 Files selected for processing (4)
  • packages/pythinker-core/src/pythinker_core/_generate.py
  • packages/pythinker-core/src/pythinker_core/contrib/chat_provider/openai_responses.py
  • packages/pythinker-core/tests/test_step.py
  • packages/pythinker-core/tests/test_stream_tool_call_metadata.py

@elkaix
elkaix merged commit dbc59bc into main Jul 15, 2026
38 checks passed
@elkaix
elkaix deleted the fix/streamed-tool-call-correlation branch July 17, 2026 19:41
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