The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Unreleased
0.5.0 - 2026-11-11
Added
- Agent primitive and
agentstep type (#120). An agent is defined once, in its own YAML file or inline: model, system prompt, tools, memory, loop policy,output_schemaand constraints (max_cost_usd,max_latency_ms,forbidden_patterns). Anagentstep runs one turn of it, and an agent can offer another as a tool.Agentruns a turn from Python,agentloom agent inspectprints a definition, and turns get their own span attributes and metrics.examples/41_agent.yamlruns offline. - Inter-agent event log and
agentloom verify-determinism(#135).--recordkeeps the run’s events in the recording (tasks, tool calls and returns, replies, state transitions), in causal order;verify-determinismreplays it strictly--runstimes and exits 1 with a diff when the status, outputs, final state or event log differ. - Deterministic faults in mock recordings (#123). An entry can declare an
http_error,timeout,connection_resetorstream_truncate, whichMockProviderraises as the real adapters do, so retries, the circuit breaker and fallback take the real path. Faults are counted inagentloom_mock_faults_total;examples/40_fault_replay.yamlrecovers from a 429 and a 503. - Replay up to a cut point and run live after it (#165).
agentloom replay --live-from <step>and--force-live <step>serve the steps before the cut from the recording and run the rest with the workflow’s providers, reporting the source of every call. - Record the live part of a cut replay (#178).
--record <file>, with--live-fromor--force-live, saves a new recording that replays strictly on its own;--record-toolsadds the tool results. - Response metadata in recordings (#166).
ProviderResponse.meta(ResponseMeta) holds the sampling parameters sent, the model fingerprint or version, the request id, the rate-limit headers and when the response arrived. Recordings keep it outside the request hash, so old and new recordings replay alike. - Tool results in recordings (#164).
--record-toolsstores each tool call of atools:loop, andagentloom replay --tools {live,replay,verify}runs the tools, serves the recorded results, or runs them and compares. - Generic OpenAI-compatible endpoint provider (#48).
provider: generic_openaiwithconfig.base_urland an optionalconfig.api_key_envtalks to vLLM, Text Generation Inference, LM Studio or any OpenAI-compatible server; no other provider is registered for that run. - Public contracts package (#159).
agentloom.contracts.stableis covered by semver (Message,ToolCall,TokenUsage,ProviderResponse, theWorkflowHandleprotocol and more) andagentloom.contracts.experimentalholds the OTel names; importing them loads no runtime. - Conversation history with token-budget trimming (#119).
llm_calltakesconversation: state.<key>, appends each turn and trims totoken_budgetwithdrop_oldest,drop_pairsorsummarize_oldest;speaker:lets several agents share one conversation. Examples 38 and 39. - Embeddings across providers (#118). A
type: embedstep andBaseProvider.embed()on OpenAI, Google and Ollama, withdimensions, fallback and cost as for a model call; Anthropic is skipped.examples/37_embeddings.yaml. - Structured output across providers (#117).
response_schemaonllm_call(pydantic,json_schemaorjson_object) uses each provider’s native mode, and instructions plus validation on Anthropic; an invalid reply is asked for again within the step’s retries, and the parsed value goes to the state.jsonschemabecomes a core dependency.examples/36_structured_output.yaml. - Native tool calling across providers (#116).
tools,tool_choiceandmax_tool_iterationsonllm_callrun the model’s tool calls through the tool registry, in parallel, until it replies; the sandbox, budget and retries apply.examples/35_tool_calling.yaml. - Run history (#77). Every run writes a JSON record (versions, workflow hash, providers and models, status, cost, tokens, duration) to
./agentloom_runs/orAGENTLOOM_RUNS_DIR, andagentloom historylists and filters them. - Quality annotations (#59).
WorkflowResult.annotate(target, quality_score=..., source=...)records a score after a run and emits it as aquality:<target>span tied to the run. - OTel GenAI semantic conventions (#125). Span, attribute and metric names live in one schema and follow the OTel GenAI registry:
agentloom_tokens_totalbecomesgen_ai.client.token.usage,agentloom_provider_latency_secondsbecomesgen_ai.client.operation.durationandagentloom_time_to_first_token_secondsbecomesgen_ai.client.operation.time_to_first_chunk, with no aliases. Provider calls get child spans,llm_callspans carry prompt metadata (the content only withcapture_prompts: true), and every span carriesworkflow.run_id. Theon_provider_callobserver hook and thetokensargument ofon_step_endare removed. - Engine collaborator protocols and shared retry helpers (#112).
agentloom.core.protocolsdescribes the engine’s collaborators, andagentloom.resilienceexportscompute_backoff,is_retryable_exceptionandDEFAULT_RETRYABLE_STATUS_CODES. retry.retryable_status_codes(#112), by default[429, 500, 502, 503, 504]: a step stops retrying on any other HTTP status; errors without one are still retried.- Reasoning tokens across providers (#127).
TokenUsage.reasoning_tokens,ProviderResponse.reasoning_contentand athinking:block on steps (enabled,budget_tokens,level,capture_reasoning), translated for each provider. OpenAI and Gemini report reasoning tokens apart; Anthropic and Ollama count them in the output tokens. - Weekly pricing refresh (#180).
scripts/update_pricing.pyupdates the OpenAI, Anthropic and Gemini prices from LiteLLM’s public list, and a workflow opens a pull request every Monday when they change. Prices are never fetched at run time. - Source distribution and package checks (#190). A release publishes an sdist next to the wheel,
scripts/check_dist.shinstalls both in a clean environment before publishing, and the release notes come from this changelog. - Python 3.14 (#192), with CI on macOS, Windows and the lowest dependency versions allowed.
- Config file for the CLI (#212).
--config FILE,AGENTLOOM_CONFIGor./agentloom.yamllists the providers with theirmodels,priority,base_urlandis_fallback; keys still come from the environment, and anapi_keyin the file is refused. agentloom replayandverify-determinismtake--providerand--model, asagentloom rundoes.persistence.existingClaimin the Helm chart: a volume of yours keeps the run history and checkpoints, so a run paused in one pod resumes in another.- Grafana: a row for tools, agents, conversations and embeddings, and a panel for mock faults.
Changed
DAG.topological_sortruns in O((V+E) log V) instead of O(V² log V), in the same order (#112).WorkflowEnginedrops two unreachable exception handlers; behaviour is unchanged (#112).- Pricing table refreshed (#180): 41 models added, among them Claude Opus 5.5, Sonnet 5.5 and Haiku 5.5, GPT-6 and Gemini 4 Argon, and
gpt-5.6-sol,-terraand-lunacorrected to OpenAI’s prices, checked against the vendors’ pages on 2026-11-07. - Comments and docstrings follow one rule, written in
AGENTS.md, and theno-history-commentspre-commit hook rejects history markers (#213). make docker-build-obsis nowmake docker-build-lite(#213); theBUILD_OBSERVABILITYbuild arg, which did nothing, and two unreferenced scripts are gone.- The documentation moved to https://cchinchilla.dev/agentloom/, which follows the version PyPI serves. MkDocs and the old GitHub Pages site are gone, and
scripts/check_docs.pychecks the pages in CI andmake check. - The Kustomize placeholder workflow uses the mock provider, so the Job runs in every overlay without keys or Ollama.
- The bundled OTel Collector keeps a series for 24 hours instead of 30 minutes, so the dashboard’s totals cover about a day; it holds more series in memory.
- The
productionoverlay pins the current release, and the version-linearity check fails when it falls behind. - Grafana: a
Provider (calls)selector for the panels labelled with AgentLoom’s provider names.
Breaking changes
- Recordings use format v2, with
_version: 2, and a request hash that also covers the model, temperature,max_tokensand extra arguments (#107). v1 recordings keyed bystep_idstill replay; those keyed by the old hash must be re-recorded. Streams are recorded likecomplete()calls. - The implicit Ollama fallback is opt-in: set
AGENTLOOM_OLLAMA_FALLBACK=1, listollamaunder the config file’sproviders:, or make it the workflow’s provider. The Helm chart sets the variable whenollama.enabled. agentloom replayis strict by default: a request that misses the recording, or a step whose prompt, model or tools changed since it was recorded, raisesRecordingMismatchError, and--allow-default-fallbackrestores the lenient match. Recordings are validated when they load, and those below v2 refused.run --provider mockstays lenient.- Unknown keys in a step or under
config:are refused at load,config.responses:among them (useresponses_file). - The default Docker image ships the
[observability]extra and is about 30 MB larger;--target production-litebuilds the image without it. - The Docker images have no pip (#219). An image built
FROMthem that installs packages must bring its own installer, such asuv(COPY --from=ghcr.io/astral-sh/uv). - A step missing what its type needs now fails when the workflow loads instead of when it runs: a
routerwith neitherconditionsnordefault, atoolwithouttool_name, asubworkflowwith neitherworkflow_pathnorworkflow_inline, anembedwithoutinputsand anllm_callwith neitherpromptnorconversation. - Unknown keys are refused at load in
retry,sandbox,notify,thinking, a router’sconditions,tool_choice,response_schemaandstate_schema, so a misspelled setting no longer disappears silently. additionalProperties: trueis refused at every level of ajson_schemaresponse schema, not only at its root; the error names where.- Two steps of one layer on one
conversationare refused at load, since parallel writes lose turns; branches of one router are exempt because only one of them runs. speakermust match^[A-Za-z0-9_-]{1,64}$, the names OpenAI accepts.- A conversation’s
summarycounts intoken_countand intoken_budget, so trimming starts earlier once a summary exists. WorkflowBuilder.build()raisesValidationErrorfor missing dependencies, router targets and subworkflow files, for cycles and for steps of one layer sharing a conversation.WorkflowParser.from_yamlreports a missing file asWorkflow file not foundwhen the argument looks like a path (aPath, or one line without a:) instead of failing withmust be a mapping.- An attachment file is readable under
allowed_pathsas well asreadable_pathswhen both are set, as for the file tools. - An attachment whose
media_typecontradicts itstypeis refused withAttachmentResolutionError. http_requestrefuses a method outsideGET,POST,PUTandDELETEbefore connecting.file_writereportswrittenin bytes, not characters.- Template rendering in
tool_argsreaches strings inside nested mappings and lists, with the same rules as top-level values.{value: ..., template: false}is the escape hatch only with exactly those two keys, so a payload that merely holds them passes through. - Unknown keys in a step’s
toolsandattachmentsentries are refused at load.ToolDefinitionandAttachmentthemselves keep ignoring them for Python callers. - A subworkflow’s
inputis rendered with the parent’s state, astool_argsare:"{state.topic}"is a template,"state.items"hands over the value, and{value: ..., template: false}keeps a string as written. Values from redacted keys stay redacted in the child. - A child’s own
state:seeds its state in the default mode too, as defaults under the parent’s values. - The first retry waits
backoff_baseseconds instead of always 1: the waits arebackoff_base ** (attempt + 1), about 2, 4 and 8 seconds by default. tool_choice: requiredand{name: ...}force the first model call of a tool loop only; the later calls useauto, so the model can answer.- The tools asked for by the reply that reaches
max_tool_iterationsno longer run, since no call would read their results. Withmax_tool_iterations: 1no tool runs. - A named
tool_choicethat ends the loop without an answer reportsmax_tool_iterations_no_answer, asrequireddoes. - A streamed reply that fails structured-output validation fails the step without a retry, as a reply that doesn’t stream does.
- A failed router skips its branches under
on_step_failure: continuetoo. - The steps of a layer all read the state as the layer started, so a step held back by
max_concurrent_stepsno longer sees what its siblings wrote. A step still reads what it wrote itself, throughupdatetoo. drop_pairsstops and lets the request go over the budget when the system messages and the summary reach it, asdrop_oldestdoes, instead of emptying the conversation.config.providerand--providernow pick the primary provider: it goes first for any model it can serve, whatever itspriority, and models listed under another provider included. The other providers that serve the name follow as fallbacks.- A workflow without
modeltakes its provider’s default (gpt-4o-mini,claude-haiku-4-5-20251001,gemini-2.5-flash), and--providerbrings the new provider’s; onollamaandgeneric_openai, which have none, a step without a model fails at load. - Models listed in a provider’s
models, and the built-in names such asgpt-4o-mini, now fall back tois_fallbackproviders when the provider that lists them fails. - OpenAI reasoning models (o-series and GPT-5,
gpt-5-chatapart) sendmax_completion_tokensinstead ofmax_tokens, and notemperature. - A step on a Claude model with
thinkingenabled and nobudget_tokens, or whosemax_tokensisn’t abovethinking.budget_tokens, fails at load. - A
responses_fileor--mock-responsesfile that doesn’t exist is an error instead of a run on placeholders (exit2for--mock-responses,1forprovider: mock);examples/32_yaml_mock.yamlneeds the recording example 31 makes. - Strict replay of a list of turns without hashes raises
RecordingMismatchErroronce the list runs out, instead of repeating its last turn. Lenient replay still repeats it. verify-determinismexits with1when a replay doesn’t end insuccess;--allow-failedaccepts a stable failure.- Misuse exits with
2in every command: conflicting options inreplayandresume, a--live-fromor--force-livestep the workflow doesn’t have, andresumeof a subworkflow’s own checkpoint, which names the run to resume. run --recordon an existing file replaces it. A cut (replay --record) still merges into the file.- A subworkflow’s steps are recorded under their path (
sub/step), LLM entries and_tools. Recordings holding them under the bare id still replay. - Recordings mask the keys of the run’s redaction policy in tool call arguments and in tool results that are JSON objects or arrays.
request_hashstill covers the real request, so a later turn that holds a masked value is served in order, unchecked. - Embedding calls report
gen_ai.operation.nameembeddings: their spans are namedembeddings <model>instead ofchat <model>, and thechatseries ofgen_ai.client.operation.durationandgen_ai.client.token.usagelose those samples. - Failed runs, steps and provider attempts set the span status to
ERROR, so Jaeger’s error filter and status queries now find them. A run counts as failed when it fails, times out or breaks its budget, not when it pauses. gen_ai.request.modelon a step span is the model the step asked for, not the id the provider answered with.- A subworkflow no longer counts as a run of its own:
agentloom_workflow_runs_total,agentloom_workflow_duration_secondsandagentloom_cost_usd_totalrecord only the outermost run, so a subworkflow’s cost is no longer counted twice. - OTLP metrics carry
service.nameagentloominstead ofunknown_service. - OTLP histograms use the explicit buckets of the Prometheus exporter, per instrument, so their
leseries and percentiles change;agentloom_workflow_duration_secondsandgen_ai.client.token.usagekeep the SDK’s defaults. - Removed configuration that nothing read: the
AGENTLOOM_OBSERVABILITY,AGENTLOOM_MAX_CONCURRENT_STEPS,AGENTLOOM_BUDGET_LIMIT,AGENTLOOM_CHECKPOINT,AGENTLOOM_CHECKPOINT_DIRandAGENTLOOM_DEFAULT_PROVIDERvariables, their config-file keys, thelog_levelandlog_formatkeys, andtimeoutandmax_retrieson a provider entry. A config file that sets one fails at load. - Removed
retry_with_policyandRetryPolicyfromagentloom.resilience; the engine used neither. --jsonprints only JSON on stdout:Running workflow:,Run ID:andResuming workflowlines go to stderr with it.runs --jsonandhistory --jsonprint[]when they find nothing, instead of a sentence.--statewithout=exits 2, not 1.history --sinceand--untilread a time without an offset as UTC, not local time.
Fixed
- Runs and resume.
agentloom resumereports a bad run id or checkpoint in one line and prints the run’s own id at a later gate (#215). Ctrl-C, or SIGTERM as Kubernetes sends it, saves acancelledcheckpoint under--checkpoint, whichresumecontinues. The workflow’stimeoutis enforced and ends the runtimeoutwith a checkpoint (#120). A resumed run goes on from the budget it had spent,resumetakes--stateand--budget, a subworkflow resumed at its child’s gate continues from the child’s checkpoint, and resuming past a finished router keeps its untaken branch skipped. A run no longer writes into its workflow’sstate:(#159),resumerunsprovider: mockworkflows against the mock,runwithout--checkpointprints noresumecommands, and a checkpoint from a newer AgentLoom is refused with a migration hint. - Approval gates and the callback server.
timeout_secondsandon_timeoutare enforced (#210): an expired gate takeson_timeout, andcallback-serverresumes expired gates on its own and answers202or409to late and repeated decisions.notify.callback_base_urladdsapprove_urlandreject_urlto the default webhook body, for gates inside subworkflows too. The callback server answers malformed bodies and traversal run ids with400, takes--token, resumesprovider: mockworkflows and reports provider spans and metrics; webhook deliveries have a deadline. - Retries and failures. A workflow’s
config.max_retriesapplies to the steps that don’t set their own (#211). Behavior change: a workflow that sets it to anything but 3 now retries that many times. A failure that can’t pass on another attempt fails its step at once: router errors, sandbox refusals, invalid templates, arguments or inputs, replay mismatches,StateWriteError, and a request no provider can serve (UnsupportedRequestError);ToolNotFoundErrorandAttachmentResolutionErrorare new.retry.retryable_status_codesapplies tollm_callandembedsteps (#182),Retry-Afteris honoured up tobackoff_max, andStepResult.error_classificationsayspermanentortransient. A failed step or router skips its dependents (config.on_step_failure, by defaultskip_downstream), a step that runs out of retries on itstimeoutfails the run (#120), and the failure log counts the attempts made. - Budget, cost and tokens. Every charge goes through
BudgetEnforcerunder a lock, so a parallel layer can’t race pastbudget_usd, and a pause wins over a budget breach in the same layer. Failed attempts, re-asks, summaries and steps cut short are charged and reported, a subworkflow’s tokens include its children’s reasoning tokens, and a run reports what it spent however it ends (#120). Reasoning tokens are priced at the output rate (#127) and charged once on OpenAI (#120), the pricing table gains undated aliases (#127),MockProviderreplays the whole recorded usage (#178), and a streamed Anthropic call that fails aftermessage_startcounts its input tokens. - Workflow files, state and templates. Duplicate step ids,
max_concurrent_stepsoutside 1 to 1024 and negative timeouts are refused at load; parallel steps writing oneoutputwarn, andconfig.strict_outputs: truerefuses them. Cycle detection no longer hitsRecursionErroron long chains (#108).StateManager.update(key, fn)updates atomically, a dotted write over a scalar raisesStateWriteError, andset_syncandget_syncbecome_set_sync_unsafeand_get_sync_unsafe(#110). A missing key deep in a template renders empty,SafeFormatDict(strict=True)raises at the first one (#110), format specs on dicts and lists fall back tostr, andtool_argstreat only real placeholders as templates, with{value: ..., template: false}as the escape. Workflow, config, pricing, checkpoint and history files are read as UTF-8 whatever the locale (#120). Redaction patterns match the workflow’s data, not AgentLoom’s own fields, and reach Pydantic instances. The builder gainsadd_embed_step,add_approval_gate_step,add_agent_stepandset_state_schema, andWorkflowParser.from_yamlreads long text as YAML. - Subworkflows.
isolated_state,inputandreturn_keyskeep a child’s state apart from its parent’s, and an approval gate inside a child pauses the parent assub.gate, whichresumecontinues. A child writesoutput:only when it succeeds (#185), and gets its parent’s observer, checkpointer, stream callback andrun_id(#111). - Routers. Conditions can chain safe method calls (
state.x.strip().lower()), compare lists and dictionaries with==and call their read-only methods (get,keys,values,items,count,index). - Tools and attachments. Sync
@toolfunctions run on a worker thread, generic type hints give correct schemas, and overwriting a registered tool warns. A tool call whose arguments are no JSON object gets an error back (#120), a missingstate.Xintool_argsfails the step, a tool declared withoutparametersuses the registered schema, andhttp_requestsends a mappingbodyas JSON.data:URLs work as attachments, a raw base64 attachment is held to 20 MB, and a refused symlinked attachment names its target. - Providers and the gateway. Gemini gets schemas it accepts, with unsupported keywords dropped, properties of those names kept,
$refinlined and Pydantic constraints translated, and tool loops work on Gemini 3, which needs itsthoughtSignatureback (#120). Anthropic and Google send every system turn (#120), conversations with tool turns reach each provider in its own format, and OpenAIbase_urls with a path are kept. A 429 becomesRateLimitErrorwithRetry-After; it no longer trips the circuit breaker, nor do a cancelled stream, a replay miss or a request overmax_tpm, and a cancelled half-open call gives its probe slot back (#106, #109). The candidate cache is bounded (AGENTLOOM_CANDIDATE_CACHE_MAX, #109). A stream falls back to the next provider before its first chunk, runs the tool loop when the step has tools (#175), raises on Anthropicerrorevents, and drops Ollama’s inline<think>trace (#214). OpenAI reasoning models getreasoning_effort, Anthropic thinking leaves room for the reply,model_aliasesmaps names for fallbacks,OLLAMA_BASE_URLis honoured and Ollama embeddings senddimensions. - Record and replay. Every entry carries a
request_hash, which strict replay checks, and recordings are validated when they load (#107). Tool loops, retried steps and retried subworkflows record and replay as they ran (#163, #164), a loop that fell over to another provider included, and embeddings record their hash too.run --recordrefusesprovider: mock(#120),--recordnames inline schemas, recordings are read as UTF-8, a cut replay needs no key for the steps it replays, lenient replay warns about a changed request, and the bundled recordings carry hashes.verify-determinismandresumeimport the project’s modules, asrundoes (#120). - Observability. Steps cut short, provider calls cut by a timeout or a cancel, and refused embedding attempts close their spans (#169), and parallel subworkflows keep their spans apart (#147). An observer that lacks a hook or raises no longer fails the step or sends a webhook again.
toolsteps emit theexecute_toolspan and the tool metrics, the budget and circuit gauges update on every change and stay bounded (#111), andagentloom_workflow_duration_secondshas buckets from 0.1 s. - Grafana. Two panels no longer share an id. Panels read counters directly instead of
rate()on one-shot series, leave out pending gates and paused runs, and show one series per provider or workflow; totals show the counter’s value instead of adding up its samples, which reported 279 runs for 14. - CLI.
--version,-hon every command,--log-leveland theAGENTLOOM_LOG_*variables work, with logs on stderr.--statedecodes JSON values,visualizenames every step type and rejects an unknown--format, Mermaid output keeps colliding ids apart and router conditions whole, and the run summary marks timeouts[TIMEOUT].--providerover ageneric_openaiworkflow warns that it dropsbase_url, the CLI’s mock has no rate limit,infolistsjsonschema, and the run history no longer warns about a read-only default directory. - Deployment and packaging. The Argo CD Application, the example Jobs and the Helm chart work as shipped (#216): the Application passes a workflow and the provider keys’ Secret and labels the namespace for Pod Security; the Jobs meet the
restrictedprofile and call the right Python; the chart’s quota dropslimits.cpu, its run waits for Ollama’s pull, its Ollama image is pinned andvalues.schema.jsonchecks its values. Theproductionoverlay reaches the collector by its full name, and examples 36 and 41 run inside the image. A release publishes its Docker image and the chart installs the released one (#190), and the declared minimum dependency versions install a working package (#192). - Docs and examples. The docs and
--helptexts promise only what the code does, the validation scripts and their Jobs pass (#213), and examples 16, 17, 18, 19, 22, 30 and 38 show what they describe.
Security
- Router validator and state wrappers (#050). A subscript with a
_-prefixed key or a non-constant index is refused, state keys are reachable only asstate.X, and templates no longer reach a state wrapper’s raw data, which leaked keys such as_secret. - Router expressions hardened against dunder access and
type(#104), closing GHSA-c37m-mv4j-972v: router conditions could run arbitrary code. The validator raises the newSecurityError. approval_gate.notify.urlpasses the workflow sandbox (#051). With the sandbox disabled, a built-in deny-list still blocks loopback, link-local and cloud metadata addresses, private ranges and schemes other than HTTP(S), in IPv4-mapped, encoded and IDN forms too;sandbox.allow_internal_webhook_targets: truewaives only the internal-host check.shell_commandchecks the paths in--key=pathandkey=patharguments.ToolSandbox.validate_pathraisesSandboxViolationErrorfor every invalid path, null bytes, symlink loops and non-strings included (#051).- State redaction for persisted artefacts (#052).
state_schema: {key: {redact: true}}orAGENTLOOM_REDACT_STATE_KEYSmask the matching keys, glob patterns included, as<REDACTED:sha256=...>in checkpoints, results,--jsonoutput, webhooks and captured prompts; subworkflows inherit the policy and the sandbox. Memory keeps the real values, and a redacted checkpoint resumes with the sentinel, not the secret. - Tool sandbox hardened (#105). Meta-executables (
env,sh,bash,python,xargsand others) needdanger_opt_in, one by one; relative paths can’t leave the working directory; URL schemes default tohttpandhttps(allowed_schemes); process substitution is refused. Behavior change: list each meta-executable a workflow runs indanger_opt_in. - The Docker images run on
python:3.14-slimwithout pip (#219); their known vulnerabilities with a fix go from 14 to 3. - Dependency updates and hardened workflows (#194): weekly Dependabot updates, workflows without stored git credentials and with write access only where needed, zizmor in
make checkand CI, andSECURITY.mdfor private reports. anyio4.14.2 or later (#208), past GHSA-82r6-8w77-94w6 (TLS host names encoded with IDNA 2003), and locked dependencies past their published advisories.- Tighter defaults for the tools, the callback server and the providers (#220). A model’s tool call runs only if its step declares the tool; a sandboxed
http_requestrefuses internal hosts thatallowed_domainsdoesn’t name and drops credential headers from its result;shell_commandhonourstimeoutand kills the processes it started; sandboxed attachments needreadable_pathsorallowed_paths;callback-server --tokenalso guardsGET /pending, compares in constant time and caps headers and bodies; the Google key goes in a header, not the URL;agentloom infoshows no part of a key. Behavior change: list internal hosts inallowed_domainsand readable paths for attachments, and don’t expect credential headers inhttp_requestresults. http_request, approval-gate webhooks and URL attachments connect to the addresses their internal-host check resolved, keeping the host name for TLS, so DNS rebinding can’t reach an internal host (#220). Behavior change: where that check runs, a host that doesn’t resolve is refused.- A sandboxed
shell_commandrefuses anything the shell would expand ($,~,*,?,[, brace lists), even inside quotes. - A sandboxed
shell_commandruns with onlyPATH,HOME,LANG,LC_ALLandTZin its environment, so it can’t read provider keys. - A command given as a path to a sandboxed
shell_commandmust be the file its allowlisted name resolves to onPATH. - A subworkflow runs under the strictest of its sandbox and its parent’s, and inherits the parent’s when it has none;
sandbox.deny_internal_hostskeeps a child’sallowed_domainsoff the internal hosts its parent refuses. - The sandbox checks paths in short flags (
grep -f/etc/shadow), paths containing=and bare names that are symbolic links. findneedsdanger_opt_in, since its-execruns anything.
0.4.0 - 2026-04-15
Added
agentloom replay <workflow.yaml> --recording <file.json>subcommand — re-executes a workflow against recorded responses with no API calls (#61)- YAML-configured MockProvider —
provider: mockwithresponses_file,latency_model,latency_msfields onWorkflowConfig(#76) - Production
MockProviderandRecordingProviderfor deterministic replay and offline evaluation (#76)MockProviderloads responses from a JSON file, keyed bystep_idor SHA-256 prompt hash- Latency models:
constant,normal(gaussian with seed),replay(uses recordedlatency_ms) RecordingProviderwraps any provider, captures completions to JSON, flushes per-callagentloom run --mock-responses <file>replays;--record <file>captures
- Webhook notifications for approval gates — outbound HTTP on pause (#42)
WebhookConfigonStepDefinition.notifywith URL, custom headers, and body template- Async webhook sender with 3-retry exponential backoff (best-effort, never blocks pause)
agentloom callback-servercommand — lightweight HTTP server for programmatic approve/reject- Routes:
POST /approve/<run_id>,POST /reject/<run_id>,GET /pending - Shared template utilities extracted to
core/templates.py StepContextnow carriesrun_idandworkflow_namefor webhook context- Grafana dashboard “Human-in-the-Loop” row with approval gate and webhook panels
- Prometheus metrics:
approval_gates_total,webhook_deliveries_total,webhook_latency_seconds - OTel span attributes:
approval_gate.decision,webhook.status,webhook.latency_s - Example workflow (30), validation script, and K8s smoke job
- Approval gate step type — human-in-the-loop decision point (#41)
StepType.APPROVAL_GATEpauses the workflow and waits for human approval or rejection- Decision injected via
_approval.<step_id>state key on resume --approve/--rejectmutually exclusive flags onagentloom resumetimeout_secondsandon_timeoutschema fields (consumed by webhook callback server in #42)- Example workflow (29), validation script, and K8s smoke job
- Workflow pause mechanism — foundation for human-in-the-loop (#40)
PauseRequestedErrorexception for step executors to signal a pauseStepStatus.PAUSEDandWorkflowStatus.PAUSEDstatus values- Engine catches pause requests, saves checkpoint with
status=pausedandpaused_step_id, and returns cleanly - Resume from paused checkpoint skips completed steps and re-runs the paused step
- CLI treats paused workflows as non-error (exit code 0)
- Functional validation script (
scripts/validate_pause_resume.py) and K8s smoke job
- Pluggable checkpoint backends with
BaseCheckpointerprotocol andFileCheckpointerdefault (JSON-to-disk) (#78)CheckpointDataPydantic model with full workflow state serialization- Engine integration: auto-generates
run_id, saves checkpoint on completion/failure, graceful handling of I/O errors WorkflowEngine.from_checkpoint()classmethod to reconstruct and resume from a checkpoint, skipping completed stepsagentloom run --checkpointand--checkpoint-dirflagsagentloom resume <run_id>CLI command to resume paused or failed workflowsagentloom runsCLI command to list all checkpointed runs- Example workflow (28) and documentation
0.3.0 - 2026-04-12
Added
- Documentation site with mkdocs-material — getting started, architecture, providers, workflow YAML reference, Python DSL, graph API, examples, observability, deployment, contributing, and changelog pages. Auto-deployed to GitHub Pages on push to main (#72)
- Multi-modal input support for
llm_callsteps — images, PDFs, and audio viaattachmentsfield (#68)- Provider-native formatting: OpenAI (images, audio), Anthropic (images, PDFs), Google (images, PDFs, audio), Ollama (images)
- URL fetching with
fetch: local(default) orfetch: providerpassthrough - SSRF protection: blocks private/reserved IP ranges (RFC 1918, loopback, link-local)
- Sandbox integration:
allowed_domains,allow_network, andreadable_pathsenforced for attachments - Attachment size limit (20 MB default)
attachment_countinStepResult, OTel span attribute, andagentloom_attachments_totalmetric- Grafana dashboard “Multi-modal” row with attachments panels
- Multi-modal workflow examples (19–24)
- Streaming support for LLM responses with real-time token output (#3)
StreamResponseaccumulator with per-provider SSE/NDJSON parsing- All 4 providers: OpenAI (SSE), Anthropic (SSE), Google (SSE), Ollama (NDJSON)
- Gateway
stream()with circuit breaker + rate limiter integration config.stream: true(workflow-level) and per-stepstream:override- CLI
--streamflag for real-time terminal output time_to_first_token_msinStepResultand OTel span attributesagentloom_stream_responses_totalandagentloom_time_to_first_token_secondsmetrics- Grafana “Streaming” dashboard row with TTFT quantiles
- Streaming examples (25–26)
AGENTLOOM_*env var prefix for all configuration overrides (#5)- YAML-based pricing table replacing hardcoded Python dict (#6)
- Provider auto-discovery moved from CLI hack to
config.discover_providers() - Ollama e2e integration tests against a live Docker instance (5 smoke tests) (#71)
- CI workflow
e2e-ollama.yml— weekly schedule,release/**branches,e2elabel on PRs, manual dispatch - Array index support in state paths (e.g.,
state.items[0],items[0].name,results[-1])_parse_path()helper with regex-based bracket parsing inStateManager_resolve_key()and_set_nested()handle list indexing with bounds checkingDotAccessListwrapper forstr.format_map()template renderingToolStep._resolve_args()refactored to reuseStateManager._resolve_key()- CLI, Docker, and K8s smoke tests; example workflow (27)
- First-class graph API for workflow DAG analysis and export (#75)
WorkflowGraphclass withfrom_workflow()andfrom_dag()factoriesGraphNodeandGraphEdgefrozen Pydantic models- Path algorithms:
all_paths(),prime_paths(),critical_path() - Export formats:
to_dict(),to_dot()(Graphviz),to_pnml()(Petri Net),to_mermaid() - Optional
to_networkx()viapip install agentloom[graph] - Properties:
nodes,edges,roots,leaves,layers
- Test coverage reporting via Codecov with 85% minimum threshold and README badge (#70)
0.2.0 - 2026-03-30
Added
- Kubernetes manifests with Kustomize overlays for dev, staging, and production (#24)
- Helm chart with Job/CronJob modes and render-time input validation (#25)
- Terraform configuration for local kind cluster with full observability stack (#26)
- ArgoCD Application CRD with automated sync and Job immutability handling (#27)
- Docker CI/CD workflow for multi-arch GHCR publishing (#23)
- Infrastructure audit scripts for static and integration validation
- Infrastructure documentation (#28)
Fixed
- Production NetworkPolicy OTel egress restricted to observability namespace
- Read-only filesystem audit check no longer false-passes when root FS is writable
- Terraform audit phase passes KUBECONFIG to all kubectl poll commands
- Removed duplicate kubeconform invocation that hung without stdin
- Terraform secret uses
string_datainstead ofdatafor plaintext values - GitHub Actions and image versions pinned to commit SHAs
0.1.2 - 2026-03-26
Added
- Sandbox enforcement for built-in tools — command allowlist, path restrictions (read/write separation), network domain filtering, shell operator injection prevention, write size limits (#4)
SandboxConfigmodel in workflow YAML (config.sandbox.*)SandboxViolationErrorexception- Sandbox workflow examples (
17_sandbox_allowed,18_sandbox_blocked)
Fixed
- Step executors (
llm_call,router,tool_step) now useawait get_state_snapshot()instead of sync.stateaccess (#8) - Removed deprecated
gemini-2.0-flashmodel
0.1.1 - 2026-03-22
Fixed
- Rate limiter now accounts for response tokens, not just prompt tokens (#11)
- README header image uses absolute URLs for PyPI compatibility (#2)
0.1.0 - 2026-03-19
First public release.
Added
- YAML and Python DSL workflow definitions (DAGs with sequential + parallel steps)
- Step types:
llm_call,tool,router(conditional),subworkflow - Provider gateway with automatic fallback (OpenAI, Anthropic, Google, Ollama)
- Circuit breaker, rate limiter, and retry with exponential backoff per provider
- Budget enforcement (hard stop when USD limit exceeded)
- Cost tracking per step, model, and provider
- OpenTelemetry traces + Prometheus metrics (optional,
pip install agentloom[all]) - CLI commands:
run,validate,visualize(ASCII + Mermaid),info - Checkpointing: save and resume workflow state to disk
- 392 tests, mypy strict, ruff clean
Known Limitations
No streaming support (falls back to full completion)(fixed in Unreleased)- Router expressions use first-match-wins, no priority ordering
Rate limiter doesn’t account for response tokens (only prompt tokens)(fixed in 0.1.1)Provider discovery from env vars only, should be a config file(fixed in Unreleased)Shell command tool has no sandboxing (FIXME in code)(fixed in 0.1.2)File tools accept arbitrary paths (no path sanitization)(fixed in 0.1.2)- Router expressions use
eval()— must be trusted input (not user-facing) Pricing table hardcoded in Python, should be YAML config(fixed in Unreleased)- No array index support in state paths (e.g.,
state.items[0]) Sync state access in step executors bypasses async lock(fixed in 0.1.2)- Budget enforcement is post-hoc — a single expensive step can overshoot before being stopped
budget_remainingmetric only emitted to Prometheus, not OTel- Checkpoint
save_checkpointuses blocking I/O inside async method
Design Decisions
- httpx over provider SDKs — keeps dependencies minimal (~5 core). Trade-off: we maintain thin adapters instead of using official SDKs.
- anyio over raw asyncio — structured concurrency via task groups. Slightly less familiar but much safer for parallel step execution.
- str.format_map over Jinja2 — one fewer dependency; prompt templates don’t need loops or conditionals. SafeFormatDict handles missing keys.
- Observability optional — core runs without opentelemetry or prometheus. NoopSpan/NoopTracer pattern gives zero overhead when not installed.
- Pydantic v2 — validation and serialization worth the Rust compilation trade-off. Could revisit for truly minimal environments.