dspy-session
dspy-session adds stateful, multi-turn behavior to DSPy modules while staying adapter-agnostic.
It works with:
dspy.Predict/dspy.ChainOfThoughtdspy.ProgramOfThoughtdspy.ReActdspy.CodeAct- composed
dspy.Moduleprograms with nested predictors - any adapter (
ChatAdapter,JSONAdapter,XMLAdapter,TemplateAdapter, ...)
Install
pip install dspy-session
Why this exists
DSPy already has dspy.History, but it is manual:
history = dspy.History(messages=[...])
out = predictor(question="...", history=history)
dspy-session automates this and adds:
- per-turn state accumulation
- turn snapshots (
history_snapshotat call time) - linearization into optimizer-ready
dspy.Examples - per-turn scoring + filtering
Quickstart (single predictor)
import dspy
from dspy_session import sessionify
class QA(dspy.Signature):
question: str = dspy.InputField()
answer: str = dspy.OutputField()
dspy.configure(lm=dspy.LM("openai/gpt-4o-mini"))
session = sessionify(dspy.Predict(QA))
session(question="What is DSPy?")
session(question="How is it different from plain prompting?")
print(len(session.turns))
print(session.session_history)
What happens:
- Session deep-copies the module
- It ensures predictors have a history input field
- Each call builds a
Historyfrom previous turns - That history is injected into predictor calls
- Turn is recorded with an exact
history_snapshot
TemplateAdapter integration
dspy-session and dspy-template-adapter work extremely well together:
dspy-sessionhandles state/history lifecycleTemplateAdaptergives exact prompt-layout control
👉 See the full integration guide (including contrived layouts like "all history in system", "all in user", and split strategies):
Program wrapping (no history kwarg required)
You do not need to modify your program’s forward signature.
import dspy
from dspy_session import sessionify
class QA(dspy.Signature):
question: str = dspy.InputField()
answer: str = dspy.OutputField()
class Agent(dspy.Module):
def __init__(self):
super().__init__()
self.gen = dspy.Predict(QA)
def forward(self, question):
# No history argument here.
return self.gen(question=question)
session = sessionify(Agent())
Internally, dspy-session wraps nested predictors and injects history via context when not explicitly provided.
Linearization for optimizers
Each turn becomes an independent training example:
examples = session.to_examples()
# each example has inputs including history snapshot + output labels
Filtering with a metric
def quality_metric(example, pred, trace=None):
return 1.0 if "good" in pred.answer.lower() else 0.0
good_examples = session.to_examples(metric=quality_metric, min_score=0.5)
Strict trajectory mode
If a bad turn should invalidate all later turns in that session:
strict_examples = session.to_examples(
metric=quality_metric,
min_score=0.5,
strict_trajectory=True,
)
Optimizer workflow
If your examples include history (include_history=True, default), optimize a module/signature that accepts history.
Typical workflow:
# collect turns
session = sessionify(dspy.Predict(QA))
...
trainset = session.to_examples()
# optimize the session-aware module
optimized = dspy.BootstrapFewShot().compile(session, trainset=trainset)
If you want to optimize a non-history base program, use:
trainset = session.to_examples(include_history=False)
History policies for explicit history input
When caller passes history=... explicitly:
history_policy="override"(default): stateless pass-through, no turn recordedhistory_policy="use_if_provided": use provided history for this call and record turnhistory_policy="replace_session": replace session seed history, clear turns, then continue
session = sessionify(my_module, history_policy="use_if_provided")
Controlling what enters history
Sliding window
session = sessionify(my_module, max_turns=10)
Cap in-memory turn storage (for long-running services)
session = sessionify(my_module, max_turns=10, max_stored_turns=200)
# max_turns controls prompt history window
# max_stored_turns controls retained session.turns in memory
Excluding fields (e.g. giant RAG context)
session = sessionify(my_module, exclude_fields={"context"})
Include only selected input fields in history
session = sessionify(my_module, history_input_fields={"question"})
Seed history / resume conversations
seed = dspy.History(messages=[{"question": "Hi", "answer": "Hello!"}])
session = sessionify(my_module, initial_history=seed)
Manual turn editing
session.add_turn(
inputs={"question": "edited question"},
outputs={"answer": "edited answer"},
)
session.pop_turn() # remove last turn
session.undo(steps=2) # remove last 2 turns
Serialization
Save/load session state (module is not serialized):
session.save("session.json")
restored = dspy_session.Session.load_from("session.json", my_module)
Hot-swap optimized module
Keep user conversation state, replace model/program weights:
session.update_module(new_optimized_module)
Async + locks
session = sessionify(my_module, lock="async")
out = await session.acall(question="...")
lock options:
"none"(default)"thread""async"
API reference
sessionify(module, **kwargs) -> Session
Session(
module,
history_field="history",
max_turns=None,
max_stored_turns=None,
exclude_fields=None,
history_input_fields=None, # alias: input_field_override
initial_history=None,
history_policy="override", # override | use_if_provided | replace_session
on_metric_error="zero", # zero | raise
strict_history_annotation=False,
copy_mode="deep", # deep | shallow | none
lock="none", # none | thread | async
)
# calls
session(...)
session.forward(...)
await session.acall(...)
await session.aforward(...)
# state
session.turns
session.session_history
len(session)
session.reset()
session.fork()
# manual editing
session.add_turn(inputs=..., outputs=...)
session.pop_turn()
session.undo(steps=1)
# scoring / examples
session.score(metric, gold=None)
session.to_examples(...)
session.to_trainset(...)
Session.merge_examples(*sessions, ...)
# persistence
session.save(path)
session.save_state()
Session.load_from(path, module)
# module lifecycle
session.update_module(new_module)
Notes
Sessionis adspy.Module, so nested use inside larger DSPy programs remains optimizer-discoverable vianamed_predictors().- For optimizer replay calls that pass explicit history, Session can behave statelessly (
history_policy="override"). - For very long sessions, use
max_turnsandmax_stored_turnsto control prompt and memory growth. - If your module is not deepcopy-friendly, use
copy_mode="shallow"orcopy_mode="none".
License
MIT
Metadata
Release files for dspy-session 0.1.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| dspy_session-0.1.2.tar.gz | 22.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| dspy_session-0.1.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 44.7 kB
Release files / dspy_session-0.1.2.tar.gz
| Download URL | dspy_session-0.1.2.tar.gz |
|---|---|
| Size | 22.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
387a13948d37efd4bc56e8d6e7e5e0703273a6c3c38dafc7768c4cf4c1771577
|
|
BLAKE2b-256 checksum How to use checksums |
365e15ad8c17b40aee70c303f4157ee2fb5d200107e69dc132d03a3d0e357bd1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.13.3
|
Release files / dspy_session-0.1.2-py3-none-any.whl
| Download URL | dspy_session-0.1.2-py3-none-any.whl |
|---|---|
| Size | 21.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
cc9fd4a2530df5c9e00fa504e2f923e9d8d4de7b244d3b829b9c5857e01e53bd
|
|
BLAKE2b-256 checksum How to use checksums |
aac234d3ccc1c4e6354ce417a0d4a874d9bdee56c8b436408be89a2f15adcefb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.13.3
|