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()
I'm using
@openrouter/agent@0.7.0with a persistentStateAccessor, and I'm trying to understand whether the current persistence behavior is intended.The docs say
StateAccessorpersists conversation state so “message history, and tool results survive across runs”, and theConversationState.messagesfield 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
inputpassed tocallModel()is not saved intoConversationState.messages. This might be a bug in the SDK.From reading the built SDK output:
saveResponseToState()appendsresponse.outputsaveToolResultsToState()appends tool resultsSo 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:
Full example: