pub struct Session { /* private fields */ }Expand description
One session: a stream, and the scope it is written under.
Implementations§
Source§impl Session
impl Session
Sourcepub async fn new(
owner: String,
grant: Option<BudgetGrant>,
meta: Option<Map<String, Value>>,
logs: &Logs,
) -> KnlResult<Self>
pub async fn new( owner: String, grant: Option<BudgetGrant>, meta: Option<Map<String, Value>>, logs: &Logs, ) -> KnlResult<Self>
Open a session for owner with an optional budget grant, on an
in-memory store.
owner is total: pass a real principal id, or ANON / SYSTEM
for the reserved ones. The session_opened event is appended here,
so a fresh session already has one event.
“In-memory” is an in-memory database, not a different kind of store:
the same backend a durable session uses, on the one in-memory log the
host holds (SqliteEventStore::open_memory), which is reclaimed when
the run ends. There is one backend, because the log is read with SQL
and a log that cannot be queried would be a second, lesser kind of
session — and because a stream in it is a stream like any other, so a
child can be opened from one and a resume can find it by name.
The stream is minted here and adopted as the session’s id, so
Session::id names the stream this session writes — the same
identity a durable session has, and what $stream binds to in a
Session::query.
Sourcepub async fn open_on(
owner: String,
grant: Option<BudgetGrant>,
meta: Option<Map<String, Value>>,
store: Box<dyn EventStore>,
) -> KnlResult<Self>
pub async fn open_on( owner: String, grant: Option<BudgetGrant>, meta: Option<Map<String, Value>>, store: Box<dyn EventStore>, ) -> KnlResult<Self>
Open a session for owner on a caller-chosen backend store (the
in-memory store, or the durable SQLite one).
Like Session::new but takes the backend, so the shell decides
whether the log is ephemeral or persisted. It appends the same
session_opened boundary, recording the session’s scope on it — the
kernel-issued ScopeId and the owner — so a later
Session::resume can recover the scope from the log alone, and
the grant, so the log says what the owner allowed.
session_opened is an open-shape reserved kind, so both extra fields
are accepted without any change to the validator.
The two are written as one batch (EventStore::append_many), so a
durable stream either carries the opening and the quota it opened
under, or carries nothing at all: an open that fails leaves no session
behind to close.
Sourcepub async fn resume(
grant: Option<BudgetGrant>,
store: Box<dyn EventStore>,
) -> KnlResult<Self>
pub async fn resume( grant: Option<BudgetGrant>, store: Box<dyn EventStore>, ) -> KnlResult<Self>
Continue an existing session by re-folding its persisted log.
The store already holds a session’s events (a reopened SQLite
stream), so resume does not append a new session_opened — the
session already opened. It reads the whole log once and restores the
state from it:
- the scope, from the first
session_openedevent: itsScopeId(FIELD_SCOPE_ID) and itsowner(FIELD_OWNER). An older log written before either was recorded falls back — to a fresh kernel-issued scope id, and toANON— rather than failing the resume; - the grant, from the last
budget_grantedthe log carries, so a reopened stream goes on keeping a ledger and a refusal still has atagto report. The balance itself needs no restoring: it isfold_balanceover the stream, and the stream is right there.
A grant passed here is the owner granting again: it is appended
as a new budget_granted and raises the restored balance, rather
than replacing it. Omit it to continue on what is left. Nothing is
deducted for the earlier llm_response usage — an append never
charged, and what was consumed is a query view’s answer over the
recorded payloads, not the quota’s.
A closed stream is not resumed. A session is disposable: it opens
once and ends once, so a log that already carries its session_closed
is an ending, not a state to continue from — the caller opens a new
session instead.
Nothing is restored for the read side: a resumed session’s reads —
events, tail, a query — go to the reopened store, so they see the
whole stream on the first call.
Sourcepub async fn grant_more(&mut self, grant: BudgetGrant) -> KnlResult<()>
pub async fn grant_more(&mut self, grant: BudgetGrant) -> KnlResult<()>
The owner granting again: record budget_granted and raise the
balance by it.
The one way the balance rises, and it is a fact in the log before it is a number in the counter — a failed append leaves the balance exactly as the ledger describes it. Refused on a closed session, like every other write: a run that has ended cannot be granted more.
This is the owner acting through a handle it holds, so it takes the
ledger as it finds it — including a stream that has none, which this
grant then starts. Session::grant_on_resume is the other door and
refuses that case: a resume is a second handle arriving at a session
that already exists, and it does not get to give one a quota it opened
without.
Sourcepub async fn open_child(
&mut self,
child_stream: String,
owner: String,
allocation: Allocation,
meta: Option<Map<String, Value>>,
child_store: Box<dyn EventStore>,
) -> KnlResult<Self>
pub async fn open_child( &mut self, child_stream: String, owner: String, allocation: Allocation, meta: Option<Map<String, Value>>, child_store: Box<dyn EventStore>, ) -> KnlResult<Self>
Open a session from this one, paying for it out of this session’s balance — one transaction, both ledgers.
The kernel’s whole part in a session tree. It records two facts and performs one move:
- the child’s
session_openedcarriesFIELD_PARENT— this session’s stream — and thebudget_grantedit opens with carries it too, so where the units came from is in the log beside them; - this session’s ledger gains a
budget_reservednaming the child (FIELD_CHILD). An allocation is a spend from here: the balance falls by exactly what the child’s rises by, and nothing is returned when the child closes. A refund would be a balance rising without an owner granting, which is the one thing the ledger does not allow.
All of it is decided and written inside one transaction on this
session’s store (EventStore::append_if_many), so two children
asking at once cannot both be given what only one balance covers, and
no reader ever meets a child that opened without the reservation that
paid for it.
The child is on the parent’s database. child_store must be a
store on the same database (EventStore::database) opened on
child_stream; anything else is a KnlError::Validation before a
word is written. A tree spread over two logs could be neither written
atomically nor read back by one statement, so it is not a tree.
The child’s stream must be empty. The two events written over
there are a session’s first, so child_stream names a stream nothing
has been written to; one that already carries an event is a
KnlError::Validation with nothing written on either side. The
emptiness is decided inside the same transaction as the rest — the
decision is shown the other stream’s first event along with this
ledger (EventStore::append_if_many) — because two allocations
naming one stream would otherwise both be told it was free and both
open a session on it.
A refusal is an error here, not a false. When the balance does
not cover the allocation, a budget_refused naming the child is
recorded on this session, nothing is opened, and
KnlError::Refused is raised: unlike Session::reserve, which is
asked in a loop that expects to be told no, an allocation either
produced a session or it did not, and there is no half-opened one to
hand back.
The parent must be open. This handle having closed is refused
straight away, and a stream whose log already carries an ending is
refused inside the transaction — the decision is shown
session_closed along with the ledger — both as
KnlError::Closed, the same answer a resume of a closed stream
gives.
The child comes back as an ordinary session with its scope restored
from the events just written (Session::resume over the two of
them): its balance is the fold of its own ledger, its owner and scope
are what the opening recorded, and nothing about it is special
afterwards. What it is not is a handle this session holds — the
parent keeps no pointer, and a supervisor reads the structure back out
of the log.
Sourcepub fn scope(&self) -> &Scope
pub fn scope(&self) -> &Scope
The scope this session is written under.
The scope and the session are two things with one lifetime: this is
the authority half — the id, the owner, the granted quota — while the
session is the stream. Session::owner / Session::scope_id
read through to it for the two call sites that want one field.
Sourcepub fn scope_id(&self) -> &str
pub fn scope_id(&self) -> &str
The scope’s kernel-issued id, as recorded on session_opened and on
every budget_* event.
Not the session id: Session::id names the stream, this names the
authority the stream is written under.
Sourcepub fn database(&self) -> Option<&str>
pub fn database(&self) -> Option<&str>
The database this session’s log lives in, or None for a backend that
is not one (EventStore::database).
Published for one caller: whoever opens a child has to open its store
on the parent’s database, and asking the parent is how it knows which
that is (Session::open_child refuses any other). It is an
identity to pass along, not a location to take apart.
Sourcepub async fn append(&mut self, event: Map<String, Value>) -> KnlResult<u64>
pub async fn append(&mut self, event: Map<String, Value>) -> KnlResult<u64>
Record an event, returning its seq. The one write path.
Any kind is welcome, the reserved ones included, as long as it meets
the shape its kind requires and is not one of the kernel’s own
(is_kernel_only). The kernel-owned seq / epoch_ms are
stamped here and overwrite any caller-supplied value; nothing else
is added, and the labels the caller declared — meta.beat among
them — are recorded as given.
No append moves the budget, this one included. A deduction is asked
for before a call (Session::reserve) or taken after it
(Session::spend) by the layer that knows what a call costs; the
history records what happened and says nothing about what was
allowed.
The session’s own boundaries are not appendable: session_opened and
session_closed are written by Session::open_on and
Session::close, and a caller asking for either is refused.
The append lands. The store assigns the seq and serializes the
write per stream, so two handles on one stream both record and the log
interleaves in the order the writes arrived. A stale view of the head
is not a reason to refuse a fact: [Session::head] is what this handle
last saw, and nothing is compared against it.
The one refusal is this handle having closed. Another handle’s close
is not: it set that handle’s flag, and a write landing after the
session_closed it wrote is recorded, because that is what happened.
Sourcepub async fn events(&self, from: u64, limit: usize) -> KnlResult<Vec<Current>>
pub async fn events(&self, from: u64, limit: usize) -> KnlResult<Vec<Current>>
Events with seq >= from, at most limit, cloned, at the current
shape.
They come back as Currents: the read went through the upcaster
seam, and the type says so, so a caller folding them cannot be folding
a shape that has been superseded. A caller that needs to own the
underlying object — the Lua bridge, building tables — takes it with
Current::into_inner.
The caller says how much it will take. A stream grows without
bound, and every event of it read here is decoded, upcasted, cloned and
(across the bridge) turned into a Lua table — so an unbounded read is
an unbounded allocation on the VM’s own thread. limit is the
caller’s answer to that; the shell reads a page at a time and pages
with from (super::query::DEFAULT_LIMIT is what the bridge asks
for). usize::MAX is still spelled out where a caller really does
want the whole stream — a restore fold, a test — and says so at the
call.
Fallible: a durable backend can hit a transient busy read or a row it
cannot decode, which surfaces here rather than being dropped silently.
The in-memory backend is always Ok.
Sourcepub async fn len(&self) -> KnlResult<usize>
pub async fn len(&self) -> KnlResult<usize>
Number of recorded events. Fallible like Session::events.
Sourcepub async fn is_empty(&self) -> KnlResult<bool>
pub async fn is_empty(&self) -> KnlResult<bool>
Whether the history is empty (only before session_opened, i.e.
never for a session built by Session::new).
Sourcepub async fn reserve(&mut self, amount: i64) -> KnlResult<bool>
pub async fn reserve(&mut self, amount: i64) -> KnlResult<bool>
Ask the budget to allow amount: true when it was deducted, false
when the balance would not cover it (and nothing was deducted).
The stop the budget exists for. A caller asks before it spends, and
a false is a planned halt with the balance untouched — not a
failure, and not a state the run has to be rolled back out of.
Whether there is a budget at all is the log’s answer, not this
handle’s. The decision is shown the stream’s budget_* events, and
a ledger with no budget_granted in it is a run with no quota: nothing
is decided, nothing is recorded, and the answer is true. A grant the
log does carry is honoured even by a handle that was opened without
one — two handles on one stream cannot disagree about whether it has a
budget, because neither of them is asked. The scope’s cached grant is
a hint about the words (§ Session::grant) and never the authority.
Both answers are recorded, by the same decision. A
budget_reserved when the balance covered it, a budget_refused
(carrying what was asked for and what there was) when it did not —
whichever the decision built lands in the transaction that took it, so
exactly one of the two is in the log and this call’s answer is which
one that was. Recording the refusal afterwards, as a second append,
made a refusal that could not be written indistinguishable from a
storage failure with nothing decided.
This is a command with an invariant, so the decision is taken inside
the store (EventStore::append_if): the backend hands the ledger to
fold_balance and writes the decision’s event in the same serialized
write, so two handles on one stream cannot both reserve the same
allowance. Nothing is set afterwards: the write moved the store’s
head, so the next read of Session::remaining refolds the ledger the
entry is now part of.
A handle that has closed refuses, like Session::spend; another
handle’s close is nothing to this one — the balance is the whole of the
invariant.
Sourcepub async fn spend(&mut self, amount: i64) -> KnlResult<()>
pub async fn spend(&mut self, amount: i64) -> KnlResult<()>
Deduct amount from the budget without asking.
The other half of Session::reserve, and an independent one: a
reserve is a deduction that refuses when the balance is short, a
spend is a deduction that does not ask — it floors at 0 rather than
refusing. Neither holds anything for the other to release, so calling
both for one beat deducts twice; the layer above decides which of them
a beat uses. It is recorded as a budget_spent, which is the whole of
the move.
Whether there is a budget at all is the log’s answer, exactly as it
is for Session::reserve: the decision is shown the ledger, and a
stream with no budget_granted in it has no account to move, so
nothing is written. That question is inside the same transaction as
the write for the same reason the balance is — a handle’s memory of
what it opened with is not what the ledger says.
There is no balance invariant to hold, so a spend is never refused for what the account holds. What the decision decides is only whether there is an account.
The write is the result. It used to hand back the balance it read
afterwards, which made a spend that landed and then failed its
read-back indistinguishable from one that never landed: the caller got
an error either way and could not tell whether the deduction was in
the log. Two questions, two calls — this one says the move was
recorded, and Session::remaining says what is left, failing on its
own terms.
A handle that has closed refuses before the store is reached; another handle’s close does not, and a deduction landing after one is recorded as what it is.
Sourcepub fn grant(&self) -> Option<&BudgetGrant>
pub fn grant(&self) -> Option<&BudgetGrant>
The grant this run opened (or resumed) with, if any.
Read for its words — the tag a caller reports when a reservation
is refused — and for its presence, which is what says this session
keeps a ledger at all. The amount on it is the last grant, not
what is left: that is Session::remaining.
Sourcepub async fn remaining(&self) -> KnlResult<Option<i64>>
pub async fn remaining(&self) -> KnlResult<Option<i64>>
The remaining balance: Ok(None) without a budget.
The ledger’s answer, not a counter’s: fold_balance over the
stream, so a handle that has written nothing still sees what another
handle spent. The fold is cached against the store’s head and retaken
only when the head has moved, so a read on a quiet stream costs one
head query.
Fallible, and deliberately so. A store that cannot be read has no
answer to give, and the two answers this call can otherwise hand back
— the last fold, or None — both read as facts about the budget:
“you have this much” and “there is no budget here”. Serving either
off a failed read would fold a failure into a value, and the caller
most likely to act on it is a loop deciding whether it may go on
spending. So the failure surfaces, and what to do about a transient
busy read (KnlError::is_retryable) is the caller’s to decide.
Sourcepub async fn exhausted(&self) -> KnlResult<bool>
pub async fn exhausted(&self) -> KnlResult<bool>
Whether the budget is used up (never true without a budget).
The same fold Session::remaining reads, asked as a question — and
fallible for the same reason: a false that meant “the store could
not be read” is the one answer a run must never be given, because it
reads as “carry on”.
Sourcepub async fn close(&mut self, reason: Option<&str>) -> KnlResult<()>
pub async fn close(&mut self, reason: Option<&str>) -> KnlResult<()>
End the session, recording session_closed with reason
(defaulting to DEFAULT_CLOSE_REASON).
Idempotent per handle: closing a session this handle already closed records nothing. Another handle closing the same stream is a second ending in the log — the truthful record of two handles both believing they owned the session, and the shape an audit needs to see.
Open children are recorded, never a refusal. In the same write,
the store looks for the streams that name this session as their parent
and carry no ending of their own (Session::open_child); if it
finds any, their ids go on the boundary as
data.open_children. The close still succeeds — the log turns no
write away, and a run that ended while what it started was still going
is exactly the fact worth having in it.
Fallible on a durable backend: the session_closed append can fail on
a database that stays contended past its retries, or a store that is
gone. On failure the session stays open (closed is not set), so the
caller knows the boundary was NOT recorded and can retry — a close
that reports success with no session_closed in the log would
silently break resume/audit reads.
Sourcepub async fn close_with(
&mut self,
reason: Option<&str>,
detail: Option<&str>,
) -> KnlResult<()>
pub async fn close_with( &mut self, reason: Option<&str>, detail: Option<&str>, ) -> KnlResult<()>
Session::close with a free-text detail recorded beside the
reason.
The reason names which kind of ending this was, and stays a short
vocabulary a reader can fold on; detail is the sentence that only
this close can tell — the message of the error that ended the scope.
Keeping them apart is what stops every distinct error message from
becoming its own reason.
Idempotent and fallible exactly like Session::close.
Sourcepub fn close_detached(&mut self, reason: &str)
pub fn close_detached(&mut self, reason: &str)
End the session by handing session_closed to the store and not
waiting for it — the drop backstop.
The one close path with nobody left to tell. A handle that was
collected without ever being closed is being dropped right now, on the
VM’s own thread, inside a Lua collection cycle: there is no task to
suspend in and nothing that may block, so the event goes to the store’s
own writer (super::EventStore::detach_append) and whether it landed
is reported to the log rather than to a caller.
The connection thread outlives this handle — its driver belongs to the
host, not to the session (super::Logs) — so the submitted
event is still executed, and the host’s shutdown drains it.
Idempotent per handle, like Session::close: a session this handle
already closed records nothing.
Sourcepub async fn view(
&mut self,
name: &str,
opts: Option<&Map<String, Value>>,
) -> KnlResult<Value>
pub async fn view( &mut self, name: &str, opts: Option<&Map<String, Value>>, ) -> KnlResult<Value>
A named projection over the history.
tail is the only name, and it reads opts.n (default
projection::DEFAULT_TAIL_N) events from the end. An unknown name
is an error — the vocabulary is closed on purpose, and it is as short
as it goes: a projection whose shape depends on what the caller does
with it is built above the kernel, from Session::events or with
SQL over the published schema (Session::query). The token
account is one of those now: it reads the llm_response payload,
which is the shell’s vocabulary and not the kernel’s.
&mut self because the signature belongs to the vocabulary rather
than to today’s members of it — a fold the kernel names again may
keep a cache, and a caller should not have to be recompiled when one
does.
Sourcepub async fn query(
&self,
sql: &str,
params: QueryParams,
opts: &QueryOpts,
) -> KnlResult<QueryRows>
pub async fn query( &self, sql: &str, params: QueryParams, opts: &QueryOpts, ) -> KnlResult<QueryRows>
Read the log with SQL.
The other half of Session::view, and the reason that list of names
can stay short: a fold whose shape is the caller’s — beats grouped,
tool calls paired against their results, a ledger — is a SELECT
against the table the log lives in, not a name the kernel had to be
taught. What the kernel keeps is the boundary around it
(super::query): one statement, and it reads; a connection that
cannot write; values bound rather than pasted; a deadline; a row cap.
Two names are reserved. $stream is this session’s own stream, and
$sessions is the set in opts.sessions — the session’s own stream
when that is omitted — expanded to one bound placeholder per id, so
reading across a tree of sessions is one statement rather than a loop
of them. The kernel does not judge the set: which streams a caller
may read is a question about who the caller is, and that lives above
the kernel.
Reads keep working after this handle closed, like every other read here: the record outlives the session.
Trait Implementations§
Source§impl Debug for Session
impl Debug for Session
Source§fn fmt(&self, f: &mut Formatter<'_>) -> Result
fn fmt(&self, f: &mut Formatter<'_>) -> Result
The store is a trait object (dyn EventStore is not Debug), so it
is summarised rather than printed; the fields that identify the
session are shown.
The length is not among them: reading it is a call to the store now,
and Debug cannot wait for one. A caller that wants it asks
Session::len.
Auto Trait Implementations§
impl !Freeze for Session
impl !RefUnwindSafe for Session
impl !UnwindSafe for Session
impl Send for Session
impl Sync for Session
impl Unpin for Session
impl UnsafeUnpin for Session
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§fn in_current_span(self) -> Instrumented<Self> ⓘ
fn in_current_span(self) -> Instrumented<Self> ⓘ
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more