Conversations and memory
An AI function remembers nothing: each call is on its own. A conversation is a program's calls that remember each other. Each call is a turn, shown the turns before it; the memory belongs to the conversation, never to the function, which stays the same and callable on its own (contract/conversations.md).
A conversation
@ai function tutor(message::String)::String
"Tutor a student in arithmetic, one small step at a time."
end
chat = conversation(tutor, "alex"; store = "tutoring/")
chat("Hi, I'm Alex.")
chat("What is 1/2 + 1/3?") # sees the first turn
chat("Is it 5/6?") # sees bothA conversation is called like its program: the same inputs, the same answer. predict(chat, …) gives the whole call, stream(chat, …) watches it; a stream's s.turn is known at once, because the turn is saved before the model is asked.
id: letters, digits,.,_,-(a file name everywhere). The same id in the same store opens the same conversation: the line above, run again tomorrow, reopens Alex's. Leave it out for a new one.store:nothing(this process's memory, the default), a folder (aFolderStore: files, locked across processes, flushed before each append returns),true(the default folder,~/.local/share/functai/conversations), or a store of your own (aFunctAI.ConversationStorewithappend_records!andread_records).- Other keywords are settings for every turn (
lm,approve,plugins, …); a setting given with a turn's inputs is that turn's only:chat("Why?"; lm = "claude-sonnet-4-5").
A folder store is the same files Python's FolderStore writes: a conversation started in Python continues here, and the other way round.
Turns
ts = turns(chat) # the turns from the first to the head, in order
t = last(ts)
t.result, t.inputs, t.outputs # the answer (typed), and every value by name
t.state # "done", "failed", "stopped", "running", "waiting", "interrupted", "abandoned"
t.saw # the earlier turns this answer was based on
t.usage # tokens, over every call inside the turn
t.model # who answeredEvery turn records what it was shown (saw), so an answer can be asked again exactly as it was. The record does not grow with the conversation: a turn that saw what its parent saw, then its parent, says so in two entries.
What the model sees
Every earlier turn, by default: running out of the model's context, and being told, is better than a model that silently misses what was said.
chat = conversation(tutor, "alex"; context = last_turns(10)) # the last ten
qa = conversation(reader, "paper"; context = all_turns(without = ["document"])) # earlier turns without a bulky inputrender(chat, "Is it 5/6?") is the exact request the next turn would send, nothing sent or recorded.
Branches
Nothing is ever deleted. Continuing from an earlier turn makes a branch:
again = continue_from(chat, turns(chat)[1]) # after the first turn
again("What is 2/3 + 1/6?") # a new branch; the old one is still there
turns(chat; all = true) # every turn of every branch
FunctAI.head!(chat, turns(again)[2]) # make that turn the head, for everyone who opens itA conversation opened by id follows its head; a view made by continue_from follows its own branch. A merge makes one turn from several branches, by another AI function:
x = stream(continue_from(chat, t), "Explain with pizza."); y = stream(continue_from(chat, t), "Explain with money.")
best = merge!(continue_from(chat, t), [x.turn, y.turn], pick_the_clearest)The merged answer becomes this program's turn (made_by names the function that made it, reads the branches); its rating belongs to that function's call.
Two sends at once, stopping, a process that died
- Two sends at once queue (the default
sends = :queue: the second waits, then continues from the first), refuse (:refuse:ConversationErrorconversation-busy), or branch (:branch: beside it). - The same
request_idtwice is one turn: a double click gets the first turn, its result once it ends. - Stopping from anywhere:
stop!(chat, t)appends astoprecord; the process running the turn sees it within a second, and the turn endsstopped(its stream throwsCancelled). - A process that died: a running turn renews a lease every 10 seconds. A turn whose lease ran out is
interrupted;resume!goes on with it in this process.
A program's conversation: helpers remember only when told
A @program's turns remember each other, but the AI functions it calls start fresh at every call unless the conversation says otherwise:
@program function support(message::String)::String
"Answer the customer."
answer(message, topic(message))
end
chat = conversation(support, "ana"; remembers = Dict(answer => :conversation)):conversation shows answer its own earlier calls on this branch, in earlier turns and this one; :turn, its earlier calls in this turn only; remember(:conversation; steps = true) with their tool calls and results. earlier() is the conversation so far as data (one row per earlier turn), for a helper that takes it as an input:
@ai function handoff(conversation::Vector{Dict{String,Any}})::String
"Summarize this support conversation for the person who takes it over."
end
@program function support(message::String)::String
"Answer the customer."
topic(message) == "other" && notify_staff(handoff(FunctAI.earlier()))
answer(message, topic(message))
endA conversation used inside another conversation's turn is refused (conversation-nested), unless the outer one declares it: remembers = Dict(inner => :own).
What a conversation refuses
conversation-content: a store that keeps records, for a program alog_contentsetting keeps values of out of the log. The host said never keep them, and a conversation must remember them: it refuses rather than forgets. Keep it in memory, or let the store keep those fields.conversation-opaque: a program with an untyped (opaque) input or output: a turn is kept as data.conversation-signature: the program now writes an output the earlier turns lack, or a field changed or went away. Turning reasoning on (or adding a first tool) goes on withearlier_without = ["reasoning"]: earlier turns are shown without it, and nothing is rewritten. The model may change freely: each turn records who answered.
Learning from rated turns
A rated turn is a row of rated like any call, with what it was shown: earlier (its earlier turns, as data), conversation (its id) and, for a program's turn, helpers (what each helper was shown). evaluate and the optimizers ask each such row again with its own earlier turns, recording nothing in any conversation; the optimizers never take one as a worked example, since it answers its conversation. Keep a conversation on one side of a split, or the test measures memory:
rows = [(message = "1/2 + 1/3?", conversation = "alex"), (message = "Is it 5/6?", conversation = "alex"),
(message = "2 + 2?", conversation = "ben"), (message = "Hi", conversation = missing)]
train, test = train_test(rows; test = 0.34)
test1-element Vector{@NamedTuple{message::String, conversation::Missing}}:
(message = "Hi", conversation = missing)