$jevwiki.ai#an LLM wiki about Jev, written for agents rather than people

Agents: read the raw Markdown of this page, or start at llms.txt.

~/wiki/cookbooks

Cookbook: Simple memory system

[ cookbook ][ updated 2026-10-15 ][ confidence high ][ jev-1.14.0 ][ python sdk 0.7.5 ]#cookbook · memory · harness · noul · choice · state

TL;DR Jev sees only the state you send, so memory lives in your code. After each call, a second request asks a Noul "gate" whether the original request states a lasting fact about the customer; at P(yes) ≥ 0.5 the request and Jev's answer are saved under the customer's ID, and a wrapper, ask_with_memory(), adds that customer's saved memories to every later state. In TypeSafe's support-routing example, all six later tickets reach the expected team with memory, two of six without. Two sequential requests per call; memory only grows; the 0.5 is a starting point to test on your own labels.

Goal

Carry facts from earlier requests into later ones. A customer said in August that they self-host on Windows Server; in September they write "Dashboards have been really slow since this morning". Each Jev request sees only its own state, so plain Jev routes that ticket to the cloud team. The cookbook calls its wrapper "a basic agent harness": the code around the model that gives it memory, here a Python dict with one list per customer ID.

When to use it: a repeated decision about the same entity (customer, account, device) where an earlier message changes how later ones should be read. When not to: facts you already hold in a database (put them in state directly), or anything where a wrong saved fact costs more than a missing one (see Gotchas).

Inputs / state shape

Two customers, written for the cookbook: C-1042 (Harbor Logistics, self-hosted edition on Windows Server, Enterprise contract) and C-2210 (Fernwood Studio, cloud edition, self-serve Team plan). Five earlier tickets, three of which state a lasting fact (an edition, a plan, a contract) and two of which do not (typing on a phone at the airport; a public holiday). Then three later messages per customer that never mention the setup: slow dashboards, a failing upgrade to version 5.2, 20 more seats. Expected teams were labelled by hand from the earlier tickets.

Each ticket's state is a dict with customer (id, company) and ticket (id, received_on, subject, messages as a list of {"from", "text"}). The wrapper adds saved memories under the key earlier_tickets_from_this_customer. A saved memory is {"request": <state before memories were added>, "jev_answered": {<question instructions>: <answer text>}}, so old memories are never nested inside new ones.

Numbers below came from jev-1.13.0 on 2026-09-28.

Questions asked

Routing (ROUTE), one Choice:

key instructions criteria
team Which support team should handle \ticket`?` cloud_ops: "Cloud team: problems with the hosted cloud edition, run by us"; selfhosted_linux: "Self-hosted team, Linux: installs, upgrades and performance of the self-hosted edition on Linux servers"; selfhosted_windows: same for Windows Server; billing: "Billing: invoices, payments, seats and plan changes for self-serve plans"; enterprise_desk: "Enterprise desk: seats, contracts and account changes for customers on an Enterprise contract (their technical problems go to the technical teams)"; general: "General product questions: features, how-to, settings"; none_suitable: "None of these teams fits the ticket"

The none_suitable option gives Jev an answer when no team fits; a ticket with too little context can land there.

Gate (GATE), one Noul, verbatim:

GATE = {
    "remember": Noul(
        instructions=(
            "In `request.ticket`, does the thread state a lasting fact about the customer that "
            "should change how their future, unrelated tickets are handled? Count what the "
            "customer or our support staff wrote in `request.ticket`. `jev_answered` shows how "
            "the ticket was routed, which may rely on things learned earlier: do not count "
            "anything it says or implies."
        ),
        criteria=NoulCriteria(
            true=(
                "Yes: in `request.ticket` the customer or our support staff state something "
                "about the customer that stays true after this ticket, such as the product "
                "edition or operating system they run, their plan or contract, or a standing "
                "preference."
            ),
            false=(
                "No: `request.ticket` only contains things specific to this one ticket or "
                "moment, such as a one-off request, today's schedule, or the device they "
                "happen to be typing on; or the lasting fact appears only in `jev_answered`."
            ),
        ),
    ),
}

The "fact only in jev_answered does not count" rule is deliberate: without it, the cookbook says, a generic answer such as "General product questions" can outweigh a real fact in the request, and an answer that relied on an earlier memory can be saved again as if it were new.

Combining logic in code

import json
import os
from pathlib import Path

from cooksafe import JsonCache
from typesafe_sdk import Choice, Noul, NoulCriteria, SystemOneResponse, TypeSafeClient

TYPESAFE_MODEL = "jev-1.13.0"
REMEMBER_AT = 0.5  # save when P(lasting customer fact) reaches this starting threshold
MEMORY_KEY = "earlier_tickets_from_this_customer"  # where saved tickets go in the state

client = TypeSafeClient(
    api_key=os.environ.get("TYPESAFE_API_KEY", "cache-only"),  # keyless kernels replay the cache
    base_url=os.environ.get("TYPESAFE_ENDPOINT"),
    timeout=120.0,
)
json_cache = JsonCache(Path("json_cache.json"))

# ROUTE and GATE as above.


@json_cache
def _system_one(model: str, state: str, questions: str) -> dict:
    response = client.system_one(
        state=json.loads(state), questions=json.loads(questions), model=model
    )
    return response.model_dump(mode="json")


def ask(state: dict, questions: dict) -> SystemOneResponse:
    """client.system_one, with the model pinned and the call cached."""
    as_json = {qid: q.model_dump(mode="json", exclude_none=True) for qid, q in questions.items()}
    raw = _system_one(TYPESAFE_MODEL, json.dumps(state), json.dumps(as_json))
    return SystemOneResponse.model_validate(raw)


def answer_text(question, answer):
    """The readable part of an answer: the chosen option's description, or the number."""
    if answer.type == "choice":
        return question.criteria[answer.choice]
    return answer.noul if answer.type == "noul" else answer.score


def as_memory(state: dict, questions: dict, response: SystemOneResponse) -> dict:
    return {
        "request": state,
        "jev_answered": {
            q.instructions: answer_text(q, response.answers[qid])
            for qid, q in questions.items()
        },
    }


def remember_probability(memory: dict) -> float:
    """P(the request states an important fact about the customer), with the answer as context."""
    return ask(memory, GATE).answers["remember"].noul


MEMORIES: dict[str, list[dict]] = {}  # customer ID -> that customer's saved memories


def customer_id(state: dict) -> str:
    return state["customer"]["id"]


def ask_with_memory(state: dict, questions: dict) -> SystemOneResponse:
    """ask(), plus memory: same arguments, same return value."""
    # 1. Answer the call with this customer's existing memories.
    saved = MEMORIES.get(customer_id(state), [])
    response = ask({**state, MEMORY_KEY: saved} if saved else state, questions)

    # 2. Ask Jev whether the original request states an important fact worth keeping.
    memory = as_memory(state, questions, response)
    if remember_probability(memory) >= REMEMBER_AT:
        MEMORIES.setdefault(customer_id(state), []).append(memory)
    return response


# Usage: same call shape as ask().
# team = ask_with_memory(ticket_state, ROUTE).answers["team"]
# print(team.choice, team.probabilities[team.choice])

ask_with_memory() takes the same arguments and returns the same response as ask(), so it swaps in without changing the routing code; ROUTE never mentions memories, because Jev reads the whole state.

Results / what the cookbook reports

Earlier tickets through the wrapper (gate probability, saved or not):

ticket customer routed to P(remember) saved
T-101 Windows Server 2022, own data centre C-1042 selfhosted_windows 0.91 yes
T-102 cloud edition, self-serve Team plan C-2210 general 0.91 yes
T-103 typing on a phone at the airport C-1042 billing 0.07 no
T-104 team out Friday for a public holiday C-2210 general 0.10 no
T-105 Enterprise contract signed in March C-1042 enterprise_desk 0.92 yes

T-102 was saved although it was routed to general: the gate counts the customer's words, not the answer.

Later messages, plain ask() vs ask_with_memory():

customer message expected plain Jev with memory
C-1042 Slow dashboards selfhosted_windows ✗ cloud_ops 0.80 ✓ selfhosted_windows 0.94
C-1042 Upgrade failing selfhosted_windows ✗ none_suitable 0.60 ✓ selfhosted_windows 1.00
C-1042 More seats enterprise_desk ✓ enterprise_desk 0.62 ✓ enterprise_desk 0.99
C-2210 Slow dashboards cloud_ops ✓ cloud_ops 0.75 ✓ cloud_ops 0.91
C-2210 Upgrade failing cloud_ops ✗ none_suitable 0.63 ✓ cloud_ops 0.86
C-2210 More seats billing ✗ enterprise_desk 0.52 ✓ billing 0.98

"Expected routes: 2/6 without memory, 6/6 with memory." The same message goes to different teams for different customers. Adding the Enterprise memory lowered C-1042's slow-dashboards answer from 0.99 to 0.94, which the cookbook gives as one reason to choose which memories to add. The gate saved none of the repeated or later messages.

Adapting it to a new domain

Per the cookbook's Next steps:

Choosing the threshold (appendix): label request-and-answer pairs from your traffic as worth remembering or not, find the probability that best separates the two groups, and check it on pairs you did not use to pick it. Raise it if a misleading memory costs more than a missing one; lower it if the reverse (Confidence vs probability).

Gotchas

Sources