Skip to content

fix: Auto-route tool call results to messages_history - #1967

Draft
emincihangeri wants to merge 58 commits into
mainfrom
investigate/tool-result-prompt-templating
Draft

fix: Auto-route tool call results to messages_history#1967
emincihangeri wants to merge 58 commits into
mainfrom
investigate/tool-result-prompt-templating

Conversation

@emincihangeri

@emincihangeri emincihangeri commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Context

Closes SAP/ai-sdk-js-backlog#471.

What this PR does and why it is needed

Taken over from @emincihangeri by @InjunPark-sap

@hyperspace-pr-bot

Copy link
Copy Markdown
Contributor

Summary

The following content is AI-generated and provides a summary of the pull request:


Fix: Auto-Route Tool Call Results to messages_history

Bug Fix

🐛 Tool call results passed via messages could contain {{?...}} patterns from external systems (e.g., search results, API responses) that would be incorrectly interpreted as prompt template placeholders. This fix automatically routes tool messages — and any messages preceding them — to messages_history, bypassing prompt templating entirely while keeping the assistant+tool message pair intact for tool_call_id validation.

Changes

  • packages/orchestration/src/util/module-config.ts: Replaced the simple routeMessagesToHistory boolean flag with a splitIndex calculation. Messages up to and including the last tool message are routed to messages_history; remaining messages stay in prompt.template. The messagesHistory build logic is simplified to always spread messages.slice(0, splitIndex) after any existing messagesHistory.

  • packages/orchestration/src/orchestration-completion-post-request.test.ts: Added a tool message auto-routing test suite covering:

    • Routing tool messages from messages to messages_history
    • Preserving existing messagesHistory when appending tool messages
    • Ensuring non-tool messages are unaffected
  • sample-code/src/orchestration.ts: Added two new sample functions:

    • orchestrationToolResultInMessages — demonstrates auto-routing of tool results containing {{?...}} syntax
    • orchestrationToolResultMaskingInMessagesHistory — verifies masking is still applied to auto-routed tool results
  • sample-code/src/index.ts: Exported the two new sample functions.

  • tests/e2e-tests/src/orchestration.test.ts: Added E2E tests for both new sample functions, asserting successful responses and that masking intermediates are present when expected.


  • 🔄 Regenerate and Update Summary
  • ✏️ Insert as PR Description (deletes this comment)
  • 🗑️ Delete comment
PR Bot Information

Version: 1.26.11

@emincihangeri emincihangeri changed the title fix: Auto-route tool call results to messages_history chore: Auto-route tool call results to messages_history Jun 25, 2026
@davidkna-sap davidkna-sap changed the title chore: Auto-route tool call results to messages_history fix: Auto-route tool call results to messages_history Jun 25, 2026

@davidkna-sap davidkna-sap left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Just had a quick look, at a glance it mostly looks good.

Comment thread packages/orchestration/src/util/module-config.ts Outdated
Comment thread packages/orchestration/src/util/module-config.ts Outdated

@hyperspace-pr-bot hyperspace-pr-bot 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.

The PR introduces auto-routing of tool-call result messages to messages_history to prevent {{?...}} content in tool responses from being misinterpreted as prompt template placeholders. There are two logic issues: the TemplateRef branch now passes an empty [] as messages into moduleRequest instead of undefined (a behavioural change from before), and the unconditional messagesHistory array construction silently changes behaviour for callers that supply only messagesHistory with no tool messages — messages_history is now always emitted even when neither condition previously triggered. Please also address the two existing reviewer comments about findLastIndex and the messagesHistory spread condition.

PR Bot Information

Version: 1.26.11

  • Correlation ID: 319e6368-3269-4852-83ae-651353e8e144
  • File Content Strategy: Full file content
  • Event Trigger: issue_comment.created
  • LLM: anthropic--claude-4.6-sonnet

routeMessagesToHistory && request
? { ...request, messages: undefined }
splitIndex > 0 && request
? { ...request, messages: messages.slice(splitIndex) }

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.

Bug: When usesTemplateRef is true, splitIndex is set to messages.length, and messages.slice(0, splitIndex) equals the full messages array. This means all messages are moved to messages_history, and moduleRequest.messages becomes an empty array (messages.slice(messages.length)[]). However, buildCompletionModulesConfig will then throw 'Either a prompt template or messages must be defined.' if the template is also empty — the same error that existed before this refactor when using a TemplateRef (the old code set messages: undefined). The real regression is that moduleRequest.messages is now [] instead of undefined, and buildCompletionModulesConfig checks !request?.messages?.length — so an empty array still passes that guard safely. But passing an empty messages array into prompt.template is a behavioral change from before (previously messages was undefined so nothing was spread). Verify this is intentional; if not, use messages: undefined for the TemplateRef branch.

Suggested change
? { ...request, messages: messages.slice(splitIndex) }
? { ...request, messages: messages.slice(splitIndex).length ? messages.slice(splitIndex) : undefined }
: request;

Double-check suggestion before committing. Edit this comment for amendments.


Please provide feedback on the review comment by checking the appropriate box:

  • 🌟 Awesome comment, a human might have missed that.
  • ✅ Helpful comment
  • 🤷 Neutral
  • ❌ This comment is not helpful

const messagesHistory = [
...(request?.messagesHistory || []),
...messages.slice(0, splitIndex)
];

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.

Logic Error: When splitIndex === 0 and request?.messagesHistory is non-empty, messagesHistory will be a non-empty array (just the existing history), but the condition messagesHistory.length will correctly include it. However, the existing comment already flags the related issue at line 415 — the suggestion there restores the pre-refactor behaviour of preserving request.messagesHistory even when no tool-message splitting occurs. Without that fix, a call with only messagesHistory (no messages with tool results) will still include messages_history in the output, which is a change from the old behaviour where messagesHistory was only spread when the routing condition was true. This is a silent breaking change for callers that rely on messages_history not being emitted.

Suggested change
];
const messagesHistory =
splitIndex > 0 || request?.messagesHistory?.length
? [...(request?.messagesHistory || []), ...messages.slice(0, splitIndex)]
: undefined;

Double-check suggestion before committing. Edit this comment for amendments.


Please provide feedback on the review comment by checking the appropriate box:

  • 🌟 Awesome comment, a human might have missed that.
  • ✅ Helpful comment
  • 🤷 Neutral
  • ❌ This comment is not helpful

@hyperspace-pr-bot hyperspace-pr-bot Bot deleted a comment from davidkna-sap Jun 25, 2026
// 1. TemplateRef configs — the template lives remotely, so messages cannot be merged in.
// 2. Tool results — their content comes from external systems and may contain {{?...}}
// patterns that are not user-defined placeholders. We route through the last tool
// message so the assistant+tool pair stays together for tool_call_id validation.

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.

You are changing the initial intent and semantic of a content behind the scene. The user is not even aware of it and has no way to control it.
What is the rationale behind such a debatable decision?
How can you guarantee that a LLM will behave the same way if a content is moved from a designed container for an intent to another one with another intent?
The whole point of an AI SDK is to interface with an API behind an structured abstraction and not to play with the specified structure of the abstraction.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Message history and prompt messages are joined and both sent to the LLM. The primary impact of moving the messages is that it changes how orchestration processes the messages.

@jerome-benoit jerome-benoit Jun 26, 2026

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.

Message history and prompt messages are joined and both sent to the LLM. The primary impact of moving the messages is that it changes how orchestration processes the messages.

Where in the context window? The chronological ordering is altered or not? What is the diff of the context window sent to the LLM with or without it? The placement of content in the context window matters, really matters.

Again, why the API is not fixed properly instead of forcing its consumer to workaround it? It's a simple boolean flag triggering two different code paths.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Assuming the split is done properly, the LLM context window will not change (for this PR I haven't done in-depth checks if this is the case here yet). If this is not the case we may have to re-evaluate doing this, and explore alternatives such as documentation changes, as the service rejected your proposed changes. I would also be hesistant to add escaping to avoid influencing the model with invisible unicode characters.

Anyway in some cases is already message history after consultation with the service.

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.

and explore alternatives such as documentation changes, as the service rejected your proposed changes.

But I think it should be put again on the table by people working directly on AI tooling at SAP in a better way that could lead to a proper fix.

I would also be hesistant to add escaping to avoid influencing the model with invisible unicode characters.

I agree that inserting invisible unicode characters is an uglier workaround than moving the culprit content to a place without template syntax interpretation if the moving it not altering the chronological content in the context window, or at least at a minimal level.

Comment thread tsconfig.json Outdated
Comment thread .changeset/yummy-ducks-run.md Outdated
Comment thread tests/e2e-tests/src/orchestration.test.ts Outdated
InjunPark-sap and others added 2 commits July 3, 2026 14:35
Co-authored-by: David Knaack <david.knaack@sap.com>
Comment thread tests/e2e-tests/src/orchestration.test.ts Outdated
- Skip tool-message auto-routing to messages_history when any config
  has a non-empty prompt.template (static template = user opted into
  templating, service handles it correctly without interference)
- Move lib: [es2023] from root tsconfig.json to packages/orchestration/tsconfig.json
  to resolve merge conflict with main's tsconfig restructuring
- Update unit tests to use a no-template config for auto-routing cases
  and add a test asserting auto-routing is disabled with static templates
- Remove static system prompt from orchestrationToolResultInMessages sample
  so auto-routing is actually exercised in e2e
prompt.tools (without template) still means the user opted into
orchestration's prompt handling — auto-routing tool messages to
messages_history in this case leaves messages empty and throws
'Either a prompt template or messages must be defined'.

Extend hasStaticPrompt check to cover prompt.tools so that
multi-turn tool-calling flows (orchestrationMessageHistoryWithToolCalling)
are not broken by the auto-routing logic.
Comment thread packages/orchestration/src/util/module-config.ts Outdated
Comment thread packages/orchestration/src/util/module-config.ts Outdated
Comment thread .changeset/yummy-ducks-run.md Outdated
Comment thread packages/orchestration/src/util/module-config.ts Outdated
@InjunPark-sap

Copy link
Copy Markdown
Member

This CodeQL failure (v4.37.0 config vs v4.36.3 runner) is a pre-existing issue in main unrelated to this PR — it was introduced before this branch was updated.

@InjunPark-sap

Copy link
Copy Markdown
Member

@davidkna-sap
Your feedbacks are applied and also e2e test is passed: "https://github.com/SAP/ai-sdk-js/actions/runs/29415289767"
Please let me know if there is anything to do.

@davidkna-sap davidkna-sap left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please avoid large-scale changes in the final stages of the review changes without justification, as these may (re)introduce new issues and increases review burden.

I believe the issues raised on 780ad79 could be solved with some minor adjustments. If you disagree I would be open to discuss this.

@InjunPark-sap

Copy link
Copy Markdown
Member

Replaced the splitIndex approach with a minimal extension of 780ad79: extracted routing into shouldRouteMessagesToHistory() and added role === 'tool' detection to route all messages to messages_history when tool results are present.

Also fixed two issues found during review:

  • prompt.template: [] caused service rejection with tools-only configs — now removed when empty
  • Message duplication when both messages and messagesHistory are provided — now only appended when routing is active

@InjunPark-sap InjunPark-sap self-assigned this Jul 16, 2026

@davidkna-sap davidkna-sap left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Putting this on hold.

The auto-routing is skipped whenever prompt.tools is set, which is exactly the scenario that produces tool call results, so the fix is inert in the real tool-calling flow.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[req] Make sure this is up to date.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Updated

Comment thread packages/orchestration/src/util/module-config.ts Outdated
Comment thread packages/orchestration/src/util/module-config.ts Outdated
Comment on lines +476 to +485
if (
!!request?.placeholderValues &&
Object.keys(request.placeholderValues).length > 0
) {
return false;
}

if (configs.some(c => isTemplateRef(c?.promptTemplating?.prompt || {}))) {
return true;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[req] Swap these.

Comment thread tsconfig.base.json Outdated
Comment on lines 3 to 14

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[req] Remove duplicate lib, and it is now possible to replace this with target: "ES2024" fully.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@davidkna-sap
Tried target: "ES2024" but the check-public-api CI tool doesn't recognize ES2024 and falls back to ES2021, causing compilation errors in packages/core. Used target: "ES2022" with lib: ["es2024"] as a compromise — removes the duplicate lib and enables ES2024 type definitions. Let me know if there's a preferred approach.

@davidkna-sap
davidkna-sap marked this pull request as draft July 17, 2026 11:14
@davidkna-sap davidkna-sap added blocked Issue or PR blocked due to other issues. don't merge labels Jul 17, 2026
@InjunPark-sap

Copy link
Copy Markdown
Member

The current fix covers routing for the no-prompt and TemplateRef cases, but you're right that it doesn't address the core tool-calling scenario where prompt.tools is set — routing is skipped there to avoid the service's template required constraint.

Looking forward to the service-side requirement being lifted so we can extend routing to cover that case as well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

blocked Issue or PR blocked due to other issues. don't merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants