Architecture¶
ExecutionKit is an in-process Python library. It provides bounded LLM call patterns, provider transport, usage accounting, and small coordination helpers. The calling application owns persistence, scheduling, identity, authorization, and user interfaces.
Package boundaries¶
flowchart TD
App["Calling application"] --> Public["Public API"]
Public --> Coordination["Kit, routing, plans, workflows, approvals"]
Public --> Patterns["Patterns and pipe"]
Coordination --> Patterns
Patterns --> Engine["Retry, budgets, parsing, voting, concurrency"]
Engine --> Protocols["Provider protocols and value types"]
Patterns --> Protocols
Protocols --> HTTP["OpenAI-compatible HTTP provider"]
HTTP --> Endpoint["Configured model endpoint"]
Public --> Batch["Anthropic batch integration"]
Public --> MCP["MCP stdio server"]
Dependencies point downward. The engine does not select providers or own application state. Patterns use the provider protocols, not the concrete HTTP client.
Module map¶
The public and top-level coordination modules are:
| File | Responsibility |
|---|---|
executionkit/__init__.py |
Public exports, version, and synchronous wrappers. |
executionkit/_constants.py |
Shared internal defaults and limits. |
executionkit/_mock.py |
Scripted provider used by tests and examples. |
executionkit/types.py |
Result, usage, tool, callback, and enum types. |
executionkit/errors.py |
Exception hierarchy. |
executionkit/provider.py |
Provider protocols, responses, and OpenAI-compatible HTTP transport. |
executionkit/cost.py |
Mutable usage tracker and price arithmetic. |
executionkit/kit.py |
Session facade, conversation history, and cumulative usage. |
executionkit/compose.py |
Sequential pipe() execution. |
executionkit/routing.py |
Ordered provider selection rules. |
executionkit/planning.py |
Sequential named steps. |
executionkit/workflow.py |
Dependency-ordered steps, checkpoints, and resume. |
executionkit/approval.py |
Approval requests, decisions, and timeout policy. |
executionkit/evals.py |
Deterministic eval cases, conversation scripts, and live-provider opt-in. |
executionkit/observability.py |
Trace callbacks, payload schema, and optional OpenTelemetry spans. |
executionkit/telemetry.py |
Per-call telemetry records and portable summaries built from trace events. |
executionkit/batches.py |
Anthropic Message Batches client and batch helpers. |
executionkit/claude_sdk.py |
Claude Agent SDK transport; authenticates with a Claude subscription rather than an API key. |
Pattern modules are:
| File | Responsibility |
|---|---|
executionkit/patterns/__init__.py |
Pattern package exports. |
executionkit/patterns/base.py |
Checked provider calls, budgets, retry integration, and checkpoints. |
executionkit/patterns/consensus.py |
Concurrent sampling and exact normalized voting. |
executionkit/patterns/refine_loop.py |
Generate, evaluate, and revise loop. |
executionkit/patterns/react_loop.py |
Tool-call loop, validation, approvals, history limits, and tool execution. |
executionkit/patterns/structured.py |
JSON extraction, validation, and repair. |
executionkit/patterns/map_reduce.py |
Concurrent map calls followed by one reduce call. |
Engine modules are:
| File | Responsibility |
|---|---|
executionkit/engine/__init__.py |
Engine package marker. |
executionkit/engine/convergence.py |
Score-threshold and patience-based stopping. |
executionkit/engine/json_extraction.py |
JSON extraction from plain or fenced model output. |
executionkit/engine/messages.py |
OpenAI-format message constructors. |
executionkit/engine/parallel.py |
Strict and resilient async gathering. |
executionkit/engine/rate_bucket.py |
Async token bucket and retry-after penalty. |
executionkit/engine/retry.py |
Retry classification and jittered backoff. |
executionkit/engine/voting.py |
Response normalization and vote tallying. |
MCP modules are:
| File | Responsibility |
|---|---|
executionkit/mcp/__init__.py |
MCP package exports. |
executionkit/mcp/__main__.py |
python -m executionkit.mcp entry point. |
executionkit/mcp/_constants.py |
Supported protocol versions and server limits. |
executionkit/mcp/_demo_tools.py |
Fixed calculator and echo tools used by the server. |
executionkit/mcp/server.py |
JSON-RPC framing and stdio server lifecycle. |
executionkit/mcp/tools.py |
MCP tool definitions, argument checks, and pattern handlers. |
Pattern call lifecycle¶
A normal pattern call follows this order:
- Validate pattern arguments.
- Build provider messages.
- Check the remaining budget and reserve one LLM call.
- Apply retry pacing, then call the provider.
- Parse the response and record returned token usage.
- Continue the pattern or build a
PatternResult. - Emit trace events and checkpoints at documented boundaries.
Retry attempts repeat steps 3 through 5. A failed attempt therefore consumes a call slot. It normally adds no token usage because the provider did not return usage.
Concurrency and accounting¶
Concurrent pattern calls share one CostTracker inside the pattern. Budget
check and call reservation occur without an await between them, so the LLM
call limit cannot be raced by coroutines on one event loop.
The token fields are pre-dispatch checks, not response-size ceilings. A successful response can exceed a token field because its usage is known only after completion. The next attempted call is blocked.
CostTracker, TokenBucket, Kit, and workflow state are not designed for
concurrent access from multiple operating-system threads. Create separate
instances or provide application-level locking.
Kit(rate_limiter=...) takes one token per top-level Kit method. A
RetryConfig(rate_limit_strategy=...) takes one token per provider attempt.
Use the setting that matches the limit being enforced.
Provider boundary¶
LLMProvider requires one async complete() method. ToolCallingProvider
adds an explicit tool-support marker. StreamingProvider adds stream(),
which returns an async iterator.
The concrete Provider:
- sends OpenAI-compatible chat-completions requests;
- accepts only
httpandhttpsbase URLs; - does not follow redirects;
- redacts credential-like values from HTTP error text;
- uses
httpxwhen installed and falls back tourllib; and - does not translate native provider request formats.
Provider and model compatibility remain configuration concerns. The transport does not discover model capabilities.
Tool execution boundary¶
react_loop() treats model-requested tool calls as untrusted input.
- It rejects duplicate tool names when the loop starts.
- It validates the argument object and the supported top-level schema subset.
- If
jsonschemais installed, it applies full JSON Schema validation. - It asks the configured
ApprovalGate, if any. - It applies a timeout and runs calls from the same round concurrently.
- It truncates tool observations before adding them to model history.
These checks do not replace authorization inside a tool. Tool code must still validate resource identifiers, caller permissions, and side-effect rules.
State, checkpoints, and resume¶
Pattern checkpoints are observation callbacks. Exceptions raised by a pattern checkpoint callback are logged and do not stop that pattern.
Workflow checkpoints are different: they contain completed outputs and
accumulated usage after each ready batch. The caller provides the persistence
function. A workflow resumed from a checkpoint skips step names already found
in checkpoint.outputs. Workflow checkpoint callback exceptions propagate to
the caller.
Checkpoint data can contain prompts, outputs, tool observations, and application context. The caller is responsible for access control, encryption, retention, and schema migration.
Value and mutation model¶
Public result types use frozen, slotted dataclasses. Field reassignment is
blocked, and metadata created by the library is generally wrapped in
MappingProxyType.
This is shallow immutability. A caller-supplied nested list or mapping can remain mutable after it is placed inside another value. Copy untrusted mutable inputs when a stable snapshot is required.
Stateful helpers are intentionally mutable:
CostTrackeraccumulates counts;TokenBuckettracks refill time and penalties;Kitstores cumulative usage and conversation messages.
Errors¶
All library exceptions derive from ExecutionKitError and can carry partial
cost and diagnostic metadata.
ExecutionKitError
├── LLMError
│ ├── RateLimitError
│ ├── PermanentError
│ └── ProviderError
└── PatternError
├── BudgetExhaustedError
├── ConsensusFailedError
└── MaxIterationsError
Approval denial and timeout errors also derive directly from
ExecutionKitError.
Retry behavior is type-based. By default, RateLimitError and
ProviderError are retryable; PermanentError is not.
Extension rules¶
- Implement
LLMProviderfor another OpenAI-compatible transport or a test double. - Implement the larger protocols only when the object actually supports tools or streaming.
- Write a pattern as an async function that returns
PatternResultand uses the checked-call helpers for consistent retry, budget, trace, and cost behavior. - Put branching, persistence, identity, and business authorization in the application.
Architecture changes that alter these boundaries should include an ADR in
docs/adr/.