soev for developers
soev-agentsReference

Glossary

The terms that this reference uses, defined once.

Thread and record

thread

One conversation or lifetime, with an id a client keeps. A thread is an entity: its identity outlives its contents, and it is what gets opened, cancelled, forked and deleted. Its contents are its , and that is the only state there is: no current-node pointer, no waiting flag, no copy of the messages sent to the model. A thread is longer than a and wider than a .

trace

A 's , read through a typed query API (last, latest, by_type, stream, view) that selects facts and applies no rule; never walk raw . A trace is a value, not an entity: a sequence of you can hold, slice, serialize and compare, with no identity of its own. That is why a trace exists where no does, and the other way round: a is handed a , a Trace over part of one 's , never the ; a copies a prefix of one 's trace into a new ; a feeds a recorded trace to a builder with no live at all. In prose the trace is also called the log, in the event-sourcing sense of "the log is the state"; the two words name the same .

stream

One append-only sequence of with its own from 0. A 's a tree of streams rather than one sequence, and every starts with a single one, named root. More streams exist because of concurrency: when a fans out, or a delegates to , the run at the same time. If they all append to one sequence, the log records which branch happened to finish first, a fact decided by timing that a cannot reproduce. So each appends to a stream of its own, never sees a sibling's, and the order across streams follows the tree rather than the clock. This is CQRS's rule of one event stream per aggregate, the with a single writer, applied to branches. Streams hang below one another as a tree, and parent, child, sibling, ancestor and descendant are used in their plain tree sense for any stream; what a stream carries is said by branch or . A stream's name says where it hangs: root/search@3 is the that started at search after 3 of the root. The verb in Thread.stream() is unrelated: it runs a while yielding its .

branch

A a started: one per target of the , walked at the same time as its siblings, ending at a or wherever its own says stop. A branch runs the same as its parent and can read the parent's up to the , but never a sibling's. That is what separates it from a 's , which runs another and sees only the it was handed. In a name a 's carries an index after #, as in root/research#0@7; a branch does not, as in root/search@3.

scope

The part of the that one run — the , or one — produced and can read. It is the region of the tree cut at every whose first is an Input: the root stream heads the 's scope, and each 's , which begins with its , heads its own. Scope is a read boundary, not only a way to select . A 's are handed nothing of the parent before the , and the parent's nothing of the beyond the it recorded in its own . That is what lets the same behave identically standalone and as a , and it makes the the one recorded place where the parent decides what a gets to see, which matters when the runs on another model. Selecting a 's part of the afterwards uses the same cut. The cut looks at a 's first only, so an answer that lands in the middle of a waiting branch does not cut the tree. A is one 's slice inside its scope.

view

What a running in a given stream can read: that stream, the below it, and its ancestors up to the it fanned out from — never a sibling branch, never an ancestor's later , never outside its . A reads the parent's facts and its own; the , and the parent after it, read every that ended. Two branches cannot see each other because whether a sibling had already landed depends on timing, and the log must never record a fact decided by timing. The view is handed to the as a Trace, and it is the only thing a , a or a receives at call time: the messages the model sees are rendered from the view plus the builder's own parameters, which were fixed when the builder was built. The Trace holds only the view's , so it cannot be widened from inside; a that reached past it by holding the store breaks the rule that a is a function of its view.

turn

The 's work on one fresh input: from that input up to the next fresh input in the same . "Turn" as in taking turns: the outside world gives an input, the works until it stops (Finished), is stopped (Cancelled) or fails (Failure), and then the world can give the next one. It is not chat-specific: a fresh input is any Input that responds to no Request, whatever sent it — a user's message, a webhook, a scheduler, or a parent's , which opens a 's first turn in its own . An Input that responds to a Request continues the turn it was asked in and does not open one. A one-shot , such as a pipeline, takes one turn; a chat takes one per user message. The 's turns are those of the root stream. The rule is a fact of the , so reads need no code: reads the current turn, a ends it, the is built for the input that opened it, and a response cannot switch its model. A turn is not a : one turn can take several runs, after a crash or a failure. Context management also counts in turns (see ).

round

One model call that asked for , and the results of those calls: from a ModelOutput with up to the next such ModelOutput in the . A ReAct turn that searched three times holds three rounds; a that answered at once holds none. A context (see ), so an that works long on one input can and compact by rounds where turns[:-2] selects nothing. Example, one of two rounds: Input (turn 1 starts) · ModelOutput calling search (round 1 starts) · ToolOutput r1 · ModelOutput calling open twice (round 2 starts) · ToolOutput r2 · ToolOutput r3 · ModelOutput answering. Here turns[-1] selects r1–r3, rounds[-1] r2 and r3, results[-1] r3.

unit

What context management counts age in when it selects results to or a part of the history to summarise: turns (see turn), rounds (see round) or results (one result each, in recorded order), each indexed or sliced like a Python list, the current one last, as Compress(rounds[:-3]) or Compact(where=turns[:-2]). All three are facts of the , so no defines its own: a selection that is not a fact, such as a time window, is a Shrink of the 's own. A summary never splits a from its results: a cut by any unit that splits them falls back to before the call.

run

One pass of the over the : from the of its until every it walks has nothing left to do. A fresh , the middle of a run, a after a crash and a retry after a failure are all the same computation, so a run needs no checkpoint. Inside a run, the traversal of one is a walk. A run belongs to the process, not to the caller that started it: Runs keeps the running per id, a second caller joins the run rather than starting another, and a caller that leaves does not end it. One per is the rule; in one process Runs enforces it, and across processes the does.

tail

The last of a . The reads it, with the , to decide what happens next in the . The store reads a with TraceStore.read, which yields from a onward and, when following, waits for more; TraceStore.read_backwards mirrors it, newest first and never following, for a reader that wants the end of a without reading its beginning.

tree order

The one order across a 's : a sorts directly after the parent it fanned out from and before the parent's next , siblings by name and then by index, each internally by its own . It depends only on the names of the and the seq of the , never on when a 's were appended. The order in which branches finish depends on timing, and the log never records a fact that timing decides. A Trace holds its in tree order.

append order

The order the store a 's , across all its . It is the one order a subscriber can bookmark, and it is what the numbers. It differs from whenever branches ran concurrently.

Graph

graph

The configured, runnable the runtime executes: , each holding its with the model, , and prompt already bound; ; an ; and optionally joins and a . A graph is not a bare shape — hand it a store and it runs. An builds one for each it runs. The shape alone, without configuration, is a topology, and nothing in the code is only that. Cycles are allowed and end when a returns None.

hidden state

A value that code, a or a keeps in memory, that changes what is recorded or what a model receives, and that cannot be derived from the . Not allowed: the next process after a crash behaves differently without it. State inside an that changes only what the outside world answers is not hidden state, because the answer is recorded. Neither is a lock that holds no data. A keeps none: it never receives the , gets what earlier calls produced as , and keeps working state in local variables of one call. The rule and its test: "The no-hidden-state rule" in the package README.

resume equivalence

The test property that rules out : a that stops after any and with every object built again produces the same as a that never stopped, ignoring ids and timestamps. The framework's runs it for each , also with the alternating between two .

test kit

The checks every 's tests run, in soev_agents.testing: (check_resume_equivalence) and the determinism check (check_determinism), which rebuilds a recorded 's requests under several configurations with and refuses any difference, between them or from the recorded .

recorded element

An that an earlier of the recorded, given to a as a parameter typed Recorded[T]: before each call, the fills it with every T that the recorded so far, the input's included, in recorded order, an id recorded twice listed twice. A that needs the latest state of an , such as whether an attached file is ready, reads the latest record of its id. The model never sees the parameter. It is how one uses what another produced without reading the : the two depend on the same class, not on each other.

refusing adapter

An that fails every call with ResourceUnavailableError, whose message the model reads: what a gives where the real one is unavailable or unconfigured (no web, no live ticket), and what and the build with (refusing(name)), so a build that called an fails at once.

resources

What a gives an 's build for one request: the objects that touch the outside world (model clients, stores, search, web, live connections). Resources hold no data that a decision reads, and a resource is never absent: an unavailable one is present and fails each call with a typed error. build only hands resources to and to the model ; it never reads, calls or tests them. The access a caller has lives in its resources, so the framework knows no permissions.

agent

An and how a is built for a : what a registers and AgentCatalog serves under a name, so that are opened for it and Opened records that name; the whole contract between a and an agent (runtime.Agent). A ready-made agent such as ReActAgent is configured, not extended: what depends on the goes in its config function. The catalog parses each fresh input with the class, records the input with the the parsed value holds, and calls the agent's synchronous build(resources, turn, model) with the the catalog's caller gives for that request, of the caller's own type and passed through unread, the parsed turn, and the model the input names, None when it names none: the whole runs on that model, and an answer that names another one is refused. An agent also declares the and types its record beyond the core ones; its are read and recorded with those and the core types only, so a that produces an undeclared type records a Failure. The code that builds a — choosing parts, topology and every prompt — is where policy lives.

ReAct config

Every setting of a ReActAgent for one but its : the model , the prompt, the limits of the model, the , the and the rest (ReActConfig). Models behave differently and need their own tuning, so a config holds the settings for one model. The takes one config for every , or a function that builds the 's config from the request's , the and the model the input names; nothing below the looks a model up. Only the and the prompt are required. A config function can refuse a whose model it has no settings for, before anything is recorded.

prompt file

Every text an chooses for its models, by key, in one YAML file per : the system prompt as a template with blocks, plain texts, and texts that replace a 's own description or argument descriptions; then a layer per group of models, by endpoint ids, that replaces keys the base defines (PromptFile). A takes its prompt from the texts its model resolves to; a keeps default texts of its own, which the prompt file overrides. The keys and the digest of each resolved set are what a prompt optimizer such as GEPA writes and compares; the digest is not an 's , which names the code that wrote it.

node

A named in the : a name plus what it holds — a , an or a . connect names, and every records the name of the node that produced it. The same configuration can sit in two nodes, which is why node and are different words.

step

The work a holds: an async function from the to new , configured completely when built, consulting no clock, randomness or environment. Steps are where the outside world is consulted — model calls, I/O — and they must return at least one , because a that records nothing is invisible to the log. A step differs from a part (a strategy it is configured with) and from a (a sync function that only reads).

effect step

The second kind of work a can hold: a two-phase shape for a that changes the world instead of observing it. It has a sync intend that computes the EffectIntent, an that performs it, a for the window between, and a for a call still in flight when the is cancelled. The drives the phases; the is never called as one function.

edge

A 's way out. Plain edges name one or more fixed targets, and a second target makes the a . A conditional edge names a . A has one or the other, never both; a with no edge is terminal.

router

A pure function from the to the name of the next , or None to stop: the switch, written as ordinary Python over recorded facts. Routing needs no of its own, because everything the router reads is already in the log and re-deriving it on gives the same answer. Routers are sync on purpose, so they cannot do I/O, and are defined with the so topology stays out of . A router that raises, or names a the does not have, ends the with a Failure on the its leaves, never because the same routes the same way again; the stays readable.

entry point

The a fresh dispatches from — in a , and in a 's alike, which is why one runs unchanged as and as .

fan-out

A with more than one plain target: the walk starts one per target and walks them at the same time, and a is where they collapse again. Not to be confused with a , which copies a whole .

join

A declared as the place branches collapse. A that reaches it ends there; the join runs once, in the parent stream, after every has ended and at least one reached it. Plain into a shared are not a join: such a runs inside each , on that branch's own facts. Two into one mean "either path" elsewhere in a , so the join must be declared.

budget

The cap on model calls that ask for in one , the that gets the wrap-up call when the cap is hit, and how many wrap-up attempts a model that keeps asking for gets before the stops the . An answer to a request continues the , so it continues the count too; a budget set to resets_on_answer starts the count again at each answer instead.

input class

The pydantic class an 's fresh inputs must parse into, in strict mode: a it refuses is never appended. It is the whole contract for a fresh input. The the parsed value holds are recorded on the Input, so reads the alone and a change to the class never renumbers a stored . A tool-shaped derives the 's arguments from the 's input class, so what the parent's model sees and what the accepts cannot drift. A Request's expects is the same idea in the other direction, as a JSON Schema: what a reply must look like.

Events

event

One immutable fact in the log. A creates it with its fields; the stamps which produced it and which stream it belongs to; the store stamps its and time at append; from then on it never changes. Event types are an open set: an defines its own by subclassing Event. An event is not a message (what the model sees is rendered from events) and not a (an event on the wire).

bookkeeping field

A field the machinery fills in, never the : stream, seq, actor, origin, ts, version. On the wire they sit beside the , not inside it.

version

The code that wrote an : one opaque string the names its code with at startup (for example myapp@<commit> (soev-agents@<commit>), +dirty for uncommitted changes), written by the runtime on every it appends, beside ts. It says which code to check out to a exactly; nothing reads it to decide anything.

request hash

ModelOutput.request_hash: the SHA-256 of the request the framework built for a call, as canonical JSON. A rebuilt request that hashes otherwise was built by other code or from something besides the .

payload

An 's own fields, the ones its class declares beyond the bookkeeping; on the wire, the nested object under payload. Input.payload is also the name of the one field on an Input that holds the message or value from outside — any JSON value, so a spec or an approval decision fits without new fields.

sealed

In the log: seq and ts stamped, always together, at append. A sealed is a fact and can never be appended again; an with only one of the two stamped is refused at construction. Everything before that point is unsealed: what a returns, what a hands back.

batch

The one call returns, or one append lands: appended together, atomically, with one timestamp, in the order produced. A crash can fall between batches, never inside one, which is why tests kill a at every batch boundary.

expected position

The length of each a writer read, named on its append. The store refuses the when any named stream has grown since — the moved — so two writers never interleave; unlisted streams are not checked. It is per , so concurrent branches never refuse each other. The always names it; a or an answer appends without one.

position

An 's place in its 's log: 1-based, without gaps, assigned when the is , in the order the store committed, across every of the . It sits on the LogEntry the store's yields and on the wire , never on the itself: it records who won a race between concurrent branches, so it is never serialized with the and never read by a ; seq records the fact's place in its own , which no race can change.

cursor

A reader's bookmark: the last it has seen. read(after=) and watch(after=) take one, the SSE id: line hands one out and Last-Event-ID sends it back, and fork(at=) cuts at one. A cursor holds a ; the belongs to the 's entry, the cursor to whoever is reading.

origin

The name of the that produced an ; None for an no produced, such as an Input from outside. The one Input a does produce, a to a , carries the delegating 's name. origin says which code ran; actor says who is responsible.

tag

The string an class declares in its type field, which EventTypes keys on to rebuild the right class. The store keeps it as an opaque string and never looks it up. Two types that exist to be subclassed, Input and ToolOutput, declare it as a plain string so a subclass can set its own.

envelope

The one shape an takes outside the process — a database row and a wire alike: the and the beside a payload object holding every other field exactly as the class declares it. Nested rather than flat so that a 's own id never collides with a wire-level id. StoredEvent is the envelope as TraceStore takes and returns it; the OpenAPI document of the API names the same shape on the wire Envelope, with the and added.

frame

One server-sent-event message on the HTTP . An frame carries one : the plus the and the , with the type on the event: line. A core type's frame is typed, <Type>Frame, its the type's own fields, which is what the OpenAPI document describes; any other registered type travels as the plain EventFrame of the OpenAPI document, the same over an untyped . A delta frame carries answer text and a reasoning_delta frame carries reasoning; both have a {"text": "..."} and no id. Answer text never carries : a citation frame, shaped as one entry of an answer's , follows the text it belongs after. A closing status frame says where the stands.

event id

An 's identity on the wire, written stream:seq, for example root:7. A client answers a request by naming its event id. It is not the , the number on the id: line of a that a subscriber keeps as its .

Event types

Compaction

A summary the model wrote of every before the one at kept_from_seq in kept_from_stream, which from then on replaces those in what the model is sent (see ). The kept is named by its and its seq (kept_from_stream, kept_from_seq), not by a count, so reading a summary needs no rule. Recorded because the summary took a model call and cannot be worked out again from the . It records the model that wrote it, its token usage, and its text without beside the taken out of it, as a ModelOutput does (see ). The keeps the it replaced.

verdict

A response from outside the system to a : the evidence needs to resolve a .

hand-off

The Input a delegating seals at the head of a 's : one per , phrased by the parent's policy, stamped with the delegating 's name. It is the one Input a produces, and it never travels in a 's .

hand-back

The the parent records for one ended , projected from the 's : typically one ToolOutput carrying the 's result, with the Sources it was shown, and what they are part of, as . A result that cites comes back as the API serves it, text without and its with real ids, and the parent's builder writes there in the parent's own and strategy. It is how a parent cites its 's sources, numbered in the parent's own , and the only thing of the the parent ever sees.

Running

runner

The loop that runs a : read a 's , get what happens next from a pure function of the and the , act, append, repeat. It keeps no variable and decides nothing itself.

transient, fatal

The two classes of failure. Transient: the service gave no answer and the same call can succeed later without any change — a 503, a timeout, a dropped connection — so the next re-enters the . Fatal: everything else, an unclassified exception being a bug until proven otherwise, so the walk ends and only a new input, or someone acting on the code, moves the . translate their vendor's errors into TransientError in one place each.

projection

Anything derived from the log by a pure function of the : a 's decision, a rendered context, the 's , a 's and , what the does next in a . Projections are sync where the type system can enforce it, so they cannot do I/O, and are never recorded, because a derived value cannot disagree with what it was derived from.

thread state

Where a stands, computed from its facts and its only, never with code or a , so a read works after any code change: finished when the root stream's last is Finished, or the holds only its Opened; cancelled when the current holds a Cancelled and no of the root stream still needs its result; running when a process holds the ; waiting when a request is open; failed when a of the current ends in a Failure, or not; else interrupted, work left and nobody running it. The computes the crash, so a client computes nothing. A failed takes a fresh input, which opens a new and leaves the failed one in the log, unless the failed turn still holds an without its result; finishes the failed turn instead. An interrupted is one a left before its ended: the process was killed, the raised, or a response landed after the 's last look; picks it up, and an input just submitted and not yet run reads interrupted for that moment too. Running and interrupted both refuse a fresh input, since the is not over.

lease

The right to run one , held by one owner at a time and kept by the store beside the : the owner, an expiry, and a counter that goes up by one at every acquisition. A takes the lease before it walks, names the counter on every append, renews the lease while the walk lasts and releases it when the walk stops. The store refuses an append whose counter is not the 's current one, so a that stalled past its expiry and lost the to another process cannot write under the lease it had, whatever the timing. The owner is the process, as Runs names it. A lease is only ever taken on a that has , the refusing an empty id before it asks for one. A releases when its walk stops and when its is cancelled; a lease that expired without a release is what a killed run leaves behind, and the next acquisition takes it; an owner that still holds a lease takes it again at once, with a new counter, which is how a process that cancelled its own starts another without waiting for the expiry.

open request

A Request a caller can still respond to: the waiting of a in the current . In a cancelled turn every request is closed except a 's . open_requests computes it, the one definition both the and the routing of responses use.

resume

Run a from its with nothing new to say: the verb for a crashed or transiently failed turn, and for continuing a forked , which is cut at a and so left with work to do.

fork

Copying a up to a into a new , which then continues on its own while the original is untouched: git's sense of the word. It has nothing to do with a , which splits a 's walk into branches inside one .

cancel

End the with one Cancelled in the root stream. It closes every of the except a 's verification, and an unanswered outranks it: still runs on the next pass, because nobody knows whether money moved. In the process that owns the , a cancel stops the running at once; one posted through another process's store takes when that 's running ends, because the 's next append is refused and it plans again; until then that continues, at most one model call or .

replay

Feeding a recorded log to a reader: reconstruct it, re-project it with a different builder, or it and continue live from a point with refused. None of these re-executes a , because state is a of the . Replay equivalence is the test behind it: identical logs must route identically.

Effects

effect

An action in the world — a mail sent, a refund issued — that must happen exactly once and must be visible after a crash. It is recorded as an before it runs and a result after, with for the window between.

intent

The declaration that an is about to run, unanswered until its result lands. See EffectIntent.

effector

The 's member that performs a and maps the world's answer to one unsealed result. A definitive refusal from the world comes back as error data; an exception means no answer exists. define their own effectors and wire their own clients; there is no family for , which is why an effector is not a .

cancellation

The 's required strategy for the call that is in flight when the caller the . It is handed the , the , the and the call itself, still running, and returns one EffectResult when it resolved what happened, one Request when only the outside world can say, or nothing when the call is to be given up and the left to recovery. It is a strategy of its own rather than a inside because the two know different things: at time the process is alive, holds the and has the live call in hand, which is the only moment an can still be stopped at its far end, while runs later, possibly in another process, with nothing but the log. Nothing is recorded when the is cancelled with no Cancelled in the log, which is a process on its way out rather than a caller's . abandon is the one that gives the call up.

recovery

The 's required strategy, consulted whenever the re-enters the while the is unanswered. It returns exactly one EffectResult — it resolved what happened, by looking the outcome up, by re-performing when that is safe, or through a — or exactly one Request when it needs the outside world to tell it. Recovery is chosen per , because re-sending a notification email is noise and re-issuing a refund is money.

torn intent

An with no result in the log: nobody knows whether the world changed, and only evidence from outside can tell. Re-entering the lands in , never in the . Prose says "torn " for a whose is one.

verification request

The Request emits when it needs a human to say what happened. It stays open through a , and its response is a .

gate

A that pauses for approval: a RequestStep emitting its configured Request, with the decision over the response on its conditional — approve and decline as targets.

Delegation

delegation

Another held by a , built per from the Input heading its (graph_for), together with two sync : hand_offs, which reads the parent's and produces, per in call order, the Input that starts its or the that answers the call without one, such as a refusal of its arguments; and hand_back, which reads one ended 's and produces what the parent records for it. Answers made without a land with the , in call order. It is the third thing a can hold, next to a and an , and it binds topology, so it lives with the rather than with the . The drives the delegating in phases, as it does an and a : seal the , walk the at the same time, and once every one has ended land the in the parent stream as one under the 's name. A still waiting or failed leaves the parent's unchanged, so every later pass re-derives the delegation and each from its own .

task

One of delegated work: one Input, one . A hands off a sequence of tasks, one per when it is tool-shaped, the way a model can spawn two researchers in one .

subagent

A run by a delegating inside another 's , in a of its own that begins with the : one per , so a asked for two researchers produces two subagents. A subagent's is numbered in order, root/research#0@7 then root/research#1@7, and heads its own , so the subagent sees nothing of the parent beyond the and the parent sees nothing of the subagent beyond the . The parent is the whose delegating opened it. Everything the subagent did stays in the 's single log; the cut is only in what are handed. Nothing marks a as a subagent: the same runs unchanged standalone as an and inside another as a subagent, and it need not involve a model.

Context and citation

context builder

A from the to everything one model call receives, "context" in the sense of a model's context window: the messages, the offered, and the model the call goes to; together with what the answer is read with, the that model was told to cite with and the it was shown. It also refuses a call whose prompt does not fit, before it is made. Sync so it cannot do I/O. Its parameters — the prompt, the , the , and any window on how much history to render — are bound when it is built, as part of the ; at call time it receives the and nothing else, so a per-call argument can never shape the rendering without being in the log. The model only calls the model with what the builder built and records the answer. The builder owns every message, the placement of the instructions included: a model that wants its instructions elsewhere gets a different builder, not a different . Rendering is what the builder does, just in time, and the rendered messages are discarded after the call because they are re-derivable. A builder decides per type what the model sees, and raises on a type it never decided about rather than silently skipping it. Each model has its own builder, so each of a can see different , a different prompt and different .

context strategy

The replaceable context management of a ReActAgent (ContextStrategy), given as its config's context: it makes the of each from the parts the computes (the model, the prompt, the offered , the , how an input renders, the , the limits), names the it offers itself (), and wraps the model (). The three are one context management because they depend on each other: starts from the size of the request the builder made, writes its summary from that request, and helps only if the builder shows the summary; is offered by the builder and run by the . MessageBuilder with its is the default and owns all three. The policy says how much the model sees, the builder's other settings (select, show, renderers) what it sees, and a strategy of one's own how the request is laid out.

chat model

A model behind a message API, the 's ChatModel and ChatRequest: it takes system, user, assistant and messages and offered . "Chat" names the API format, as the industry does, not the shape: a pipeline calls a chat model too.

context policy

How an keeps what a model is sent within the model's limits, one policy per model, held by the MessageBuilder in the 's ReActConfig for that model (context=MessageBuilder(ContextPolicy(...))). It holds when to old results and how (start_at, target and the , below); how a result looks in each (see ), one function per for every plus a 's own Forms by name; the output reserve, the room kept free for the answer: a call whose leaves less than that in the window is refused before it is made, and fails the ; the reserve is not sent as a limit, so an answer can use more. And a cap per result: a result the model reads at more than that many tokens, whatever its left, is cut at that point and says how much was left out; a can have its own cap or none, such as a reading that bounds what it reads itself, because an oversized result is rendered again on every later call and otherwise leaves every call over the window. And which earlier answers' reasoning is sent back to the model: never (the default), those of the running turn, or always; reasoning sent back counts toward the context like any text. And the , offered once something is shortened (see ). Every setting's default is a visible default of the policy itself; a setting given replaces it whole, and nothing is added behind the scenes.

prompt estimate

How many tokens a prompt about to be sent is taken to be, without a tokenizer. The latest model call recorded its prompt's exact token count and its characters, so only the characters added since are divided by the model's characters per token; the whole prompt is divided when that call was another model's, recorded no counts, or was longer than the prompt now is, since a removal estimated with a pessimistic ratio counts too few tokens. The ratio's error thus only applies to what was added.

form

How much of one result the model is shown: full, as its 's full function gives it (as recorded by default); compressed, as its 's compressed function gives it (by default its as with their numbers but without text, its own text and data kept), under a line naming its , so the model can tell text was left out and what it needs; or stubbed, its output replaced by one line naming its handle and the keep, while the call that asked for it stays visible. are never recorded: each call works them out again (see ). While the model stays the same, a result's form only goes down. A form that frees nothing is never given, so the line always means text was left out: a result holding only JSON stays full until a stub reaches it, unless its 's compressed function shortens the JSON (json_cap).

forms

How one 's results look in each : a function per from the whole result to a smaller one (Shorten), the policy's full and compressed for every and a 's own Forms(full, compressed, cap) by name, a field left out using the policy's. A function can change the result's text, its data and the text of its ; it keeps every and the error, which the drawing checks, because and find by id. An a lifts shows its text in the it is lifted to. An input's are always shown as recorded.

shrink

What a does to old results when the context fills: an ordered list of shrinks, such as compressing the results of every but the last two, then stubbing those of every but the last four. A shrink without its own when starts above the policy's start_at and goes on to target; one with when (an emergency, such as stubbing within the last above 85% of the window) starts above it and stops once under it. A pass that could free less than the policy's minimum gain is not made, so the prompt cache is not broken for a few tokens. Which results a shrink applies to is a slice of the 's , rounds or results (see ), counted at the call being judged, the current one last. On every call the builder walks the 's model calls from the first, keeping a running total of the context's size in its own rendering (characters divided by the ratio); wherever the total passed the policy's start_at, the shrinks were applied, oldest results first, one at a time, until it was under target. So each call reaches the decisions of the calls before it with nothing stored, and the walk is judged by the model the upcoming call goes to: a switch to another model re-judges the whole history. Not to be confused with a , the work a holds.

compaction

The last resort of a , when shrinking results is not enough: the history before the last , rounds or results (see ) is replaced by a summary the model writes, user messages included. An opts in by giving its a compact, a Compact with its own threshold (when, 80% of the window by default), the it replaces (every but the last two by default; the running is always kept) and the instructions that ask for it. The cut never splits a from its results: it falls back to before the call. It is not one of the : it always comes after all of them. It is context management inside the model , not part of the : a wrapper around the model builds the context once, and when that context is still over the threshold after every , or does not fit at all, the model about to answer first writes the summary, not streamed, and then answers from it. The summary call is the earlier conversation exactly as the earlier calls sent it, plus the instructions. The Compaction and the answer are recorded together, so and never see compaction; a failed answer records neither, and the next attempt writes a new summary. From then on the builder renders the prompt and the latest summary in the system message, then the after those it replaced; those no longer count toward the context's size. numbers and are still those of the whole , so the summary keeps the the earlier answers gave the facts it keeps, in the of the model that will read it, and reopen brings any of them back in full. Nothing is compacted when no whole is left to replace; the call then fails as full. is another mechanism than shrinking (it writes to the , with a model call of its own) but the same context management, so it is configured in the policy of the builder that shows its summaries (see ); a summary after which the builder builds the very same request is refused.

floor

The lowest some can reach whatever their result has: by default, a passage cited before its result shrank stays full, so a compressed result keeps that passage's text and a stubbed result still shows it, inside the whole it is part of. A made after the result shrank changes nothing in it, so the earlier message stays as the model was sent it; the model reads that passage where it read it last, in a . An 's is its result's lifted to its highest floor. A floor is a predicate and a , Floor(applies, form): the predicate is given the , the name of the that recorded it, and the before the call where its result shrank, so what happens later never moves a floor. The default is Floor(cited, FULL), cited being the citing toolkit's predicate; any predicate sets one, such as every of a class; a policy with no floors lets everything . Floors only show more of a result; they never change its .

result handle

A result's name for the model, r1, r2, …, by recorded order among the results of the , a 's result included: derived from the , never renumbered, independent of any configuration. It names the result by the call id it answers, and Numbering is the one place that counts it; a result with no call id has none. A compressed or stubbed result shows it, and reopening takes it to show a whole output again. A argument holding handles is translated like an , when the model records the call: the gets the call id of the the named result answers, a handle naming no result is refused before the runs, and the API serves the call with call ids.

reopen

Showing the model again what a shortened or removed: by number, whole results by handle. It is part of the , not one of an 's : the policy holds the , and the offers it from the first model call that shortens a result or follows a summary, on every call after it, until the 's are withdrawn; and required-tool instructions never apply to it. Its recorded output names what it reopened, real ids and call ids, and stores no copy; the draws that content from where it was first recorded, as the reopen's own result at the bottom of the conversation, each inside its whole in the of the that recorded it and each whole result under its handle and its 's cap. Nothing earlier in the conversation changes. A reopen result is a result like any other: it has a handle and is compressed or stubbed as it ages; it is never cut as a whole, since its parts were cut under their own ' caps.

element

Something the model is shown that it can refer to again: as a argument, or as a . Being an element makes it known to the framework, which can then act on it: show it under a number and translate that number back before a runs, resolve and check , verify claims against a source's text, hide it when access is revoked. Anything the model can refer to must be an element, and its real id never appears in plain text. What an element is — a document, a knowledge base, a passage — is its class: an app subclasses Reference or Source once per kind of thing, never the base, and registers its subclasses beside its types. An element has a real id, properties passed through by the and never read by the framework, and optionally ref, the real id of the element it is part of. It is stored under its class's name in kebab case, knowledge-base for KnowledgeBase, unless the class declares its own type; renaming a class therefore renames its stored name, and a class renamed in code declares the old name to keep old readable. Elements ride in the elements of an ElementEvent: a ToolOutput, an Input (read from its fields typed with an element type, in the order the input's class declares them), or an 's own type that extends ElementEvent, such as a pipeline 's retrieved sources; counts them all.

numbering

The number each is shown under, derived from the alone: every real id, by first appearance, with an 's ref counted just before the itself. The same real id always has the same number, and numbers already given never change as the grows. Never stored: each that needs it computes it once. Nothing recorded holds a number: the model records the real id each cited number and each argument named, so the rule can change without changing what an old answer means.

layout

How the of one are shown to the model: the format, the or field names, what nests inside what. It is wholly policy, the format included: the takes it as show, a function from one 's , the and the 's name to text, so it can differ per model and per . It shows numbers, never real ids. The toolkit's default, xml, shows every as a named by its stored type with every field and property as an attribute, a Source's text as the 's content; an picks and renames attributes and per class, or passes a function of its own. The layout shows each text as its 's left it; it never shortens text itself. It is also handed the of the model about to read it, and xml writes on every Source, never on a Reference, the exact that model cites it with (<passage id="7" cite="[7]">), so the context itself says what can be cited and how, while id stays the handle for . xml escapes every text, so text inside a source never reads as a , and keeps its own attributes (id, cite, the one naming the whole): a field or property of the same name is shown with data- before it, so the number is never replaced.

citation strategy

How one AI model is told to cite and how its are read back, held by one declaration so the two never disagree: a pattern matching one , the characters that can start one, how to write one, and what a prompt shows for a that named no source. Which strategy a model gets is the 's policy, set in that model's , because models are trained to cite differently; the parsing, the and the streaming are the framework's, the same for every strategy. The model reads an answer's with the strategy the told the called model to cite with, once, when it records the answer (see ), so a later change of the mapping or the format never re-reads an old answer differently.

citation

One number an answer cited, as the API serves it: its in the answer's text, which is served without , and the passage and document it resolved to, or invalid with the reason. rather than in the text, as most model APIs do, so no client parses again and a bracket quoted from a document is never taken for one. A is removed with the spaces before it; a citing several numbers is one citation per number. An that does not cite is served as written, with none.

marker

What a model writes in its text to cite, such as [4], as its reads it. The model takes every marker out of an answer, or a summary, when it records it, and records each as a Marker: its in the text without markers, the exact characters cut out there, the marker and the spaces before it, and each number it cited as a CitedNumber, the number written and the real id of the Source it named then, or none. Putting the cut-out characters back gives the text exactly as the model wrote it. Nothing recorded holds a number to be read again later: the builder writes each marker into a later prompt with the source's number there and the current strategy's format, and leaves out a source the prompt does not number; a marker with a number that named no source is shown as the model wrote it, followed by what the strategy's write_invalid adds, by default a note that it is invalid.

Tools

tool

What the model can call: a plain async function made a tool with the decorator @tool(name=..., description=..., repeats=..., limit=...). The function gets four things and nothing else: the arguments the model writes (its parameters before *), its , values of the 's input (both given with bind in build), and (Recorded[T]). It returns exactly one ToolOutput, whose hold what the tool showed: a grounding tool's Sources, a listing's References. Control, whether a call runs and what a tool is given, belongs to the and the framework, never to the tool. name is a wire contract and description is what the model reads; the function's docstring is for developers only. Below tool is the runtime's Tool: a name, a description, an arguments class and a run(arguments) that takes the call's arguments parsed into that class and returns one ToolOutputBase; reopen is one written as a class. A tool is the tool 's extension point, not a world boundary: a tool that reaches the world does so through a named , as a search tool does through soev_retrieval's VectorStore, awaiting its search through sources_of. A tool cannot halt a : it raises TransientError or InvalidQueryError for a fact about the world, and the tool answers the call with a ToolOutput whose error says so; any other exception is a fault that becomes a Failure. What the model is shown of a tool is a ToolDef, which the framework builds from the tool, the parameters being the JSON schema pydantic generates for the arguments class, without the titles pydantic derives from class and field names and without class docstrings, and refusing an argument name it does not declare. A ToolDef can exist with nothing executable behind it: ask_user is declared so the model can call it, and its call is answered by a recorded Request instead of an execution.

element id argument

A argument that names an , declared as Annotated[str, ElementId(Document)] with the classes it accepts, subclasses included. The model writes the number it was shown; the model translates it when it records the answer, with the the model was shown, into the real id of the recorded under that number, and records the call with real ids and each replaced value as a Translation, its place in the arguments and what the model wrote there. Putting the written values back gives the call as the model wrote it; the builder shows an earlier call with each real id under its number in the prompt. A number with no , or one of a class the argument does not accept, becomes null and is refused before the runs, like any argument that fails validation: the call is answered with a ToolOutput whose error says why, and the does not run. So is a call to a its request did not offer, whose numbers were never translated. never see numbers, and the API serves every call as recorded, with real ids. An argument typed as the class itself, Annotated[Document, ElementId()], arrives as the first recorded under that id, and its schema shows the model a string; ElementId() without classes accepts the classes of the type it marks. A tool-shaped shows a 's fields the same way and hands the the parent's themselves, since the numbers from its own .

tool state

What one asks of one : off, not offered to the model; auto, offered, the model decides; required, offered, with an instruction to use it added to that turn's message, on every later call too, worded as its words it now. A request, not a guarantee: the model can still answer without a call to it. A 's default is its state on a that names none, and locked keeps a from naming another.

tool declaration

A an has, by name, with the deployment's policy for offering it: default, locked, and the instruction a that requires it adds, "Use the {} for this message." unless the declaration words its own. Not the : the same is offered differently by different deployments. An declares its once; per its wiring supplies a for each declared name or leaves it out.

tool call

One requested invocation inside a ModelOutput: id, name, parsed arguments. It is never an of its own; its answer is a ToolOutput linked by the call's id. A call id is unique in its : the model keeps the provider's id unless it is empty, already answered by a result, or used by an earlier call of the same answer, and then records a fresh one, so no two results share a .

Boundary

port

A protocol at the world boundary that framework code consumes and a technology implements: a port implies an . Three exist: TraceStore, ChatModel, and Observer, which only watches what is recorded, for example to export telemetry. Its OpenTelemetry implementation lives in soev_agents.observability. search through soev_retrieval's VectorStore protocol directly, which is the retrieval port. A port is shaped from what the runtime needs, always async because its implementations do real I/O. The soev_agents.ports module is therefore the framework's egress map: every seam where the framework's own code reaches outside the process, and only those. A protocol between two modules of the framework, such as Step or Tool, is not a port. The question that tells them apart: what sits in soev_agents.adapters for this?

adapter

An implementation of one for one technology, or its stand-in: InMemoryTraceStore, PostgresTraceStore, OpenAIChatModel. Named after the module it implements. An adapter translates its vendor's errors into TransientError, and a retrieval adapter its vendor's rejected-query errors into InvalidQueryError, in one place, and lets everything else propagate as .

double

A scripted stand-in for a or contract in tests, answering from a script or from what it sees in the request: ScriptedChatModel, ScriptedTool, ScriptedEffector, ScriptedStep. Verification harness, not product. A double is not an oracle: that word means a test oracle, such as seal for the store .

contract suite

The executable semantics of a , which every implementation must pass. For the store, a meta-test fails the build for any store class no contract subclass names, so a new store cannot be added without running the suite.

Serving

server

The application that serves over HTTP, for example soev-server. It calls create_app, and gives the hooks and the .

status

Where a stands for a caller: the , the with their , and the 's last . It is read from facts alone, so reading it never builds a ; the API serves it with every answer and alone at GET /threads/{id}/status.

On this page