soev_agents.toolkit
Ready parts to build agents: the ReAct agent, its inputs, prompts and tools, and its context management.
Each part is one choice among others, and you can write your own part in its place. ReActAgent puts the parts
together into one agent. A graph of your own uses the same parts, such as MessageBuilder in an
LlmStep.
ReAct agent
A ready agent that calls tools until the model writes its final output.
ReActAgent
classReActAgent[R, T: BaseModel](config, *, input=Text, element_types=(), event_types=())A ready agent that runs the ReAct loop: the model calls tools until its output calls none.
Each model output with tool calls runs the tools, and the model is called again with their results. The turn
ends with the first model output that calls no tools. Register the agent in an AgentCatalog under a name.
The settings come from config: one ReActConfig for every turn, or a function that builds the config of each
turn. The function gets the resources of the turn, the parsed turn and the model that the turn's input names. So
it can give each model its own settings, or refuse a turn with InputDeniedError. The whole turn runs on that
model. The adapter is never asked which model it is.
Nothing is recorded beyond the conversation. The system prompt and the instructions of required tools follow from the config and each recorded input, so each later model call writes them again.
Example
from openai import AsyncOpenAI
from soev_agents.adapters import InMemoryTraceStore, OpenAIChatModel
from soev_agents.runtime import AgentCatalog
from soev_agents.toolkit import BRACKETS, ReActAgent, ReActConfig
# PROMPT is your system prompt text, and search is a tool that the `tool` decorator made.
model = OpenAIChatModel(AsyncOpenAI(), model="gpt-4.1-mini")
assistant = ReActAgent(ReActConfig(model=model, prompt=PROMPT, tools=[search], citations=BRACKETS))
agents = AgentCatalog(InMemoryTraceStore(), {"assistant": assistant}, version="1")Make the agent.
paramconfigReActConfig[Any] | Callable[[Any, Any, str], ReActConfig[Any]]paraminputtype[Any]= TextThe pydantic class that each fresh input's payload parses into. Text, the default, takes a
plain string.
paramelement_typesSequence[type[Element]]Raises:
ValueError:configis aReActConfigthat keeps the defaultrender_inputortool_states, andinputhas notextortoolsfield for that default to read.
methodbuild(resources, turn, model) -> GraphBuild the graph for turn, on the model that its input records.
parammodelstr | NoneThe model that the turn's opening input names.
Raises:
InputDeniedError: The input records no model, the config function refuses the turn, or the turn names a tool that the agent never declared, asks another state for a locked one, or requires one thattoolsleaves out.ValueError: The config that a function built keeps a default that reads a fieldinputdoes not have, or itstoolsholds a tool twice or a tool that is not declared.PromptFileError: The prompt file gives texts to a tool that is not intools, or to an argument that the model does not write.
ReActConfig
dataclassReActConfig[T: BaseModel](*, model, prompt, limits=…, context=MessageBuilder(), citations=None, render_input=elements_then_text, declared=None, tools=(), tool_states=…, passes=10, tool_rounds_after_cap=1, resets_on_answer=False, repeats=False)Every setting of a ReActAgent for one turn, except its input class.
Only model and prompt are required. A ReActAgent takes one config for every turn, or a function that
builds the config of each turn.
The tool settings work together. declared lists the tools that the agent has, and tools are the tools that
your code supplies for this turn. tool_states gives the state that the turn asks of each tool. A declared tool
is offered when it is supplied and its state is not "off", in the order of declared. A turn that requires a
tool gets the tool's instruction after its message, in the words of the tool's declaration.
passes caps the model calls of one turn that call tools. Past the cap, the tool calls do not run, and the
default context strategy gives each of them a directive to finish as its result. The model keeps its tools for
tool_rounds_after_cap more calls. Then it is called without tools, so it can only write its final output.
The tools that context adds, such as the reopen tool, are not declared. The context strategy offers them, and
the tool step runs them each time that the model calls them.
parammodelChatModelThe adapter that calls the model.
parampromptstr | PromptFile | Callable[[T, str | None], str]The system prompt: a text, a PromptFile, or a function that writes the text. A prompt file gives
the texts for the turn's model. Its system template can read only citation, an example marker of
citations, and its tool texts replace the texts of the supplied tools. A function gets the parsed turn
and the example marker, such as [1] for BRACKETS. The example marker is None when citations is
None. A prompt file must have a system template, or each turn fails at its
first model call.
paramlimitsModelLimits= …What the model accepts. The default window of 32,000 tokens fits most chat models, so a context that the model cannot take fails before it is sent.
paramcontextContextStrategy= MessageBuilder()The context strategy that builds what each model call receives. The default is a MessageBuilder
with the default ContextPolicy, which shrinks old tool results to fit limits and keeps room for the
model's output. It does not compact: give the policy a Compact for that. A call whose prompt leaves
less room fails the turn.
paramcitationsCitationStrategy | None= NoneHow the model is told to cite and how its markers are read back, such as BRACKETS. None
means that the model does not cite.
paramrender_inputCallable[[T, ShowElements, Input], UserMessage | None]= elements_then_textHow the model reads an input of the thread. It gets the parsed turn, the function that shows
elements under their numbers, and the recorded Input, for what the payload does not hold, such as when
the input was sealed. It returns None to keep the input from the model. The default shows the turn's
elements, then its text. An earlier input that the input class no longer parses is shown as recorded:
its elements, then its payload.
paramdeclaredSequence[ToolDeclaration] | NoneparamtoolsSequence[Tool[Any]]paramtool_statesCallable[[T], Mapping[str, ToolState]]paramtool_rounds_after_capint= 1The number of model calls past passes that still get the tools. At least 0.
paramresets_on_answerbool= FalseIf true, each response to a Request starts the count of passes again. If false, the
count covers the whole turn.
Raises:
ValueError:tool_rounds_after_capis negative, two declarations share a name, or a declared or supplied tool has the name of a tool thatcontextadds.PromptFileError: The system template of the prompt file, or a block that a model layer replaces, reads a variable other thancitation.
Inputs
The input class of an agent says what a turn holds. These names show a turn to the model.
Text
classTextA plain-text input: the payload is the message.
It asks nothing of any tool and brings no context. It is the default input class of a ReActAgent.
attributetextstrThe message, which is the whole payload.
attributetoolsMapping[str, ToolState]The tool states that the input asks for: none.
attributecontextstr | NoneThe context for the system prompt: always None.
ChatTurnInput
modelChatTurnInputA chat turn: the message, the state that it asks of each tool, and context for the agent's system prompt.
Unknown fields are refused, so a misspelt field fails instead of being dropped.
attributetextstrThe message.
attributetoolsdict[str, ToolState]attributecontextstr | None= NoneText for the agent's system prompt, which prompt_template writes in place of
{context}, or None.
rendered_by
functionrendered_by(input_type, render=elements_then_text) -> RenderInputReturn a RenderInput that parses each input's payload into input_type and renders it with render.
Give it as the render_input of a MessageBuilder in a graph of your own. The payload is parsed strictly. A
payload that input_type no longer parses is shown as recorded: its elements, then the payload, as text when it
is a string and as JSON otherwise. So a change to input_type leaves older threads readable.
paraminput_typetype[BaseModel]The agent's input class.
paramrenderRenderTurn= elements_then_textHow the model reads the parsed turn. The default shows its elements, then its text.
Returns
RenderInputThe function for render_input.
elements_then_text
functionelements_then_text(turn, show, input=None) -> UserMessageReturn the elements that turn's fields hold, as show renders them, then its text after a blank line.
This is the default RenderTurn. A turn that holds no elements is its text alone.
paramturnAnyThe parsed turn. It must have a text.
paramshowShowElementsThe function that shows elements under their numbers.
paraminputInput | None= NoneThe recorded input. It is not read.
Returns
UserMessageThe user message.
Raises:
AttributeError:turnhas notext. Its agent needs aRenderTurnof its own.
RenderTurn
type aliasRenderTurn = Callable[[Any, ShowElements, Input], UserMessage | None]RenderInput
type aliasRenderInput = Callable[[Input, ShowElements], UserMessage | None]A function that renders an Input as the user message that the model reads.
It gets the input, and the function that shows the elements of the input. It returns None to keep the input from
the model on purpose. rendered_by makes one.
Prompts
The system prompt is a text, a function of the turn, or a prompt file with a layer of overrides per group of models.
Prompt
type aliasPrompt = Callable[[Any, str | None], str]A function that writes the system prompt of a turn.
It gets the parsed input of the turn, and an example marker of the citation strategy of the model about to be
called, or None when the model does not cite. prompt_template makes one.
prompt_template
functionprompt_template(template) -> PromptReturn a prompt that writes template with the example marker and the turn's context in it.
{citation} becomes the example marker of the model's citation strategy. {context} becomes the turn's
context, as ChatTurnInput holds it, without the spaces around it. Each is empty when there is none. Both are
filled in one pass, so text that the turn brings is never read as a placeholder. Other braces stay as they are,
and the spaces around the result are removed.
paramtemplatestrThe system prompt, with {citation} and {context} where they go. A turn needs a context field
only when the template holds {context}.
Returns
PromptA Prompt, for the prompt of a ReActConfig.
PromptFile
modelPromptFileOne agent's prompt file: every text that its models read, with a layer of overrides per group of models.
Load the file with load and give it as the prompt of a ReActConfig, which uses the texts of the turn's
model. python -m soev_agents.toolkit.prompts FILE --model ENDPOINT prints what one model reads.
The system prompt is a Jinja template. A model layer replaces its {% block %}s, or extends them with
{{ super() }}. A placeholder in a plain text is filled by the code that reads the text. A model layer replaces
only keys that the base defines. Tool names and argument names are checked when the texts are applied
(Prompts.described), because only the agent knows its tools.
Raises:
pydantic.ValidationError: The file has another shape, a layer replaces a block or text that the base does not define, or two layers name one endpoint.jinja2.TemplateSyntaxError: The system template is not valid Jinja.
attributesystemstr | None= NoneThe system prompt as a Jinja template, or None when the file has none. The template is plain text,
not HTML, so nothing is escaped. A variable that the render does not get fails the render.
attributetextsdict[str, str]= {}Plain texts, by key.
attributetoolsdict[str, ToolTexts]attributemodelstuple[ModelLayer, ...]= ()The model layers.
attributeblocksfrozenset[str]The names of the system template's blocks.
attributeendpointsfrozenset[str]Every endpoint a model layer names.
methodload(path) -> SelfLoad the prompt file at path, a YAML document.
parampathPathThe path of the file, in UTF-8.
Returns
SelfThe prompt file.
Raises:
pydantic.ValidationError: See the class.jinja2.TemplateSyntaxError: The system template is not valid Jinja.yaml.YAMLError: The file is not valid YAML.
methodcheck_variables(known) -> NoneRefuse a system template or block override that reads a variable outside known.
So a renamed field fails when the agent is built, not mid-chat.
paramknownIterable[str]The variables that the agent supplies. Jinja's super and self are always allowed.
Raises:
PromptFileError: A template reads an unknown variable.
methodfor_model(model) -> PromptsReturn the texts that model reads: the base, with the layer that names model over it.
A model that no layer names, or None, gets the base alone.
parammodelstr | NoneThe endpoint id of the model, or None for the base.
Returns
PromptsThe resolved texts.
Prompts
dataclassPrompts(template, blocks, texts, tools)The texts of one agent for one model, which PromptFile.for_model resolves from the prompt file.
paramtemplatestr | NoneThe base system template, or None when the file has none.
paramblocksMapping[str, str]The blocks of the template that the model's layer replaces, by block name. Empty for the base.
paramtextsMapping[str, str]The plain texts, by key: the base's, with the layer's over them.
attributedigeststrThe name of this set of texts: the first 16 hex characters of a SHA-256 hash of the texts.
The same file and model give the same digest, and any change to a text that the model reads from the file gives another. A tool's own default texts are code and not part of it.
attributesourcestrThe template that system renders.
It is the base template, or a template that extends the base with the block overrides.
methodsystem(**variables) -> strReturn the system prompt: the template rendered with variables.
paramvariablesAnyThe value of each variable of the template, by name. A ReActAgent gives only
citation.
Returns
strThe system prompt, with the blocks of the model's layer in it.
Raises:
PromptFileError: The prompt file has no system template.jinja2.UndefinedError: The template reads a variable that is not given.
methodtext(key) -> strReturn the text under key.
paramkeystrThe key of the text in the prompt file.
Returns
strThe text, as the file writes it. The code that reads it fills its placeholders.
Raises:
PromptFileError: The prompt file defines no such text.
methoddescribed[T: Tool[Any]](tools) -> list[T]Return tools, each with the texts that the prompt file gives it in place of its own.
Returns
list[T]A new list in the same order. A tool that the file gives texts is a copy, and the other tools are the same objects.
Raises:
ModelLayer
modelModelLayerThe overrides of a prompt file for a group of models, which endpoints names.
A layer replaces only blocks and texts that the base of the file defines. Unknown fields are refused.
attributeendpointstuple[str, ...]The endpoint ids of the models that the layer applies to. At least one. Two layers of a file cannot name the same endpoint.
attributeblocksdict[str, str]= {}The blocks of the system template that the layer replaces, by block name. A block can extend the
base block with {{ super() }}.
attributetextsdict[str, str]= {}The texts that the layer replaces, by key.
ToolTexts
modelToolTextsWhat the model reads of one tool in a prompt file: its description and its arguments' descriptions.
Each text replaces the tool's own default. Unknown fields are refused.
attributeargumentsdict[str, str]= {}The description of each argument, by argument name.
PromptFileError
errorPromptFileErrorA prompt file that does not fit its agent, or a model layer that does not fit the file's base.
PromptFile raises it, inside a pydantic.ValidationError, for a model layer that replaces a block or a text
that the base does not define, or that names an endpoint of another layer. PromptFile.check_variables raises it
for a template that reads a variable that the agent does not supply, and ReActConfig calls that check.
Prompts.described raises it for texts of a tool or an argument that the agent does not have, Prompts.text
for a text that the file does not define, and Prompts.system for a file without a system template.
Citations
A citation strategy tells the model how to cite, and reads its markers back.
BRACKETS
constantBRACKETS: Final = …The citation strategy that writes markers such as [4], and [4][5] for two sources.
It reads [4], [4, 5], [4;5], 【4】 and 【4†source】, so a model that writes another of these forms is still
read.
FULLWIDTH
constantFULLWIDTH: Final = …The citation strategy that writes markers such as 【4】, and 【4】【5】 for two sources.
It reads the same markers as BRACKETS.
Tools
A declaration says how an agent offers a tool. A tool state says what one turn asks of the tool.
ToolDeclaration
dataclassToolDeclaration(*, name, default='auto', locked=False, required_tool_instruction=…)A tool that an agent has, by name, and the agent's rule for offering it.
A declaration is not the tool: two agents can offer the same tool by different rules. On each turn, the code supplies a tool for each declared name or leaves it out.
paramnamestrThe name that the model calls the tool by.
paramlockedboolRaises:
ValueError:required_tool_instructionuses a placeholder other than{tool}.
methodstate(requested) -> ToolStateToolState
type aliasToolState = Literal['off', 'auto', 'required']Subagents
A delegation runs one subagent for each call of a tool.
ToolDelegation
dataclassToolDelegation[I: BaseModel](name, description, input, build, result=…, result_schema=None)A subagent that the parent's model calls as a tool: one subagent for each call.
Each subagent runs on build(its input, its model), the model that the hand-off names. Put the
delegation in a node of the parent's graph, and offer its spec to the parent's model.
The model is shown input's fields as the tool's arguments. An element field is shown as the id of an element of
its class, which the hand-off turns into the parent's element with the parent's numbering. An input that is not
an object, such as a RootModel[str], is shown under one input argument. A call whose arguments do not fit
gets an error as its result and starts no subagent.
paramnamestrThe name that the parent's model calls the tool by.
paramdescriptionstrWhat the parent's model reads about the tool.
paraminputtype[I]The input class of the subagent. The tool's arguments come from its fields.
parambuildCallable[[I, str | None], Graph]paramresultCallable[[Trace], JsonValue]paramresult_schemadict[str, JsonValue] | None= NoneThe JSON Schema that each result must fit, or None for any result. A result that
does not fit gets the reason as the tool result's error.
Raises:
ValueError: An element field sits inside another model, inside a collection other than a list, tuple orSequence, or in a union with a type other thanNone. The hand-off cannot fill it.
attributespecToolDefThe definition of the tool that the parent's model is offered, with the schema of its arguments.
methodgraph_for(task) -> Graphmethodhand_offs(parent) -> Sequence[Event]methodhand_back(child) -> Sequence[Event]Return the result of one subagent as the tool result of the call that handed it off.
The tool result holds every Source that the subagent was shown, and each shown element that such a source
is part of. With the default result, a final model output that cites comes back in the form of a final
reply to a user: text without markers and citations for the sources that resolved, so the parent shows
markers in its own numbering. Any other result is data. A result of None, or one that does not fit
result_schema, gets an error.
Returns
Sequence[Event]One ToolOutput, linked to the call by its call_id.
TaskInput
modelTaskInputThe input that a ToolDelegation hands off: one tool call of the parent's model, as a subagent's task.
The payload is the call's arguments, parsed into the delegation's input class, with each element id replaced by
the parent's element. The input records the elements of the payload. The hand-back reads call_id and expects
to link and check the subagent's result, so it reads nothing outside the subagent's stream. A task that names no
model runs on the model of the parent's latest input.
attributetypestr= 'task_input'The stored type, "task_input".
attributeactorActor= 'model'"model": the parent's model asked for the task.
attributeexpectsdict[str, JsonValue] | None= NoneThe JSON Schema that the subagent's result must fit, or None for any result.
Context
The context strategy builds what each model call receives. MessageBuilder is the default, and its context policy
keeps the context within the model's limits.
ContextStrategy
protocolContextStrategyThe context management of a ReActAgent: what each model call of the agent receives.
The agent uses it in three places. for_turn makes the context builder of one turn, from the parts that the
agent computes for that turn. added_tools are the tools that the strategy offers itself, such as reopen, which
the agent's tool step runs. wrap gives the model step the strategy's own step, such as compaction.
MessageBuilder is the default strategy.
The three parts are one context management, because they depend on each other. A strategy that compacts must
build contexts that show the summary of each Compaction in place of the events that it replaced. A strategy
that adds a reopen tool must offer it, and show what it reopens.
Whatever the strategy, the tool step checks the rules of each call, the runner ends the turn past the budget, and the model step reads citations and element numbers with the numbering of the built context.
methodfor_turn(*, render_input, model, prompt, tools, budget, citations, limits) -> ContextBuilderReturn the context builder of one turn.
paramrender_inputRenderInputHow an Input renders as a user message, with the instructions that its tool states ask
for.
parammodelstrThe name of the model that the turn runs on.
parampromptCallable[[str | None], str] | NoneparamcitationsCitationStrategy | NoneThe citation strategy of the model, or None when the agent does not cite.
paramlimitsModelLimitsThe limits of the model.
Returns
ContextBuilderThe context builder of the turn's model step.
methodwrap(step, limits) -> StepMessageBuilder
dataclassMessageBuilder(policy=ContextPolicy(), *, show=xml(), select=None, renderers=dict(), render_input=None, model=None, prompt=None, tools=(), budget=None, citations=None, limits=None)The context builder for a chat model: the messages and the tools that one model call gets.
The messages are the system prompt that prompt writes, then the events that select picks from the trace. Each
element shows by show, under its number in the whole thread, so a selection never renumbers. An agent's own
event types become messages by renderers.
The builder is also the default ContextStrategy of a ReActAgent. Its own settings come first: policy,
show, select and renderers. The parts of one turn follow. A ReActAgent gives them with for_turn. A graph
of your own gives them when it makes the builder. The policy's reopen is the tool that the builder adds, and
the policy's compact wraps the model step. So the shrinks, reopen and compaction stay in one place: the builder
that reads the summaries that they write.
With limits, the policy decides the form of each tool result, again at each call. Each result is made smaller
by the function of its tool for that form, and cut at the cap of its tool. A compressed result starts with a line
that names its handle. A stubbed result is only that line and what floors keep, and its call stays. A shrunk
result keeps what floors kept when it shrank, so a later citation never changes it. An input's elements always
show as recorded. Without limits, each result is full and each text shows as recorded.
The policy's reopen tool is offered after tools, from the first call that shortens a tool result or follows a
summary. After a Compaction, the events that it replaced are left out, and its summary follows the prompt in
the system message. On the call after the last trip of budget, no tools are offered, reopen included, so the
model writes its final output.
Example
from soev_agents.runtime import LlmStep, tool_def
from soev_agents.toolkit import ContextPolicy, MessageBuilder, ModelLimits, Text, Tokens, rendered_by
# model is a ChatModel, turn_model is the model name of the turn, PROMPT is the system prompt text,
# search is a tool that the `tool` decorator made, and graph is the Graph of the turn.
builder = MessageBuilder(
ContextPolicy(result_cap=Tokens(4_000)),
render_input=rendered_by(Text),
model=model.resolve(turn_model),
prompt=lambda citation: PROMPT,
tools=(tool_def(search),),
limits=ModelLimits(window=Tokens(128_000)),
)
graph.add_node("answer", LlmStep(builder=builder, model=model))parampolicyContextPolicy= ContextPolicy()How the builder keeps the context within the limits of the model.
paramselectCallable[[Trace], Sequence[Event]] | NoneparamrenderersMapping[type[Event], RenderEvent]paramrender_inputRenderInput | None= NoneHow an Input renders, or None before the turn's parts are given.
parammodelstr | None= NoneThe name of the model that each call goes to, or None before the turn's parts are given.
parampromptCallable[[str | None], str] | Noneparamtoolstuple[ToolDef, ...]= ()The tools that each call offers.
paramcitationsCitationStrategy | None= NoneThe citation strategy of the model, or None when the agent does not cite.
paramlimitsModelLimits | None= NoneThe limits of the model, or None to shrink nothing and check no size.
methodfor_turn(*, render_input, model, prompt, tools, budget, citations, limits) -> MessageBuilderReturn a copy of this builder with the parts of one turn.
A ReActAgent calls it for each turn.
paramrender_inputRenderInputHow an Input renders as a user message.
parampromptCallable[[str | None], str] | Noneparamtoolstuple[ToolDef, ...]The tools that each call offers.
paramcitationsCitationStrategy | NoneThe citation strategy of the model, or None when the agent does not cite.
paramlimitsModelLimitsThe limits of the model.
Returns
MessageBuilderA new builder. This builder does not change.
methodwrap(step, limits) -> StepReturn step, with compaction around it when the policy has a compact.
paramstepLlmStepThe model step whose context this builder builds.
paramlimitsModelLimitsThe limits of the model that step calls.
Returns
Stepstep itself when the policy has no compact. Else a step that first lets the model write a summary when
the context is over the when of compact, or does not fit. Then the step makes the call from the
context that this builder builds after the summary. The step raises CompactionError when the summary
replaces nothing, and ContextFullError when the context does not fit and no summary makes room.
method__call__(trace) -> ModelContextBuild the context of the next model call from trace.
Returns
ModelContextThe ChatRequest, the name of the model, the citation strategy and the numbering of the call.
Raises:
ContextFullError: Withlimits, the estimated prompt plus the policy'soutput_reservedoes not fit in the model's window. A retry of the same call cannot help.ValueError: The policy'stargetis not below itsstart_atin the model's window.UnrenderedEventError: A selected event has a type that is neither built in nor inrenderers.MissingTurnError: The builder has norender_inputor nomodel.ShortenError: A function of a form removed, added or reordered an element, or changed the error.
methodexplain(trace) -> tuple[Shrunk, ...]Return why each shrunk tool result in trace has its form, for the next model call.
Returns
tuple[Shrunk, ...]One Shrunk for each shrunk result, in the order of the results in the trace. Empty without limits.
Raises:
MissingTurnError: The builder has norender_inputor nomodel.ValueError: The policy'stargetis not below itsstart_atin the model's window.UnrenderedEventError: A selected event has a type that is neither built in nor inrenderers.ShortenError: A function of a form removed, added or reordered an element, or changed the error.
ContextPolicy
dataclassContextPolicy(*, start_at=Share(0.6), target=Share(0.4), min_gain=Share(0.1), shrink=…, compact=None, protect=…, full=as_recorded, compressed=drop_text, tools=dict(), result_cap=Tokens(8000), output_reserve=Tokens(8192), reasoning=NEVER, max_images=None, reopen=Reopen())How a MessageBuilder keeps what a model is sent within what the model accepts.
Each default is the whole default: a field that you give replaces it, and nothing is added to it.
At each model call, the builder estimates the size of the context. When the size is over start_at, the builder
applies the shrinks of shrink in order, until the size is under target. A shrink with its own when runs
over that size instead. The builder works the forms out again at each call, from the first model call of the
view, so nothing is stored. When the context is still over the threshold of compact after the shrinks, the
model summarises earlier history. A call whose prompt leaves less than output_reserve in the window is refused
before it is made: the builder raises ContextFullError.
Example
from soev_agents.toolkit import (
Compact,
Compress,
ContextPolicy,
Forms,
MessageBuilder,
ReActConfig,
Stub,
turns,
)
# model is a ChatModel, and PROMPT is the system prompt text.
policy = ContextPolicy(
shrink=(Compress(turns[:-2]), Stub(turns[:-4])),
compact=Compact(where=turns[:-2]),
tools={"read_document": Forms(cap=None)},
)
config = ReActConfig(model=model, prompt=PROMPT, context=MessageBuilder(policy))paramstart_atThreshold= Share(0.6)The size of the context above which the shrinks start.
paramtargetThreshold= Share(0.4)The size of the context that the shrinks bring it under. It must be below start_at.
parammin_gainThreshold | None= Share(0.1)The least that one pass of the shrinks must free, or None for any gain. A pass that frees less
is not made, so that the prompt cache does not break for a few tokens.
paramshrinktuple[Shrink, ...]paramcompactCompact | None= NoneHow the model summarises earlier history, or None to never summarise.
paramprotecttuple[Floor, ...]paramfullShortenparamcompressedShortenparamtoolsMapping[str, Forms]paramresult_capTokens | Noneparamoutput_reserveTokens= Tokens(8192)The tokens kept free in the window for the model's output. It is not sent as a limit, so the output can use more.
paramreasoningReasoning= NEVERWhich reasoning of earlier model outputs is sent back to the model.
parammax_imagesint | NoneparamreopenReopen | None= Reopen()The tool that shows the model again what was shortened or removed, or None for no such tool. The
builder offers it from the first model call that shortens a tool result or follows a summary, until the
turn's tools are withdrawn. Tool states and required-tool instructions never apply to it.
Raises:
ValueError:targetis not belowstart_at, ormax_imagesis negative.
methodshorten_for(tool, form) -> Shortenmethodcap_for(tool) -> Tokens | NoneShowElements
type aliasShowElements = Callable[[Sequence[Element]], str]RenderEvent
type aliasRenderEvent = Callable[[Any, ShowElements], Sequence[Message]]only
functiononly(*types) -> Callable[[Trace], Sequence[Event]]MissingTurnError
errorMissingTurnErrorUnrenderedEventError
errorUnrenderedEventErrorContextFullError
errorContextFullErrorA context that does not fit in the model's window, so the model call is not made.
A MessageBuilder with limits raises it when it builds a context: the estimated prompt plus the output_reserve
of its policy is more than the model's window. A retry of the same call cannot help.
When the context does not fit, the step that MessageBuilder.wrap makes for a Compact first lets the model write
a summary. That step raises this error when the where of the Compact selects nothing that a summary did not
replace yet, or when the context still does not fit after the summary.
Model limits
What one model accepts, in tokens or as a share of its window.
ModelLimits
dataclassModelLimits(window, characters_per_token=3.0)What one model accepts: its window in tokens, and how many characters make one of its tokens.
The app gives these numbers for each model. The framework uses no tokenizer: it estimates the tokens of a prompt from its characters. After a call to the same model records the real token count of its prompt, only the characters added since then are estimated.
paramwindowTokensThe most tokens that one call can send and receive together.
paramcharacters_per_tokenfloat= 3.0The average number of characters in one token of the model. The default, 3.0, is low on purpose, so that the estimate counts more tokens than the prompt has.
Tokens
dataclassTokens(count)A number of tokens, as its own type so that it is never taken for a share of a window.
Two numbers of tokens compare with < and >.
paramcountintThe number of tokens.
Share
dataclassShare(fraction)A share of the window of the model that the next call goes to.
It is its own type, so that it is never taken for a number of tokens. Share(0.6) of a window of 100 000 tokens
is 60 000 tokens.
paramfractionfloatThe share, above 0 and at most 1.
Raises:
ValueError:fractionis not above 0 and at most 1.
Threshold
type aliasThreshold = Share | TokensA size of a context: a Share of the model's window, or a number of Tokens.
A Share follows the window of the model that the call goes to. A number of Tokens is the same for each model.
Shrinks
When the context fills, shrinks lower old tool results to a smaller form.
Shrink
protocolShrinkA function that offers tool results to shrink, each with the form that it goes to.
At each model call of the view, the context policy asks its shrinks while the context is too big. It applies the offers one at a time, in the order of the shrink, and stops when the context is small enough. So a shrink never decides when to stop. An offer that raises a form, or that frees nothing, is skipped.
Compress and Stub are the built-in shrinks. A plain function with the signature of __call__ is a shrink
too.
A shrink can also have this attribute, which the context policy reads:
when: aThreshold, orNone. A shrink with awhenstarts when the context is over it, and stops when the context is no longer over it. A shrink without one, or withNone, starts when the context is over the policy'sstart_at, and goes on down to the policy'starget.
method__call__(view, forms) -> Iterable[tuple[str, Form]]Return the tool results to shrink at the end of view, in the order to try them.
paramviewTraceThe events before the model call that is judged.
paramformsMapping[str, Form]The form of each result so far, by result handle ("r1", ...). A result that is not in it is full.
Returns
Iterable[tuple[str, Form]]Pairs of a result handle and the form that the result goes to.
Compress
dataclassCompress(where, when=None)A shrink that compresses the tool results that where selects, oldest first.
It offers each selected result that is full. Without when, it starts when the context is over the policy's
start_at, and goes on down to the policy's target. With when, it starts when the context is over when,
and stops when the context is no longer over it.
paramwhereSelectionThe results to compress.
paramwhenThreshold | None= NoneThe size of the context above which it runs, or None to follow the policy's start_at and target.
method__call__(view, forms) -> list[tuple[str, Form]]Return each full result that where selects in view, with the form COMPRESSED.
paramviewTraceThe events before the model call that is judged.
paramformsMapping[str, Form]The form of each result so far, by result handle. A result that is not in it is full.
Returns
list[tuple[str, Form]]Pairs of a result handle and COMPRESSED, oldest result first.
Stub
dataclassStub(where, when=None)A shrink that replaces the output of the tool results that where selects by one line, oldest first.
The line names the result handle. The elements that floors keep follow it, and the calls of the results stay.
It offers each selected result that is full or compressed. Without when, it starts when the context is over
the policy's start_at, and goes on down to the policy's target. With when, it starts when the context is
over when, and stops when the context is no longer over it.
paramwhereSelectionThe results to stub.
paramwhenThreshold | None= NoneThe size of the context above which it runs, or None to follow the policy's start_at and target.
method__call__(view, forms) -> list[tuple[str, Form]]Return each result that where selects in view and that is not stubbed, with the form STUBBED.
paramviewTraceThe events before the model call that is judged.
paramformsMapping[str, Form]The form of each result so far, by result handle. A result that is not in it is full.
Returns
list[tuple[str, Form]]Pairs of a result handle and STUBBED, oldest result first.
Selection
dataclassSelection(unit, key)Some tool results of a view, selected by a Python index or slice of its turns, rounds or results.
Make one with turns, rounds or results: turns[:-2] selects the results of every turn but the last two.
The units count from the oldest, the current one last. An index out of range selects nothing.
paramunitUnitThe unit that key counts.
paramkeyint | sliceThe index or slice.
methodhandles(view) -> list[str]Return the handles of the selected tool results in view, oldest first.
paramviewTraceThe events to select from.
Returns
list[str]The result handle, such as "r1", of each tool result in a selected unit.
methodpicked(count) -> list[int]turns
constantturns = …rounds
constantrounds = …results
constantresults = …Unit
type aliasUnit = Literal['turns', 'rounds', 'results']Shrunk
dataclassShrunk(result, form, by, at)Floors
A floor keeps some elements in a higher form than their tool result.
Floor
dataclassFloor(applies, form)The lowest form that some elements can reach, whatever form their tool result has.
The form of an element is the form of its result, lifted to the highest floor that applies to it. A floor only
shows more of a result: it never changes the form of the result. The default floor of a ContextPolicy is
Floor(cited, FULL): a Source that the model cited before its result shrank keeps its text.
Protects
protocolProtectsA predicate that says whether a floor applies to an element.
It reads only the events before the model call at which the result of the element shrank. So what happens later
never changes a message that the model was sent already. cited is the built-in predicate.
method__call__(element, tool, before) -> boolcited
constantcited = …Whether an earlier model output cited an element: the predicate of the default floor, Floor(cited, FULL).
A floor calls it with the element, the name of the tool that recorded the element, and the events before the call where the element's result shrank. It is true when a marker of a model output in those events cites the element's id. So a passage that the model relied on keeps its text after its result shrinks.
Forms
How the results of a tool look in each form.
Form
enumFormHow much of a tool result the model is shown.
Forms are ordered: STUBBED is below COMPRESSED, and COMPRESSED is below FULL. A shrink only lowers the
form of a result, and a floor only lifts the form of an element. Forms are not recorded: each model call works
them out again.
attributeSTUBBED= 0The output is one line that names its result handle, and the elements that floors keep. The call stays.
attributeCOMPRESSED= 1The output as the compressed function of its tool makes it, under a line that names its handle.
FULL
constantFULL = Form.FULLCOMPRESSED
constantCOMPRESSED = Form.COMPRESSEDSTUBBED
constantSTUBBED = Form.STUBBEDForms
dataclassForms(full=None, compressed=None, cap='policy')How the results of one tool look in each form.
Give it to a ContextPolicy in tools, by the name of the tool. A field that is None uses the policy's own
function. A stubbed result is always its handle line and the elements that floors keep, so it has no function.
Shorten
type aliasShorten = Callable[[ToolOutput, ModelLimits], ToolOutput]A function that makes a tool result look as it does in one form, for the model that reads it.
It gets the tool result as recorded, and the limits of the model that the call goes to. It can change text, data
and the text of each element. It must keep every element, in order, and error, because floors and reopen find
elements by id, and the model reacts to the error. A MessageBuilder raises ShortenError for a function that
breaks this rule.
Give one to a ContextPolicy as full or compressed, or to one tool in a Forms. as_recorded, drop_text,
sources and json_cap make the usual ones.
ShortenText
type aliasShortenText = Callable[[str, ModelLimits], str]A function that shortens one text for the model that reads it.
It gets the text and the limits of the model that the call goes to. sources applies one to the text of each
Source of a tool result. cap and first_words make the usual ones.
as_recorded
functionas_recorded(output, limits) -> ToolOutputdrop_text
functiondrop_text(output, limits) -> ToolOutputReturn the tool result with the text of each Source removed: the default compressed form.
With the xml layout, each source then shows as a tag without content. The result's own text and data stay.
paramoutputToolOutputThe tool result.
paramlimitsModelLimitsThe limits of the model that reads the result. Not used.
Returns
ToolOutputA copy of output. output does not change.
sources
functionsources(shorten) -> ShortenReturn a Shorten that applies shorten to the text of each Source of a tool result.
Other elements, and the result's own text and data, stay as they are. sources(cap(Tokens(500))) cuts the
text of each source at 500 tokens.
paramshortenShortenTextThe function for the text of one source.
Returns
ShortenThe function for the whole result.
cap
functioncap(limit, cut=…) -> ShortenTextReturn a ShortenText that cuts a text at limit tokens, before the word that the limit falls in.
A cut text ends with cut, so the model can tell a cut text from a short one. A text that fits stays as it is.
paramlimitTokensThe most tokens of the text, in tokens of the model that reads it.
paramcutstr= …What follows the cut text. Its {tokens} becomes about how many tokens were left out. The default is
"[… about {tokens} more tokens not shown]".
Returns
ShortenTextThe function for one text.
first_words
functionfirst_words(count) -> ShortenTextReturn a ShortenText that keeps the first count words of a text.
A text with more words becomes its first count words, with one space between them, followed by …. A
shorter text stays as it is.
paramcountintThe number of words to keep.
Returns
ShortenTextThe function for one text.
json_cap
functionjson_cap(limit, cut=…) -> ShortenReturn a Shorten that writes the data of a tool result as JSON and cuts it at limit tokens.
When the JSON is longer than limit, the result gets no data, and the cut JSON follows its text on a new
line. Shorter data stays as it is, and so does a result without data.
paramlimitTokensThe most tokens of the JSON, in tokens of the model that reads it.
paramcutstr= …What follows the cut JSON. Its {tokens} becomes about how many tokens were left out. The default is
"[… about {tokens} more tokens not shown]".
Returns
ShortenThe function for the whole result.
ShortenError
errorShortenErrorCompaction
When shrinks are not enough, the model writes a summary that replaces the earlier conversation.
Compact
dataclassCompact(where=turns[:-2], when=Share(0.8), instructions=…, citing=…)How the model summarises earlier history when the context is still too big after every shrink.
When the context of a model call is over when after every shrink, or does not fit at all (the context builder
raises ContextFullError), the model first writes a summary of the units that where selects. Then the call is
made from the summary. The Compaction and the model's output are recorded together. From then on, the
MessageBuilder shows the summary in the system message, in place of the events that it replaced. Element
numbers and result handles stay those of the whole thread.
The running unit is always kept. The summary never splits a tool call from its results: a cut that would split
them moves to before the call. The summary call offers no tools and is not streamed. When a summary replaced
every unit that where selects already, nothing is compacted, and a call that does not fit raises
ContextFullError.
Give it to a ContextPolicy as compact.
paramwhereSelectionparamwhenThreshold= Share(0.8)The size of the context above which the model summarises. The default is Share(0.8) of the window.
paraminstructionsstr= …The text that asks the model for the summary. It follows the earlier messages as a user message.
paramcitingstr= …The text that follows instructions when the model cites. Its {citation} becomes an example marker
of the model's citation strategy.
methodkept_from(events) -> intCompactionError
errorCompactionErrorA summary that replaces nothing.
The model step that MessageBuilder.wrap makes for a Compact raises it in two cases. The model wrote an empty
summary. Or the context builder built the same ChatRequest after the summary as before it, because the builder
does not show Compaction events.
Reopen
The reopen tool shows the model again what the context policy shortened or removed.
Reopen
dataclassReopen(name='reopen', description=…, arguments=ReopenArguments)The tool that shows the model again what a context policy shortened or removed.
The tool records only which elements and whole tool results to show again. The MessageBuilder draws them from
where they were first recorded, as the result of the reopen call. Each element shows inside its whole, in the
forms of the tool that recorded it. Each whole result shows under its handle, cut at the cap of its tool.
Nothing earlier in the conversation changes.
A ContextPolicy holds the tool as reopen. The builder offers it from the first model call that shortens a
tool result or follows a summary, until the turn's tools are withdrawn.
paramnamestr= 'reopen'The name that the model calls the tool by.
paramdescriptionstr= …What the model reads about the tool.
paramargumentstype[ReopenArguments]= ReopenArgumentsThe class of the arguments that the model writes.
methodrunasync (arguments) -> ReopenedReopenArguments
modelReopenArgumentsThe arguments that the model writes to the reopen tool: elements by number, whole results by handle.
LlmStep replaces each number with the real id of its element, and each handle with the call id of its result,
when it records the call.
Raises:
pydantic.ValidationError: The arguments name no element and no result, or hold a field of another name.
attributeelementslist[Annotated[str, ElementId(Element)]]attributeresultslist[Annotated[str, ResultHandle()]]= []The handles of the tool results to show again whole, such as "r12".
Reasoning
Which earlier reasoning of the model is sent back to it.
Reasoning
enumReasoningWhich reasoning of earlier model outputs the MessageBuilder sends back to the model.
Reasoning that is sent back counts toward the context like any text. CURRENT_TURN lets a model continue its
reasoning between tool calls. When a new turn starts, the reasoning of the earlier turn is dropped, so the prompt
cache misses once in each turn.
attributeNEVER= 'never'Send no reasoning back. The default of ContextPolicy.
attributeCURRENT_TURN= 'current_turn'Send back the reasoning of the model outputs of the running turn.
attributeALWAYS= 'always'Send back the reasoning of every model output.
NEVER
constantNEVER = Reasoning.NEVERSend no reasoning of earlier model outputs back to the model, Reasoning.NEVER.
CURRENT_TURN
constantCURRENT_TURN = Reasoning.CURRENT_TURNSend back the reasoning of the model outputs of the running turn, Reasoning.CURRENT_TURN.
ALWAYS
constantALWAYS = Reasoning.ALWAYSSend back the reasoning of every earlier model output, Reasoning.ALWAYS.
Layout
The layout shows the elements of one tool call as text.
Show
type aliasShow = Callable[[Sequence[Element], Numbering, str | None, CitationStrategy | None], str]The text of one tool call's elements: a layout.
It gets four arguments: the elements, the thread's numbering, the name of the tool that ran, and the citation strategy of the model. The tool name is None for the elements of an input. The strategy is None when the agent
does not cite.
A layout shows the numbers of the numbering, never real ids. xml makes the default layout. Give one to a
MessageBuilder as show.
xml
functionxml(fields=None, tags=None, order=None, cite='cite') -> ShowReturn the layout that shows each element as an XML tag named by its type, with its number as id.
A Source, the only element that can be cited, also carries the exact marker the model cites it with,
written by its citation strategy, in the attribute cite names, e.g. <passage id="7" cite="[7]">. So the
model copies what to cite instead of choosing among the numbers it sees.
Every field a subclass adds and every properties key is an attribute, unless fields names the class. A
Source's text is the tag's content. An element nests inside the element its ref names when both are in the
same call, in arrival order. Otherwise its tag names the ref as an attribute, called by the tag of the element
recorded under that number, or ref when none was.
id, the cite attribute and the attribute naming the whole are the layout's own: an attribute of the
same name is shown with data- before it. Text is escaped, so it never reads as a tag.
paramfieldsMapping[type[Element], Mapping[str, str]] | None= NoneThe attributes of each class that it names: the paths it lists, each under the attribute
name it maps to, e.g. {Document: {"properties.modified": "date"}}. It applies to subclasses of the
class named. None shows every field and property of each class.
paramtagsMapping[type[Element], str] | None= NoneThe tag of each class that it names, in place of its type. It applies to subclasses of the
class named.
paramorderMapping[type[Element], str] | None= NoneThe path per class that sorts the parts nested in one whole, e.g.
{Passage: "properties.chunk_index"} for reading order. Parts without a value follow in arrival order,
and elements not nested keep arrival order, so the record can keep a search's ranking while the model
reads in document order.
paramcitestr | None= 'cite'The name of the attribute that holds the marker of a Source, or None for no such
attribute.
Returns
ShowThe layout, for the show of a MessageBuilder.
Raises:
ValueError: a path infieldsororderdoes not start with a field of its class.