Changelog¶
All notable changes to AgentFlow are documented here.
0.12.0¶
Added¶
-
parse_json_response()-- reads the JSON value out of a model response that was supposed to be JSON and very nearly is. A model told to "return a JSON array and nothing else" complies almost every time; the remainder is not random noise but a small set of recognisable shapes, and a caller doing plainjson.loadsfails on all of them:- the value wrapped in a markdown fence, despite being asked not to
- a sentence of preamble before it, or a summary after it
- a self-correction -- a malformed first attempt, a line of prose noticing the mistake, and then the correct value
The last of those is what prompted this. Captured verbatim from a live extraction run against a real datasheet:
["Figure 13-1 shows a typical application circuit...","page":8]
Wait, must output JSON array only.
[{"quote":"Figure 13-1 shows a typical application circuit...","page":8}]
The correct answer is right there and json.loads cannot reach it. Where several candidates
parse, the last one wins, because that is the direction a self-correcting model moves in --
it writes forwards and does not revise what it has already written.
An optional expect= type (typically list) skips candidates of the wrong type, so a schema
example in a model's preamble cannot stand in for the array that follows it.
Invalid JSON is never repaired. A response with a brace missing raises rather than being patched into something plausible; recovering from that belongs to the caller, which can retry the call. Relatedly, a truncated array fails rather than returning the complete objects that survived inside it -- returning those would hand back a plausible short result with no sign that anything was lost, which for extracted citations is the worst available outcome.
JSONResponseError, raised by the above, carrying the offending response asresponse_text.json.JSONDecodeErroron its own reports something likeExpecting ',' delimiter: line 1 column 9-- which tells you a response was malformed and nothing whatsoever about how. These failures are intermittent, so there is frequently no second chance to look.
0.11.1¶
Fixed¶
temperatureis no longer sent to Anthropic SDKs that removed it. Theanthropic1.x line droppedtemperaturefrommessages.create. Sending it raises a plainTypeErrorclient-side, before any network call, which the existingBadRequestErrorretry cannot catch -- so every chat with a non-1.0 temperature failed outright againstanthropic>=1.0. SinceAgentConfig.temperaturedefaults to0.7, that was most calls.
The provider now checks whether the installed SDK's messages.create accepts temperature and
omits it when it does not. Resolved by introspection rather than a version comparison: the
parameter's presence is what actually matters, and a version check would need updating for every
future SDK release -- the same trap the model-name suffix convention fell into. When the callable
cannot be introspected it is treated as accepting temperature, so an unreadable SDK behaves
exactly as it did before rather than silently dropping a caller's parameter.
Found while diagnosing a downstream suite that passed only because it ran against an older SDK on
a different interpreter -- the failure was invisible on anthropic 0.121.0 and total on 1.2.0.
0.11.0¶
Added¶
paramsonLLMProvider.chat()-- a dict forwarded verbatim to the underlying vendor SDK call, implemented on all three real providers plusMockLLMProvider. Vendors add parameters faster than any framework can name them, and predicting which model supports which is not a problem AgentFlow can win. The caller chooses and owns correctness: a parameter a model ignores is the caller's business, not an AgentFlow error.
-
AgentConfig.params-- the same dict, declarable per agent in*.prompt.mdfront matter and forwarded byAgentExecutor. Declared at the agent level because that is where the model is chosen, so a parameter and the model it applies to stay together. -
A reserved-key refusal.
paramsmay not contain a key AgentFlow itself sets --model,messages,system,toolsand friends. Those change what is asked rather than how, and a passthrough that could silently redirect a call to a different model is worse than no passthrough.ValueErrornames every offending key at once rather than only the first. Bare**kwargswas considered and rejected for exactly this: it makes a misspelled known argument stop being an error and quietly become a vendor argument.
Removed¶
- BREAKING: the
-low/-medium/-highmodel-name suffix convention. Anthropic and Gemini reasoning effort was previously selected by decorating the model name and parsing it back off withrsplit("-", 1). That is gone. It silently truncated any legitimate model whose real name ended in one of those three words and misread the tail as an effort level, with nothing in the response to say so; it was invisible to callers; and it had no equivalent onopenai_compat, so the same request meant different things depending on which provider served it.
Migration: restore the model name to its real value and pass the effort through params.
# before
AgentConfig(name="a", model="claude-sonnet-5-high")
# after
AgentConfig(
name="a",
model="claude-sonnet-5",
params={"thinking": {"type": "adaptive"}, "output_config": {"effort": "high"}},
)
Anthropic's rule that temperature must be absent whenever thinking is active is preserved, and
now keys off params rather than off a parsed model name.
0.10.0¶
Added¶
NodeOutput.metadata["tool_calls"]onAgentExecutor.run(). Every tool call made during a run — across all tool-use rounds, on both the normal-completion and exhausted-tool-rounds return paths — is now included as[{"name": str, "input": dict, "result": str, "is_error": bool}, ...]. Previously the only way to observe an agent's tool calls was subscribing anEventBustoTOOL_CALLED/TOOL_RESULTbefore callingrun(); that's still supported, but callers who just want "what tools did this run use and what did they return" no longer need to set up event plumbing for it.onError: aborton workflow nodes. By default, a node that raises still doesn't abort its workflow — the failure is captured into that node'sNodeOutput(metadata={"error": True}) and the DAG keeps running, same as before. SetonError: aborton a specific node to opt out of that: its exception now propagates out ofWorkflowExecutor.run()asWorkflowNodeError(carrying.node_idand.original) instead of being swallowed. Applies uniformly acrosssync,parallel,async, andforeachnodes.
Fixed¶
mode: asyncnow actually does what the docs always said it did. Nodes markedmode: asyncwere accepted by the workflow schema but silently executed identically tosync— onlyparallelwas ever branched on inWorkflowExecutor. Async nodes are now genuinely fire-and-forget: dispatched without the wave loop waiting on them, so a sibling with no dependency on the async node doesn't get blocked behind it.WorkflowExecutor.run()still waits for and captures every async node's result (or error) before returning, so nothing is silently dropped even when nothing in the DAG depends on it.WorkflowNode.modeand the newonErrorfield are now validated. A typo likemode: asnycused to be silently accepted and treated assync; it's now aValueErrorat config-parse time. Same for an unrecognizedonErrorvalue.RuleEvaluatorconditions fail loudly on unrecognized syntax instead of silently evaluating false. A condition matching none of the supported patterns — e.g. a typo, or (previously) a double-quoted string literal — used to returnFalseand let the caller fall through to the next rule or the router's fallback target, indistinguishable from a legitimate non-match. It now raisesRuleConditionError. Also: string literals accept double quotes as well as single quotes (area == "schematic", not justarea == 'schematic') — previously only single quotes worked for==,!=, and'substring' in field;field in [...]already accepted either.
0.9.0¶
Changed¶
- Relicensed from MIT to Apache License 2.0. Releases 0.8.2 and earlier remain available under the MIT License; those rights are not revoked by this change. See LICENSE and NOTICE.
0.8.2¶
Fixed¶
AnthropicProvidersupport for Claude's newest reasoning model lines (first hit:claude-sonnet-5). These models reject an explicittemperatureoutright ("temperatureis deprecated for this model") and use a newer adaptive-thinking + effort interface (thinking: {"type": "adaptive"}+output_config: {"effort": ...}) instead of the older, fixedbudget_tokensone.chat()now: (1) recognizes a-low/-medium/-highmodel-name suffix to opt into adaptive thinking at that effort level, mirroringGoogleGenAIProvider's identical convention for Gemini thinking models rather than inventing a second, different one; (2) retries once withouttemperatureon the specific "temperature is deprecated" 400, rather than hardcoding a model-name allowlist that would need updating for every future release.- Stale default models.
AnthropicProvider's default is nowclaude-sonnet-5(wasclaude-sonnet-4-6).GoogleGenAIProvider's default is nowgemini-flash-latest(wasgemini-2.5-flash-preview, which 404s — it no longer exists on Google's real model list).gemini-flash-latestis Google's own stable rolling alias, chosen over a dated snapshot so this default doesn't itself go stale the same way.
0.7.4¶
Added¶
HANDLER_RESULTevent from code handler nodes. Handler nodes (registered Python functions) now emit aHANDLER_RESULTevent after execution, carrying the fullNodeOutput(text, artifacts, metadata). Observers — such as asset collectors — can react to handler outputs the same way they react toTOOL_RESULTevents from agent nodes, without parsing text.
0.7.3¶
Added¶
raw_resultinTOOL_RESULTfor local tools.LocalToolDispatchernow parses JSON results and setslast_raw_tool_result(the sameContextVarused byHTTPToolDispatcher).TOOL_RESULTevents for locally dispatched tools now includeraw_resultwith the structured dict output, enabling asset collectors to capture document URLs and other structured data from local tools.
0.7.2¶
Added¶
- Enriched
TOOL_RESULTevents.HTTPToolDispatcherstashes the pre-formatted tool result dict in an asyncio-safeContextVar.AgentExecutorreads it and includesinput,result(formatted string), andraw_result(raw dict) in everyTOOL_RESULTevent. Downstream consumers can now capture structured tool output at call time without regex-parsing the agent's final response.
0.7.1¶
Fixed¶
- Langfuse SDK compatibility.
LangfuseEventHandlernow wrapsresource_attributesinitialization in atry/except TypeError. Langfuse SDK 4.0.x does not support theresource_attributesparameter; older SDK versions skip it gracefully rather than raising at startup.
0.7.0¶
Added¶
- Langfuse session and trace context. New
set_trace_context()method onLangfuseEventHandlerlets callers inject per-request conversation context —session_id,trace_name,user_id,tags, andmetadata— before each workflow execution. Context is consumed once whenWORKFLOW_STARTEDfires, then cleared. resource_attributesparameter.LangfuseEventHandler.__init__now acceptsresource_attributes: dict[str, str]for attaching static service metadata (e.g.service.name,service.version) to the Langfuse client instance.DOMAIN_ROUTEDspan.LangfuseEventHandlernow handles theDOMAIN_ROUTEDevent, recording the routing decision as a child span on the root trace withdomain,target,confidence, androutermetadata.
0.6.0¶
Added¶
- Code handler nodes. Workflow nodes can now specify
handler: <name>instead ofagent: <name>. Handlers are registered Python async functions (async def fn(message: str, prior_outputs: dict) -> NodeOutput) passed toWorkflowExecutor(handlers={...}). Use handler nodes for deterministic processing steps that don't need an LLM call.
async def normalize_text(message: str, prior_outputs: dict) -> NodeOutput:
return NodeOutput(node_id="transform", agent_id="normalize_text", text=message.lower())
executor = WorkflowExecutor(
config=wf_config,
runner_factory=runner_factory,
handlers={"normalize_text": normalize_text},
)
- Foreach iteration. Workflow nodes can specify
foreach: <dotted-ref>pointing to a list artifact from a prior node. The node body executes once per item; each iteration receivesloop_item,loop_index,loop_total, andloop_prior_resultsinjected into the message. Results are collected intoartifacts["results"]on the synthetic__loop__output.
nodes:
- id: extract
handler: extract_items
next: [process]
- id: process
agent: item_processor
foreach: "extract.artifacts.items"
Handler nodes and foreach can be combined: a handler node with foreach iterates the handler function over the list.
HANDLER_RESULTevent constant. Importable fromagentflow.
0.5.2¶
Changed¶
- Named inputs deliver labeled sections to agents. When a workflow node
defines
inputswith keys other thanmessage, each key is now resolved in YAML-definition order and delivered as a labeled[key]\nvaluesection. Previously, non-messagekeys were processed through_predecessors()and concatenated without labels in non-deterministic (set iteration) order.
Before (0.5.1 behavior — unlabeled, unordered):
After (0.5.2 behavior — labeled, definition order):
Migration: Agent system prompts that receive named inputs should be
updated to reference the labeled sections by key name. The message key
pattern is unchanged.
Fixed¶
NodeRunner._predecessors()renamed to_input_node_ids()with corrected docstring. The method is used only for scratchpad context filtering; it is no longer involved in message resolution.
Added¶
- Four new tests covering named inputs: labeled section format, definition order preservation, missing upstream node handling, and end-to-end workflow integration.
0.5.1¶
Fixed¶
- Release workflow: no longer fails if a GitHub release already exists for the current tag.
0.5.0 (alpha)¶
Breaking Changes¶
- VectorMemory is now embedding-agnostic. The constructor requires
embed_fn(an asyncstr -> list[float]callable) andembedding_diminstead of using a hardcoded embedding model. This decouples VectorMemory from any specific embedding provider.
Changed¶
- Migrated embedding from deprecated
text-embedding-004togemini-embedding-001in examples and tests.
0.4.0 (alpha)¶
Added¶
- Hierarchical domain routing. New
DomainRouterclass implements two-tier routing: a top-level router classifies messages into domains, then per-domain routers pick specific agents or workflows. - New
DomainConfigschema for*.domain.mdfiles. ConfigLoadernow scanscontext/domains/for domain definitions.DOMAIN_ROUTEDevent constant for domain routing telemetry.RoutingResultnow includes adomainfield.
0.3.3¶
Fixed¶
- Subdirectory context loading:
ConfigLoadernow correctly loads*.context.mdfiles from arbitrary subdirectories (not justshared/).
0.3.0¶
Added¶
- Context profiles (
*.context.mdwithtype: profile) for conditional context loading. ContextProfileandConditionalIncludeschemas.ConfigLoader.get_profile()andConfigLoader.is_profile()methods.- Shared context files loaded from all subdirectories, not just
agents/.
0.2.0¶
Added¶
WorkflowExecutorwith DAG-based execution.NodeRunnerfor per-node agent execution.Scratchpadfor per-node working memory and summaries.ArtifactStorefor named artifact storage.MultiUserHistoryandHistoryPersistencefor multi-user session support.FileMemoryandVectorMemorybackends.MemoryManagerfor coordinating memory operations.ComplexityClassifier,DAGExecutor,Plan,PlanSteporchestration primitives.LangfuseEventHandlerfor Langfuse telemetry integration.GoogleGenAIProviderfor Google Gemini models.
0.1.0¶
Added¶
- Initial release.
ConfigLoaderwith.prompt.md,.workflow.md,.context.mdparsing.RouterEnginewith YAML rules and LLM fallback.RuleEvaluatorfor Python expression-based routing rules.AgentExecutorwith tool loop support.ContextAssemblerandPromptTemplate.EventBuspub/sub event system.ToolRegistry,LocalToolDispatcher,HTTPToolDispatcher.AnthropicProvider,OpenAICompatProvider,MockLLMProvider.FileSystemStorage,InMemoryStorage,S3Storagebackends.SessionManagerandSession.- Core types:
Message,AgentResponse,ToolCall,ToolResult,NodeOutput. - Protocols:
LLMProvider,StorageBackend,ToolDispatcher,MemoryStore,EventHandler.