Skip to content

fix(agent-core): harden tool_use/tool_result exchange integrity - #1340

Merged
RealKai42 merged 4 commits into
mainfrom
kaiyi/chennai
Jul 3, 2026
Merged

fix(agent-core): harden tool_use/tool_result exchange integrity#1340
RealKai42 merged 4 commits into
mainfrom
kaiyi/chennai

Conversation

@RealKai42

@RealKai42 RealKai42 commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Turn-end teardown for abandoned tool exchanges: a turn that died between a recorded tool.call and its paired tool.result (e.g. a transcript write failure mid-batch) left pendingToolResultIds open forever — every later message was stranded in deferredMessages and user input was silently swallowed. runOneTurn now defensively closes dangling calls on every exit path, synthesizing an error result that names the cause (cancelled / failed + error), with a warn log and a tool_exchange_abandoned telemetry event. The teardown is itself guarded so a still-broken persistence layer cannot escalate.
  • Duplicate tool_use id recovery via the strict resend: a provider that emits two tool_use blocks with the same id (buggy providers, or per-response counter ids like call_0) produces a request strict providers reject with "tool_use ids must be unique" — previously unrecoverable. The strict-resend projection now dedupes duplicate call ids (first occurrence wins) together with their extra recorded results so no dangling tool message survives, and the error classifier recognizes the uniqueness 400 so it actually triggers the strict resend. The normal projection intentionally keeps duplicates verbatim: the lax provider that produced them accepts them, and deduping there would silently erase its later tool exchanges.
  • Observability for resume-side synthesis: closePendingToolResults now reports what it closes — warn for a mid-history gap (results lost before a step boundary, worth investigating), info for the routine trailing interruption closed at end of resume.
  • Minor built-in prompt and tool-result wording adjustments.

Test plan

  • Both fixes were reproduced with failing tests first:
    • a persistence append that throws once on the first tool.result record shows the turn failing mid-batch and a follow-up user message being swallowed; the teardown closes both dangling calls and the message lands in history
    • projector tests cover: normal path preserving duplicates verbatim; strict flag dropping a duplicate within one assistant message, across assistant messages together with its recorded result, reattaching a surviving result when the kept call has none, and dropping an assistant message left empty; plus a strictMessages wiring test and a classifier test for the uniqueness 400
  • pnpm typecheck clean in agent-core and kosong; full suites green (agent-core 3436 passed / 215 files, kosong 1137 passed / 48 files).

RealKai42 added 2 commits July 3, 2026 13:13
…d tool calls

- system.md: add a diagnose-before-retrying paragraph next to the existing
  permission-denial guidance, covering failed tool calls
- permission: when the user rejects an approval on the main agent, tell the
  model not to re-attempt the exact same call (sub agents already had an
  equivalent hint)
…tool_use ids

A turn that dies between a recorded tool.call and its paired tool.result
(e.g. a transcript write failure mid-batch) used to leave
pendingToolResultIds open forever: every later message was stranded in
deferredMessages and user input was silently swallowed.

- runOneTurn now defensively closes any dangling tool calls when a turn
  ends (completed, cancelled, or failed), synthesizing an error result
  that names the cause, with a warn log and a tool_exchange_abandoned
  telemetry event
- the projector drops assistant tool calls whose id already appeared
  earlier (first occurrence wins): a duplicate id is wire-invalid on
  strict providers and not repairable by the strict resend; reported via
  the existing projection-repair log and telemetry
- resume-side closePendingToolResults now logs what it closes (warn for
  a mid-history gap, info for the routine trailing interruption)
@changeset-bot

changeset-bot Bot commented Jul 3, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 7335d49

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@moonshot-ai/kimi-code Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new

pkg-pr-new Bot commented Jul 3, 2026

Copy link
Copy Markdown
pnpm dlx https://pkg.pr.new/@moonshot-ai/kimi-code@7335d49
npx https://pkg.pr.new/@moonshot-ai/kimi-code@7335d49

commit: 7335d49

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c8633ca62d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +217 to +219
if (seenToolCallIds.has(toolCall.id)) {
onAnomaly?.({ kind: 'duplicate_tool_call_dropped', toolCallId: toolCall.id });
return false;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep results aligned when dropping duplicate calls

When a provider reuses a toolCall.id in a later step/turn and that later call records its own tool.result, this filter removes only the later assistant tool_use; the later result is still kept because dropOrphanToolResults checks only whether the id exists anywhere in the request. The projected history can therefore contain a tool message whose originating assistant call was removed (or have that result associated with the earlier call), so strict resend can still be rejected or show the model the wrong tool outcome; the duplicate call's paired result needs to be dropped/remapped along with the call.

Useful? React with 👍 / 👎.

@RealKai42 RealKai42 changed the title fix(agent-core): harden tool exchange integrity and add anti-repeat prompt guidance fix(agent-core): harden tool_use/tool_result exchange integrity Jul 3, 2026
@RealKai42

Copy link
Copy Markdown
Collaborator Author

@codex

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5316887bad

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +217 to +219
if (seenToolCallIds.has(toolCall.id)) {
onAnomaly?.({ kind: 'duplicate_tool_call_dropped', toolCallId: toolCall.id });
return false;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Drop results for removed duplicate calls

When a provider reuses a tool call id in a later assistant step after the first exchange already has a result, this filter removes the later tool_use but leaves that step's tool result in the message list. dropOrphanToolResults only checks for any matching call id, so the second tool message is kept and strict-provider conversion can still send extra tool_result blocks for the single remaining tool_use, leaving the repaired request invalid. Please also drop/de-dupe tool results for removed duplicate ids, or keep the call/result pair you intend to preserve.

Useful? React with 👍 / 👎.

Unconditional dedup regressed providers that emit per-response counter
ids (e.g. call_0 in every step) and accept their own duplicates: later
tool exchanges silently vanished from the projected history, and a
duplicate call's own recorded result was left dangling.

- the dedupe pass is now opt-in via dedupeDuplicateToolCalls and enabled
  only in strictMessages, so the normal projection keeps the history the
  provider produced
- the pass also drops every tool result after the first for an id, so no
  dangling tool message survives; when the kept call has no result of
  its own, the surviving one is reattached by the adjacency repair
- kosong now classifies the Anthropic "tool_use ids must be unique" 400
  as a recoverable request-structure error so it triggers the strict
  resend

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7335d49de4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +853 to +855
const closed = this.agent.context.closeAbandonedToolExchange(
abandonedToolResultOutput(ended),
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Route abandoned results through the dispatcher

When a tool.result dispatch throws after tool.call.started has already been emitted (the scenario this repair targets), this path appends synthetic results directly to ContextMemory. Because it bypasses buildDispatchEvent/mapLoopEvent, SDK/TUI listeners never receive the matching tool.result; event consumers see an unterminated tool call until replay even though history was repaired. Please publish the same synthetic result events, or run the closed ids through the normal dispatcher.

Useful? React with 👍 / 👎.

@RealKai42
RealKai42 merged commit e2fe62a into main Jul 3, 2026
10 checks passed
@RealKai42
RealKai42 deleted the kaiyi/chennai branch July 3, 2026 06:26
@github-actions github-actions Bot mentioned this pull request Jul 3, 2026
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