Skip to main content

Session

Struct Session 

Source
pub struct Session { /* private fields */ }
Expand description

One session: a stream, and the scope it is written under.

Implementations§

Source§

impl Session

Source

pub async fn new( owner: String, grant: Option<BudgetGrant>, drivers: &IsleDrivers, ) -> 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 SQLite backend a durable session uses, on a database that is reclaimed when this session lets go of it (SqliteEventStore::open_memory). 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.

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.

Source

pub async fn open_on( owner: String, grant: Option<BudgetGrant>, 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.

Source

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_opened event: its ScopeId (FIELD_SCOPE_ID) and its owner (FIELD_OWNER). An older log written before either was recorded falls back — to a fresh kernel-issued scope id, and to ANON — rather than failing the resume;
  • the grant, from the last budget_granted the log carries, so a reopened stream goes on keeping a ledger and a refusal still has a tag to report. The balance itself needs no restoring: it is fold_balance over 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.

Source

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.

Source

pub async fn open_child( &mut self, child_stream: String, owner: String, allocation: Allocation, 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_opened carries FIELD_PARENT — this session’s stream — and the budget_granted it opens with carries it too, so where the units came from is in the log beside them;
  • this session’s ledger gains a budget_reserved naming 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.

Source

pub fn id(&self) -> &str

The session-correlation id.

Source

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.

Source

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.

Source

pub fn owner(&self) -> &str

Whose scope this is (a principal id, or ANON / SYSTEM).

Source

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.

Source

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 a beat the caller declared is 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.

Source

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.

Source

pub async fn len(&self) -> KnlResult<usize>

Number of recorded events. Fallible like Session::events.

Source

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).

Source

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.

Source

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.

Source

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.

Source

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.

Source

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”.

Source

pub fn is_closed(&self) -> bool

Whether the session has ended.

Source

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.

Source

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.

Source

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::IsleDrivers) — 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.

Source

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.

Source

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

Source§

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§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self>

Converts 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 more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
where F: FnOnce(&Self) -> bool,

Converts 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
Source§

impl<T> MaybeSend for T

Source§

impl<T> PolicyExt for T
where T: ?Sized,

Source§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow only if self and other return Action::Follow. Read more
Source§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow if either self or other returns Action::Follow. Read more
Source§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more