Session is a transcript helper, not a higher-order runtime. It owns a ModelInput, exposes
builder-style turn launchers that accept a Lutum execution context per turn, and auto-commits
turns to the transcript when collect() is called. It deliberately does not own hidden loops,
retries, or branch graphs.
On This Page
Building and Pushing Messages
Construct a Session, then pass the Lutum context when launching a turn:
use lutum::{Lutum, Session};
let mut session = Session::new();
// Push messages by role
session.push_system("You are a code review assistant.");
session.push_developer("Internal instruction visible to the model only.");
session.push_user("Review this function for readability.");
// Access and modify the underlying ModelInput directly
session.input_mut().push_user("... or build it programmatically");Commit Model
collect() on a session-originated turn auto-commits the turn to the transcript immediately.
The returned result is ready to use — no separate commit call is needed:
// collect() auto-commits when called on a session-originated turn
let result = session.text_turn(&llm).collect().await?;
// The turn is already in the transcript — use the result directly
println!("{}", result.assistant_text());To inspect or discard a result before committing, use collect_staged(). It returns a
StagedTextTurnResult whose .turn field holds an UncommittedAssistantTurn:
use lutum::CommitTurn as _;
// Does NOT commit — returns a staged result
let staged = session.text_turn(&llm).collect_staged().await?;
// Inspect...
println!("{}", staged.assistant_text());
// Commit when satisfied (CommitTurn extension trait)
staged.turn.commit(&mut session);
// or discard without modifying the transcript
// staged.turn.discard();Commit behaviour by turn kind:
| Turn kind | Auto-commit path | Explicit-commit path |
|---|---|---|
| Text turn | session.text_turn(&llm).collect() |
collect_staged() + staged.turn.commit(&mut session) |
| Structured turn | session.structured_turn().collect() |
collect_staged() + staged.turn.commit(&mut session) |
Tool turn — Finished |
auto-committed when collect() returns the variant |
collect_staged() not available; see tool-rounds section |
Tool turn — NeedsTools |
round.commit(&mut session, tool_results)? after executing |
round.discard() to opt out |
Note: Lutum::text_turn(input).collect() (not session-originated) is never auto-committed.
Tool Rounds
When the model requests tool calls, the result is a NeedsTools variant carrying a round object.
round.commit(...) validates tool result ordering against the committed turn and atomically records
both the assistant turn and the tool results:
use lutum::TextStepOutcomeWithTools;
loop {
let outcome = session
.text_turn(&llm)
.tools::<MyTools>()
.collect()
.await?;
match outcome {
TextStepOutcomeWithTools::NeedsTools(round) => {
let tool_results = round.tool_calls.iter()
.cloned()
.map(|call| execute_tool(call))
.collect::<Vec<_>>();
round.commit(&mut session, tool_results)?;
// loop continues — session now has the assistant + tool results
}
TextStepOutcomeWithTools::Finished(result) => {
// The assistant turn is already committed to the session.
println!("{}", result.assistant_text());
break;
}
}
}round.expect_one()? extracts exactly one tool call and panics if there are zero or more than one.
round.expect_at_most_one()? returns Option<ToolCall>. Both are convenience helpers for
single-tool scenarios.
Direct Input Access
Session exposes the underlying ModelInput for low-level inspection and mutation:
// Read-only access
let input: &ModelInput = session.input();
// Mutable access
let input: &mut ModelInput = session.input_mut();
// Take ownership (consumes the session)
let input: ModelInput = session.into_input();Branching
Session::clone() is the branch mechanism. Use collect_staged() on each branch so you can
inspect both results before deciding which one to commit to the original session:
use lutum::CommitTurn as _;
// Both branches share the same history up to this point
let mut branch_a = session.clone();
let mut branch_b = session.clone();
branch_a.push_user("Explain with an analogy.");
branch_b.push_user("Explain with pseudocode.");
// collect_staged() does not auto-commit
let staged_a = branch_a.text_turn(&llm).collect_staged().await?;
let staged_b = branch_b.text_turn(&llm).collect_staged().await?;
// Pick the better result and commit it to the original session
if pick_a(&staged_a, &staged_b) {
staged_a.turn.commit(&mut session);
staged_b.turn.discard();
} else {
staged_b.turn.commit(&mut session);
staged_a.turn.discard();
}The library does not introduce transcript graph semantics or first-class branch structure. If you
need a full branch tree, maintain it in your own data structures using Session::clone() as the
primitive.
Turn Defaults
SessionDefaults lets you set temperature, max output tokens, and a per-request token budget
that apply to every turn launched from the session, without repeating them on each builder call.
It has two public fields — generation: GenerationParams and budget: RequestBudget:
use lutum::{Session, SessionDefaults, GenerationParams, Temperature};
let defaults = SessionDefaults {
generation: GenerationParams {
temperature: Some(Temperature::new(0.3)?),
..Default::default()
},
..Default::default()
};
let mut session = Session::new().with_defaults(defaults);
// System messages are pushed directly — SessionDefaults has no system-prompt field.
session.push_system("You are a code review assistant.");Agent Loop
Session::agent_loop() encapsulates the standard tool-calling loop so you only need to
supply a dispatch closure. The loop drives the model through rounds of tool calls until it
produces a text-only response or the round limit is reached:
use lutum::{AgentLoopError, Session};
let output = session
.agent_loop::<AppTools>(&llm)
.max_rounds(20) // default is 20
.on_text_delta(move |delta| { // optional — stream text to a channel
let _ = tx.send(delta);
})
.run(|call| async move {
match call {
AppToolsCall::WebSearch(c) => {
let result = web_search(c.args().clone()).await?;
Ok(c.complete(result).unwrap())
}
AppToolsCall::ReadFile(c) => {
let result = read_file(c.args().clone()).await?;
Ok(c.complete(result).unwrap())
}
}
})
.await?;
println!("done in {} rounds, {} tokens", output.rounds, output.usage.total_tokens);AgentLoopOutput carries two fields: rounds (number of turns executed) and usage (token
usage accumulated across all rounds). All turns and tool results are auto-committed to the
session as the loop runs.
Errors returned from the run future are surfaced as AgentLoopError::Dispatch(e).
Exceeding the round limit returns AgentLoopError::RoundLimit(n).
To restrict which tools are available during the loop, use .available_tools(selectors).
AgentLoop is a convenience wrapper; the equivalent hand-written loop using
TextStepOutcomeWithTools works identically and is shown in the Tool Rounds
section.
Ephemeral Messages
Ephemeral items are included in the next model request but are stripped from the transcript before the result is committed. They are useful for per-request instructions or context that should not persist in conversation history:
// Push a system message visible only to the next turn
session.push_ephemeral_system("Focus on performance issues only.");
// Or a user message, or a developer message
session.push_ephemeral_user("(Internal: reply in under 50 words.)");
session.push_ephemeral_developer("Active feature flags: [ff-search]");
// The next collect() sees the ephemeral items; they are removed after commit
let result = session.text_turn(&llm).collect().await?;
// session transcript no longer contains the ephemeral messagesTo include a previously-committed turn in the next request without making it permanent in the
transcript, use push_ephemeral_turn:
// Include an earlier turn as context for one request only
session.push_ephemeral_turn(some_committed_turn);
let result = session.text_turn(&llm).collect().await?;
// the ephemeral turn is not part of the session after this pointFor arbitrary ModelInputItem values, use the generic push_ephemeral(item).
All push_ephemeral_* methods track the inserted position and atomically remove the items
when the next turn is collected.