soev for developers
soev-agentsReference

soev_agents.runtime

Build agents as graphs of steps, run them on threads, and read what the threads recorded.

An parses each input and builds the that runs the . Each of the reads the 's and returns the to record. The is the only state, so a can stop after any and in another process.

Agents

An is the contract between your code and the that runs it.

Agent

protocolAgent[R, T: BaseModel]

One : the whole contract between it and the that runs it.

An says which class each fresh input's must parse into, strictly, and builds the for a parsed turn. ReActAgent in soev_agents.toolkit is a ready . Write your own class with these attributes and build for any other .

attributeinputtype[T]

The pydantic class that each fresh input's must parse into. A that does not fit is refused with InputDeniedError, and nothing is recorded.

attributeelement_typesSequence[type[Element]]
= ()

The classes that this 's record beyond the core ones.

attributeevent_typesSequence[type[Event]]
= ()

The classes that this 's record beyond the core ones. The are read with these and the core types only, so a that produces a type that is left out records a Failure instead.

methodbuild(resources, turn, model) -> Graph

Build the that runs the whole that turn opened.

build hands resources to and , and never branches on them. It is synchronous, so it cannot call a resource: what any model of the receives follows from the code, turn, model and the .

paramresourcesR

The that the caller gives for this , such as model clients and stores.

paramturnT

The of the input that opened the , parsed into input.

parammodelstr | None

The model that the 's opening input names, or None when it names none.

Returns

Graph

The of the .

Raises:

  • InputDeniedError: The is refused before anything is recorded.

Resources

type aliasResources = Callable[[Agent[Any, Any]], Awaitable[Any]]

An async function that returns the for an 's build.

The catalog calls it each time that it builds a , never for a read, a or a . It gets the , so one function can serve that need different .

accepted

functionaccepted[T: BaseModel](input_type, input) -> tuple[Input, T]

Parse an input's , strictly, and return the input with the that the holds.

The catalog calls it for each fresh input, so that the records the of the input. Code that builds without the catalog calls it too.

paraminput_typetype[T]
paraminputInput

The fresh input.

Returns

tuple[Input, T]

A tuple (recorded, turn): the input to record, with its , and the parsed .

Raises:

  • InputDeniedError: The does not parse into input_type.

Graphs

A holds the that run a , and the and between them.

Graph

classGraph()

The that run a , and the between them.

A fresh input starts at the . After each , the follows the from that . A without an ends the , as does a Request, which waits for a response. A that raises stops the with a Failure. If the failure is , the runs again when the runs again.

Build a once for each , in Agent.build. The does not change it.

Example

from soev_agents.domain import ModelOutput, Trace
from soev_agents.runtime import Graph, LlmStep, ToolStep

# builder is a ContextBuilder, model is a ChatModel, and tools are the Tool objects of the turn.
def after_model(view: Trace) -> str | None:
    output = view.latest(ModelOutput)
    return "tools" if output is not None and output.calls_tools else None

graph = Graph()
graph.add_node("model", LlmStep(builder=builder, model=model))
graph.add_node("tools", ToolStep(tools={tool.name: tool for tool in tools}))
graph.set_entry_point("model")
graph.add_conditional_edge("model", after_model)
graph.add_edge("tools", "model")

Make an empty , without , or .

attributebudgetBudget | None

The of the , or None for a without a limit.

attributeentry_pointNodeName

The that runs first for each fresh input.

Raises:

methodadd_node(name, step) -> None

Add a that runs step.

paramnameNodeName

The name of the . It must be unique in the and must not contain / or #, because stream names use these characters.

paramstepStep | EffectStep | Delegation

What the runs: a Step, an EffectStep, or a Delegation that runs .

Raises:

  • ValueError: The name contains / or #, or the already has a with this name.
methodadd_edge(source, target) -> None

Add a fixed from source to target.

Call it again with the same source to add more targets. With one target, the target runs next in the same . With more targets, each target runs in a parallel branch of its own, and the meet at a (see add_join).

paramsourceNodeName

The that the leaves.

paramtargetNodeName

A that runs after source.

Raises:

  • ValueError: A is unknown, source already has a conditional , or the already exists.
methodadd_conditional_edge(source, router) -> None

Add an from source whose router picks the next , or ends the .

paramsourceNodeName

The that the leaves. It must not have an yet.

paramrouterRouter

The function that picks the next from the .

Raises:

  • ValueError: source is unknown or already has an .
methodadd_join(name) -> None

Make a the where parallel branches meet.

A ends when it reaches a . When all branches of a ended, the runs once, in the that fanned out. If branches reach different joins, the records a Failure. A that ends without a does not stop the . If no reaches a , the ends after the .

paramnameNodeName

The that becomes a .

Raises:

  • ValueError: The is unknown.
methodis_join(name) -> bool

Return whether the name is a .

paramnameNodeName

Raises:

  • ValueError: The is unknown.
methodset_entry_point(name) -> None

Set the that runs first for each fresh input.

An input that responds to a Request does not start at the : the continues on the of the that asked.

paramnameNodeName

Raises:

  • ValueError: The is unknown.
methodset_budget(budget) -> None

Limit the model calls that call in each , and replace any earlier .

parambudgetBudget

Raises:

  • ValueError: budget.resume_at is not a of the .
methodnode(name) -> Node

Return the name.

paramnameNodeName

Raises:

  • ValueError: The is unknown.
methodhas_node(name) -> bool

Return whether the has a name.

paramnameNodeName
methodedge(source) -> Edge | None

Return the that leaves source, or None if the ends after it.

paramsourceNodeName

Raises:

  • ValueError: The is unknown.

Node

dataclassNode(name, step)

A named of a and the that it runs.

Graph.add_node makes . Read them back with Graph.node.

paramnameNodeName

The name of the , unique in its . that the records carry it as their origin.

paramstepStep | EffectStep | Delegation

What the runs: a Step, an EffectStep, or a Delegation that runs .

Edge

dataclassEdge(source, targets=(), router=None)

Where a goes after a : fixed targets, or a that picks one.

Graph.add_edge and Graph.add_conditional_edge make . Read them back with Graph.edge.

paramsourceNodeName

The that the leaves.

paramtargetstuple[NodeName, ...]
= ()

The that run next. One target runs next in the same . More targets run in parallel branches, and the meet at a (see Graph.add_join). Empty when router is set.

paramrouterRouter | None
= None

The function that picks the next , or None for an with fixed targets.

Router

type aliasRouter = Callable[[Trace], NodeName | None]

A function that picks the next from the .

The calls it with the of the after the of the recorded its . It returns the name of the next , or None to end the . A is a pure function of the : if it raises or names a that the does not have, the records a Failure and does not try again.

Budget

dataclassBudget(cap, resume_at, tolerance, resets_on_answer=False)

A limit on the model calls that call in one of a .

When a model output that calls brings the count past cap, the records BudgetExceeded instead of the next . The then at resume_at, at most tolerance times. After the last of these, the next model call gets no , so the model can only write its final output. One more trip ends the .

The model call loses its only when no other than a Compaction comes between the trip and the call.

paramcapint

The number of model outputs that call that the allows.

paramresume_atNodeName

The that runs after each trip, usually the model itself (ReActAgent uses its model ). It must be a of the . A that records an before the model call gives that call its again.

paramtoleranceint

How often the after a trip before it ends.

paramresets_on_answerbool
= False

If true, the count starts again at each response to a Request, so each response brings a fresh cap. If false, the count covers the whole .

methodwithdraws_tools(view) -> bool

Return whether the next model call on view gets no .

A that offers asks this before each model call. It is true after the last trip that the from: the latest , aside, is a BudgetExceeded, and the trips reached tolerance.

paramviewTrace

The as the sees it.

Returns

bool

True if the model call must get no , so that the model writes its final output.

Delegation

protocolDelegation

Another that a runs as , one for each that hand_offs finds in the parent's .

A whose is a Delegation starts one for each , each in a of its own. When all ended, the records what hand_back returns for each of them in the parent stream, and the parent continues on the 's .

methodgraph_for(task, /) -> Graph

Build the of the that task starts.

The calls this again on each pass, with the Input that heads the 's , so it must return the same for the same .

paramtaskInput

The input that opened the 's .

Returns

Graph

The that runs the .

methodhand_offs(parent, /) -> Sequence[Event]

Return the that the parent's latest model call asked for, in call order.

paramparentTrace

The parent stream's of the .

Returns

Sequence[Event]

One for each call. An Input starts a with that . Any other is the result of its call without a , for example a refusal, and lands with the , in the same order.

methodhand_back(child, /) -> Sequence[Event]

Return the that report one 's result to the parent.

paramchildTrace

The stream's of the , after the ended.

Returns

Sequence[Event]

At least one , recorded in the parent stream. An empty sequence records a Failure instead.

Steps

A reads the and returns the to record.

Step

type aliasStep = Callable[[Trace], Awaitable[Sequence[Event]]]

An async function that reads the and returns the that its records.

The calls it with the of the 's , and records what it returns with the 's name as origin. It must return at least one , no Input, and a Request only as the last . If it raises, the records a Failure and the stops. A TransientError makes that failure , so the runs again when the runs again.

LlmStep

dataclassLlmStep(builder, model)

A that makes one model call with the context that builder builds, and records the output.

The model's output records:

  • the model that replied: the model of the call, unless the names another, as a fallback does
  • the characters that the model was sent, and the hash of the ChatRequest
  • its , with the numbers that they name translated to real ids and call ids

When the context has a , the removes the from the output's text and resolves each number to the source that it named. Streamed text reaches a listener without , and each follows its text.

parambuilderContextBuilder

What builds the context of the call from the .

parammodelChatModel

The that replies.

method__call__async (trace) -> Sequence[ModelOutput]

Build the context from trace, call the model, and return its output as the one .

paramtraceTrace
methodanswerasync (context) -> ModelOutput

Call the model with a context that is already built, and return its output as the records it.

Use it in a of your own that changes the context before the call.

paramcontextModelContext

The context of the call.

Returns

ModelOutput

The model's output, ready to record.

Raises:

  • ValueError: The model's ended without an output.

ToolStep

dataclassToolStep(tools, repeats=False)

A that runs the of the latest ModelOutput and records one result for each call.

Each call's result is what its returns. When the is unknown or the arguments do not parse, the result is a ToolOutput with an error instead, and the does not run.

Other results, for which the does not run:

  • A call that is identical to an earlier call of the , by name and arguments, gets an error that says so, when the earlier call succeeded or is in the same model output. repeats lets each run it again, and a 's own repeats replaces the 's.
  • A call to a that its turn_limit says is spent gets the limit's error.

The calls run in parallel. A call to a with a limit waits for the calls before it in the same model output and sees their results, so the results are the same in any order of completion. An argument of type Recorded gets the of its type that the recorded. A that raises TransientError, InvalidQueryError or ResourceUnavailableError gets that error as its result. Any other exception stops the , and the records a Failure.

paramtoolsMapping[str, Tool[Any]]

The that the model can call, by name.

paramrepeatsbool
= False

Whether each runs an identical call again. False gives it the repeated-call error as its result.

Raises:

  • MutableToolError: When the is made: a , or an object that share, could keep state between calls.
  • ToolDefinitionError: When the is made: a 's run takes more than the call's arguments, or a has parameters that still need a value (unbound).
method__call__async (trace) -> Sequence[Event]

Return the result of each of the latest model output, in call order.

paramtraceTrace

Raises:

  • ValueError: The latest model output calls no .
  • ToolResultError: A returned something other than one ToolOutputBase.
  • UnrecordedWholeError: A showed a part of an that nothing recorded.

RequestStep

dataclassRequestStep(kind, actor, expects=None)

A that stops the to wait for a response from outside, such as an approval.

It records one Request. The then waits. The next input is the response, and the continues on the of this , not at the .

paramkindstr

What kind of request this is, in the 's own words: "approval", "clarification".

paramactorActor

Who is responsible for the request, for example "system" for a fixed approval .

paramexpectsdict[str, JsonValue] | None
= None

The JSON Schema that the response must satisfy, or None to accept any JSON.

method__call__async (trace) -> Sequence[Event]

Return the one Request that this records.

paramtraceTrace

Model context

These names build what a model call receives, and say how the model cites.

ModelContext

dataclassModelContext(request, model, citations, numbering)

Everything that one model call receives, and what its output is read with.

A ContextBuilder makes it from the , and LlmStep sends it.

paramrequestChatRequest

The ChatRequest that the model receives: the messages and the .

parammodelstr

The name of the model that the call goes to. The model's output records it.

paramcitationsCitationStrategy | None

How the in the model's output are read, or None to read no .

paramnumberingNumbering

The that the model was shown. The model's and arguments name by these numbers.

ContextBuilder

type aliasContextBuilder = Callable[[Trace], ModelContext]

A function that builds the context of one model call from the alone.

It is synchronous, so it cannot call a model, a store or any other resource: the same code and the same always give the same ChatRequest. MessageBuilder in soev_agents.toolkit is the usual builder.

CitationStrategy

dataclassCitationStrategy(pattern, opens, write, write_invalid=…)

How one model is told to cite and how its are read back.

soev_agents.toolkit has ready strategies, such as BRACKETS for like [1].

parampatternre.Pattern[str]

The pattern of one . Each integer in a match is a cited number. A match must start with one of the characters in opens, and it must not be able to grow once complete.

paramopensstr

The characters that start a . A holds text back from each of these characters until it knows whether a follows.

paramwriteCallable[[Sequence[int]], str]

The function that writes a for the given numbers, for the prompt's example and wherever are put back into text.

paramwrite_invalidCallable[[str], str]
= …

The function that gets a that named no source, as the model wrote it with the spaces before it, and returns what the prompt shows in its place. The default adds a note that the named no source.

Effects

An changes the world exactly once. These names define the , perform its , and decide about the after a crash or a .

EffectStep

protocolEffectStep

A that changes the world exactly once: it records its before the .

When the runs, intend returns the and the records it. Then effector performs it, and the records the result, linked to the . If the process stops between the two, the next finds the without a result and calls recovery instead of a second . If the caller the during the , cancellation decides. A response to a Request of the returns the to the same .

attributeeffectorEffector

The that performs the .

attributerecoveryEffectRecovery

What resolves an without a result when the runs again.

attributecancellationEffectCancellation

What decides about an in progress when the caller the .

methodintend(trace) -> EffectIntent

Return the of the , from the alone.

paramtraceTrace

The of the 's .

Returns

EffectIntent

The , with the verb as kind and the argument as args. The records it before the .

Effector

protocolEffector

The that changes the world for an EffectStep: it sends the mail, or makes the refund.

methodperformasync (intent) -> EffectResult

Do the that intent declares.

The recorded intent before this call, so a crash during the call leaves an without a result, which the 's recovery resolves.

paramintentEffectIntent

The recorded , with its kind and args.

Returns

EffectResult

The result: what the world replied. Put a definitive refusal of the world in error. The can route on it.

Raises:

  • Exception: Any error that leaves the outcome unknown. The records a Failure, and the stays without a result for recovery.

EffectRecovery

type aliasEffectRecovery = Callable[[Trace, EffectIntent, Effector], Awaitable[Sequence[Event]]]

An async function that resolves an without a result, after a crash during its .

The calls it when the 's runs again and the 's last has no result. It gets the , the and the , and it can ask the world whether the happened. It must return exactly one : the EffectResult of the , or a Request that asks a person. A response to that request brings the back to its , with the response in the .

EffectCancellation

type aliasEffectCancellation = Callable[[Trace, EffectIntent, Effector, asyncio.Task[EffectResult]], Awaitable[Sequence[Event]]]

An async function that decides about an in progress when the caller the .

It gets the , the , the and the asyncio task that performs the . It can await that asyncio task, or ask the world. It returns the one EffectResult of the , one Request that asks a person, or nothing. Nothing leaves the without a result for the 's recovery. The the asyncio task when the function returns. Use abandon to give the up at once.

abandon

functionabandonasync (trace, intent, effector, performing) -> Sequence[Event]

Give up the in progress: the EffectCancellation that awaits nothing and records nothing.

intent stays without a result, so the 's recovery resolves it when the runs again.

paramtraceTrace

The of the 's .

paramintentEffectIntent

The of the in progress.

parameffectorEffector

The 's .

paramperformingasyncio.Task[EffectResult]

The asyncio task that performs the . The it.

Returns

Sequence[Event]

No .

Tools

A is what a model can call. The tool decorator makes a of an async function.

tool

functiontool(*, name, description, repeats=None, limit=None) -> ToolDecorator

Return the decorator that makes an async function the that the model calls as name.

The parameters before * are what the model writes, and the 's argument schema comes from their types and defaults. A parameter after * has the type Recorded, or keeps its default, or gets its value later from FunctionTool.bind. The model reads description, never the function's docstring.

Example

from soev_agents.domain import ToolOutput
from soev_agents.runtime import tool

# SearchIndex is your own search client class, and resources are the resources that `Agent.build` gets.
@tool(name="search", description="Search the documents. Use short keyword queries.")
async def search(query: str, *, index: SearchIndex) -> ToolOutput:
    ...

agent_tools = [search.bind(index=resources.index)]
paramnamestr

The name that the model calls the by.

paramdescriptionstr

What the model reads about when and how to use the .

paramrepeatsbool | None
= None

Whether a call that is identical to an earlier successful call of the runs again. None follows the repeats of the ToolStep.

paramlimitLimit | None
= None

The 's limit for each , such as at_most(3, calls=name, error="..."), or None for no limit.

Returns

ToolDecorator

The decorator. Apply it to an async function that returns a ToolOutputBase.

ToolDecorator

dataclassToolDecorator(name, description, repeats=None, limit=None)

The decorator that tool(...) returns. Applied to a function, it makes the FunctionTool.

paramnamestr

The name that the model calls the by.

paramdescriptionstr

What the model reads about the .

paramrepeatsbool | None
= None

Whether an identical call runs again, or None to follow the 's repeats.

paramlimitLimit | None
= None

The 's limit for each , or None for no limit.

method__call__[**P, R: ToolOutputBase](function) -> FunctionTool[P, R]

Make the of function.

paramfunctionCallable[P, Coroutine[Any, Any, R]]

Raises:

  • ToolDefinitionError: The function does not return a ToolOutputBase, or has a parameter that the model cannot write.
  • ValueError: An ElementId accepts no class, or sits deeper than a field, a list item or an optional.

FunctionTool

dataclassFunctionTool[**P, R: ToolOutputBase](name, description, arguments, function, repeats=None, turn_limit=None, unbound=())

A that the tool decorator made of an async function.

run calls the function with the call's arguments and the Recorded values. Calling the itself calls the function, with its own signature and result type, so a test can call it directly.

paramnamestr

The name that the model calls the by.

paramdescriptionstr

What the model reads about the .

paramargumentstype[BaseModel]

The pydantic model of the parameters that the model writes, made from the function's signature.

paramfunctionCallable[P, Coroutine[Any, Any, R]]

The function, with the bound parameters already in it.

paramrepeatsbool | None
= None

Whether an identical call runs again, or None to follow the 's repeats.

paramturn_limitLimit | None
= None

The 's limit for each , or None for no limit.

paramunboundtuple[str, ...]
= ()

The keyword-only parameters that still need a value from bind. ToolStep refuses the until each of them has one.

method__call__(*args, **kwargs) -> Coroutine[Any, Any, R]

Call the function directly, for example in a test.

paramargsP.args
paramkwargsP.kwargs
methodrunasync (arguments) -> R

Run one call: call the function with the parsed arguments as keyword arguments.

paramargumentsBaseModel
methodbind(**values) -> FunctionTool[..., R]

Return this with values bound to parameters, such as the or the values of a .

The model never sees a bound parameter. Bind the keyword-only parameters after * that have no default, in Agent.build, before a ToolStep gets the .

paramvaluesAny

The value of each parameter, by name.

Returns

FunctionTool[..., R]

A new . This does not change.

Raises:

  • ToolDefinitionError: A name is not a parameter of the function, is bound already, or has the type Recorded, which the fills. Or a value is a , a store or a model.
  • MutableToolError: A value could keep state between calls.

Tool

protocolTool[A: BaseModel]

What the model can call: a name, a description, the arguments that the model writes, and run.

run gets only the call's arguments, parsed into arguments, with each id already real. What the recorded so far reaches a as a Recorded argument, never as the . A is a frozen dataclass whose fields hold , code constants and values of the 's input. ToolStep refuses any other , so that no keeps state between calls. The tool decorator makes a of an async function.

A can also have these attributes, which ToolStep reads:

  • turn_limit: a Limit for each , for example at_most(3, calls="search", error="You searched three times already.").
  • repeats: whether a call that is identical to an earlier successful call of the runs again. It replaces the 's repeats for this .
  • unbound: the parameters that still need a value. ToolStep refuses a that names any, so a missing value fails when the is built, not when the model calls the .
attributenamestr

The name that the model calls the by. It is unique among the of a .

attributedescriptionstr

What the model reads about when and how to use the .

attributeargumentstype[A]

The pydantic model of the arguments. The model is shown its JSON schema (see tool_def).

methodrunasync (arguments) -> ToolOutputBase

Run one call of the .

paramargumentsA

The call's arguments, parsed, with ids already replaced by real ids.

Returns

ToolOutputBase

The one result of the call. Put every and text of the call in it.

Raises:

  • TransientError: A resource failed for a moment. The model reads the error as the result.
  • InvalidQueryError: The resource refused the query. The model reads the error as the result.
  • ResourceUnavailableError: The resource is not available. The model reads the error as the result.

tool_def

functiontool_def(tool) -> ToolDef

Return what the model is shown of tool, with the id fields that its calls record.

The schema of the arguments has no titles and no class docstrings: the model reads the property names and the field descriptions.

paramtoolTool[Any]

The to show.

Returns

ToolDef

The definition that a ChatRequest offers the model.

Raises:

  • ValueError: An ElementId in tool.arguments sits where no call could be translated.

ElementId

classElementId(*accepts)

A mark on a argument that holds the id of an of one of accepts, subclasses included.

Use it as Annotated[str, ElementId(Document)], alone, in a list or optional. The model writes the number that it was shown, and LlmStep replaces the number with the real id when it records the call. The mark itself changes no validation, so code can build arguments from real ids. The JSON schema shows a plain string.

A field with an type instead, such as Annotated[MailReference, ElementId()], gets the first recorded under that id. ElementId() without classes is only for a field with an type. It accepts the classes of that type.

Make the mark.

paramacceptstype[Element]

The classes that the argument accepts. Leave it empty on a field with an type.

ResultHandle

classResultHandle

A mark on a argument that holds the handle of a result, such as "r12".

Use it as Annotated[str, ResultHandle()], alone, in a list or optional. The model writes the handle that it was shown, and LlmStep replaces it with the call id of the that has that result, as it does for ElementId.

Recorded

type aliasRecorded = …

The type of a parameter that gets the of one type that the recorded so far.

Write Recorded[Document] as the type of a parameter after *, or of a field of the 's arguments. Before each call, ToolStep fills it with every Document of the , the input's included, in recorded order. The model never sees the parameter, and a value that the model writes for it is replaced. This is how a reads what the built up without the .

Limit

type aliasLimit = Callable[[Trace], str | None]

A limit of one for each .

It gets the that a call would see, and returns the error that the model reads when the is spent, or None while the can run. Give a a limit as its turn_limit. at_most makes the usual limit.

at_most

functionat_most(n, *, calls=None, elements=None, error) -> AtMost

Return a limit that makes error a call's result, and does not run the , once the reached n.

The limit counts the calls with a result of the named calls, or the distinct of the type elements. An recorded again counts once. Give the limit to a as limit in tool, or as its turn_limit.

paramnint

The count at which the is spent. At least 1.

paramcallsstr | None
= None

The name of the whose calls with a result count. Usually the 's own name.

paramelementstype[Element] | None
= None

The type whose distinct count, for example the documents that the read.

paramerrorstr

What the model reads instead of a result when the is spent. Tell the model what to do next.

Returns

AtMost

The limit.

Raises:

  • ValueError: n is below 1, or not exactly one of calls and elements is given.

AtMost

dataclassAtMost(n, error, calls=None, elements=None)

A limit of one for each , which at_most makes.

Exactly one of calls and elements is set.

paramnint

The count at which the is spent.

paramerrorstr

What the model reads when the is spent.

paramcallsstr | None
= None

The name of the whose calls with a result without an error count, or None.

paramelementstype[Element] | None
= None

The type whose distinct of the count, or None.

method__call__(view) -> str | None

Return error once the in view reached n, and None before.

paramviewTrace

ToolDefinitionError

errorToolDefinitionError(tool, parameter, problem)

A is defined so that it could reach state, or so that the model could not call it.

Make the error.

paramtoolstr

The name of the .

paramparameterstr | None

The parameter at fault, or None when the fault is the 's own.

paramproblemstr

What is wrong, worded as the fix.

MutableToolError

errorMutableToolError(tool, fields, holds)

A , or an object that share, could keep state between calls.

The object is not a frozen dataclass, or one of its fields holds a mutable value: a list, dict, set, bytearray or asyncio primitive. ToolStep raises it when it is made, and FunctionTool.bind raises it for a mutable value.

paramtoolstr

The name of the .

paramfieldstuple[str, ...]

The path of field names from the to the refused object. Empty for the itself.

paramholdstype | None

The class of the mutable value, or None when the object is not a frozen dataclass.

ToolResultError

errorToolResultError(tool, returned)

A returned something other than one ToolOutputBase, so its call has no single result.

paramtoolstr

The name of the .

paramreturnedtype

The class of what the returned.

UnrecordedWholeError

errorUnrecordedWholeError

A showed an whose ref names nothing recorded, in its output or earlier.

The model would see a handle it can do nothing with, so a that shows a part records its whole beside it.

Threads

A is one conversation in a store. A catalog opens and runs their .

AgentCatalog

classAgentCatalog(store, agents, runs=None, *, version, resources=…)

A catalog of , by name, over one store: the object that opens, runs and reads .

open makes a under an 's name and records Opened with the first input. thread reads that Opened back and returns the Thread, with a that its built for the that the caller is about to run, or without a for a caller that only reads.

A fresh input is parsed by its 's input class and recorded with the that it holds, so a 's never depends on the 's code.

Example

from soev_agents.adapters import InMemoryTraceStore
from soev_agents.domain import Input
from soev_agents.runtime import AgentCatalog
from soev_agents.toolkit import ReActAgent, ReActConfig

# chat_model: a ChatModel adapter, such as an OpenAIChatModel.
config = ReActConfig(model=chat_model, prompt="Answer briefly.")
catalog = AgentCatalog(InMemoryTraceStore(), {"assistant": ReActAgent(config)}, version="1.4.0")
thread, _ = await catalog.open("assistant", Input(payload="Hello", model="my-model"))
trace = await thread.resume()

Make a catalog.

paramstoreTraceStore

The store of the .

paramagentsMapping[str, Agent[Any, Any]]

The , by the name that a is opened under.

paramrunsRuns | None
= None

The registry of this process's . Share one between catalogs of one process. None makes a new one.

paramversionstr

The of the code that runs the . Each that the catalog's append records it, for people and that inspect a . No code reads it to decide.

paramresourcesResources
= …

The function that gives each 's build its .

methodopenasync (name, input, *, thread_id=None) -> tuple[Thread, Sequence[LogEntry[Event]]]

Open a for the name and record its first input.

Nothing runs yet. Call Thread.resume or Thread.stream on the returned to run the first .

paramnamestr

The that runs the , by its name in the catalog.

paraminputInput

The first input. Its must parse into the 's input class.

paramthread_idstr | None
= None

The id of the new , or None to make a fresh id.

Returns

tuple[Thread, Sequence[LogEntry[Event]]]

A tuple (thread, entries): the , with a for the first , and the log entries that the open appended.

Raises:

  • UnknownAgentError: The catalog has no of that name.
  • InputDeniedError: The 's refuses the , or build refuses the . Nothing is recorded.
  • ThreadExistsError: thread_id names a that already has .
methodthreadasync (thread_id, *, agent=None, input=None, resuming=False) -> Thread

Return the Thread of the that its Opened names, over a built for the about to run.

paramthread_idstr

The to return.

paramagentstr | None
= None

The that the must run, or None for no check.

paraminputInput | None
= None

The fresh input that the caller submits next. The is built for it.

paramresumingbool
= False

If true, the is built for the input that opened the in progress, so that the caller can it or respond to its request. Without input or resuming, no is built: the reads, takes responses to requests, and , but cannot run.

Returns

Thread

The .

Raises:

  • UnknownThreadError: No has under thread_id.
  • UnknownAgentError: The names no , or an that the catalog does not have.
  • AgentMismatchError: The runs another than agent.
  • InputDeniedError: input, or the input that opened the , does not parse into the 's class.
  • ValueError: Both input and resuming are given.

Thread

classThread(thread_id, graph, store, runs, agent=None, accept=None, event_types=CORE_EVENT_TYPES, version=None)

One of a store: it reads the , takes inputs, runs its , and .

Get a from AgentCatalog.open or AgentCatalog.thread. A from the catalog takes no fresh input other than the input that its was built for. To start the next , get a new with AgentCatalog.thread(thread_id, input=...). A from the catalog without a reads, takes responses to requests, and , but cannot run.

The usual calls:

  • send records an input and runs the to its end. submit only records the input.
  • stream runs the and yields each as it is recorded. resume runs it to its end.
  • status reads the state of the current , and trace reads all .
  • cancel ends the current , fork copies the up to a , and delete removes it.

Make a handle on a . Most callers get a from an AgentCatalog instead.

paramthread_idstr

The id of the in the store.

paramgraphGraph | None

The that runs the next , or None for a that does not run.

paramstoreTraceStore

The store of the 's .

paramrunsRuns

The registry of this process's .

paramagentstr | None
= None

The name of the that runs the .

paramacceptCallable[[Input], Input] | None
= None

Turns a fresh input into the input to record, or refuses it with InputDeniedError. None records a fresh input as given.

paramevent_typesEventTypes
= CORE_EVENT_TYPES

The types that the 's are read and recorded as. An input that does not read back as one of them raises UnreadableEventError.

paramversionstr | None
= None

The of the code. Each that the appends records it, except the copies of a , which keep the that wrote them.

attributeid

The id of the in the store.

methodopenasync (agent, input) -> Sequence[LogEntry[Event]]

Record Opened for agent and the first input, in one append. Nothing runs.

AgentCatalog.open calls it. Call it yourself only for a made without a catalog.

paramagentstr

The name of the that runs the .

paraminputInput

The first input.

Returns

Sequence[LogEntry[Event]]

The log entries that the open appended.

Raises:

  • ThreadExistsError: The already has .
  • InputDeniedError: The 's accept refused the input.
methodsubmitasync (input, *, answering=None) -> LogEntry[Event]

Route, validate and append one input; nothing runs.

paraminputInput

The input. A fresh input opens a new . A response goes to the of its request and must satisfy the request's expects schema.

paramansweringRequest | None
= None

The waiting request that input responds to, or None for a fresh input. With one waiting request, None is refused: name the request, or the .

Returns

LogEntry[Event]

The log entry of the recorded input.

Raises:

  • ThreadActiveError: A fresh input while the has not ended, a request waits, or an has no result. A response to an is accepted.
  • InputDeniedError: The named request is not waiting, the does not satisfy its expects schema, the response names another model than the , or the was built for another fresh input.
methodsendasync (input, *, answering=None) -> Trace

Record one input and run the to its end: submit, then resume.

paraminputInput

The input, as in submit.

paramansweringRequest | None
= None

The waiting request that input responds to, or None for a fresh input.

Returns

Trace

The whole after the .

Raises:

  • ThreadActiveError: As in submit.
  • InputDeniedError: As in submit.
  • ValueError: The was made without a . The input is recorded before the error.
methodresumeasync () -> Trace

Run the from its until the stops, and return the whole .

Use it to continue a that was interrupted, or to run a after submit. If this process already runs the , the call joins that run.

Raises:

  • ValueError: The was made without a .
  • LeaseHeldError: Another process runs the .
methodstreamasync () -> AsyncGenerator[LogEntry[Event], None]

Run the from its and yield each as the records it.

If this process already runs the , the call follows that run instead of a second one. A reader that stops does not stop the : cancel does. Once Runs.close has begun, nothing new starts, and the keeps its work for the next process.

Yields:

  • LogEntry[Event]: Each log entry that the appends, in log order.

Raises:

  • ValueError: The was made without a .
  • LeaseHeldError: Another process runs the .
methodwatchasync (*, after=0, follow=True) -> AsyncGenerator[LogEntry[Event], None]

Yield the 's stored past a log . Nothing runs.

paramafterint
= 0

The log to read after. 0 reads from the start.

paramfollowbool
= True

If true, wait for new after the stored ones, until the is deleted. If false, stop after the stored .

Yields:

  • LogEntry[Event]: Each log entry, decoded, in log order.
methodfollowasync (*, after=0) -> AsyncGenerator[LogEntry[Event], None]

Yield the 's past after while a process is running it, and stop once none is.

Nothing runs. The process that holds the writes the , and follow reads the that it writes. follow ends when the is free: the ended, or the stopped and the is interrupted. To run the , use stream. To read until the is deleted, use watch.

paramafterint
= 0

The log to read after. 0 reads from the start.

Yields:

  • LogEntry[Event]: Each log entry past after, in log order.
methodstatusasync () -> Status

Return the state of the current and the requests that wait for a response.

It reads the current and the , not the whole .

methodrunningasync () -> bool

Return whether a process holds this 's , and so is running it.

It reads only the , and status reads the whole . Use it to wait until another process stops the .

methodheld_elsewhereasync () -> bool

Return whether another process holds this 's , so that stream here is refused.

methodtraceasync () -> Trace

Read all stored of the and return them as a .

methodcancelasync (*, input=None) -> Status

End the current : record Cancelled and stop this process's of the .

The is over once Cancelled is recorded. An in progress decides about its with its cancellation. The call does not wait for that.

paraminputint | None
= None

The log of the input that opened the to , or None for the current . With a , a that another input opened in the meantime is not cancelled.

Returns

Status

The after the .

Raises:

  • NothingToCancelError: The is finished, failed or cancelled already, input did not open the current , or the moved since it was read.
methodforkasync (*, at) -> Thread

Copy the 's up to a log into a new .

The copies keep the that wrote them. The new has the same and , and a fresh id. Use a to try another input or other code from a point of a recorded . To run another input on the , get it with AgentCatalog.thread(fork.id, input=...), which parses the input and builds its .

paramatint

The log of the last to copy. At least 1.

Returns

Thread

The new .

Raises:

  • PositionOutOfRangeError: at is outside the .
methoddeleteasync () -> None

Stop this process's of the , and delete all its from the store.

Runs

classRuns(*, lease_ttl=30.0)

The running asyncio tasks of this process, one per .

A caller joins the running asyncio task instead of a second one, so one process never runs a twice. Across processes each holds the 's in the store, under this registry's owner, for lease_ttl seconds at a time.

Make one Runs for each process and give it to the AgentCatalog. Call close when the process stops.

Make an empty registry.

paramlease_ttlfloat
= 30.0

How many seconds a stays valid without renewal. A process that stops without release blocks its for at most this long.

attributeowner

The owner name of this process's , a fresh id.

methodstart(thread_id, run) -> asyncio.Task[object] | None

Return the 's running asyncio task, and start one when none runs.

paramthread_idstr

The to run.

paramrunCallable[[], Coroutine[Any, Any, object]]

Makes the coroutine of the . It is called only when an asyncio task starts, so no coroutine is made and dropped.

Returns

asyncio.Task[object] | None

The running asyncio task, or None when no asyncio task runs after close began, because no new one starts.

methodstopasync (thread_id, *, wait=True) -> None

the 's running asyncio task.

paramthread_idstr

The whose asyncio task to . Nothing happens when none runs.

paramwaitbool
= True

If true, wait until the asyncio task stops. An 's cancellation can make this slow. If false, ask the asyncio task to stop and return at once.

methodcloseasync (timeout) -> None

Wait up to timeout seconds for the running asyncio tasks, then the rest.

It then waits for each cancelled asyncio task to stop. An 's cancellation can make this take longer than timeout. After this call, start starts no asyncio task. A whose was cancelled keeps its work for the next process.

paramtimeoutfloat

How many seconds to wait for the running asyncio tasks.

Status

dataclassStatus(state, pending, position)

The state of a 's current , read from its and its .

paramstateThreadState

The state of the : "running", "waiting", "failed", "interrupted", "cancelled" or "finished".

parampendingtuple[LogEntry[Request], ...]

The requests that wait for a response, each with its log (see open_requests).

parampositionint

The log of the 's last . Read on from here with Thread.watch.

ThreadState

type aliasThreadState = Literal['running', 'waiting', 'failed', 'interrupted', 'cancelled', 'finished']

The state of a 's current , read from its and its alone.

  • "running": a process holds the 's .
  • "waiting": a request waits for a response.
  • "failed": a failed, and the stopped on the Failure.
  • "interrupted": the stopped without an end of the , for example in a crash. the .
  • "cancelled": the caller cancelled the .
  • "finished": the ended, or the has no yet.

InputDeniedError

errorInputDeniedError

An input was refused, and nothing was recorded.

The does not fit the 's , a response does not fit the schema of its request, the request that it responds to is not waiting, or Agent.build refused the . The default HTTP hooks respond with 422.

ThreadActiveError

errorThreadActiveError

The 's has not ended, so it takes no fresh input.

The is running or was interrupted, a request waits for its response, or an has no result. A response to a waiting request is accepted. or the first. The default HTTP hooks respond with 409.

ThreadExistsError

errorThreadExistsError

A with this id already has , so it cannot be opened again.

UnknownAgentError

errorUnknownAgentError

The catalog has no of this name, or the names no .

UnknownThreadError

errorUnknownThreadError

No has under this id.

NothingToCancelError

errorNothingToCancelError

The is finished, failed or cancelled already, or the named input did not open the current .

PositionOutOfRangeError

errorPositionOutOfRangeError

A log is outside the , for example the of a .

AgentMismatchError

errorAgentMismatchError

The runs another than the one that the caller named.

Reading a turn

These names read facts of a 's and , without code.

cancelled

functioncancelled(trace) -> bool

Return whether the caller cancelled the current : its root stream holds a Cancelled.

A that does long work can check it, but the already stops the after a .

paramtraceTrace

opening_input

functionopening_inputasync (store, thread_id, types) -> Input | None

Return the root input that opened the current , None for a without one.

It reads the store backwards only as far as that input, so a can build the 's before it loads the whole .

paramstoreTraceStore

The store of the .

paramthread_idstr

The .

paramtypesEventTypes

The types to read the with, for example from event_types_of.

Returns

Input | None

The input, or None.

turn_starts

functionturn_starts(events) -> list[int]

Return the in events of the inputs that open a , each judged in its own .

An input opens a when it responds to no request: the before it in its is not a Request.

parameventsSequence[Event]

in log order, of one or of several.

Returns

list[int]

The , in ascending order.

open_requests

functionopen_requests(trace) -> tuple[Request, ...]

Return the current 's requests that wait for a response, in all streams of the .

After a , only the requests that ask for the outcome of an with an unknown result still wait.

paramtraceTrace

The 's .

Returns

tuple[Request, ...]

The waiting requests. Respond to each with Thread.submit(..., answering=request).

numbering

functionnumbering(trace) -> Numbering

Number every in trace by first appearance, a ref before the naming it.

A real id keeps its number wherever it appears again, and the numbers do not change as the grows.

Each result with a call id gets a handle, r1 first, in recorded order.

paramtraceTrace

The , or the that the model was shown.

Returns

Numbering

The that the model sees.

Numbering

dataclassNumbering(numbers, elements, calls=dict(), handles=dict())

Each real id's number, and the that first showed it.

The model never sees a real id: it sees the number, and it cites and passes that number to . numbering makes a from the .

paramnumbersdict[str, int]

The number of each real id.

paramelementsdict[int, Element]

The that first showed each number. A number of a ref that was never shown as an itself has no entry.

paramcallsdict[str, str]
= dict()

The call id that each result's handle (r1, r2, ...) stands for.

paramhandlesdict[str, str]
= dict()

The handle of each call id's result.

methodnumber(real_id) -> int | None

Return the number of real_id, or None for an id that the never showed.

paramreal_idstr
methodelement(number) -> Element | None

Return the shown under number.

paramnumberint

Returns

Element | None

The , or None for an unknown number, and for a ref that was never shown as an itself.

methodcall(result) -> str | None

Return the call id whose result has the handle result, or None.

paramresultstr
methodhandle(call_id) -> str | None

Return the handle of the result of call_id, or None when the call has no result.

paramcall_idstr
methodsource(number) -> str | None

Return the real id of the Source shown under number, or None when no source is.

paramnumberint
methodby_id(real_id) -> Element | None

Return the recorded under real_id, or None for one never shown as an itself.

paramreal_idstr

Stored events

A store keeps each as JSON with a type . These names write as a StoredEvent and read them back.

EventTypes

classEventTypes(*types, elements=…)

The and classes stored are read back into, each by its type .

CORE_EVENT_TYPES holds the types of the framework. Add an 's own types with including, or use event_types_of.

Make the set of types.

paramtypestype[Event] | type[Element]

The classes, and classes, to read.

paramelementsElementTypes
= …

The classes that the hold, before the classes in types.

Raises:

  • ValueError: Two classes share a type .
methodincluding(*types) -> EventTypes

Return a new set with types added. This set does not change.

paramtypestype[Event] | type[Element]

Raises:

  • ValueError: Two classes share a type .
methoddecode(stored) -> Event

Read a stored back into its class.

paramstoredStoredEvent

The stored .

Returns

Event

The , with the that it holds read back into their classes.

Raises:

  • UnknownEventTypeError: The 's type is not in this set.
  • UnknownElementTypeError: An 's type is not in this set.
methodencode_readable(event) -> StoredEvent

Return event as stored, once it is proven to decode back into an equal .

parameventEvent

Raises:

  • UnreadableEventError: The , or an in it, would not read back as it was recorded.
methodentry(entry) -> LogEntry[Event]

Return a log entry with its decoded, at the same .

paramentryLogEntry[StoredEvent]

Raises:

  • UnknownEventTypeError: The 's type is not in this set.

CORE_EVENT_TYPES

constantCORE_EVENT_TYPES: Final[EventTypes]

The and types of the framework, which every records.

A of an with its own types is read with event_types_of([agent]), which adds them to these.

event_types_of

functionevent_types_of(agents) -> EventTypes

Return the core types plus every class that agents declare their record.

Use it to read stored of these , for example in replay or a store migration.

paramagentsIterable[_Declaring]

The , or any objects with element_types and event_types.

Returns

EventTypes

The types to read the with.

Raises:

  • ValueError: Two classes share a type .

encode

functionencode(event) -> StoredEvent

Return event as a StoredEvent: its type , its and its as JSON.

A store keeps each as a StoredEvent. EventTypes.decode reads it back. Use it to write a directly into a store, for example in a test or an import.

parameventEvent

The to store.

Returns

StoredEvent

The stored , with the same seq and ts, which are None until the store appends it.

Raises:

  • TypeError: The 's class declares no type .

UnknownEventTypeError

errorUnknownEventTypeError

A stored has a type that is not among the types that the is read with.

Add the class to the 's event_types, or read the with the types that event_types_of returns for its .

UnreadableEventError

errorUnreadableEventError

An would not read back as it was recorded.

Its type, or the type of an that it holds, is not among the types that the is read with, or one of its fields does not survive JSON. A raises it before it records the , and the records a Failure instead of an from a .

Replay

rebuilds each model call of a recorded with the current code.

replay

functionreplayasync (events, graph_for) -> list[ModelCall]

Rebuild every model call in events, one 's in log order.

graph_for builds the 's for the opened by an input, as the did when the ran; it is called once per , and its models are asked to prepare but never to reply. Nothing is executed, so the result is what today's code renders, which differs from what was sent if the changed since.

Use it to see what each model call of a recorded received, or what a change of a prompt or a does to recorded . Build the with refusing , so that a build that calls a resource fails at once.

Example

from soev_agents.domain import Input
from soev_agents.runtime import Graph, accepted, refusing, replay

# agent is the Agent of the thread, and thread is a Thread with recorded turns.
def graph_for(input: Input) -> Graph:
    return agent.build(refusing("resources"), accepted(agent.input, input)[1], input.model)

events = [entry.event async for entry in thread.watch(follow=False)]
calls = await replay(events, graph_for)
parameventsSequence[Event]

One 's in log order, decoded, for example from Thread.watch.

paramgraph_forCallable[[Input], Graph]

Builds the 's for the that an input opened. It gets that input, not the parsed turn. Parse the with accepted before the call to Agent.build.

Returns

list[ModelCall]

One ModelCall for each ModelOutput and Compaction in events, in log order, and one for each Failure of a model call that can be rebuilt. A Failure of another , for example of a , is no call.

ModelCall

dataclassModelCall(answer, started, request=None, prepared=None, unreplayable=None, view=None, builder=None)

One model call of a , as rebuilt it.

paramanswerModelOutput | Compaction | Failure

What the call recorded: a ModelOutput, the Compaction of a call that wrote a summary, or the Failure of a call that the model refused or never answered.

paramstarteddatetime | None

The timestamp of the before answer, so answer.ts - started is about the call's duration.

paramrequestChatRequest | None
= None

The ChatRequest that today's code builds for the call, or None when the call cannot be rebuilt.

parampreparedPreparedRequest | None
= None

What the model would send for that ChatRequest on the wire, or None when the cannot say.

paramunreplayablestr | None
= None

Why the call cannot be rebuilt, or None when it was rebuilt.

paramviewTrace | None
= None

The of the that the ChatRequest was built from, or None.

parambuilderContextBuilder | None
= None

The of the call's , so a reader can ask it more about the call, or None.

attributesame_requestbool | None

Whether request has the hash of the ChatRequest that the call recorded.

False means that today's code builds another ChatRequest from the same . None means that either hash is missing, which is always so for a Failure.

refusing

functionrefusing(name='this resource') -> Any

Return an that refuses every call with ResourceUnavailableError, whatever the method.

Give it to build in place of each resource when replay builds a , so a build that calls an fails at once.

paramnamestr
= 'this resource'

The name of the resource, which the error names.

Returns

Any

The .

On this page