An agent's context window is finite. Before every model call the runtime must decide which of the accumulated messages still fit. Naively slicing the last N messages corrupts the conversation: you can end up starting mid-turn, with an assistant message answering a user message that is no longer present.
Write the trimmer that a production agent loop would use.
A chat transcript is a list of messages, each with a role (system, user or assistant) and a pre-computed tokens count. Two invariants must survive trimming:
Any non-system messages appearing before the first user message belong to no turn and are discarded.
Implement:
def trim_history(messages, max_tokens):
Algorithm:
Return the kept messages as a list of the original dicts, in the original chronological order (system messages keep their original positions).
msgs = [
{"id": "s0", "role": "system", "tokens": 20},
{"id": "u1", "role": "user", "tokens": 30},
{"id": "a1", "role": "assistant", "tokens": 30},
{"id": "u2", "role": "user", "tokens": 25},
{"id": "a2", "role": "assistant", "tokens": 25},
]
print([m["id"] for m in trim_history(msgs, 100)])
# ['s0', 'u2', 'a2']
Full history costs 20 + 60 + 50 = 130 > 100. Drop the oldest turn (u1,a1, 60 tokens) leaving 70, which fits.
msgs = [{'id':'s0','role':'system','tokens':20},{'id':'u1','role':'user','tokens':30},{'id':'a1','role':'assistant','tokens':30},{'id':'u2','role':'user','tokens':25},{'id':'a2','role':'assistant','tokens':25}]
print([m['id'] for m in trim_history(msgs, 100)])['s0', 'u2', 'a2']
Base cost is the system message: 20 tokens. Turn 1 = u1+a1 = 60, turn 2 = u2+a2 = 50. Total 130 > 100, so the oldest turn is dropped, leaving 20 + 50 = 70 <= 100. The kept set starts on a user message, as required.
0 <= len(messages) <= 5001 <= max_tokens <= 10**6; every tokens value is a non-negative introle is one of "system", "user", "assistant"user message and its following assistant messages are dropped together