multi-bot-agentic is a standalone AI-agent engineering showcase: a deterministic agent coordinator with explicit Observe -> Decide -> Act loops, durable event logs, rationale traces, provider adapters, and bounded safety controls.
It is built as a portfolio-quality recreation of the multi-bot product idea without depending on private infrastructure. The default path runs fully offline with a deterministic fake provider. Real adapters are included for GPT-5.5/OpenAI-compatible models, Claude Sonnet 4.6 via Claude Code CLI, Gemini 3.x, and Kimi K2/Moonshot.
Most agent demos let the LLM decide everything. This repo takes the production-minded path:
- The LLM is an input source, not the control plane.
- A deterministic decision engine chooses actions.
- Every decision has a rationale trace.
- Every lifecycle transition is persisted.
- Every external integration goes through an adapter.
- Safety controls bound scope, runtime, tools, and cancellation.
LLM-first agents often skip straight from prompt to action. When something goes wrong, the transcript may show what the model said, but not which control rule allowed the action.
multi-bot-agentic writes every decision as a durable event with a RationaleTrace: rule id, observations used, rejected actions, and explanation. You can replay the run later and inspect exactly why the engine chose call_llm, call_tool, finish, or cancel.
multi-bot-agentic replay --event-log data/runs.sqlite --event-type decision --format textMany agent frameworks let the model choose and invoke tools directly. That is convenient, but risky for production workflows where tool access should be explicit, bounded, and auditable.
This repo treats model output as an observation. The deterministic decision engine interprets constrained text like TOOL:checklist:<payload>, checks the safety policy, and only then executes an allowlisted tool adapter.
Provider-specific SDKs and response shapes make agent code hard to port. A prototype built around one model often leaks provider details into the orchestration layer.
multi-bot-agentic normalizes providers behind one adapter interface:
OpenAIAdapterfor GPT-5.5/OpenAI-compatible chat completions.ClaudeCodeCLIAdapterfor local Claude Code CLI workflows with Claude Sonnet 4.6.GeminiAdapterfor Gemini 3.xgenerateContent.KimiAdapterfor Moonshot/Kimi K2 chat completions.FakeLLMAdapterfor deterministic CI and demos.
The runner consumes all provider responses as ModelOutput, so orchestration logic stays provider-neutral.
Portfolio and CI demos should not depend on live model credentials, model availability, or network behavior.
The fake provider produces deterministic model-like outputs that the real runtime consumes. It still exercises Observe -> Decide -> Act, tool routing, safety checks, event logging, replay, and reports.
multi-bot-agentic run --goal "Create a launch checklist for an AI agent platform" --provider fakeLong-running agent tasks need post-run inspection. Without durable state, crashes and restarts turn into guesswork.
The sqlite event log records lifecycle transitions, observations, decisions, action requests, action results, failures, cancellations, and completion. Replay does not call any provider or tool, so postmortems are safe and deterministic.
multi-bot-agentic report --event-log data/runs.sqliteUnbounded agent loops are a common failure mode. They waste time, cost money, and make incident response harder.
SafetyPolicy bounds run scope with max_steps, provider/tool timeouts, prompt size limits, cancellation files, and tool allowlists. If the run reaches its budget, the decision engine finishes or fails through explicit lifecycle events.
Teams often want to test GPT-5.5 vs Gemini 3.x vs Kimi K2 vs Claude Sonnet 4.6, but provider-specific code makes comparisons noisy.
With provider adapters, you can keep the same runner, same decision engine, same event log, and same replay/report UX while swapping the provider:
multi-bot-agentic run --goal "Draft a migration plan" --provider openai
multi-bot-agentic run --goal "Draft a migration plan" --provider gemini
multi-bot-agentic run --goal "Draft a migration plan" --provider kimi
multi-bot-agentic run --goal "Draft a migration plan" --provider claude_codeAgent demos often end with prose. Real workflows need structured, repeatable artifacts.
The built-in checklist tool turns a goal into a deterministic launch checklist and records the tool result in the event log. It is intentionally simple, but it demonstrates the production pattern: model suggests, policy validates, adapter executes, event log records.
Agent systems can become hard to explain when planning, tool use, model calls, retries, and state are mixed together.
This repo keeps the boundaries visible:
runner.py: owns Observe -> Decide -> Act.decision.py: deterministic rules and rationale traces.lifecycle.py: state-machine transitions.event_log.py: durable sqlite events.llm/: provider adapters.tools/: allowlisted tool adapters.safety.py: bounds and cancellation.
Live provider tests are useful, but they should not be required for every pull request.
CI runs lint, format, typecheck, tests, and a fake-provider smoke demo across Python 3.10, 3.11, and 3.12. Live provider calls remain operator-triggered because they require credentials and external systems.
Goal
|
v
Observe -> durable observation event
|
v
Decide -> deterministic rule + rationale trace
|
v
Act -> LLM adapter or allowlisted tool
|
v
Event log + replay
python -m venv .venv
. .venv/bin/activate
python -m pip install -e ".[dev]"
ruff check .
ruff format --check .
mypy src tests
pytest
multi-bot-agentic run --goal "Create a launch checklist for an AI agent platform" --provider fakeReplay the durable event log:
multi-bot-agentic replay --event-log data/runs.sqlite
multi-bot-agentic replay --event-log data/runs.sqlite --format text
multi-bot-agentic report --event-log data/runs.sqlite- Explicit Observe -> Decide -> Act runtime loop.
- Deterministic decision engine with rationale traces.
- State-machine lifecycle: created, observing, deciding, acting, succeeded, failed, cancelled.
- Durable sqlite event log with replay.
- LLM adapters for GPT-5.5/OpenAI-compatible models, Claude Sonnet 4.6 via Claude Code CLI, Gemini 3.x, and Kimi K2/Moonshot.
- Tool adapters with allowlisted execution, including deterministic checklist generation.
- Safety controls for max steps, prompt bounds, cancellation, and timeouts.
- Human-readable replay and run reports for inspecting durable rationale traces.
- Production-minded layout:
src/,tests/,scripts/,migrations/,.github/workflows/, env config, docs.
| Provider | Adapter | Live credential |
|---|---|---|
| Fake | deterministic local provider | none |
| GPT-5.5 / OpenAI-compatible | OpenAIAdapter |
OPENAI_API_KEY |
| Claude Sonnet 4.6 / Claude Code | ClaudeCodeCLIAdapter |
local claude command |
| Gemini 3.x | GeminiAdapter |
GEMINI_API_KEY |
| Kimi K2 / Moonshot | KimiAdapter |
KIMI_API_KEY |
All adapters normalize output into ModelOutput. The runner consumes that output as an observation before the decision engine selects the next action.
checklist: deterministic launch checklist generator used by the fake-provider demo.echo: safe text echo for adapter tests.readonly_file: root-contained read-only file reader.calculator: sandboxed arithmetic evaluator. It parses expressions into an AST and walks an allowlist of numeric literals and+ - * / // % **operators — nevereval— so names, calls, and attribute access are rejected and exponents are bounded against CPU/memory exhaustion. Results that are not real numbers (for example a fractional power of a negative base) or not finite (overflow toinf, ornan) are refused rather than returned as opaque values. A model requests it withTOOL:calculator:2 + 3 * 4, matching the same model-suggests / policy-validates / adapter-executes pattern as every other tool.json_format: validates a JSON document and returns it canonicalized (sorted keys, 2-space indent). Invalid input yields a structured failure with the parser's message instead of raising, and the non-standardNaN/Infinity/-Infinitytokens (which RFC 8259 forbids and strict parsers reject) are rejected rather than round-tripped into invalid output. A model requests it withTOOL:json_format:{"b":1,"a":2}, giving agents a safe way to verify and normalize JSON produced by earlier steps.yaml_format: validates a constrained safe YAML subset and returns it canonicalized (sorted mapping keys, 2-space indentation). It supports block mappings/sequences, JSON-style flow collections, and finite scalar values using the Python stdlib only; unsupported full-YAML features such as anchors, tags, document markers, and constructors return structured failures. A model requests it withTOOL:yaml_format:enabled: true, giving agents a safe way to normalize YAML handoff snippets.toml_format: validates TOML (viatomllibon Python 3.11+ ortomliwhen available) and returns a deterministic serialization with sorted keys for tables/arrays/strings/ints/floats/bools. Dates/times, non-finite floats, empty or oversized input, and a missing parser return structured failures. A model requests it withTOOL:toml_format:enabled = true, giving agents a safe way to normalize TOML configuration snippets.json_path: extracts one value from a JSON document using a small deterministic path dialect (.foo.bar,items[0].name, or$/empty for the whole document). Supplytext+path, or a single payload split on<<<JSON_PATH>>>forTOOL:json_path:...directives. Recursive descent, filters, scripts, pipes, oversized input, and oversized serialized results return structured failures; the tool usesjson.loadsonly and never executes code.spreadsheet_slice: parses CSV text and returns a deterministic row/column subset as JSON (header+rows). Row ranges use zero-based, end-exclusive body-row slices viarows=1:3orrow_start/row_end; columns may be selected by exact header name and/or zero-based index. A singleTOOL:spreadsheet_slicepayload can embed options after<<<SPREADSHEET_SLICE>>>. Empty input, oversized tables, blank headers, invalid ranges, missing names, ambiguous names, and out-of-bounds indexes return structured failures; the tool uses stdlibcsvonly and never executes code.redact: scrubs common PII (email addresses, phone numbers, US Social Security numbers, IPv4 addresses) from text, replacing each match with a typed placeholder such as[EMAIL]and reporting per-category counts in the tool metadata. A model requests it withTOOL:redact:<text>, giving agents a safe way to sanitize content before it is persisted to the durable event log.hash: computes a hex digest of text with a small allowlist of well-known algorithms (md5,sha1,sha256,sha512; defaultsha256). Empty, oversized, or unsupported-algorithm requests return a structured failure. A model requests it withTOOL:hash:<text>, giving agents a deterministic fingerprint for deduplication, cache keys, or integrity checks between steps.base64: encodes text to standard Base64 or decodes Base64 back to text (operation: encode|decode; defaultencode). Decoding validates the payload strictly and requires the decoded bytes to be valid UTF-8, so invalid Base64 or non-text payloads return a structured failure. A model requests it withTOOL:base64:<text>, giving agents a safe way to move opaque payloads between steps.url_parse: splits an absolute URL into its components (scheme, host, port, path, query, grouped query parameters, fragment) using the standard library — never a network request. Relative URLs, empty input, and invalid ports return a structured failure. A model requests it withTOOL:url_parse:<url>, giving agents a safe way to route on a host or inspect a query parameter relayed by an earlier step.uuid5: computes a deterministic version-5 UUID from a name and a namespace (dns,url,oid,x500, or a custom UUID string; defaultdns). Because a v5 UUID is a hash of(namespace, name), the same inputs always yield the same id — keeping the runtime deterministic, unlike a random v4 UUID. Empty, oversized, or unusable-namespace requests return a structured failure. A model requests it withTOOL:uuid5:<name>, giving agents stable primary keys, idempotency keys, or correlation ids shared across steps.slugify: converts free-form text into a URL- and filesystem-safe ASCII slug. It strips diacritics, lowercases, collapses every run of non-alphanumeric characters into a single separator (default-, overridable), trims the ends, and can cap the length on a word boundary viamax_length. Empty, oversized, unusable-separator, invalid-max_length, or slug-empty requests return a structured failure. A model requests it withTOOL:slugify:<text>, giving agents deterministic branch names, path segments, cache-file names, and anchor ids from arbitrary text.datetime: normalizes an ISO-8601 timestamp to a canonical UTC form (YYYY-MM-DDTHH:MM:SS+00:00) and reports its Unix epoch and weekday. A trailingZ(Zulu) designator and numeric offsets are both accepted; a naive timestamp fails unlessassume_utc=trueis passed. It reads no wall-clocknow, so it stays fully deterministic. Empty, oversized, unparseable, or naive-without-assume_utcrequests return a structured failure. A model requests it withTOOL:datetime:<timestamp>, giving agents one canonical instant to compare, sort, and log timestamps that arrive in mixed shapes.duration: parses an ISO-8601 duration (PT1H30M,P1DT2H,P2W, with an optional leading-and a fractional smallest component) into its total length in seconds plus a normalized component breakdown. Only fixed-length components (weeks, days, hours, minutes, seconds) are supported; calendar components (years/months) are refused because they have no fixed second length. It reads no wall-clocknow, so it stays fully deterministic. Empty, oversized, calendar, componentless, or unparseable requests return a structured failure. A model requests it withTOOL:duration:<duration>, giving agents one exact scalar for retry backoffs, TTLs, and time budgets.diff: produces a deterministic unified diff between two texts viadifflib. Supply sides astext+other, or as a singletextsplit on the<<<DIFF>>>sentinel (soTOOL:diff:...still works with one payload). Optionalcontextcontrols hunk size (default 3). Empty/oversized sides, ambiguous splits, and invalid context return a structured failure. Gives agents a trustworthy before/after comparison for observations and tool outputs — matching the gap popular agent frameworks fill with a dedicated diff/patch tool.html_strip: strips HTML markup to plain text via the stdlib HTML parser. Documents containing<script>or<style>are rejected; empty or oversized input returns a structured failure. A model requests it withTOOL:html_strip:<html>, giving agents a deterministic way to turn scraped snippets into readable text without inventing or leaking markup.html_table: extracts the first HTML table, or a 1-basedtable_index, and renders it as GitHub-flavored markdown or CSV. It uses stdlibhtml.parseronly, rejects<script>/<style>, caps document/output chars plus rows and columns, and returns structured metadata for GPT-5.5 / Claude Sonnet 4.6 / Gemini 3.x / Kimi K2 workers that need safe tabular observations from HTML.template_render: fills simple{var}or Jinja-like{{ var }}placeholders from scalar JSON variables. It HTML-escapes every substituted value, rejects expressions/filters/attribute access instead of evaluating them, caps template/variable/output sizes, and supports a single directive payload split on<<<TEMPLATE_VARS>>>. A model requests it withTOOL:template_render:Hello {name}<<<TEMPLATE_VARS>>>{"name":"Ada"}for safe, repeatable snippets without raw string surgery.
src/multi_bot_agentic/ runtime, lifecycle, decision engine, event log, adapters
tests/ deterministic unit and integration tests
scripts/ demo and verification scripts
migrations/ sqlite schema scaffold
docs/ architecture, safety, config, quickstart, demo
.github/workflows/ CI for lint, format, typecheck, tests, demo smoke
- Quickstart
- Configuration
- Safety
- Architecture
- JSON Path Tool Guide
- Spreadsheet Slice Tool Guide
- HTML Table Tool Guide
- Template Render Tool Guide
- TOML Format Tool Guide
- YAML Format Tool Guide
scripts/check.shscripts/check.sh runs ruff, format check, mypy, pytest, and a fake-provider smoke run with replay/report. CI runs the same script on Python 3.10, 3.11, and 3.12.
For a richer local demo:
scripts/run_demo.shThe README GIF is reproducible:
python scripts/render_demo_gif.pyThe repo also keeps docs/demo.svg as a static architecture card.
MIT — see LICENSE.
Real issues this agent solves — deterministic ODA loop, rationale traces, durable event log, GPT-5.5 / Claude Sonnet 4.6 / Gemini 3.x / Kimi K2 adapters, and safety controls (timeouts, bounded scope, cancellation).
| Issue | Problem | Solution doc |
|---|---|---|
| #001 | Non-deterministic agent loops hard to debug | doc |
| #002 | Long-running tasks need cancellation | doc |
| #003 | Tool failures should not crash the run | doc |
| #005 | Unreadable files should not crash the run | doc |
| #007 | OpenAI-compatible gateways may return structured content | doc |
| #011 | PII redaction missed parenthesized area-code phone numbers | doc |
| #012 | PII redaction over-redacted non-address dotted numbers | doc |
Full index: docs/use-cases/README.md
- Decision engine — deterministic step selection with logged rationale
- State machine —
created → observing → deciding → acting → succeeded | failed | cancelled - Event log — SQLite/JSON audit trail for replay
- Tool adapters — pluggable HTTP/LLM/retrieval integrations
- Safety — timeouts, cancellation tokens, bounded run scope
