Cursor SDK agent.send() does not retain conversation context across runs

Cursor SDK Issue: agent.send() does not retain conversation context across runs

Summary

When using @cursor/sdk with local agents, calling agent.send() multiple times on the same agent does not retain conversation context between runs. Each send() creates an isolated run with no awareness of previous messages, despite the agent being persistent (same agentId).

Environment

  • @cursor/sdk versions tested: v1.0.12, v1.0.11, v1.0.7 — all exhibit the same behavior
  • Node.js: v22.x
  • OS: Linux (Ubuntu, remote SSH host)
  • Agent type: local agent

Expected Behavior

Based on the SDK documentation, Agent.create() creates a durable agent, and subsequent agent.send() calls should maintain conversation context. The agent should be aware of previous messages and responses from earlier send() calls within the same agent.

Actual Behavior

Each agent.send() call creates a new, independent run. The agent has no memory of previous runs:

  • run.conversation() consistently returns turns=1 for each run
  • User message content shows as "(none)" in the conversation turns
  • The agent cannot recall any information from previous send() calls (e.g., a name told in the first message is unknown in the second)

Minimal Reproduction Script

#!/usr/bin/env node

/**
 * Minimal reproduction: @cursor/sdk agent.send() does not retain context across runs.
 *
 * Expected: Turn 2 should recall the name "Alice" from Turn 1.
 * Actual:   Turn 2 has no awareness of Turn 1.
 *
 * Usage:
 *   CURSOR_API_KEY=crsr_xxx node repro-context-loss.mjs
 */

import { Agent } from '@cursor/sdk'

const API_KEY = process.env.CURSOR_API_KEY
if (!API_KEY) {
  console.error('CURSOR_API_KEY is required')
  process.exit(1)
}

async function collectText(run) {
  let text = ''
  for await (const event of run.stream()) {
    if (event.type === 'assistant') {
      for (const block of event.message?.content ?? []) {
        if (block.type === 'text') text += block.text
      }
    }
  }
  await run.wait()
  return text
}

async function main() {
  console.log('Creating agent...')
  const agent = await Agent.create({
    apiKey: API_KEY,
    model: { id: 'claude-sonnet-4' },
    local: { cwd: process.cwd(), settingSources: [] },
  })
  console.log(`Agent created: ${agent.agentId}`)

  // ── Turn 1: establish context ──
  console.log('\n── Turn 1 ──')
  const run1 = await agent.send('My name is Alice. Please remember this. Reply with: "Got it, Alice."')
  const text1 = await collectText(run1)
  console.log(`Response: ${text1}`)

  // Inspect run1 conversation
  const conv1 = await run1.conversation()
  console.log(`run1.conversation(): turns=${conv1.length}`)

  // Check runs on the agent
  const runs1 = await Agent.listRuns(agent.agentId)
  console.log(`Agent runs after Turn 1: ${runs1.length}`)

  // ── Turn 2: test recall ──
  console.log('\n── Turn 2 ──')
  const run2 = await agent.send('What is my name? Just reply with the name, nothing else.')
  const text2 = await collectText(run2)
  console.log(`Response: ${text2}`)

  // Inspect run2 conversation
  const conv2 = await run2.conversation()
  console.log(`run2.conversation(): turns=${conv2.length}`)

  const runs2 = await Agent.listRuns(agent.agentId)
  console.log(`Agent runs after Turn 2: ${runs2.length}`)

  // ── Result ──
  const remembered = text2.toLowerCase().includes('alice')
  console.log(`\nResult: ${remembered ? '✓ PASS' : '✗ FAIL'} — Agent ${remembered ? 'remembered' : 'did NOT remember'} the name from Turn 1`)

  if (!remembered) {
    console.log('\nDiagnostic details:')
    console.log(`  run1 conversation turns: ${conv1.length}`)
    console.log(`  run2 conversation turns: ${conv2.length}`)
    console.log(`  Total runs on agent: ${runs2.length}`)
    console.log('  Each send() creates an isolated run with no context from prior runs.')
  }

  agent.close()
  process.exit(remembered ? 0 : 1)
}

main().catch(err => {
  console.error('Script crashed:', err)
  process.exit(1)
})

Reproduction Steps

  1. Install the SDK:

    npm install @cursor/sdk@latest
    
  2. Run the script with your API key:

    CURSOR_API_KEY=crsr_your_key_here node repro-context-loss.mjs
    
  3. Observe: Turn 2 response does NOT contain “Alice” — the agent has no memory of Turn 1.

Diagnostic Output (from our testing)

── Turn 1 ──
Response: Got it, Alice. I've noted your name. How can I help you?
run1.conversation(): turns=1           # ← only 1 turn, not accumulated
Agent runs after Turn 1: 1

── Turn 2 ──
Response: I don't have access to any previous conversation...
run2.conversation(): turns=1           # ← still only 1 turn, no context from run1
Agent runs after Turn 2: 2             # ← each send() creates a new run

Result: ✗ FAIL — Agent did NOT remember the name from Turn 1

Key observations:

  • run.conversation() always returns exactly 1 turn per run
  • User message content in the conversation shows as "(none)"
  • Agent.listRuns() confirms each send() creates a separate, independent run
  • The behavior is identical across SDK versions v1.0.12, v1.0.11, and v1.0.7

Workaround

We implemented manual conversation history injection — prepending formatted history to each agent.send() prompt:

// Before each send(), build a prompt with conversation history
function buildPromptWithHistory(history, newMessage) {
  if (history.length === 0) return newMessage

  const formatted = history
    .map(t => `${t.role === 'user' ? 'User' : 'Assistant'}: ${t.content}`)
    .join('\n\n')

  return `<conversation_history>\n${formatted}\n</conversation_history>\n\nBased on the conversation above, respond to this new message:\n\n${newMessage}`
}

// After each send(), record the turn
history.push({ role: 'user', content: userMessage })
history.push({ role: 'assistant', content: assistantResponse })

// Next send() includes full history
const prompt = buildPromptWithHistory(history, nextMessage)
const run = await agent.send(prompt)

This works but has significant drawbacks:

  1. No native context compression — we must manually manage token limits and summarization
  2. Prompt bloat — conversation history is re-sent with every message, consuming extra tokens
  3. Lost tool call context — the text-only history misses tool call details that the SDK would normally track

Feature Request

If multi-turn context retention is not currently supported for local agents via agent.send(), it would be very helpful to have one of:

  1. Native context retentionagent.send() automatically includes prior runs’ context
  2. A messages parameter — e.g., agent.send(message, { messages: [...] }) to explicitly pass conversation history in a structured format
  3. Clear documentation — explicitly state that agent.send() runs are isolated for local agents, and recommend the workaround pattern

Additional Context

We’re building an Electron desktop application that uses @cursor/sdk as the backend for a remote SSH workspace coding assistant. The application manages conversation sessions where users interact with an AI agent across multiple turns. The lack of native context retention was a critical UX issue — the agent appeared to “forget” everything after each message.

Hey, thanks for the detailed report and the minimal repro, it really helps. This is a confirmed bug: each agent.send() on a local agent starts a fresh run and does not pull in the previous conversation state. Cloud agents cloud: { repos } do this server-side and work correctly. We’re tracking this internally, but I can’t share an ETA for a fix yet.

The same bug is already being discussed here: [SDK] Local agents do not retain conversation context between agent.send() calls`. I’ll merge your post into that thread so all discussion stays in one place.

Workarounds until this is fixed:

  • If your use case allows it, switch to cloud: { repos }, multi-turn works out of the box there.
  • Otherwise, your approach of manually injecting history into the prompt is the right path. I get the downsides like prompt bloat, losing tool call context, and having to manage token limits by hand. The team is working on a native solution.

Once I have an update on the fix, I’ll post it in the main thread.