LaserData Cloud
Laser SDK

Context

Assemble a bounded, prompt-ready history of one conversation or session

Context reads one conversation's messages back from the log and trims them to fit a prompt. The conversation ID ties messages, memory, and state together, and a session ID is a conversation ID, so the same reads work on a session. Use Context when you build a prompt from earlier turns or rebuild state from what a conversation recorded.

Quick example

Append two turns to a conversation, then read them back as the last 20 messages trimmed to about 4,000 tokens.

import { AgentTopic, Chain, ConversationId, LastN, Laser, TokenBudget, TopicRetention } from "@laserdata/laser-sdk"

const utf8 = (text: string) => new TextEncoder().encode(text)

await using connection = await Laser.connectEnv()
const laser = connection.withDefaultStream("ops")
await laser.bootstrap(4, TopicRetention.expireAfter(7 * 86_400_000))

const ctx = laser.context(ConversationId.new())
await ctx.append(AgentTopic.Sessions, utf8("drain node-7"))
await ctx.append(AgentTopic.Sessions, utf8("drained, 0 connections left"))

const turns = await ctx.fetchWith([AgentTopic.Sessions], new Chain([new LastN(20), new TokenBudget(4_000)]))
for (const turn of turns) {
  console.log(new TextDecoder().decode(turn.payload))
}
use laser_sdk::prelude::full::*;
use std::time::Duration;

#[tokio::main]
async fn main() -> Result<(), LaserError> {
    let laser = Laser::connect_env().await?.with_default_stream("ops");
    laser
        .bootstrap(4, TopicRetention::expire_after(Duration::from_secs(7 * 86_400)))
        .await?;

    let scope = laser.context(ConversationId::new());
    scope.append(AgentTopic::Sessions, "drain node-7".as_bytes()).await?;
    scope.append(AgentTopic::Sessions, "drained, 0 connections left".as_bytes()).await?;

    let policy = Chain(vec![Box::new(LastN(20)), Box::new(TokenBudget::new(4_000))]);
    let turns = scope.fetch_with(vec![AgentTopic::Sessions], Box::new(policy)).await?;
    for turn in &turns {
        println!("{}", String::from_utf8_lossy(&turn.payload));
    }
    Ok(())
}
import asyncio

import laser_sdk as ls


async def main():
    async with await ls.Laser.connect_env() as connection:
        laser = connection.with_default_stream("ops")
        await laser.bootstrap(4, ls.TopicRetention.expire_after(7 * 86_400_000))

        ctx = laser.context(ls.new_conversation_id())
        await ctx.append(ls.AgentTopic.Sessions, b"drain node-7")
        await ctx.append(ls.AgentTopic.Sessions, b"drained, 0 connections left")

        turns = await ctx.fetch_with([ls.AgentTopic.Sessions], ls.Chain([ls.LastN(20), ls.TokenBudget(4_000)]))
        for turn in turns:
            print(bytes(turn.payload).decode())


asyncio.run(main())

laser.context(id) does no I/O. append keys each message by the conversation ID, so one conversation's messages on a topic share a partition. In Rust, context needs the agent feature.

Trim history with a policy

A policy decides which messages reach the prompt. Combine them with Chain, which applies each policy to the output of the one before.

PolicyKeeps
LastN(n)The newest n messages
TokenBudget(n)The newest messages that fit n estimated tokens, and always at least one
RoleFilter(agents)Messages written by the given agents
Chain([...])Each policy applied in turn

The token estimate is the payload size in bytes divided by four, rounded up. Pass your own estimator to TokenBudget to use a real tokenizer. You can also write your own policy, see Context in depth.

Get one prompt string

block(topics, n, token_budget) returns the last n messages joined by newlines, ready to paste into a prompt. The token budget is optional and trims after n. Rust takes it as an Option, TypeScript as an optional third argument, and Python as keywords: block(topics=, n=, token_budget=), where topics defaults to agent.sessions and n to 50.

Assemble context for a session

Inside a session, session.assemble(policy) applies a policy to the session's records and returns them with a manifest of what the model received. Pass the result to session.model(request, assembled) and the manifest is recorded with the model call. session.context() returns the last 50 records trimmed to about 4,000 tokens, each with its timeline type. See Sessions.

Keep memory for the conversation

memory(namespace) on a scope returns memory with the conversation already applied, so remember and recall need no conversation argument.

const notes = ctx.memory("notes")
await notes.remember(utf8("drain node-7 before the upgrade")).send()
const hits = await notes.recall().limit(5).folded().fetch()
let notes = scope.memory("notes");
notes.remember("drain node-7 before the upgrade".as_bytes()).send().await?;
let hits = notes.recall().limit(5).folded().fetch().await?;
notes = ctx.memory("notes")
await notes.remember("drain node-7 before the upgrade")
hits = await notes.recall(limit=5, folded=True)

Folded recall rebuilds memory from the log, so it works on plain Apache Iggy. Default recall reads the managed key-value view on Laser Stack or LaserData Cloud. See Memory.

Rebuild state from the conversation

state(topics, bound, init, fold) folds the conversation's messages into your own state. The bound says how much to read: the last N messages, everything after saved offsets or a checkpoint, everything up to a checkpoint, or the whole partition. checkpoint(topics) records where the topics end now, so a later fold reads only what came after it. Snapshot stores save folded state so a restart replays only the tail. See Context in depth.

Good to know

  • Session reads use the agent.sessions topic, and so do Python's fetch, block, and checkpoint when you pass no topics. Rust and TypeScript always take the topics. Run bootstrap(partitions, retention) once per stream to create it.
  • A context read examines at most the newest 10,000 records of each partition. Many conversations share a partition, so older messages of a quiet conversation on a busy partition can fall outside the read.
  • Messages from several topics are ordered by the broker's append time, because each topic has its own offsets.
  • graph(name) on a scope returns the shared graph. It is not narrowed to the conversation.
  • Context needs no managed backend. Only default memory recall and the key-value snapshot store need Laser Stack or LaserData Cloud.

On this page