Repository navigation
Releases
Releases · BowlOfData/maki
v0.6.0
[0.6.0] - 2026-11-10
Highlights: maki.eval (compare models through the whole agent), context-window awareness for local models, Agent Skills, a production-hardening pass on the connector and maki serve, much deeper tests, and a cleaner package (no tests in the wheel, mypy-clean, stricter CI).
Upgrade notes
Content-derived URLs (web plugins) can no longer reach loopback; set MAKI_ALLOW_LOOPBACK_URLS=1 to allow it (see Security)
Minimum dependency versions are higher (see Changed)
The wheel no longer ships maki/test; use a source checkout or the sdist to run the suite or the dev tools
Packaging and project
The wheel no longer contains the test suite and dev tools (maki/test); the sdist still does. include-package-data is off, so only declared data (py.typed, the GUI's QML) ships
SPDX license = "MIT" with license-files (needs setuptools >= 77); Development Status :: 4 - Beta classifier
CI: coverage floor (85%), pip-audit, a package job (build, twine check, install the wheel and import it); publishing now runs the test suite first and twine checks the artifacts. Dependabot for pip and GitHub Actions
Type checking: the shipped package is clean under mypy maki (config in pyproject.toml; the test suite and dev tools are excluded) and CI runs it. Fixes were annotations and implicit-Optional defaults, plus two small behaviour-neutral guards; Decision.CONTINUE/STOP are declared ClassVars and aiterate is typed as an AsyncGenerator
Added SECURITY.md, CONTRIBUTING.md (with the stability and deprecation policy), CODE_OF_CONDUCT.md, CODEOWNERS, issue and PR templates; .vscode/ is no longer tracked
Testing
Mutation check (python -m maki.test.mutation_check FILE --tests ...): changes one thing at a time in a source file (a comparison flipped, and/or swapped, a not dropped, a constant changed, a raise removed, a return value replaced) inside throw-away copies of the package and reports which changes no test noticed. Used to find and close gaps in the SSRF validators, the connector and the server
Load and soak check (python -m maki.test.load_check): the real server stack over real sockets with a mixed workload (valid calls plus malformed JSON, oversized bodies, missing and wrong credentials, unknown paths, wrong methods) that samples memory, threads and file descriptors and fails on errors, hangs or drift
Adversarial tests: every file plugin against 30 hostile paths (traversal, symlinks out, a sibling directory sharing the base's prefix, NUL bytes, odd separators) with a check that nothing outside base_dir changes; SSRF differential and fuzz tests against an independent oracle (every IANA special-purpose range, IPv6 forms that embed IPv4, thousands of spellings of 127.0.0.1 and 169.254.169.254); a check that every server route rejects missing or wrong credentials; concurrency stress tests for the shared usage ledger, a shared fallback chain, one agent used from many threads, the served app and async fan-out with cancellations
Tests for connector edge cases (TLS settings, request-body replay, mid-stream failures, async error classes), server error reporting and response shapes, and the request-size limiter
Fixed
The SSRF guard now refuses multicast, the whole 6to4 range (including 2002:7f00:1::, which stands for 127.0.0.1, and the metadata address), Teredo, documentation and other non-global ranges, and judges IPv6 forms that embed an IPv4 address (mapped, NAT64, IPv4-compatible) on the IPv4 address they carry. These were found by the differential test; the 6to4 and Teredo cases only matter on hosts with such tunnels, but a guard like this should not depend on that
A malformed URL such as http://[::1 raised a bare ValueError instead of MakiValidationError
POST /loop/{id}/resume on a loop that was still running answered 404 "Loop not found" when it had no checkpoint yet; it now answers 409 "this loop is still running"
A 429 retry could resend an already-consumed file upload as an empty body when files= was given in the list form [(name, (filename, fileobj))]
Security
Content-derived URLs can no longer reach loopback. The connector used by the web plugins refused private ranges but allowed 127.0.0.1 and localhost, so a prompt-injected page could point an agent at services on the same machine (Ollama, a database console, an unauthenticated maki serve). Loopback is now refused, including ::ffff:127.0.0.1 and legacy spellings (127.1, 2130706433) that resolve to it. Set MAKI_ALLOW_LOOPBACK_URLS=1 to permit it. Endpoints you configure yourself (Ollama, llama.cpp, vLLM, remote agents, RAG) are unaffected. Behaviour change: a web plugin can no longer fetch a page served from the same machine unless you set the switch
maki serve: every request body is now capped (default 4 MiB, --max-body-mb; 413 beyond it, including chunked uploads without a Content-Length), where before only /loop was
maki serve: the API key can come from MAKI_API_KEY (the --api-key flag still works but now warns that it is visible in the process list); a server listening off-loopback without a key, or without TLS and a key, prints a warning
maki serve: with an API key set, /docs, /redoc and /openapi.json are switched off (they were served unauthenticated)
Connector error messages and retry log lines mask credential-looking query parameters (?api_key=, ?token=, ?key=, ...)
Agent.remember() (and so POST /memory/set) refuses new keys beyond Agent.MAX_MEMORY_ENTRIES (10,000) and keys over 512 characters, so a caller cannot grow an agent's memory without end
Changed
Minimum dependency versions raised to the oldest releases with no published security advisory (pip-audit; the suite passes on exactly these): requests>=2.33.0, httpx>=0.28.1, python-dotenv>=1.2.2. The old floors (requests>=2.25.0 and the like) allowed versions with dozens of advisories. urllib3>=2.8.0 is now declared because the connector imports it directly (it was only an undeclared transitive dependency)
maki serve: /health is an async endpoint and answers while every worker thread is busy waiting for the agent (verified: 40 of 40 probes in 10 ms with 60 queued tasks); new --max-concurrency (503 beyond N requests in flight) and --graceful-timeout (default 30 s for in-flight requests on shutdown); the OpenAPI version now reports the real maki version instead of 0.1.0
Added
maki.eval: run_eval(cases, models, runs=) compares models on tasks run through the whole agent (tools, memory, policies) and reports pass rate, median time and tokens per model, plus a per-case matrix and JSON. Cases check with contains, a check(answer[, agent]) callable, or a judged criteria; each run gets a fresh agent, runs are grouped by model and a warm-up keeps local model load time out of the timings. Built-in suites basic, tool_calling, long_context and memory (maki.eval_suites); CLI python -m maki eval --suite ... --models ... --runs N [--no-think] [--judge] [--json]
Context-window awareness (maki.context): a local server that silently cuts a prompt is now noticed. Ollama, llama.cpp and vLLM backends compare the prompt tokens the server reports with what was sent and, on a large shortfall, log it (last_truncation) or raise MakiContextError (on_truncation="warn" | "error" | "ignore"). Verified live: Ollama at num_ctx 2048 evaluated 1,027 of about 7,900 tokens
MakiLLama(num_ctx=..., num_ctx_cap=...): an integer sets Ollama's context window; "auto" picks one value per model (reusing an already loaded window, else the smallest of 8k/16k/32k that fits the agent's budget with headroom, capped) and keeps it, since changing it reloads the model. Agent hints its history budget to the backend. context_info() reports a server's context: Ollama (/api/show, /api/ps), llama.cpp (/props), vLLM (model list max_model_len)
Agent Skills (maki.skills): Agent(skills=True | folders | SkillRegistry) adds the open SKILL.md format shared with Claude Code and Codex. The model sees a size-capped catalog and two read-only tools, skills.load and skills.read (confined to the skill's folder); skills are looked up in explicit folders, then project .maki/.agents/.claude skills folders up to the git root (opt-in with trust_project=True), then the same under ~. Skill scripts are never executed and allowed-tools is ignored. skills: is also an agent YAML section
v0.5.0
[0.5.0] - 2026-11-07
Added
Typed messages: Message can carry tool_calls, a tool result (role="tool" with tool_call_id and name) and a stored tool_trace; ToolCall and LLMResponse (new tool_calls field) serialise them. New fields are written only when set, so plain messages and old snapshots are unchanged
LLMBackend.chat_turn(messages, tool_specs, system=, config=): one tool-aware round over typed messages that returns an LLMResponse with tool_calls (every call gets an id, invented when the provider has none) and real token counts on tool rounds. Implemented by the Ollama, OpenAI, OpenRouter, llama.cpp, vLLM and Anthropic backends (supports_typed_messages, to_wire_messages); the dict-based chat_with_tools / append_tool_results still work for custom backends
The agent's native tool loop runs on typed messages for those backends, and a FallbackBackend of typed backends hands a half-finished tool loop to the next backend as it is (no flattening to text)
MemoryPolicy(tool_history=True) (needs history_mode="messages"): the tool calls and results behind each answer are remembered, clipped to tool_result_chars, counted against the token budget, replayed ahead of that answer and described to the summariser. Also settable under memory: in agent YAML
Conformance suite: typed-turn contract tests for every backend adapter; live typed tool-round and tool-history checks against Ollama and OpenRouter
Changed
Tool-call rounds are now counted in agent.usage (they were not before: only text answers were)
With a typed backend, AgentCallback.on_model_start receives typed Message objects instead of provider dicts, and on_model_end receives the response (with token counts) on tool rounds instead of None
The agent's closing round after ToolPolicy.max_rounds now offers the tools with tool_choice="none" (Anthropic insists on tool definitions while the history holds tool blocks) and carries a short "answer now" instruction, because some local models return nothing when simply asked to continue after a tool exchange (seen with gemma4). The instruction is not stored in the remembered trace
The async per-agent gate also works on event loop implementations that cannot be weakly referenced
Anthropic: an empty tool result is sent as "(no output)" because empty tool_result content is not reliably accepted
Remembered tool-call arguments are clipped to tool_result_chars like results, so a large write is not replayed whole
Anthropic: the text of a response is the join of its text blocks (a response that opens with a tool_use or thinking block no longer raises)
v0.4.6
[0.4.6] - 2026-11-04
Added
Async API: Agent.aexecute_task, aexecute_task_with_retry, astream_task and arun_loop; AgentManager.aassign_task, agather_tasks (concurrent across agents, results in order, optional return_exceptions), acollaborative_task and arun_workflow; AgentProxy.aexecute_task and astream_task. They run the existing synchronous code on worker threads (maki/aio.py), so memory, tool policies and callbacks behave as before; one agent still runs one task at a time and different agents overlap. Closing astream_task early stops the stream and frees the agent. Context variables (trace ids, logging context) reach the worker thread, and calls queued on one busy agent wait on the event loop rather than each holding an executor thread, so they cannot starve other agents
429 handling: the HTTP backends (Connector and AsyncConnector) retry a 429 up to MAKI_RATE_LIMIT_RETRIES times (default 2), waiting the server's Retry-After or a 1s/2s/4s backoff, and raise at once when Retry-After exceeds MAKI_RATE_LIMIT_MAX_WAIT (default 30s). MakiAPIError.retry_after carries the server's value for every backend (SDK ones included), and FallbackBackend keeps a rate-limited backend out of rotation at least that long. Retry-After accepts seconds or an HTTP date (a zone-less date is read as UTC). A request whose body is a stream or file is never replayed, so it is not retried
AgentManager.add_agent() accepts stateful, allow_dangerous_tools, memory, tool_policy and callbacks (keyword-only) and passes them to Agent, so a manager-created agent no longer needs them set afterwards
v0.4.5
[0.4.5] - 2026-11-03
Added
maki/test/test_backend_live_openrouter.py: nine contract checks against the real OpenRouter API (chat, history, streaming, async, usage, the native tool loop, a typed plugin through an Agent, an unknown model, fallback from a dead backend), deselected by default and skipped without OPENROUTER_API_KEY; run with pytest -m network
FallbackBackend([...]): tries backends in order and moves to the next on a transient failure (network error, timeout, 408/429; configurable with on=). Spec strings such as "openai:gpt-4o" are accepted, streaming falls back before the first chunk, and a native tool loop can switch backend mid-turn without running a tool twice. Agent YAML accepts a fallback: list of provider:model strings or backend mappings (base_url, temperature, ...). A backend that just failed is tried last for cooldown seconds (default 30) so a dead primary does not cost its full timeout on every call; async_chat() and session() are supported
Changed
README and the architecture and backends docs describe init_backend, FallbackBackend, typed tool schemas and policies, and the summary memory tier
v0.4.4
[0.4.4] - 2026-11-03
Added
maki/test/test_backend_live.py: ten contract checks against a real Ollama server (chat, system/history, streaming, async, usage, the native tool loop, a typed plugin through an Agent, 404 and unreachable-server errors), deselected by default; run with pytest -m network
python -m maki.test.summary_bench: a live benchmark of the summary tier. On gemma4:26b it recalled 4/4 early facts where dropping turns recalled 0/4, with 42% fewer prompt tokens than keeping everything (see docs/user/agent-memory.md)
Shared backend conformance suite (maki/test/backend_conformance.py, run by test_backend_conformance.py): one set of contract tests applied to Ollama, OpenAI, Anthropic, OpenRouter, llama.cpp and vLLM through per-backend transport fakes. New backends join by writing an Adapter
Summary memory tier: MemoryPolicy(summarize_older=True, summary_chars=...). Turns evicted from a stateful agent's conversation are folded by the model into a running summary kept at the top of the history (sent as text or as structured history), instead of being dropped. It runs before the next task, counts against token_budget, falls back to the [N earlier turn(s) dropped] note if the summary call fails, can be replaced with agent.summarizer, and is saved by to_dict/from_dict. Also settable under memory: in maki serve YAML
Typed tool schemas: native tool specs are now built from a plugin method's type hints (int, float, bool, list[T], Optional, Literal, Enum) and the Args: section of its docstring, instead of treating every parameter as a string. Unannotated parameters stay strings
Tool-call argument validation: the model's arguments are checked against the method signature on both the native and TOOL: paths ("3" becomes 3, enum values become enum members, also inside lists; unknown, missing or mistyped arguments, and a null for a required parameter, return a message the model can correct and the method is not called; a null for a parameter with a default uses the default)
ToolPolicy and Agent(tool_policy=...): max_rounds, per-tool retries for transient errors with exponential backoff and jitter, and opt-in parallel execution of one turn's tool calls (results keep the model's order). Methods in a plugin's DANGEROUS_METHODS are never retried (unless retry_dangerous=True) and a turn containing one runs sequentially. Serialised by to_dict/from_dict; also settable under tool_policy: in maki serve YAML
Tool descriptions use the docstring's summary paragraph instead of only its first line, and async def plugin methods (e.g. image_classifier.classify_image_async) are now awaited instead of returning a coroutine's repr. Values for unannotated parameters are passed through unchanged. Tokens a plugin spends on helper threads (parallel tools, async tools) count toward the calling thread, so loop token caps see them; nan/inf are rejected as numeric arguments; one unresolvable annotation no longer disables validation of the others
init_backend("provider:model"): build any backend from one string ("ollama:gemma4:26b", "openai:gpt-4o", or a bare provider for its default model), with a temperature= shorthand. Agent YAML accepts the same form in model: when no backend: is given
AgentCallback, LoggingCallback and Agent(callbacks=[...]): task, model and tool events (on_tool_start/end/error/retry, on_model_start/end, on_task_start/end/error). A callback that raises is logged and skipped
Changed
OpenAI, Anthropic, OpenRouter, llama.cpp and vLLM now classify HTTP errors exactly as the Connector (and so Ollama) always did, through the new maki.exceptions.error_for_status: 5xx become MakiNetworkError and 408/504 MakiTimeoutError (both retried by execute_task_with_retry, and a maki serve backend failure answers 502/504), while 4xx stay MakiAPIError with status_code. Before, every status from those SDKs was a MakiAPIError: a provider outage was neither retried nor reported as an unavailable backend. 429 (rate limit) is still a MakiAPIError everywhere
Fixed
from maki import * no longer fails when transformers is not installed: HFBackend is still importable by name but no longer listed in __all__
The live Ollama tests no longer contact the server while pytest merely collects them
The HFBackend tests now skip when transformers is missing (they only checked for torch), and the Ollama async error path is tested through the real AsyncConnector
v0.4.2
[0.4.2] - 2026-11-01
Added
MemoryPolicy and Agent(memory=...): opt-in limits on what an agent keeps and sends, all off by default. history_mode="messages" sends a stateful agent's prior turns as structured chat history (backends declare supports_history; the others keep the text form), token_budget sets the conversation budget, tool_result_chars clips plugin results on both the native and TOOL: paths, and history_entry_chars bounds what task_history and reasoning_history store. maki serve YAML accepts the same fields under memory:; to_dict/from_dict round-trip the policy
Tiered memory: MemoryPolicy(pinned_chars=..., recent_turns=..., older_turn_chars=...). agent.remember(key, value, pin=True) puts a fact in the system message of every call and never evicts it (operator-only and size-capped: a plain remember() on a pinned key raises, /memory/set answers 409, and neither clear_memory() nor DELETE /memory removes pins, only unpin() does; pins survive reset_conversation() and to_dict()); the last recent_turns turns stay verbatim and older messages are cut to older_turn_chars, and cut turns count at their shortened size against the budget. When turns are evicted the first remaining message is prefixed [N earlier turn(s) dropped]
LLMResponse.cached_tokens and Usage.cached_tokens: prompt tokens served from the provider's cache, reported by the Anthropic and OpenAI backends and by streaming chat_collect() on OpenAI-compatible servers such as vLLM and llama.cpp (0 elsewhere)
maki/test/memory_bench.py: an offline benchmark of prompt tokens, recall and history size (python -m maki.test.memory_bench)
Documentation
New guide docs/user/agent-memory.md: what goes wrong in long conversations, the MemoryPolicy settings, pinned facts and how to choose values, with measured results. In a live test on Ollama gemma4:26b (4096-token window, one run per setting), a 13-message chat that stated a codename in message 1 was answered wrongly by the default setup (Response: Response: Response:) and correctly by both policy settings, which sent about 45% less text (108,670 to 59,462 characters). Offline: 51% fewer prompt tokens over 20 turns, and 90% less in a large-tool-result run and in stored history. The token estimate (len // 4) ran low on log text, so keep token_budget near half of the model's window
memory.md now covers only Obsidian and RAG and links to the new guide; the README, docs index, nav and agents.md link to it
v0.4.1
[0.4.1] - 2026-11-01
file_mover plugin: move(src, dst, overwrite=False, dry_run=False, create_dirs=False), undo_last() and list_moves() move and rename files and directories identically on Linux, macOS and Windows (pure Python, no shell). Atomic rename on one volume; across volumes it copies, verifies size and SHA-256, moves the copy into place and only then removes the source. Case-only renames work on case-insensitive disks; symlinks move as links. dst is always the full new path; conflicts are refused and overwrite=True replaces a file with a file only (and cannot be undone). Scope is the operator's: base_dir by default, plus allowed_roots, or allow_any_path=True; the model cannot widen it. Every move is written to a monthly journal in ~/.maki/moves (retention_months, default 12) and undo_last() reverses only the latest move, refusing if the item changed since. Windows file-name rules apply on a Windows host, or everywhere with strict_names=True. move and undo_last need Agent(allow_dangerous_tools=True). list_moves and undo_last only see journal entries inside the plugin's own scope, so agents with different scopes can share a journal safely
Agent.load_plugin(name, plugin_path=None, **options) forwards options to the plugin's register_plugin(maki, **options)
CI runs the file_mover tests on macOS and Windows (Windows is best effort)
Documentation
docs/user/plugins.md lists file_mover and postgres_store and describes load_plugin options; plugin count corrected to 18 in the README and index
docs/user/loops.md and examples/loop_fix_tests.py: on_iteration is not called for the iteration that ends the loop; the example prints that last event from result.events