Changelog¶
All notable changes to this project are documented in this file. Format follows Keep a Changelog. Versioning follows Semantic Versioning.
[Unreleased]¶
[0.4.0] - 2026-09-23¶
Added¶
ClaudeAgentProvider(executionkit/claude_sdk.py,[claude]extra) — a third transport that reaches Claude throughclaude-agent-sdkand a Claude subscription sign-in rather than an API key, making the patterns usable to operators who pay for a subscription instead of API credits. SatisfiesLLMProviderandStreamingProvider, so it drops intoconsensus(),refine_loop(),map_reduce(),structured(),pipe(), andKitunchanged. The base install stays dependency-free (ADR-004 is not superseded); the extra additionally requires the Claude Code CLI onPATH, which pip cannot express, and a missing CLI surfaces asPermanentErrorwith that instruction. Three deliberate gaps, surfaced rather than emulated: the Agent SDK has no sampling temperature and no output-token ceiling, sotemperatureandmax_tokenswarn (useeffortandmax_budget_usd), and OpenAI tool schemas raisePermanentError. Because the class does not setsupports_tools,react_loop()rejects it up front rather than failing mid-loop — andconsensus()over this transport is weaker evidence than overProvider, since its sample diversity normally comes from temperature. The CLI subprocess hasANTHROPIC_API_KEY/ANTHROPIC_AUTH_TOKENblanked: the SDK spawns it with{**os.environ, **options.env}, so a process holding a key (any process usingAnthropicBatchClient) would otherwise hand the child that key and silently bill the API account instead of the subscription, failing outright when the key is unfunded. Passenv=to restore API-key auth deliberately. See ADR-016.- Instrument the checked-call trace events with schema-v2 payloads and add a
telemetry collector (
executionkit/telemetry.py).llm_call_start/llm_call_end/llm_call_errornow carryschema_version, acall_idcorrelating every physical attempt of one logical call, per-attemptattempt_ids, ISO-8601 dispatch timestamps andduration_ms, the requested and provider-served model names,attempts/retry_count, an explicitbudget_exhaustedflag separating budget refusals from provider failures, andusage_reportedwithNone(never fabricated0) token counts when a provider returns no usage — streaming calls included. The newTraceCollectorcallback joins those events into per-callCallRecords and renders a portable, JSON-safesummary(); per-call usage comes from each end event's own token keys, never from the cumulativecosttracker snapshot, and cost is computed only from caller-supplied rates over calls whose providers reported usage. Payloads are additive over the original keys, and the budget-check →reserve_call()no-await critical section is unchanged.
Changed¶
- MCP server: advance
PROTOCOL_VERSIONfrom2025-06-18to2025-11-25, the newest revision that still uses theinitialize/notifications/initializedhandshake this server implements, and add2025-06-18toSUPPORTED_PROTOCOL_VERSIONSso peers already negotiating that version keep working. Deliberately stops short of2026-07-28, which removes the handshake entirely in favor of per-request_metaversioning and aserver/discoverRPC — advertising that revision from inside aninitializeframe would claim a wire contract this server does not speak. A2026-07-28client still degrades correctly against this server. - Rewrite the maintained documentation around the current v0.3 API, including complete API navigation, execution and evaluation guides, provider examples, integration boundaries, recipes, architecture, and concise decision records. Documentation checks now compare the public exports, source modules, tracked pages, and pattern surfaces with the repository.
RetryConfig.max_retriesnow means "retries after the initial call", sowith_retry()makes up to1 + max_retriesattempts instead of capping total attempts atmax_retries(behavior change). This makes the field agree withstructured()'s ownmax_retries, which has always run1 + max_retriesattempts. Practically: an existingRetryConfig(max_retries=3)now dispatches up to 4 calls rather than 3 — one more call, one more unit of cost, and one more backoff sleep in the worst case for a request that keeps failing. To keep the old behavior exactly, decrement the value (max_retries=N→max_retries=N-1);max_retries=0is unchanged and still means a single attempt with no retries. Amax_costllm_callsceiling still caps total dispatched attempts, so budget-capped callers never see an extra wire call — but where that ceiling equals the old attempt count, the call now ends inBudgetExhaustedError(aPatternError) instead of the provider's ownProviderError, so a caller catching onlyProviderErrorshould widen itRetryConfignow raisesValueErrorwhenmax_retriesis negative, matchingstructured(). Previously a negative value made zero attempts and raisedRuntimeError("unreachable")from insidewith_retry
Fixed¶
Workflow.run()now raisesValueErrorwhen aninitial_contextkey collides with a step name, instead of silently returning the caller's seed as that step's output without ever running it. Step completion is now tracked in a set independent fromoutputsmembership, which previously doubled as both "seeded by initial_context" and "already executed" (behavior change)react_loop()now raisesValueErrornaming every collision, before the first provider call, when two or moreToolobjects share a name. The registry was previously a last-write-wins dict comprehension, so tool calls (resolved by name alone) could only ever reach the last-registered handler — the earlier tool's schema was still sent to the model, but the tool itself was silently unreachable. Callers whose tool names were already unique see no behavior change (behavior change)- Anthropic Message Batches results with a duplicate
custom_idnow raiseProviderErrornaming the id, instead of silently keeping the last-seen entry and dropping the rest. Unreachable with a conformant Batches file, but a contract violation should fail loudly rather than silently drop a sample - Add streaming test coverage for the httpx SSE transport (
Provider._stream_httpx) viahttpx.MockTransport— a successful end-to-end stream plus 429/5xx and transport-error paths — closing a CI blind spot on the provider's default active transport (httpx is always installed as adevextra, so this was previously untested, "green" code) - Correct the
map_reduce()docstring:reduce_callsis always1(the reduce call always runs, even for emptyinputs; the prior "0 when empty" clause contradicted the implementation), andtotal_callsnow documents the retry caveat —map_count + 1only when no attempt was retried, matching thellm_callsaccounting already documented in the README - CI now actually installs and exercises the
jsonschemaandotelextras. A stale lock file meant the four Tier-B JSON-Schema tool-argument validation tests had been import-skipping in every required job sincejsonschemaentered thedevextra (2026-06-16 lock vs. 2026-06-18 extra change), and no workflow at any trigger installedotelat all, so the four OTel span-attribute tests skipped everywhere and the README's OTel claims rested on undated local testing.scripts/check_lock_parity.pynow fails CI on the same drift instead of letting it disarm tests silently, and a dedicated observability job asserts the SDK imports before running pytest. No runtime behavior changed; this restores test coverage on two already-shipped features that had gone dark
[0.3.0] - 2026-07-08¶
Added¶
- Add multi-turn conversation primitives.
react_loopnow acceptsmessages=(a prior conversation to continue, mutually exclusive withprompt) and returns the full updated transcript asmetadata["messages"], so callers can thread state across turns - Add
Kit.turn(user_text, tools=...)plus aKit.messagestranscript (and an optionalmessages=seed onKit), giving a stateful conversational API on top ofreact_loop - Add message-construction helpers
system_message,tool_message, andassistant_tool_calls_messagetoexecutionkit.engine.messages - Add
examples/conversational_assistant.pydemonstrating multi-turn tool use with context carryover - Add a multi-turn eval harness —
ConversationScript,Turn, andrun_conversation_script(script, kit)— that drives a scripted conversation through a singleKit(state carries across turns) and returns anEvalReport, one result per turn - Add optional rate limiting to
Kitvia arate_limiter: TokenBucket | Noneparameter; a token is acquired before every pattern dispatch - Add an optional
summarizer=hook toreact_loopthat compresses history dropped bymax_history_messagestrimming into a system note for the active window (the stored transcript is unchanged); asummarizedcount is reported in metadata react_loopcheckpoint state now includes the runningmessagestranscript, enabling conversation-level resume- Add a "Building a conversational assistant" recipe (
docs/recipes/assistant.md) covering stateful turns,structured()intent/slot NLU, the streaming limitation, andConversationScriptevals - Add the
map_reduce()pattern — parallel fan-out over a collection of inputs, each processed independently, then reduced to a single answer (ADR-011) - Add a stdlib-only MCP server —
python -m executionkit.mcpspeaks newline-delimited JSON-RPC 2.0 over stdio and exposesconsensusand a demo-toolset-restrictedreact_loopas MCP tools (ADR-012) - Add Anthropic Message Batches fan-out —
consensus_batch()andmap_batch()submit samples as a single batch job over a stdliburllibclient and score with the sametally_votesas the liveconsensus()pattern (ADR-014)
Changed¶
react_loop()no longer silently swallows unknown keyword arguments (the**_sink was removed); an unsupported kwarg now raisesTypeError.promptis now optional (defaults toNone) andtoolsdefaults to()(behavior change)
[0.2.0] - 2026-06-08¶
Added¶
- Add lightweight orchestration primitives:
Router/RouteRulefor provider selection before a pattern call,Workflow/Stepfor dependency-ordered async fan-out, andPlan/PlanStepfor ordered plan-then-act execution - Add approval gates —
ApprovalGate,ApprovalRequest,ApprovalDecision, andApprovalDeniedError— that require human or policy approval before tool execution, workflow steps, or plan steps; wired intoreact_loop(a denial becomes a tool observation) and intoWorkflow/Plan(a denial aborts) - Add observability hooks —
TraceEvent,TraceCallback, andemit_trace— emitting structured sync-or-async events for LLM call start/end/error, tool calls, workflow steps, plan steps, and approvals; add atrace=parameter toconsensus,refine_loop,react_loop, andstructured - Add an eval harness —
EvalCase,EvalResult,EvalReport, andrun_eval_suite()for deterministic golden checks, pluslive_provider_from_env()for opt-in live evals gated onEXECUTIONKIT_LIVE_EVAL;EvalReportreportsaccuracyandsummary() - Add an output-correctness eval suite: deterministic per-pattern golden datasets and a curated model-failure corpus that run offline in CI under a dedicated "Eval suite" gate, plus opt-in judge-calibration and per-pattern live-provider regression tiers and a scheduled/manual
Live Evalworkflow that runs them against a local Ollama model - Add an
approval_gate=parameter toreact_loop,Workflow.run, andPlan.execute
Changed¶
- Budget accounting now counts every dispatched wire attempt — including failed retries — toward
llm_calls, and the deadrelease_call()slot-release path was removed; amax_costllm_callsceiling now caps total attempts, not just successes (behavior change) Router.run(pattern, prompt, *, context=..., **kwargs)takes routing inputs through an explicitcontextmapping disjoint from the pattern's keyword arguments, so a routing key (e.g.tier) can no longer leak into the pattern call and raiseTypeError- Broaden credential redaction to match common keyless token shapes (
ghp_/gho_,AIza,xox[bpoa]-,gsk_, andkey=/token:/bearer …variants) and apply it to transport-failure messages and malformed tool-argument echoes as well as HTTP error bodies
Security¶
- Harden the
refine_loopdefault judge: embedded</response_to_rate>envelope tags are stripped from candidate text so adversarial content cannot break out of the scoring sandbox - Bounds-check provider-reported token usage (
_usage_int): reject booleans, negatives, and absurdly large counts asProviderErrorso a hostile or buggy endpoint cannot under-count and bypassmax_cost - Add
ApprovalGateas an opt-in control for human/policy review before tool, workflow, or plan side effects
Fixed¶
- Map urllib read-phase
TimeoutErrorto a retryableProviderErroron the default (no-httpx) transport path, matching thehttpxbackend Kit.usagenow records the partial cost carried by a raisedExecutionKitError(e.g.BudgetExhaustedError,MaxIterationsError) instead of dropping it when a pattern aborts- Harden
Router.runagainst apromptkey in the routing context colliding with the positionalpromptargument toselect() - Strengthen ReAct history-trimming tests (remove
asyncio.coroutine, unavailable since Python 3.11) and assert onrounds/tool_calls_made/truncated_observations/messages_trimmedmetadata
[0.1.0] - 2026-05-22¶
Security¶
- Fix prompt injection in
refine_loopdefault evaluator via XML delimiter sandboxing and input truncation to 32 768 chars - Mask API key in
Provider.__repr__— previously leakedsk-...values in logs and tracebacks - Redact credential-pattern substrings from HTTP error messages using
_redact_sensitiveregex - Return only exception type name (not message) from tool error handler in
react_loopto prevent leaking internal details to the LLM - Add Bandit SAST job to CI and Dependabot weekly auto-update configuration for pip and GitHub Actions
Added¶
- Add the
structured()pattern andstructured_sync()wrapper for JSON extraction, optional validation, and repair retries - Add optional
httpxbackend for HTTP connection pooling — install withpip install executionkit[httpx]; falls back tourllibwhenhttpxis absent - Add
max_history_messages: int | Noneparameter toreact_loopfor capping message history size; always preserves the original user prompt - Add
_validate_tool_argshelper inreact_loopthat validates tool call arguments against JSON Schema (required fields,additionalProperties, and type checks) before execution — uses stdlib only, nojsonschemadependency - Add
aclose()and async context manager support (__aenter__/__aexit__) toProviderfor explicit HTTP client lifecycle management - Add
messages_trimmedcounter toreact_loopmetadata - Add MkDocs Material documentation site with public guides for installation, provider setup, patterns, recipes, API reference, contributing, license, and changelog
- Add architecture decision records for structural protocols, flat package layout, and single OpenAI-compatible provider design
- Add GitHub Pages documentation deployment workflow and CodeQL analysis workflow
- Add supply-chain hardening with
requirements.lockand SBOM artifact generation in the publish workflow
Fixed¶
- Fix
consensusvoting incorrectly splitting semantically identical responses that differ only in trailing newlines or internal whitespace — votes now use normalized text while the original winning response is preserved - Fix
_parse_scoresilently accepting scores outside the 0–10 range; now raisesValueErrorfor out-of-range values - Remove phantom
pydantic>=2.0fromproject.dependencies; pydantic was never imported in the library source - Fix strict MkDocs builds by excluding internal documents with stale cross-references from the public site
Changed¶
- Change
PatternResult.metadatatype fromdict[str, Any]toMappingProxyType[str, Any]to enforce true immutability on a frozen dataclass - Update GitHub Actions workflow dependencies to current major versions
- Replace the old Astro documentation site with the MkDocs Material site