Repository navigation
Releases
br-agentic-nodes 0.3.0
0.3.0 - 2026-11-02
Implements the sealed intent of minor 0.3: a structured reply for the ReAct agent and the generator.
Added
OutputSchemaPortandPorts::with_output_schema: an optional port that gives, at Start, theStructuredShapeof the agent that makes steps (labelagentof a ReAct agent,generatorof a generator-critic), or none. It is never called for the critic or the programmatic critic.- With a shape, every step of that agent asks the client for
OutputMode::Structured(shape), a step that offers tools included. Without the port, or with no shape, steps ask for plain text as before. - The structure rule: a final response of an agent that has a shape must hold a structured block. Otherwise the call is a failed call (
CallFailure::NoStructuredReply): the next client of the list when the fallback covers the step, a fault otherwise. A refusal, a filter or an exhaustion is exempt; a response restored at Start is not checked. ReactOutcome::structured_replyandGeneratorCriticOutcome::structured_reply: the first structured block of the last response.- Building blocks:
structured_reply,check_structure,Worker::with_shape.
Changed
CallFailurehas a new variant,NoStructuredReply: an exhaustivematchonCallFailuremust handle it.
br-agentic-nodes 0.2.0
0.2.0 - 2026-10-31
Rework to the sealed intent of minor 0.2, on br-agentic-contract 0.2.0.
Removed
Fault::NoAuthor,Fault::Thread; theFromimpls ofFaultfor the 0.1PortFault,ThreadErrorandBoundaryError.Outcome(withresponse,step_limit_reached,verdict) andOutcomePort;Ports::newno longer takes the outcome port.ReactAgent::with_author,ReactAgent::restart,GeneratorCritic::restart,GeneratorCritic::with_authors,with_generator_author,with_critic_author,with_critic_thinking_field. The shape of the critic answer follows theThinkingExpectationof the client of each attempt.Worker::new,Worker::load,WorkerParts::author,WorkerParts::system,WorkerParts::tools.- The free function
mail_tools; a worker gives its mail tools withWorker::mail_tools.
Changed
- Every port returns
PortFuturewithPortError;Fault::Portholds aPortError,Fault::Boundaryholds the 0.2BoundaryError. PromptPort::promptandCriticRequest::textuseString; a blank value is a fault.CriticRequestPort::requesttakes the last model response of the generator; it is called once per round.Ports::new(thread_port, agent_port, thread_access, prompt_port, tools_port, workspace_port).ReactAgent::new(name, ports, settings, ReactSpec).WorkerParts::inboxis anOption<Arc<dyn InboxPort>>.Worker::draingivesDrained { stored, request_stored }.Worker::close_turntakes&mut selfand an&AiTurnRef.GeneratorCriticSpecgainsgenerator_author,critic_authorandoutcome.
Added
Worker::open(async, reads the conversation once),Worker::conversation,Worker::steps_made,Worker::with_author,with_system,with_tools,Worker::mail_tools,Worker::drain_at_start;Drained.ReactSpec { fallback, outcome, author },AuthorSource::{Fixed, Port},AuthorPort.ReactOutcome(response,steps,step_limit_reached) withReactOutcomePort;GeneratorCriticOutcome(response,judgement,iterations,step_limit_reached) withGeneratorCriticOutcomePort;Judgement::{Validated, Rejected, NotJudged}.RoundLimitEnding::{FinalGeneration (default), KeepRejectedGeneration, Fail}andGeneratorCritic::with_round_limit_ending.- The programmatic critic:
ProgrammaticCritic { port, author, refusal_limit },ProgrammaticCriticPort,ProgrammaticVerdict::{Accept, Refuse},ProgrammaticRefusalLimit,PROGRAMMATIC_CRITIC_LABEL,GeneratorCritic::with_programmatic_critic. Fault::{Session, ProgrammaticRefusal, BlankExplicitReason, CriticAtRoundLimit},InvalidLimit::Refusals.- The crate README (also the crate documentation).
Behaviour
- The conversation is read once at Start; the instance applies each write to its copy first and then hands it to the thread access.
- The queue is read at Start with an awaited read and later by one background read at a time, never awaited at a boundary; an error of a read or of a mark is not a fault.
- The thinking rule applies only to the calls the fallback covers; a refusal, a filter or an exhaustion without tool call is exempt.
- The round counter and the programmatic-refusal counter go back to 0 only when a request item is stored.
- All results of a tool run are opened before any call starts. Inbox errors in the mail tools are error results, never faults.
- The critic projection drops agent mails and runtime notices from input turns.
- A failed discard at
Resultis a fault of the instance, asbr-agentic-contract0.2.0 rules; this crate has no code for it. - Web access is held in the settings but not enforced by this crate. No tool can reach the file system of the node: a
ToolCodegets only its arguments, a cancel view and a result stream. - All calls of a tool run start together: a call to a tool the node does not hold gets its error result after the real calls have started.
get_mail_filesgives each attachment name quoted, like the mail text, so a name written by the sender cannot forge a line of the result.
br-agentic-contract 0.2.0
0.2.0 - 2026-10-31
Rework of the thread access, the ports, the in-memory ports and the queue rules to the sealed intent of minor 0.2. Breaking: the 0.1 API of the thread access, the inbox, the agent port and the tool content does not compile against 0.2.
Breaking
- One error type for the ports,
PortError(Refused(Reason),Fault(Cause)), replacesPortFault,ThreadErrorandInboxError. Only the inbox refuses (send,documents); a refusal by the thread access is a fault of the instance. - All port methods return
PortFuture, a boxedSendfuture owned by this crate, instead of theBoxFutureofbr-llm-clients-core(the same type behind the alias). The agent port and the inbox need no clients-core import. ThreadAccessis rebuilt: oneread(thread)gives the wholeConversation, used once at the start; the other operations receive complete additions (add_input_turn(InputTurn),add_entry(AiTurnRef, TurnEntry),close_turn,open_response/open_resultgiving a stream,complete_*,discard_*) and answer accepted, refused or fault. Removed:loadof aProjectedThread,add_incoming, the agent parameter,Incoming,WrittenEntry,TurnTarget,Stored,OpenedandCompletedwith theirHeldanswers.open_resultno longer takes the tool call.AgentPortgives anAgentId; theAgenttype and the optional author are removed (the author is a build value of the node or a port of the node library).MailItem::ToandNewMailcarry the author name of the sender and the source conversation;InboxPort::mark_readtakes theEntryIdof any item (mail, human message, runtime notification, runtime notice);InboxPort::waitgives nothing;documentsrefuses with a reason when the agent is not a recipient into, for an unknown mail too.ThreadRef::newis replaced byThreadRef::from_uuid, which refuses a UUID that is not a v7.AgentLabelrefuses only the empty text.InboxEntry::newreturnsInvalidInboxEntry.ToolContentholdsToolBlocks (text, image, structured) without identity;into_resultgives each block its identity;ToolContent::error_texttakes no block id, andinto_resulttakes a generator of block ids (impl FnMut() -> BlockId) for the blocks.ToolCodeandResultStreamusePortFuture.FileSystemMode::startandsave_filesgivePortError.Sessionholds the conversation copy:Session::start(async, givesPortError) replacesSession::newand reads once;Session::open_responsetakes the&AiTurnRefinstead of aTurnTargetand givesResult<ResponseStream, SessionError>instead ofOpened;open_resulttakes no tool call;complete_responseandcomplete_resulttake theAiTurnRefand the complete object and giveSessionError;discard_response,discard_resultanddiscard_allgivePortError; every addition is applied to the copy under the rules ofbr-llm-messagesbefore it is handed over; a refused completion discards the open object. Errors areSessionError.store_at_boundarytakes the session, aMailEntryBuilder(the node library builds the entry of a mail) and the id of a new input turn: items the copy holds are skipped and marked; the others go into the open AI turn of the agent after its last entry, else into one new input turn with one entry each; every kind of item is marked as read after its entry is handed over.BoundaryErrorcarries aSessionError;BoundaryFaultis removed.AgenticNode::restartandRestartare removed.- The in-memory ports are rebuilt (
MemoryHubis removed): they take one sharedRecordand keep one ordered record of every call, with what the streams take; the thread access keeps the completed additions and starts from the conversation of the test; there is no link between the in-memory inbox and the in-memory thread access;MemoryAgentPorttakes its identities from the test (with_agentreplaceswith_author; it mints a UUIDv7 for a label the test did not name);MemoryHuband its methods (create_thread,conversation,inbox,thread_access,deliver_entry,put_document,is_open,is_read,set_author,set_source,response_deltas,result_fragments) andMemoryWorkspace::loadsandsavesare removed (readRecord::callsinstead); every constructor of an in-memory port takes the&Record;FixedThreadPort,FixedToolsPortandMemoryWorkspacetake the record. Boundary,BoundaryError,SessionError,ToolBlockandmemory::Callare#[non_exhaustive];SessionErrorhas noFrom<PortError>(an inbox error never becomes a fault through?);BoundaryErrorkeeps its fault as thesource.
Added
Session::add_input_turn,Session::add_entryandSession::close_turn: each applies the addition to the copy under the rules ofbr-llm-messages(a refusal is aSessionError::Rules, or aSessionError::WrongTurnforadd_entryandclose_turn, and nothing is handed over; the firstadd_entryfor anAiTurnRefcreates the AI turn), then hands it to the thread access (a fault is aSessionError::Port).PortFuture,PortError(withPortError::refused,PortError::fault,PortError::is_refusalandPortError::reason),AiTurnRef(public fieldsid,agentandauthor),ToolBlock(withToolBlock::text),SessionError(Port,Rules, withFrom<br_llm_messages::Error>),MailEntryBuilder,InvalidThreadRef,InvalidInboxEntry.mail_documents(documents only for a mail whose entry the conversation holds as an incoming entry),Queue::read(fills the queue from the batches of the inbox; it gives aPortError::Faultwhen the inbox gives the same cursor twice in a row, and keeps the items it already read),Boundary::unmarked,Session::holds,Session::holds_incoming,Session::open_ai_turn,Session::threadandSession::conversation(the thread reference and the copy of the conversation that the instance holds).- Re-exports of the types of
br-graph-contractthat the ports name:Fields,InstanceId,Reason,Cause,CancelView,Event,InputReader,Node, and of theStreamSinkofbr-llm-clients-core. memory::Record(Record::new,Record::calls) andmemory::Call(#[non_exhaustive]; one variant per call that a port receives, such asCall::Read,Call::AddInputTurnorCall::InboxSend, and per item a stream takes),memory::SentMail,MemoryThreadAccess::new(the record, the thread and the conversation the test gives),MemoryInbox::new,MemoryAgentPort::newandMemoryWorkspace::new(each takes the&Record;MemoryWorkspace::newalso takes the files),MemoryThreadAccess::empty(an empty conversation with the identity the test gives),MemoryThreadAccess::conversationandMemoryThreadAccess::is_open(a test looks at the conversation and at the open objects),MemoryInbox::deliver_entry,MemoryInbox::deliver_mail(delivers a mail from a sender to its recipients, the waysenddoes:PortError::RefusedwhenNewMail::refusalsays so, no change for a mail id delivered already),MemoryInbox::put_document,MemoryInbox::mail(theSentMailof a mail id),MemoryInbox::is_read(whether an agent has marked an item as read) andMemoryAgentPort::with_agent(sets the identity of a label).SessionError::WrongTurn(a call names a turn that is not the AI turn of that agent and author, or closes an AI turn that does not exist) andSessionError::WrongMailEntry(theMailEntryBuildergave an entry that is not the entry of its mail: another id than the mail id, or not an agent mail for atoitem or not a runtime notice for accitem, or an agent mail whose sender, author name or source conversation is not that of the item;store_at_boundarythen stores and marks nothing and restores the queue).- A crate README (the contract of the crate).
Changed
WebAccess::reaches_nothingis also true for an empty whitelist.resultgivesEvent::Failedwith the first error of the discards as its cause when a discard of an open object fails (in 0.1 it ignored the error);failedandcancelledstill ignore it.- An inbox error is never a fault of the instance: mark errors no longer stop
store_at_boundary(the item counts as stored or skipped andBoundary::unmarkedlists the ids).
br-flow-nodes 0.2.0
0.2.0 - 2026-10-29
A map-reduce node has a fault mode for the fault of one element.
Added
FaultMode(NonStrictby default,Strict) andMapReduce::fault_mode(FaultMode). InNonStrict, an element fault (the internal instance sendsFailedorRefused, sendsCancelledwhile the child token is not triggered, has an update that is refused or gives an invalid state, a panic in the apply being one case, or has an event end that reportsVanished) is not a fault of the node: the internal instance of that element is dropped, a failure is recorded for it, the child token is not triggered, the other elements go on, and the slot of the failed element is free for the next element. InStrict, the first element fault stops the node, as in 0.1.0.ElementOutcome(Done(State),Failed(String), withstate()andfailure()): the end of one element as the reduce function gets it. A failure holds theDisplayof theMapReduceErrorfor that fault as text only, without the internal state of the element, also when its update was rejected.
Changed
- BREAKING: the default fault mode of
MapReduceisNonStrict; in 0.1.0 every element fault stopped the node. A runner that wants the old behavior callsfault_mode(FaultMode::Strict). - BREAKING: the reduce function of
MapReduce::newtakes&[ElementOutcome<G>], the outcomes of the elements in map order, instead of&[State<G>]; inStrictno outcome is a failure. When every element failed, the node still sends aResult. - The faults of the node (an invalid initial value, an internal instance that cannot be created or carries another instance id, an internal instance whose input end is closed when it gets its one input) stop the node in both modes; the cancel token wins over any fault in both modes.
br-agentic-nodes 0.1.0
0.1.0 - 2026-10-29
First release: the agent nodes and their building blocks. It builds on br-agentic-contract, br-graph-contract, br-llm-messages and br-llm-clients-core.
Added
ReactAgent<F>: a node of kindagenticand subkindreact_agent. One node runs the whole loop: step, tool round, drain, next step, until a final response. Builder: name,Ports<F>,FallbackList,Settings(step limit,Timeouts,WebAccess,FileSystemMode) and optionallywith_author.GeneratorCritic<F>: a node of kindagenticand subkindgenerator_critic. A generator (a ReAct pass) and a critic (one structured call, no tool, no AI turn) share one thread.GeneratorCriticSpecholds the twoLadders, theRoundLimitand theCriticRequestSource(Fixedor aCriticRequestPort);with_authors,with_generator_author,with_critic_author,with_step_limit_ending(StepLimitEnding::{WithoutCriticCall, WithCriticCall})(default: without) andwith_critic_thinking_field.Outcome(last response, step-limit flag, verdict) with theOutcomePortand thePromptPortthat the runner gives.Ports<F>groups the ports of the contract with these.- The building blocks, public:
WorkerandWorkerParts(step,tool_round,drain,close_turn,load,steps_remaining),Slot(the settled client of a fallback list),FallbackList,Ladder,ModelClient(built with a requiredThinkingExpectation),check_thinking/holds_thinking(the thinking rule),warning_text/WARNING_WINDOW(the warning near the limit),mail_toolswithSEND_MAILandGET_MAIL_FILES, andFileSystemandSandboxPath. Fault,CallFailureandStop: the faults of an instance with their source chain.
Behaviour to know
- A client that should think is declared by the runner;
br-llm-clients-coregives no such report. The declaration is required:ModelClient::newtakes aThinkingExpectation(ExpectedorNotExpected) and has no default. The node never reads the settings of a client. - The sealed intent asks the instance to enforce the web access and the sandbox. Version 0.1.0 holds the web access and the file system mode, loads the files at the start and saves them before
Result, and enforces neither the web access nor the sandbox. AToolCodereceives only its arguments, a cancel view and a result stream, so no tool can reach the file system of the node. Until a later minor, the runner must give only tools that respect the web access. - The critic request is added to the projected thread of the critic for one call only: into the last input turn when it shows no agent output, else in a new input turn. It is never stored.
- In a generator-critic the round counter goes back to 0 when a drain stores items, also a drain inside a pass; the critic of that round keeps the ladder entry read at the start of the pass.
get_mail_filesacts only on ids whose entry in the projected thread is an incoming agent mail; any other entry or an absent id gives a result with the error flag.- After a tool round that leaves the AI turn at a boundary (no outstanding runtime call, no pending provider execution, no provider continuation) the drain stores the queue into the open AI turn; after any other tool round nothing is stored until the next boundary; at the start of a pass and after a final response it stores it where
store_at_boundaryputs it for an agent with no open AI turn (the last real input turn, else a new one).
br-agentic-contract 0.1.0
0.1.0 - 2026-10-29
First release: the contract of the agentic nodes. It builds on br-graph-contract, br-llm-messages and br-llm-clients-core, and on no node library.
Added
AgenticNode<F>(aNode<F>that tellshas_inbox()and declaresrestart()asRestart::{Resets, Keeps, NoCounters}) andAGENTIC, the kind of an agentic node.- The ports a runner gives a node, all asynchronous (
BoxFuture), none of them retried:ThreadPort(state snapshot toThreadRef),AgentPort(snapshot, instance id andAgentLabelto anAgent: anAgentIdand an optionalAuthorwhen the runner supplies the name at run time),ToolsPort,WorkspacePort(load,save),ThreadAccessandInboxPort. A failure of a port is aPortFaultwith an opaque cause. The state snapshot is the fieldsFof the first input. ThreadAccess:loadtheProjectedThreadof one agent;add_input_turn,add_incoming(to aTurnTarget: an input turn, or an AI turn with its author, created by its first entry),close_turn;open_response/complete_response/discard_responseandopen_result/complete_result/discard_result. AnIncomingaddition is aMailItem(the thread access builds the entry: an agent mail for a recipient into, a runtime notice for one incc, with the mail id as the entry id and no attachment in the content), anInboxEntry(a human message, a runtime notification or a runtime notice, as the inbox gave it) or aWrittenEntry(an agent mail the instance writes, with the author name it gives). An addition whose identity the thread holds already answersStored::Heldand changes nothing, before the rules ofbr-llm-messagesapply; a new addition those rules refuse isThreadError::Refusedwith aReason; any other failure isThreadError::Fault.open_*answersOpened::StreamorOpened::Held(object);complete_*answersCompleted::AddedorCompleted::Held(object).ResponseStreamis aStreamSinkofbr-llm-clients-core;ResultStreamtakes fragments of text; neither has an error.InboxPort:items(oldest first, bounded batches, an opaqueCursorfor the next batch,Nonewhen the list has no more items),wait(resolves with the next item for the agent that the caller has not been given yet, by a read or by an earlierwait),mark_read(the only change to the state of a mail),send(aNewMail, whose sender is the agent of theInboxContext; refused asInboxError::MailRefusedwith aReasonwhen it has no recipient intoor lists a recipient twice;NewMail::refusalis that check; sending an id sent already changes nothing) anddocuments.InboxItemis aMailItemor anInboxEntry;MailItem::Tocarries the mail id, sender, recipients, subject, body and attachment names,MailItem::Ccthe mail id and subject.Tool(aToolSpecwith aToolCodethat gets the arguments, aCancelViewand aResultStream),ToolContent(whole content and error flag; an expected error is content with the error flag, not a fault),ToolFault(opaque cause) andToolSet, which refuses a name that appears twice among the tools and the mail tools (DuplicateTool).WebAccess(Full,Whitelist(Vec<Site>),None),FileSystemMode(Always,Might,Never) withFileSystemMode::start(loads the files per mode:Neverdoes not call the load;Mighthas a file system only when the load gives at least one file) andsave_files.BoundaryErrorcarries theBoundarydone before the failure (faultanddone);BoundaryFaultis the cause.store_at_boundaryneeds a new turn id on every call: a turn id the thread holds, with an item that is not held, is refused.- The rules of an instance, as functions for the node libraries:
Queueandstore_at_boundary(stores the queued items in arrival order into the open AI turn, else the input turn that ends the thread (an input turn that shows the AI turn of another agent is not one: a new input turn is made), else one new input turn; skips and marks an item the projected thread holds; marks a mail as read only after its entry is stored; leaves unstored items in the queue),Session(opens, completes and discards objects, and discards every object still open), andresult,failedandcancelled, which close the input end and discard the open objects, ignoring the errors of the discards. memory:MemoryHubwithMemoryThreadAccess,MemoryInbox,MemoryAgentPort,MemoryWorkspace,FixedThreadPortandFixedToolsPort, for tests. The hub keeps oneConversationfor each thread and projects it for an agent; an addition is stored whole or not at all; an open object is never in the projection; a send to a stream that has ended is ignored. The hub knows the author name of an agent only when the test gives it (set_authororMemoryAgentPort::with_author); a mail of an agent without a name is a fault.
br-graph-executor 0.1.0
0.1.0 - 2026-10-26
First release: the executor of the graph family. It depends on br-graph-contract only, never on a library of nodes.
Added
startandresume: run a graph ofbr-graph-contractfrom a state and a list of nodes, or from a checkpoint.RunConfig(cancel token, grace period, optional checkpoint sink and observer),Outcome(RunEnded,RunFailed,RunCancelled) andRunFailure. ThecauseofRunFailure::NodeFailedis optional:Somewhen theFailedevent of the node carried one,Nonewhen the executor itself counts aCancelledwithout a cancel request as a failure.Checkpoint: plain serializable data (state, structure, names of the nodes that are not idle),StartError,ResumeError.Checkpoint::state_json()gives the state text andCheckpoint::nodes()the names of the nodes that are not idle; the structure is read through the serialized form.StartError:NoNodes(the list of nodes is empty),UnknownNode(a listed node is not in the graph),DuplicateNode(a node is listed twice).ResumeError: today the node names are checked first (UnknownNode: the checkpoint names a node the graph lacks;DuplicateNode: a node twice;NoNodes: none), then the structure (VersionMismatch: the version differs from the graph;StructureMismatch: the node names or the edges differ), then the state (StateRejected: the state cannot be rebuilt: refused, over the limit, empty, panicking, malformed or of the wrong shape); the intent does not guarantee which fault is returned when there are several, so a checkpoint from another version can returnUnknownNode,DuplicateNodeorNoNodesbeforeVersionMismatch.Malformed(the document cannot be read) comes only fromCheckpoint::from_json_slice;resumenever returns it.CheckpointSink,SinkFutureandSinkRefusal(made bySinkRefusal::new(reason));Observer,Observation,ObservedEventandOutcomeKind.RunFailure,Outcome,OutcomeKind,StartError,ResumeError,ObservedEventandObservationare#[non_exhaustive](match them with a wildcard arm);RunConfigis not.- When the host drops the future of
startorresume(or aborts the task that polls it), every live instance is dropped and the creation, forwarder and sink-call tasks are aborted; an abort takes effect at the next poll of each task, not before the drop returns. The observer task is not aborted: it ends when its queue is empty, so observations queued before the drop can still reachObserver::observe. - The
DebugofCheckpointshows the byte length of the state text, the structure and the node names, never the state text itself; so theDebugofOutcome::RunFailed,Outcome::RunCancelledandRunFailure::SinkRefuseddoes not print the state text of the checkpoint. The reasons and causes print as the node, the fields, the edge or the sink gave them. Comparestate_json()in assertions. RunFailure::SinkRefused { reason, checkpoint }: the failure carries the checkpoint the sink refused (the one handed to the sink, which holds the state of its step). The checkpoint of the outcome stays the most current one and can be newer.RunFailure::CheckpointNotBuilt: the state of a run could not be serialized (error or panic) when a checkpoint was built. The checkpoint of the outcome then holds an empty string as its state (never valid JSON, so everyFieldsrefuses it):resumereturnsStateRejected, never a fabricated state. The failure can be listed twice (once for a step, once for the outcome).- Every
Resultstep that does not end the run builds the checkpoint, whether or not the host gave a sink (unless the executor has seen the token triggered or the run is failing): a state that cannot be serialized fails the run withRunFailure::CheckpointNotBuilteven without a sink. The sink, if any, is then handed that checkpoint; without a sink the host gets checkpoints only from outcomes other thanRunEnded. - A recorded failure is never lost as
RunEnded:RunEndedis returned only when every node is idle and no failure was recorded. Every node idle with a recorded failure givesRunFailedwhen the executor triggered the token, andRunCancelledwith the failure listed when the host did first. Such an outcome can carry a checkpoint that names no node (every node idle with a failure listed, inRunFailedorRunCancelled): resuming it is refused asNoNodes. - A
Resultalready queued when the grace period ends is applied, and its edge evaluated, before the instances are dropped. With a zero grace the instances are dropped at once and a queuedResultis not applied. Checkpointhas one wire form, exact and independent of the serde format:{"state": "<json text>", "structure": ..., "nodes": [...]}. The state is a JSON string that holds the compact JSON text of the state (the text ofState::to_json; the checkpoint of aCheckpointNotBuiltoutcome holds an empty string instead), so it is kept byte for byte: object key order and integers beyond 64 bits survive through every door (Checkpoint::from_json_slice,serde_json::from_str,serde_json::from_value,#[serde(flatten)], internally tagged enums) and with any serde format.Checkpoint::state_jsonreturns that text, andPartialEqcompares it exactly (two states that differ only by key order are different).Checkpointdeserialization refuses unknown fields (at the top level of the checkpoint).Checkpoint::from_json_slicereturnsResumeError::Malformedfor any document it cannot read, also whenstateis not a string.resume: a panic ofFields::validateor of the state deserialization while the state is rebuilt is refused asResumeError::StateRejectedwith the panic text.- A grace period whose deadline cannot be represented, or is too close to the limit of
Instantto be rounded up by the runtime timer (Duration::MAXfor instance), counts as about 30 years: the deadline never falls back to a zero grace and never panics. - A node, creation or edge error whose
Displaypanics does not escape the run: the panic text is the reason of theRunFailure, and the cause is kept. - The payload of a caught panic is read for its text and then dropped under a guard; a payload whose
Droppanics is leaked, so it cannot turn a caught panic into a panic of the run. Observer::observeis synchronous and runs in a task of the host's runtime: it must return quickly. An observer that blocks its thread stalls acurrent_threadruntime, and so the run with it.
Known limits
- A panic of
Cloneof the host'sFieldswhile the final state of aRunEndedis returned is not caught. The state is moved out without a clone when the run holds the only reference to it. - A panic is caught only when unwinding is enabled.
br-flow-nodes 0.1.0
0.1.0 - 2026-10-26
First release: two generic programmatic nodes for the control of a graph,
built on br-graph-contract 0.1.0 only.
Added
Merger: a node of kindprogrammaticand subkindmerger, made from a
name, a ready function and a spent function. It calls the spent function
first, then the ready function, on the state of each input alone; when the
ready function gives true it closes its input end, reads the inputs it had
accepted and sends oneResultwith the empty update. A triggered cancel
token closes the input end and sendsCancelled.Merger::late_input(LateInput)sets the policy;LateInput(Silentby default,Fail,FireAgain): what a merger does
with an input that the spent function calls spent;Failcloses the input
end and sendsRefusedwith a reason.MapReduce: a node of kindprogrammaticand subkindmap_reduce, made
from a name, a map function, an internal node, a limit (NonZeroUsize) and a
reduce function, with an optional internal size limit set by
MapReduce::internal_size_limit(defaultDEFAULT_STATE_LIMIT). It works on the state of the last input it had
accepted, creates the internal instances one at a time in list order (at
mostlimitlive, a fresh UUIDv7 each and a view of the one child cancel token
of the outer instance), applies each
internalResultto its internal state and sends the update of the reduce
function, which receives the final states in map order.MapReduceError: the cause of theFailedof a map-reduce instance, one
named case per fault (invalid initial value, instance not created, instance
id mismatch, input refused, internal failed, internal refused, internal
cancelled while the child token is not triggered, internal vanished, update
rejected), each with the position of the internal state;#[non_exhaustive].- The
DisplayofMapReduceError::InstanceNotCreatedand ofMapReduceError::InternalFailedends with: {cause}, the text of theircause, so the reason of aRunFailurebuilt from the top-levelDisplayshows the text of the internal node; thecausestays thesource. The executor reads only the top-levelDisplay; a host that walkssource()as well prints the cause twice (accepted design). - A panic in the map or reduce function of
MapReduce, or in the ready or spent function ofMerger, ends the instance with no terminal event: the executor readsEventEndError::Vanished, neverFailed. A panic in the work of an internal instance (its own task) givesFailedwithMapReduceError::InternalVanished. A panic is caught only when unwinding is enabled. - A panic in the creation of an internal instance (the call to
createor the poll of its future) givesFailedwithMapReduceError::InstanceNotCreated(the cause is an opaque cause holding the panic text). A panic inState::applyof an internal state (so also in theClone,validateandSerializeof its fields, and also whenvalidatepanics during an apply) givesFailedwithMapReduceError::UpdateRejected; a panic inState::create_with_limitof an initial internal state givesFailedwithMapReduceError::InvalidInitialValue. Each carries the position and the panic text (the message when the payload is a&stror aString, elsepanic without a text message). The other internal instances are dropped as for any fault. The payload of a caught panic is read for its text and then dropped under a guard; a payload whoseDroppanics is leaked and cannot escape. Once the creation of an internal instance has given its result or its panic, a later poll givesPendingand never polls the inner future again. - An internal node that closes its input end before it receives its one input gives
MapReduceError::InputRefused, even if it has already sent a terminal event: the map-reduce node does not read the event before it gives the input. - A triggered cancel token before the first input gives
Cancelledand closes the input end, forMapReduceand forMerger. - A dropped map-reduce instance (either end) triggers the child cancel token and drops its internal instances.
br-graph-contract 0.1.0
0.1.0 - 2026-10-23
First release: the contract of the graph family, with no run logic, no
executor and no node library.
Added
Fields: the conditions on the runner's data (serializable, copyable,
comparable, self-validating, with an update type that has an empty update
and that the fields apply to themselves).Stateholds the fields behind
three checked operations:create,apply(all or nothing, the first
state is untouched) andrebuild, each refused asInvalidState(create,
rebuild) orApplyError(Refusedby the fields, orInvalidState) with a
Reason, under a size limit on the JSON form of the
fields (DEFAULT_STATE_LIMIT, 128 KiB, a positive limit given by the
runner); a refusal for size names the serialized size and the limit.
Inputis a read-only view of a state,Reasonthe text of a refusal and
Causethe opaque cause.Node(name, kind, subkind, async creation of anInstancefrom a
CancelViewand anInstanceId),PROGRAMMATIC,EdgeandTarget
(Target::End,Target::END;Target::node("END")isTarget::End).Instance, built withInstance::channel: the executor'sInputEnd
(give, never waits, refused asInputClosedafter the close) and
EventEnd(recv, which givesResult<Event, EventEndError>:
Finishedafter the terminal event was delivered or the instance was
dropped,Vanishedwhen theEventSenderwas dropped withoutfinish),
the node'sInputReader(next,close,drain) andEventSender
(finishconsumes the sender and sends at most one terminalEvent;
droppedgives the named futureDropped).EventisResult,Failed,
RefusedorCancelled.InstanceId: a UUIDv7 given by the creator, refused asInvalidInstanceId
otherwise.CancelToken,CancelHandle,CancelView: a cooperative, runtime-free
signal with child tokens that keep their parent alive;CancelView::triggered
gives the named futureTriggered.GraphBuilderandGraph: nodes, edges, entries and a version validated
as a whole (BuildError, one named case per refusal), the checked edge
evaluation (EdgeError) andGraph::structure.Structure: the graph as plain, sorted, serializable data, rebuilt from
JSON objects (a positional array is refused) through the same build rules
(StructureError).
br-llm-graph 0.6.0
0.6.0 - 2026-10-16
Breaking: a built graph describes itself as data (Graph::describe), so a
host can compare the graph it built with the graph it meant to build; edges
may declare their targets, which the build and the run then hold them to.
Added
Graph::describegives aGraphDescription: the graph's entry, its
signature, its schema (state and configuration keys with their kinds,
and the keys that have a default, by kind; an agent's private keys are
not part of it) and its nodes in
registration order, each with its id, its kind, its realizes labels, its
edge's declared targets in the order given (or undeclared) and the
fields of its kind, nested graphs included. It runs nothing, calls no
client, reads no state and no configuration; two builds from the same
parts give equal descriptions, in the same order.- A description writes no state value, default, prompt, client, author
name or secret; it does write fixed bounds (a fixedLimitorCount,
the generator-critic'sfinal_generation). The schema is{"state", "config", "defaults"?},defaultsmapping each state key that has a default to
its kind, never the default's value, whatever its kind (a string, a
list, a conversation). An author (of a model-calling node, of an LLM
call's conversation request) is written by its id,{"id"}, never its
name, which is rendered into what a model reads. - Its JSON,
br-llm-graph-description/1(DESCRIPTION_VERSION):
{"description_version", "entry", "signature", "schema", "nodes"}; a
node{"id", "kind", "realizes"?, "join"?, "edge", ...}; an edge
{"targets": [{"node"} | {"end"}]}or{"undeclared": true}; a nested
graph without its version. Written only:GraphDescriptionand
NodeDescriptionimplementSerializeand notDeserialize(pinned by
compile_faildoctests). - The kinds:
nodeandjoin(a host's node, by default),call,
react_agentandgenerator_critic(a call of an agent's graph, with
everything a call holds),map,llm_call,model_call(an agent's
model-calling step and the critic's call) andtools(a tool node). A
node of another kind registered as a join carries"join": true. - What each kind holds:
- a call: its inputs, configuration keys and outputs with their sources
and targets, its end label, its failure policy with its captures, and
the description of its graph; a fixed value is written as its kind
({"const": kind}), never the value; - a map: its list and item keys, its concurrency limit, its item-failure
policy and its body, a node without id or edge; - an LLM call: how its model and system are given, its request and its
output; - a model-calling node: its author's id, how its model, system and history
are given (fixed,ready_madeorfunction, never the value), its
tool names, its enabled key, its tool concurrency and round limit. The
critic's node adds the critique budget,final_generation, the verdict
retries, how the review request is given and whether the verdict shape
is the default; the generator-critic'scheckholds the revision limit
and how the rule check and the empty-answer critique are given; - a tool node: the names of its tools.
ALimitor aCountis{"fixed": n}or{"config": key}.
- a call: its inputs, configuration keys and outputs with their sources
Node::describe, a provided method: what a node says of itself
(NodeDescription), by defaultnodeorjoinwith nothing nested. A
host's node may give its own kind label (NodeDescription::labelled; a
label that is not an identifier or that names a library kind is refused,
Identifierwith the fieldkind_label) and a nested description
(NodeDescription::nesting). A node that wraps another forwards it; the
generator-critic'spreparedoes.- Realizes labels:
Node::realizing(labels)wraps any node in
Realizing<N>, which runs, checks and describes as its node does and
adds the labels, free text in the order given; the library never reads
them.NodeDescription::realizingadds labels to a description. Edge::declared, a provided method: the targets an edge may give, known
without running it;Noneby default (undeclared).Alwaysdeclares its
target;FnEdge::targetsdeclares an edge built from a function, its list
kept as given. The graph asks for the declared targets once, at build,
and keeps them in the order given, a repeated target once at its first
place: the run checks that copy and the description writes it.GraphError::UndeclaredTarget { node, target }: a declared edge that
returns a target it did not declare fails the run, the run's error
itself, before any target runs. Per edge, in registration order: the
edge's error,EmptyEdge,UndeclaredTarget, thenUnknownNode.GraphError::EdgeToUnknownNode { node, target }: the build refuses a
declared node target the graph does not hold, checked last, after the
node checks; undeclared targets and end labels are not checked.- The library's own graphs declare every edge's targets: the ReAct agent
(enter, the model call with its tool nodes, its round-limit node and
the answer, the tool nodes, the round-limit node), the generator-critic
(route,check,critique,finishwith its labelsvalidatedand
exhausted,prepare). Targetis displayed (node {id},end {label}) and serialized
({"node": id},{"end": label}). The two new refusals read alike: "the
edge of node a declares node ghost, which is not in the graph", "the edge
of node a returned node ghost, which it does not declare".- The public module
describeholdsGraphDescription,NodeDescription,
RealizingandDESCRIPTION_VERSION, all re-exported at the root.
GraphDescriptionderivesDebug,CloneandPartialEq;
NodeDescriptionderivesDebug,Clone,PartialEqandDefault(the
description of a node without a kind of its own).Realizing::labels
gives a wrapped node's labels,Realizing::nodethe node it carries. The
rustdoc ofGraphDescriptiontables the fields of each kind. - The example
describeprints the description of a graph with declared
edges, a call, a map and an agent.
Changed
- Breaking: an
Always(or a declaredFnEdge) naming a node the
graph does not hold is refused at build withEdgeToUnknownNode; 0.5
built it and failed the run only when the edge ran (UnknownNode). - Breaking:
GraphErrorgainsUndeclaredTargetand
EdgeToUnknownNode; amatchon it without a wildcard arm no longer
compiles. - Breaking:
Edgegains the provided methoddeclaredandNodethe
provided methodsdescribeandrealizing: a host type that implements
them and also has inherent or trait methods of these names may need to
disambiguate its calls. GraphBuilder::builddocuments and applies the new refusal order:
EdgeToUnknownNodeafterInvalidNode.