Context Has a Timestamp

Rishi Kundargi - Developer Advocate
by Rishi Kundargi
September 5, 2026

The most powerful input to an agent isn't the system prompt. It's your data. And not stale data. It's a snapshot of the world as it exists at the moment of request through numbers. Agent context is a feature value computed in real-time.

Most agents access data through retrieval. The data is right. But it's static. All-time total, this month's average, today's count - audited, backfilled, and exact to the decimal. These numbers are true but the one that matters is the one calculated right now, using all the data available. A lifetime average computed last night is stale, and the same average computed at the decision isn't.

I recently spoke at Temporal's "Agent Context is Everything" developer meet-up using poker as an example. Disclaimer: Chalk doesn't condone gambling, and Chalk has nothing to do with poker. That being said, poker strategy is about making decisions based on the changing signals you receive about other players, in real time. So, let's build an agent that plays poker intelligently.

Poker Basics:

A really quick intro so the rest of this makes sense.

You never see an opponent's cards. Every bet is a guess about what they have. And there's a mathematically optimal baseline for that guess: a GTO (Game Theory Optimal) chart, which is a big lookup table that says: in this exact spot, fold this often, call this often, raise this often. It's a probability distribution.

Most importantly, Nobody actually plays GTO. Not you, not the pro. The chart is a prior. The money is in measuring how far this person you are currently playing with has drifted from GTO, right now, to inform your bets, your decision to stay in, and your game strategy.

A Hand

Let's say I've been playing with the same people for 5 hours. That means I have 5 hours of intuition on these players. That's important for later. But right now, all that matters is the current hand.

The board is K♥ 7♣ 2♦, rainbow. Three different suits, so no flush draw. My cards are A♥ K♦. Top pair, best kicker. Strong, but not a monster. My opponent bets small.

The entire decision: do I call, raise, or fold?

Layer 1: GTO Chart

I look this spot up in my GTO chart. It says fold 0.08, call 0.62, raise 0.30. That means I'm supposed to call 62% of the time. Basically, the book says to call.

That's the answer you get out of any agent whose context is static - a document. It's a lookup.

But let's look at what layer 1 is missing. There's no opponent in it. We have 5 hours of data on how our opponent has played. With Chalk you can ensure those 5 hours of play affect our decision now.

4 Layers of Context

The same opponent, four ways. Only the bottom row knows what’s happening right now.

The same opponent, four ways. Only the bottom row knows what’s happening right now.

The same opponent, four ways. Only the bottom row knows what's happening right now.

Layer 2 is lifetime. Every hand this player has ever played in the demo scenario: 2,800 hands, VPIP 18.1%, 3-bet 3.1%. (VPIP is how often he voluntarily puts money in preflop. 3-bet is how often he re-raises.) That's a rock. Tight, patient, doesn't fight back. If those were the only numbers you had, you'd raise him off that small bet without thinking twice. He basically bets when he has it, and doesn't when he doesn't.

Layer 3 is tonight's session. 300 hands, VPIP 19%. Same guy. The session confirms the lifetime number, which feels reassuring and tells you nothing you didn't already know.

Layer 4 is trailing windows, anchored to the moment of the decision. In the last two hours: 120 hands, VPIP 24.2%, 3-bet 8.3%. Something's up. When we zoom in to the last thirty minutes: 30 hands, VPIP 40%, 3-bet 23.3%.

His 3-bet rate went from 3.1% over his lifetime to 23.3% in the last half hour. More than seven times. That's not a rock. The "aggression factor" is 7x. That's a guy on tilt, right now. "On Tilt" is just poker speak for "the guy's angry and isn't thinking rationally." Before we look at what that implies, let's talk about how we got these numbers.

The Code

The fourth layer - the trailing windows - is one simple declaration in Chalk.

# src/features.py

class PlayerSession
  hands_recent: Windowed[int] = windowed(
    "10m",
    "30m",
    "2h",
    expression=_.participations[
        _.at >= _.chalk_window,
        _.at <= _.chalk_now,
    ].count(),
    default=0,
  )
  vpip_hands_recent: Windowed[int] = windowed(
    "10m",
    "30m",
    "2h",
    expression=_.participations[
        _.voluntarily_entered == True,
        _.at >= _.chalk_window,
        _.at <= _.chalk_now,
    ].count(),
    default=0,
  )

10m, 30m and 2h are declared inline, and Chalk computes all three on demand, straight off the raw rows.

This is the entire way you capture a complicated materialized aggregation in Chalk, which is how you attribute recent actions to a player's overall strategy. There is no streaming job in this repo. No topic, no rollup table, no cron, no materialized view sitting somewhere with those numbers in it. It's simply a definition of how to compute a number. Our agent asks for that number right now and Chalk handles everything else, returning the most up to date number possible. In other words, a snapshot of the current state of the world in numbers.

The agent

# cmp/advisor.py
@chalkcompute.function(
    secrets=[
        Secret.from_chalk_integration("pg"),
    ],
    image=Image.debian_slim(python_version="3.12").pip_install(
        [
            "chalkpy>=2.130.5",
            "openai",
            "psycopg2-binary",
        ]
    ),
)
def advise_hand(
    spot_prompt: str,
    spot_json: str,
    hand_id: str,
    chart_key: str,
    villain_player_id: str,
    player_session_id: str,
    now_iso: str,
) -> str:

The image and the secrets are declared in place, on the function. The decorator is the deploy.

There is no Dockerfile in this repo. No cluster, no service, no deployment manifest. Simply running the file ships the function, and the sandbox comes with its own identity. Plenty of platforms will host a sandbox for you. What I care about is how little of it I had to describe. The runtime credentials are named right there on the function, I registered them once with a Chalk secret set, and the platform injects them into the sandbox at run-time.

And then the tool itself:

# illustrative - single-tool shape, not in this repo
def chalk_query(inp: dict) -> str:
    ctx = chalk_client.query(input=inp["input"], output=inp["output"], now=now)
    return "\n".join(f"{a.field}: {a.value}" for a in ctx.data)

The tool is literally a Chalk query. The model picks the feature list, and now= comes from the enclosing call, not from the model.

That's the whole agent. A sandbox and a single tool - the ability to Chalk query. The windows are computed as of the instant of the hand being decided, not precalculated. In poker, freshness and latency matter. A number that doesn't take the last 5 hands into account simply doesn't matter. A number that's correct but arrives after the shot clock is worth the same as no number. Chalk enables real-time context to be served to agents.

Outcome

The GTO prior for this spot, against the distribution the agent returned.

The GTO prior for this spot, against the distribution the agent returned.

The GTO prior for this spot, against the distribution the agent returned.

At the instant of the decision, the model had all four layers in front of it - the chart row, the lifetime profile, tonight's session, and the opponent's last thirty minutes. That last one was computed at the moment it was asked for, straight off the raw hand history. Nothing had written it down in advance.

As we can see, with recent opponent history our decision changes. Our agent says we should raise 71% of the time. That's a 41% delta. Simply adding a real time context engine changes the distribution by 41%. That's an entirely different "best answer".

Why Chalk

One declaration, three live horizons. The windowed() call is the feature. It isn't the interface to a pipeline that computes the feature. There's no orchestration logic to maintain, and no data pipelines that can break.

Federated, not materialized. The hand history lives in a database and it stays there. Chalk queries the source of truth at request time instead of precomputing the aggregates into a cache and then owning the problem of keeping that cache in sync.

Low-latency serving. Serve the current state of the world within the time bound of a poker action. Enabling feature serving in single-digit milliseconds. Designed for high-throughput production workloads: 100,000 QPS with <5ms latency.

Aggregate of an aggregate, no DAG. Counting a derived boolean that is itself a count, inside a time window, is the kind of thing that in most stacks turns into three jobs and an ordering problem. Chalk makes incredibly complicated computations simple.

Swap the Nouns

If we swap some nouns: Opponent becomes cardholder. Hand becomes transaction. GTO baseline becomes model expectation.

And suddenly, we have an agent that detects changes in fraud patterns.

Nothing else changes. Not the feature classes, not the windows, not the tool loop. A cardholder who's behaved one way across 2,800 transactions and a completely different way for the last thirty minutes is the same computation as a poker player on tilt.

An agent with Chalk knows what just happened. Chalk enables real time context to be served in milliseconds to your agents.

If you're looking for ways to assemble and serve agents and models fresh, real-time context, reach out anytime.

Want to stay up-to-date with Chalk?

Subscribe for updates on what we’re building (and shipping!) at Chalk