Skip to content

StateAccessor omits fresh user input from ConversationState.messages #47

@fpoli

Description

@fpoli

I'm using @openrouter/agent@0.7.0 with a persistent StateAccessor, and I'm trying to understand whether the current persistence behavior is intended.

The docs say StateAccessor persists conversation state so “message history, and tool results survive across runs”, and the ConversationState.messages field is described as the full message history. The multi-run example also says each run appends its input and response to state history.

However, it seems that the fresh input passed to callModel() is not saved into ConversationState.messages. This might be a bug in the SDK.

From reading the built SDK output:

  • saveResponseToState() appends response.output
  • saveToolResultsToState() appends tool results
  • I do not see the fresh caller-supplied input appended to persisted state

So after a process restart, the loaded state can contain assistant/tool output from prior turns, but not the actual user messages that caused those turns.

Note that since the user messages are missing, depending on the model/provider, prompt caching can result in an unintented miss.

Sketch of an example:

const first = await openrouter.callModel({
  model,
  input: "The secred code is 'pizza'. Answer OK.",
  tools,
  state,
}).getText()

console.log("First answer:", first)
// First answer: OK.

const second = await openrouter.callModel({
  model,
  input: "What's the secret code?",
  tools,
  state,
}).getText()

console.log("Second answer:", first)
// Second answer: I don't have a secret code, and I'm not sure what you're referring to. Could you give me more context? I'm happy to help if you can clarify what you're looking for! 
Full example:
import {
  type ConversationState,
  OpenRouter,
  type StateAccessor,
  type Tool,
} from "@openrouter/agent"

let persisted: string | null = null
const state: StateAccessor<readonly Tool[]> = {
  load: async () =>
    persisted
      ? (JSON.parse(persisted) as ConversationState<readonly Tool[]>)
      : null,
  save: async (next) => {
    persisted = JSON.stringify(next)
  },
}

async function main(): Promise<void> {
  const apiKey = process.env.OPENROUTER_API_KEY
  if (!apiKey) throw new Error("Set OPENROUTER_API_KEY")

  const firstClient = new OpenRouter({ apiKey })
  const firstAnswer = await firstClient
    .callModel({
      model: "anthropic/claude-sonnet-4.6",
      input: [
        {
          role: "user",
          content: "The secred code is 'pizza'. Answer OK.",
        },
      ],
      state,
    })
    .getText()
  console.log("First answer:", firstAnswer)

  const secondClient = new OpenRouter({ apiKey })
  const secondAnswer = await secondClient
    .callModel({
      model: "anthropic/claude-sonnet-4.6",
      input: [{ role: "user", content: "What's the secret code?" }],
      state,
    })
    .getText()

  console.log("Second answer:", secondAnswer)

  console.log("\nPersisted messages:")
  console.dir(JSON.parse(persisted ?? "null")?.messages, { depth: null })
}

void main()

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions