Skip to content

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

Open
emincihangeri wants to merge 12 commits into
mainfrom
investigate/tool-result-prompt-templating
Open

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

Conversation

@emincihangeri

Copy link
Copy Markdown
Contributor

Context

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

What this PR does and why it is needed

@hyperspace-insights

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-insights hyperspace-insights 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-insights hyperspace-insights 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
{
"compilerOptions": {
"target": "ES2022",
"lib": ["es2023", "dom"],

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.

Suggested change
"lib": ["es2023", "dom"],
"lib": ["es2023"],

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

this causes an error on weather-mcp-server.ts. should I work on it in this PR?

Comment thread .changeset/yummy-ducks-run.md Outdated
Comment on lines +345 to +349
const roles = templating!.map(m => m.role);
const toolIdx = roles.lastIndexOf('tool');
const userIdx = roles.lastIndexOf('user');
expect(toolIdx).toBeGreaterThan(-1);
expect(userIdx).toBeGreaterThan(toolIdx);

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.

I would prefer something like this (might have gotten names wrong):

const roles = templating!.map(m => m.role);
expect(roles).toEqual(['assistant', 'tool', 'user']);

InjunPark-sap and others added 2 commits July 3, 2026 14:35
Co-authored-by: David Knaack <david.knaack@sap.com>

const templating = response.getIntermediateResults().templating;
const roles = templating!.map(m => m.role);
expect(roles).toEqual(['assistant', 'tool', 'user']);

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] This test fails, please resove. The prompt.template with the system prompt is not being properly handled in the routing, and also consider disabling autorouting if something like that is provided.

https://github.com/SAP/ai-sdk-js/actions/runs/28664711694

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.

4 participants