Skip to content

Conversation

@a-rebets
Copy link

Problem:

When a tool execution throws an error, the assistant message containing the tool-call is not saved to the database. Only the tool-result message with the error is persisted, breaking the message chain.

Root Cause:

In src/mapping.ts, the serializeNewMessagesInStep function uses step.toolResults.length to determine which messages to save:

const messages: MessageWithMetadata[] = await Promise.all(
  (step.toolResults.length > 0
    ? step.response.messages.slice(-2)  // assistant + tool messages
    : step.content.length
      ? step.response.messages.slice(-1)  // Only last message
      : [{ role: "assistant" as const, content: [] }]
  ).map(...)
);

When a tool fails:

  • step.toolResults is empty (no successful execution result)
  • step.response.messages contains both the assistant message with tool-call AND the tool message with error
  • The code incorrectly takes only the last message, missing the assistant message

What gets saved to the database (example):

[
  {
    content: "list the datasets",
    role: "user",
  },
  // MISSING: assistant message with tool-call
  {
    content: [
      {
        output: {
          type: "error-text",
          value: "Your request couldn't be completed. Try again later.",
        },
        toolCallId: "call_somehash",
        toolName: "datasets_listDatasetsTool",
        type: "tool-result",
      },
    ],
    role: "tool",
  },
]

After all of this we see warnings like:

[WARN] 'Tool result without preceding tool call.. adding anyways'

Proposed Fix:

Check if there are tool messages in step.response.messages instead of relying solely on the length


By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@a-rebets
Copy link
Author

Could be related to #162

@a-rebets
Copy link
Author

Adjusted the fix, can confirm in my DB the history is now saved properly:

[
	{
	  content: "so what datasets do I have?",
	  role: "user",
	},
	{
	  content: [
	    {
	      text: `**Executing dataset tool**
	
	I need to call the datasets_listDatasetsTool since this request is actionable. I’ll make sure to ...`,
	      type: "reasoning",
	    },
	    {
	      args: {},
	      toolCallId: "call_3GrNAa0RsNexZ7F3D2frl1kN",
	      toolName: "datasets_listDatasetsTool",
	      type: "tool-call",
	    },
	  ],
	  role: "assistant",
	},
	{
	  content: [
	    {
	      output: {
	        type: "error-text",
	        value: "This is a test!!!",
	      },
	      toolCallId: "call_3GrNAa0RsNexZ7F3D2frl1kN",
	      toolName: "datasets_listDatasetsTool",
	      type: "tool-result",
	    },
	  ],
	  role: "tool",
	},
	{
	  content: [
	    {
	      text: `**Clarifying tool response**
	
	The tool returned an unusual message: "This is a test!!!"...`,
	      type: "reasoning",
	    },
	    {
	      text: `I checked the saved datasets — there are no usable datasets found.`,
	      type: "text",
	    },
	  ],
	  role: "assistant",
	}
]

const toolFields = { sources: step.sources };
const messages: MessageWithMetadata[] = await Promise.all(
(step.toolResults.length > 0
(step.finishReason === "tool-calls"
Copy link
Contributor

Choose a reason for hiding this comment

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

in the case of error, is the finishReason tool-calls?
My understanding of tool-calls finishReason was that it was the signal that there were tool calls that need response, e.g. when you have a tool with no execute function, it will finish with tool-calls but there will only be one response message - the assistant response.

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.

2 participants