Skip to main content

Multi-turn conversations

Keep one Redactions store per conversation. Pass earlier results as history, oldest first.

from jes import Redactions

redactions = Redactions(scope=conversation_id.encode()) # one per conversation
history = [] # earlier input and output Results, oldest first

incoming = guard.check_input(user_text, redactions=redactions, history=history)

reply = call_model(
incoming.onward,
# sanitized, not onward: an earlier reply's onward has PII restored.
[turn.sanitized for turn in history],
)
outgoing = guard.check_output(
reply,
prompt=incoming,
redactions=redactions,
history=history,
)
show(outgoing.onward) # the restored reply, or a refusal
history += [incoming, outgoing]

How placeholders work​

The model only ever sees the placeholder.

  • On input, pii() replaces each detected value, and every other standalone occurrence of it, with a placeholder of the form [JES_PII_<22 characters>]. The same value always gets the same placeholder within one store.
  • On output, a placeholder is restored only when the store issued it, the model saw it in this generation's context, and it appears exactly in the reply.
  • On a tool call, PII in the arguments blocks the call. Placeholders are never restored into arguments.
  • PII that first appears in the model's reply is hidden before any judgment backend sees it. With the default output_mode="flag", judges see a per-check [JES_LOCAL_…] marker, and the value is put back in onward and flagged. output_mode="redact" replaces it with [REDACTED_{ENTITY}] for good, and "block" blocks.
  • Placeholder syntax typed by a user, or found in retrieved text, is never trusted. jes rewrites it as literal text and flags placeholder_in_text.

Without a store​

If you don't pass redactions, check_input creates a fresh in-memory store and keeps it on Result.redactions. check_output(prompt=incoming) then uses that same store. That is enough for a single turn.

If you pass an explicit store whose identity differs from the one on the result, jes raises RedactionError instead of picking one.

Persisting a store​

Stores refuse pickle and copy. To persist one between requests, encrypt it with a 32-byte key (needs jes[crypto]):

blob = redactions.dumps(key, associated_data=b"tenant-42")
# … later …
redactions = Redactions.loads(
blob, key, scope=conversation_id.encode(), associated_data=b"tenant-42"
)

loads needs the same key, scope, and associated data. Key rotation and replay protection are your responsibility.

Output rules​

  • Buffer the model's output and call check_output once with the complete reply. jes does not check or restore a stream.
  • Restoration produces plain text. Escape values before rendering Markdown, HTML, JSON, or passing them to a shell.
  • jes never restores tool-call arguments. Parse, authorize, and populate tool inputs yourself.
  • A placeholder inside a URL is restored only for origins listed in pii(restore_origins=[...]). Otherwise it stays in place and gets placeholder_in_url, which blocks by default.

The runnable version is PII across one conversation cookbook page.