fix: Auto-route tool call results to messages_history#1967
fix: Auto-route tool call results to messages_history#1967emincihangeri wants to merge 12 commits into
Conversation
SummaryThe following content is AI-generated and provides a summary of the pull request: Fix: Auto-Route Tool Call Results to
|
davidkna-sap
left a comment
There was a problem hiding this comment.
Just had a quick look, at a glance it mostly looks good.
There was a problem hiding this comment.
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) } |
There was a problem hiding this comment.
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.
| ? { ...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) | ||
| ]; |
There was a problem hiding this comment.
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.
| ]; | |
| 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
| // 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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
Co-authored-by: David Knaack <david.knaack@sap.com>
| { | ||
| "compilerOptions": { | ||
| "target": "ES2022", | ||
| "lib": ["es2023", "dom"], |
There was a problem hiding this comment.
| "lib": ["es2023", "dom"], | |
| "lib": ["es2023"], |
There was a problem hiding this comment.
this causes an error on weather-mcp-server.ts. should I work on it in this PR?
Co-authored-by: David Knaack <david.knaack@sap.com>
| 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); |
There was a problem hiding this comment.
I would prefer something like this (might have gotten names wrong):
const roles = templating!.map(m => m.role);
expect(roles).toEqual(['assistant', 'tool', 'user']);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']); |
There was a problem hiding this comment.
[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.
Context
Closes SAP/ai-sdk-js-backlog#471.
What this PR does and why it is needed