Changelog¶
Unreleased¶
Models¶
- New Model strings carry an endpoint and capabilities inline (
aimu.models.model_client.resolve_model, mirrored on the async path). The text model string grammar is nowprovider:model_id[@base_url][;flags]. Appending@<base_url>overrides the endpoint for the OpenAI-compatible local-server providers (llamaserver,lmstudio,vllm,hf-openai,sglang,ollama-openai), so a single string can target a remote llama.cpp / vLLM server. A new genericopenai-compat:<model_id>@<base_url>prefix reaches any OpenAI-compatible server not tied to a known provider (the@<base_url>is required there). A model id not in the provider catalog is allowed for these providers when its capabilities are declared with;<flags>(comma-separated fromtools,thinking,vision,audio,structured); such ids resolve to a newAdHocModel(exported fromaimu.models) instead of raising. Known ids keep their catalog spec and reject;flags. Cloud providers (openai,gemini) and non-OpenAI-compat providers (anthropic,ollama,hf,llamacpp) reject@<base_url>with an actionable error. No authentication is added;api_keystays unset. Tests:tests/test_model_string.py,tests/test_adhoc_model.py,tests/test_resolve_model.py,tests/test_model_client_base_url.py,tests/test_aio_model_client_base_url.py. - New
ModelConnectionErrorwhen an inference server is unreachable (aimu.models.base, exported fromaimu.modelsandaimu.aio). The OpenAI-compatible clients (aimu.models.providers.openai_compat,aimu.aio.providers.openai_compat; sync + async, streaming + non-streaming) now catch the OpenAI SDK'sAPIConnectionErrorat thechat.completions.createcall (and during stream consumption, where a mid-stream drop can surface it) and re-raise it asModelConnectionErrorfrom the original error, so the specific transport cause (e.g.httpx.ConnectError: [Errno 61] Connection refused) is preserved on the exception chain. This mirrors the existingMCPConnectionError/A2AConnectionErrorwrappers and lets a front end distinguish "server is down" from a generic failure instead of receiving a raw, provider-specific exception. OnlyAPIConnectionErroris wrapped; genuine HTTP/API errors still propagate with their own detail. Tests:tests/test_openai_compat_connection_error.py. - New Gemma 4 model catalog for the OpenAI-compatible providers (
aimu.models.providers.openai_compat). Added the full suite (GEMMA_4_E4B,GEMMA_4_12B,GEMMA_4_26B,GEMMA_4_31B) to every local-server enum (OllamaOpenAIModel,LMStudioOpenAIModel,VLLMOpenAIModel,HFOpenAIModel,LlamaServerOpenAIModel,SGLangOpenAIModel), replacing the loneGEMMA_4_12Bentry each previously carried. Capabilities are set from Google's Gemma 4 model card:tools=True, thinking=True, vision=Trueon all four (thinking surfaces over OpenAI-compat via<think>-tag parsing). Provider-appropriate ids include the MoEgoogle/gemma-4-26B-A4B-itand the densegoogle/gemma-4-31B-itfor the HuggingFace-repo servers.vision=Truewas also backfilled onto the existing Gemma 3/4 entries. Audio is deliberately left off (only E4B/12B are natively audio-capable, and audio input isn't reliably exposed by these local servers); each enum carries an inline comment recording the transport-specific reason. The async providers inherit these enums, soaimu.aiopicks up the new members automatically.
Agents and workflows¶
- Fix Agentic loop no longer ends silently on a degenerate turn (
aimu.agents._tool_loop,aimu.aio._tool_loop; both the plain and streamed paths). The loop treated any turn without tool calls as the final answer, so a model that returned an empty turn (no content and no tool calls — common with small local models, e.g. after a tool result mid multi-step plan) ended the run with an empty string and abandoned the plan; and hittingmax_iterationswith a tool call still pending returned the dangling tool-only turn. The loop now classifies its terminal turn (classify_terminal_turn→ pending-tools / empty / healthy) and guards both cases: an empty turn is nudged withcontinuation_prompt(tools still enabled, so the model can resume its plan), bounded bymax_iterations; at the cap with tools pending it forces one tools-disabled wrap-up turn (use_tools=False). The forced wrap-up is now unconditional — previously it required an opt-infinal_answer_prompt, which now only customizes the wrap-up prompt (a built-inDEFAULT_WRAP_UP_PROMPTis used when unset). If even the wrap-up yields no answer, the loop raises the newDegenerateTurnError(exported fromaimu.agentsandaimu.aio) instead of returning empty output. Injected continuation nudges are taggedPROVENANCE_CONTINUATION(revived) so a UI can hide them. Behavior change: an agent left at the defaultfinal_answer_prompt=Nonethat reaches the cap with pending tools now performs a wrap-up turn rather than returning the dangling turn.continuation_prompt(previously wired but never invoked) is now threaded into the loop and used for empty-turn recovery. Tests:tests/test_agents.py,tests/test_aio_agents.py,tests/test_vision.py.
Fixes¶
- Fix OpenAI-compat and llama-cpp streaming chat is now incremental (
aimu.models.providers.openai_compat,aimu.aio.providers.openai_compat,aimu.models.providers.llamacpp;_chat_streamed).chat(stream=True)drained the entire upstream stream into a buffer before yielding any chunk, so every OpenAI-compatible provider (llama-server, LM Studio, vLLM, HF-Serve, SGLang, ollama-openai) and in-process llama-cpp delivered the whole response as one end-of-generation burst instead of token-by-token; nativeOllamaClient, which yields per part, was unaffected (which is why streaming appeared to work there but not via the OpenAI-compat path). The buffering existed to detect tool calls before yielding content, but content and tool-call deltas are separate in the OpenAI streaming protocol, so content/thinking chunks are now yielded as they arrive while tool-call deltas accumulate independently (matching the already-incremental_iter_streamused bygenerate(stream=True)). No caller-visible change beyond incremental delivery; message recording, thinking-key attachment, tool handling, and usage capture are preserved. Tests:tests/test_models_api.py,tests/test_aio_models_api.py. - Fix Thinking from
reasoning_contentis no longer dropped on OpenAI-compat / llama-cpp (same modules;_chat,_generate,_chat_streamed,_iter_stream, sync + async). Servers that parse reasoning tags server-side (llama-server with the default--reasoning-format deepseek/auto, vLLM/SGLang reasoning parsers) return reasoning in a separatereasoning_contentfield and strip<think>tags fromcontent. The clients only parsed inline<think>tags, so on these servers thinking was silently lost (e.g.gemma-4-31b-iton llama-server emitted noTHINKINGchunks and leftlast_thinkingempty). The clients now readreasoning_contentoff the delta/message and surface it asTHINKING(streaming) or store it inlast_thinking(non-streaming); when present it takes precedence over the<think>parser (which stays for servers that inline tags) and is not gated onsupports_thinking(if the server sent it, it is reasoning). Tests:tests/test_models_api.py,tests/test_aio_models_api.py. - Fix
HAS_LLAMACPPno longer reports installed when llama-cpp-python is absent (aimu.models.providers.llamacpp). The module deferredfrom llama_cpp import LlamaintoLlamaCppClient.__init__, so the provider module imported cleanly without the dep and every guarded import (aimu.models.model_client,aimu.models.__init__,aimu.aio.providers.llamacpp) setHAS_LLAMACPP = Trueas a false positive.llamacppthen appeared inresolve_model's "available providers" list and in_provider_registry(), only to fail later at client construction. The module now does a hard top-levelimport llama_cpp(matching the diffusers/soundfile convention that keepsHAS_HF_IMAGE/HAS_HF_AUDIOtruthful); theLlamaweights are still loaded lazily in__init__. With the dep uninstalled,llamacppcorrectly drops out of the registry. - Fix
resolve_modelstops advertisingopenai-compatwhen its extra is missing (aimu.models.model_client; the async path reuses the same resolver). The "unknown provider" error unconditionally appendedopenai-compatto the "available providers" list even whenHAS_OPENAI_COMPATwasFalse, so the message contradicted itself: it namedopenai-compatas available, and using it then failed with a differentImportError("requires the openai-compatible extra"). The list now includesopenai-compatonly when theopenai_compatextra is installed. - Fix Ollama thinking + multi-tool-call turn crash (
aimu.models.providers.ollamaandaimu.aio.providers.ollama). Non-streaming_chatrecorded the turn'sthinkingontoself.messages[-1 - len(tool_calls)], but_record_tool_callsappends exactly one assistant message, so the offset pointedlen(tool_calls)messages too far back: it wrotethinkingonto an earlier message, and on a short history (e.g. a freshly spawned sub-agent's first turn,messages=[user, assistant]) with two or more tool calls it raisedIndexError: list index out of range, surfacing asTool call 'spawn_subagent' failed: list index out of range. Now indexes the just-appended assistant message (self.messages[-1]), matching the streaming path and the OpenAI-compat provider. Tests:tests/test_ollama_streaming.py.
Tools¶
- New web-interaction tools in
aimu.tools.builtin.get_webpage_html(url)is a stateless@tool(added tobuiltin.web+ALL_TOOLS+ the MCP server) that returns a page's raw HTML markup (truncated), complementing the existing text-strippingget_webpage.make_web_tools(*, session=None, timeout=15, max_content_chars=20000, user_agent=...)is a factory returning[find_forms, submit_form]closing over a sharedrequests.Session, so cookies persist across calls and a GET-then-POST form flow works:find_forms(url)parses every<form>(stdlibhtml.parser; no new dependency) into a listing of resolved-absolute action / method / fields includingtype=hidden(CSRF tokens surface), andsubmit_form(url, method="POST", data=None)submits via POST (form body) or GET (query params), returning status + final URL + truncated body —method="GET"doubles as a session-aware raw fetch for pages behind a login. Pass them together:Agent(client, tools=[get_webpage_html, *make_web_tools()]). Server-rendered HTML only (no JavaScript execution): JS-rendered SPAs and anti-bot-protected pages are out of scope; a headless-browser backend is a possible future addition.submit_formis the mutating tool — gate it via thetool_approvalhook when confirmation is wanted. Both are re-exported fromaimu.aio.tools.builtin(dispatched viaasyncio.to_thread). How-to: Fetch HTML and submit web forms. Tests:tests/test_web_tools.py. - Change
@tooldocstring and schema generation (aimu.tools.decorator). The model-facing description is now the full prose before the first Google-style section header (Args:,Returns:, ...) rather than only the first paragraph, so guidance placed in later paragraphs is no longer silently dropped. AnArgs:/Arguments:/Parameters:section is parsed into per-parameterdescriptions (name: textorname (type): textentries, with more-indented continuation lines joined), and aLiteral[...]parameter now emits a JSON Schemaenum(with the element type when the literals are homogeneous) advertising the exact allowed values instead of a bare"string". Previously an opaquedict/Literalparameter reached the model as a structureless{"type": "object"}/"string"and multi-paragraph guidance was truncated, so models routinely guessed wrong argument shapes._json_type_for(used by structured-output schema generation) is unchanged; the tool path uses a newLiteral-aware_schema_for. Tests:tests/test_tool_decorator.py.
Internal¶
- Change De-duplicated identical sync/async code into shared homes (no public API or behavior change). Three blocks that were byte-for-byte identical between the sync surface and its
aimu.aiotwin now live in one place, continuing the established sharing pattern (_ChatStateMixin,_internal/streaming.py, the composed provider format adapters, the shared tool-loop terminal classification) rather than inverting the sync/async dependency: - The tool-call recording helpers (
_prepare_tool_calls,_append_assistant_tool_calls,_record_tool_calls) and structured-request resolution (_structured_request) moved fromBaseModelClient(aimu/models/_base/text.py) andAsyncBaseModelClient(aimu/aio/_base.py) into the shared_ChatStateMixin(aimu/models/_internal/chat_state.py) that both already inherit. - The
async-free members of the tool-loop engine (__init__,_current_tools,_pending,_tag_injected,_wrap_up_prompt,_tool_call_kwargs,_not_approved) extracted into a new_BaseToolLoopin aimu/agents/_tool_loop.py, subclassed by both_ToolLoopandaimu.aio._tool_loop._AsyncToolLoop; only the loop drivers and dispatch (threads vsasyncio.TaskGroup,await) stay per-surface. - The near-identical in-process async wrappers
AsyncHuggingFaceClient/AsyncLlamaCppClientreduced to ~5-line subclasses of a new_AsyncInProcessClient(aimu/aio/providers/_inprocess.py) that holds the state-sharing properties,_generate/_chat, and_stream_via_thread(each subclass sets onlyMODELS+_SYNC_CLASS; the wrap-refusal error is preserved).
Net −339 lines. Verified by the existing mock suites (models, structured, agents, tools, decorator, approval, provenance, checkpointing — sync + async).
Documentation¶
- Change The
notebooks/tutorial collection migrated from Jupyter.ipynbto plain-text Quarto.qmd(markdown with executablepythoncells), so notebooks diff cleanly and are easy to edit or hand to an AI assistant. Files are renamed to kebab-case (01 - Model Client.ipynb→01-model-client.qmd), and anotebooks/_quarto.ymlmakes the set a browsable Quarto website (quarto preview notebooks/). Notebooks are not executed at render time (execute: eval: false): most need a live backend (Ollama, a cloud API key, or a GPU) and gracefully skip; opt a cheap notebook intoeval: true+freeze: autoper file. The docs site (MkDocs + Material) is unchanged; how-to/tutorial deep-links now point at the.qmdfiles. The[notebooks]extra is nowjupyter+jupytext(the latter lets JupyterLab open the.qmdfiles as native notebooks); the Quarto CLI installs separately (a standalone binary). Convention documented in Contributing.
v0.12.0 (2026-07-14): tool-calling refactor, dynamic sub-agent spawning¶
Agents and workflows¶
- New dynamic sub-agent spawning:
aimu.tools.builtin.make_subagent_tool(model, *, system_message=, tools=, agent_types=, max_depth=1, ...)(async twinaimu.aio.tools.builtin.make_async_subagent_tool) returns aspawn_subagent@toolthat lets an agent delegate an independent subtask to a fresh sub-agent with its own isolated context at runtime — AIMU's answer to the Claude-Code-styleTaskpattern. It is the dynamic complement toOrchestratorAgent: an orchestrator dispatches to a fixed roster wired up front, whilespawn_subagentlets the LLM decide the fan-out. Two shapes: genericspawn_subagent(task)(a general-purpose sub-agent) or, withagent_types=, typedspawn_subagent(agent_type, task)over a registry of named specialists (unknown types are returned to the model to self-correct; the menu is listed in the tool description). Each spawn builds a freshModelClient(isolated history, themake_workersidiom); parallelism is free — give the parentconcurrent_tool_calls=Trueand multiple spawns in one turn run concurrently (ThreadPoolExecutorsync /asyncio.TaskGroupasync).max_depth(default 1) bounds recursion. Non-streaming by design (keeps the concurrent path and avoids interleaving). Composes as a plain tool (no newRunnersubclass). How-to: Spawn sub-agents; demo:examples/news-summarizer --method spawn. - New approval gate for spawned sub-agents:
make_subagent_tool/make_async_subagent_toolgained atool_approval=parameter — the same(name, arguments) -> boolhookAgentaccepts — forwarded into every spawned sub-agent (and, withmax_depth > 1, into the sub-agents they spawn). A parent can route a delegated sub-agent's tool calls through its own approval policy instead of letting them run unattended; the default is unchanged (approve_all). How-to: Gate a sub-agent's tools. - Change Tool calling is now split into three layers, one responsibility each, so the boundaries are apparent:
- Model client — a pure provider adapter.
chat()is a single model turn: it advertises thetools=it's given, issues one request, parses any tool calls, and stores them on the assistant message (content+tool_calls+ thinking) without executing them. It no longer holds a persistent tool registry or any approval/deps/concurrency state (self.toolsis an internal per-call transient defaulting to[]). - Tool-loop engine (internal) —
aimu.agents._tool_loop._ToolLoop(sync) /aimu.aio._tool_loop._AsyncToolLoop(async) owns the iterative tool-calling logic: when a turn requests tools it dispatches them (arg coercion, approval,ToolContext(deps)injection,concurrent_tool_calls), appends therole:"tool"results, and calls the client again until a turn makes no tool calls (bounded bymax_rounds), then the optionalfinal_answer_promptwrap-up. Not public API; the ladder stayschat()→Agent→ workflows. Agent— autonomy + composition. Configures and drives the engine (tool callables,deps,tool_approval,concurrent_tool_calls,max_iterations,final_answer_prompt), and adds identity,Runner,as_tool(),as_model_client(),restore(),from_config(), and theschema=short-circuit. Tool config lives on theAgent(fields + per-runrun(tools=/deps=/tool_approval=)overrides); theAgentnever pushes it onto the model client.
This removes the old double-generation (a tool-using turn used to produce the answer twice) and the muddiness of the client both parsing and executing tools. chat() gained an optional user_message (default None = "run a turn on the current messages, appending no new user turn" — the continuation primitive the engine uses). Applies to sync + aimu.aio, non-streaming + streaming, every provider, and FallbackClient. Behavior change: a bare client.chat("q", tools=[...]) now parses and stores the tool call but does not execute it — use Agent(client, tools=...).run("q") (or agent.as_model_client()) for a full tool-using answer. Removed from the model client: _handle_tool_calls / _handle_tool_calls_streamed / _call_plain_tool and the tool_approval / tool_context_deps / concurrent_tool_calls attributes (they live on the Agent + engine now). Anthropic stores the real tool_use block IDs directly (folding in the old _patch_tool_ids). The async→sync tool bridge (aimu.aio.providers._sync_tool_bridge, added in v0.9.0) is removed: it existed only so the sync _chat's in-thread tool dispatcher could call async tools, but the sync _chat no longer dispatches (it just advertises + stores), so wrapped in-process async clients (AsyncHuggingFaceClient / AsyncLlamaCppClient) now pass async tools straight through and the async engine dispatches them. Deprecated (kept as accepted no-ops): Agent.continuation_prompt / DEFAULT_CONTINUATION_PROMPT; PROVENANCE_CONTINUATION is no longer produced (kept for legacy transcripts). max_iterations and final_answer_prompt are unchanged.
v0.11.0 (2026-07-05): personal-assistant primitives, streaming structured output, sessions¶
Models¶
- New streaming structured output:
schema=now combines withstream=Trueonchat()/generate()(sync +aimu.aio), lifting the previousValueError. The call returns aStreamChunkiterator so thinking / generation stream live, then a terminalDONEchunk carries{"result": <validated object>}; the object is also stored onclient.last_structuredonce the stream is consumed (mirrorslast_usage; proxied throughModelClient/_AgenticView/FallbackClientand their async twins, cleared byreset()). Aninclude=filter still applies to the thinking/generation phases, but the terminal result chunk is always emitted. Ollama and OpenAI-compatible thinking models stream thinking alongside the schema-constrained answer (Ollama now threadsformat=into its streamed call); Anthropic streams the answer JSON as it is built (GENERATING viainput_json_delta) with no thinking, because its structured mode is a forcedtool_choicethe API forbids alongside extended thinking (no regression: Anthropic structured output never produced thinking). Also threaded throughAgent.run(schema=..., stream=True)(sync + asyncAgent/SkillAgent). Docs: Get structured output. - Fix Ollama streaming (
OllamaClient+AsyncOllamaClient,chat(stream=True)) sometimes dropped a tool call and streamed an empty response instead. The logic decided tool-vs-answer from the single part that ended the thinking stream, so when Ollama emitted an empty transitional part (content="", notool_calls) before the part carryingtool_calls, the tool call was missed and one or more emptyGENERATINGchunks were yielded (a stray/empty response bubble in the web UI)._chat_streamednow consumes each turn fully, collectingtool_callsfrom any part and yielding only non-emptyGENERATINGchunks (which also drops the cosmetic empty trailingdonechunk). As defense-in-depth, the personal-assistantWebChannelandaimu.aio.CLIChannelskip emptyGENERATINGchunks. - Fix local thinking models now record their reasoning in
self.messagesconsistently. The llama-cpp and OpenAI-compat local-server clients (sync +aimu.aio) previously dropped a turn's reasoning from the conversation entirely, keeping it only on the (overwritten-each-call)last_thinking, while HuggingFace and Ollama attached it to the assistant message under a"thinking"key. All four now attach it under the same key (omitted when a turn produced no reasoning), so per-turn reasoning is uniformly available for UI display (examples/web/streamlit_chatbot.py) andConversationManagerpersistence. This also covers the tool-call turn in an agentic loop: the reasoning that precedes a tool call is attached to the assistant message carryingtool_calls(matching the existing HuggingFace/Ollama behavior), so every assistant message that had reasoning carries its own. The"thinking"key is inert metadata: chat templates and request adapters read onlyrole/content/tool_calls, so prior-turn reasoning is not re-fed to the model on subsequent turns (the recommended behavior for Qwen3/Gemma/DeepSeek-R1). New explanation page Thinking and the model context.
Sessions and persistence¶
- New
aimu.sessions: a multi-user session store keyed bychannel:sender, so one process can serve many users/chats.Sessionholds a conversation'slist[dict]history (OpenAI format) + an optionalmemory_namespace+metadata;SessionStore(ABC) hasInMemorySessionStore(non-durable) andTinyDBSessionStore(durable, reusingConversationManager's TinyDB mechanics, no new dep).session_key(channel, sender)collapses single-user to"default:default", andSessionLocksgives a lazy per-keyasyncio.Lock(serialize a session's turns; run different sessions concurrently). Generalizes the single-conversationConversationManagerusing the existingreset()+restore()per-turn seam (agents never share a livemessageslist). First piece of the personal-assistant substrate roadmap (network channel adapters and run-safety hooks are separate follow-ups).
Memory¶
- Fix
DocumentStorenow canonicalizes every path through a single_normalizehelper (single leading slash, forward slashes,posixpath.normpathto collapse redundant separators and contain..). Previouslywrite("foo.md", ...)stored the key verbatim while_load_from_disk()always re-keyed it with a leading slash, so in persistent mode a document written without a leading slash became unreadable by its original key after reload.write/read/deleteand thelist_paths(prefix=...)filter now normalize their inputs, so"foo.md"and"/foo.md"address the same document consistently across ephemeral and persistent stores.
Agents and workflows¶
- New async-first channel transport under
aimu.aio.channels: aChannelABC (receive()async-generator,async send(),aclose()) andChannelMessageplain-data type, plus aCLIChannelstdin/stdout adapter. A new uniform interface alongsideAsyncRunner/MemoryStorefor talking to a user over a transport; network adapters (Telegram/Slack) are a deferred follow-up behind an optional extra +HAS_*guard, kept out of core. Exported fromaimu.aio. - New
WebChannel(aimu.aio.channels.web, exported fromaimu.aio): the WebSocket twin ofCLIChannel. Bridges one browser WebSocket onto theChannelABC (a server pumpfeed()s inbound text into a queuereceive()drains;send()relays a finished string or a streamed reply as JSON frames). The frame protocol is{"type": "message"|"token"|"thinking"|"tool"|"done", ...}(a finishedmessagecarriesproactivewhen there is noreply_to); a publicsend_frame(frame)is the subclass seam for apps adding their own frame types (conversation lists, approval prompts). The websocket is duck-typed (send_json/close), so the adapter needs nostarletteimport and is unit-testable with a fake. The Starlette server, route, and HTML page stay app-side (seeexamples/personal-assistant/web_assistant.py); only the reusable adapter moved into the library. - New
aimu.aio.Scheduler: runs interval (every) and one-shot (at) async jobs concurrently under oneasyncio.TaskGroup, for proactive assistant triggers (reminders, check-ins). A job that raises is logged and the loop continues (one bad reminder can't kill the daemon);run()is single-use and honors astop()signalled before it started (no lost-stop race). Persistence is intentionally out of scope. Exported fromaimu.aio. - New
aimu.aio.RunHandle: cooperative cancellation for an in-flightaio.Agent.run(...).RunHandle.start(coro)schedules the run as a task;cancel()stops it at the nextawait,await result()returns the result or raisesasyncio.CancelledError. The asyncAgentloop now snapshots its messages in afinally, so a cancelled run still records its partial turn for resume viarestore(). Async-only (asyncio cancellation; no threaded token). The personal-assistant example gains a/stopthat cancels the current reply. How-to: Cancel a run.
Skills¶
- New runtime skill authoring:
aimu.skills.write_skill(name, description, body, *, skills_dir, ...)writes a discoverableSKILL.md(slug validation + traversal guard + no-clobber + parser round-trip), andaimu.skills.make_skill_authoring_tool(manager, skills_dir)returns an asyncauthor_skill@toolfor the Hermes-style self-improvement loop. NewSkillManager.refresh()invalidates the discovery cache so a skill authored mid-run is visible. - New skill scripts (Python + shell), authored and runnable mid-turn: a skill's
scripts/*.pyandscripts/*.share each registered as a{skill}__{stem}tool that runs the script as a subprocess (.pyvia the current Python,.shviabash), now with an optionalargsstring forwarded to the script's argv (shlex-split; backward-compatible).write_skill(..., scripts={"name.py"|"name.sh": source})writes them (.shmarked executable);aimu.skills.make_skill_script_tool(agent, manager, skills_dir)returns an asyncadd_skill_script@tool. NewSkillAgent.reload_skills()(sync +aimu.aio) rebuilds the skills server, re-snapshots the skill tools (surfaced through the Agent's_effective_tools, re-read each engine round), and re-injects the catalog in place, so a script the assistant authors is callable in the same turn and a newly authored skill now appears in the catalog mid-conversation (retiring the prior "catalog injected once" limitation). Scripts run with full access (no sandbox), matching OpenClaw/Hermes;builtin.execute_pythonremains the sandboxed alternative.
Tools¶
- New
MCPClient(syncaimu.tools.MCPClient+ asyncaimu.aio.MCPClient) accepts a remote server byurl=, plusauth=(a bearer-token string or"oauth") andheaders=. Aurl=is folded into a single-servermcpServersconfig so FastMCP infers SSE vs streamable-HTTP and applies auth/headers in one path (shared_build_transporthelper);auth/headerswithouturlraises. This makes hosted MCP services usable through the existingas_tools()path with no config-dict boilerplate.auth=also accepts a configured provider object (a FastMCPOAuth/httpx.Authinstance) for persistent OAuth token storage or a custom redirect handler; it is forwarded straight to thefastmcp.Client(and cannot be combined withheaders=). - New
make_document_tools(store)inaimu.tools.builtin(parallel tomake_memory_tools): wraps aDocumentStore's path API assave_document/read_document/list_documents/search_documents@tools. The names are distinct frommake_memory_tools' triad, so one agent can carry both aSemanticMemoryStore(facts) and aDocumentStore(documents).make_memory_tools,make_document_tools, andmake_retrieval_toolare now re-exported fromaimu.aio.tools.builtinfor async discoverability. - New tool-call approval hook (
aimu.ToolApproval+aimu.approve_all): an optional gate(tool_name, arguments) -> boolrun right before each tool call; deny appends a refusal tool message so the model can react. Additive and off by default (approves everything). Set it on a client (client.tool_approval = policy) for barechat(), or on anAgent(Agent(tool_approval=...)/ per-runrun(tool_approval=...)), on both the sync andaimu.aiosurfaces (async policies may be coroutines). It gates every dispatch path (non-streaming, streaming, concurrent). The personal-assistant example uses it to confirm the full-accessadd_skill_scripttool in the terminal by default. How-to: Gate tool calls.
Examples & docs¶
- New
examples/personal-assistant/: a single-user, always-on assistant (OpenClaw / Hermes Agent style) assembled from the primitives above (CLIChannel+Schedulerfor a proactive reminder + aSkillAgentthat authors skills viaauthor_skilland runnable Python/shell scripts viaadd_skill_script, persisted viaConversationManager, with a small fixed set of built-in toolsbuiltin.web + builtin.misc). A deliberately minimal teaching reference: selectable tool groups, remote MCP servers, and persistent memory are capabilities AIMU ships (see the how-to guides) but the example leaves out. Includes a CLI entry point and mock-only tests. - New web front end for the personal assistant:
examples/personal-assistant/web_assistant.py(a Starlette +uvicornWebSocket server) with an example-localWebChannel(aChannelover a browser WebSocket) and a dependency-free static page. Streams replies and pushes proactive scheduler messages to the browser, with no change to theAssistantloop, a worked example of extending theChannelABC. - New both personal-assistant channels can surface per-turn reasoning and tool calls, not just the final answer.
CLIChannelgains opt-inshow_thinking/show_toolsflags (off by default, preserving the minimal library default); the example-localWebChannelemitsthinking/toolframes the page renders as distinct blocks. The example enables both viaAssistantConfig.show_thinking/show_tools. - New how-to guide Build a personal assistant (incl. a "Web front end" section);
aimu.aioandaimu.skillsAPI references extended with the new symbols.
Packaging (breaking)¶
- Moved the Streamlit/Gradio chat apps from
web/toexamples/web/, consolidating all runnable programs underexamples/. - Breaking
streamlitandgradioare no longer core dependencies; they (withstarlette/uvicornfor the personal-assistant web UI) moved to a new optional[web]extra. Install the web UIs withpip install aimu[web].aimu[all]now includesweb. - New
[tuning]extra (pandas,tqdm) for the prompt-tuning subsystem and the evalsBenchmarkharness, which previously imported these without declaring them.aimu.promptsnow imports thePromptTunersubclasses lazily, soimport aimuandfrom aimu.prompts import PromptCatalog/Scorerwork without the extra; touching a tuner class raisesModuleNotFoundErroronly if[tuning]isn't installed. Included inaimu[all]. - Breaking the
[deepeval]extra is renamed to[evals](pip install aimu[evals]); the DeepEval adapters, module paths, andHAS_DEEPEVALflag are unchanged. Extras are now documented in two groups, provider backends (ollama,anthropic,openai_compat,google,llamacpp,hf) and capabilities (web,tuning,evals,a2a), withdev/notebooks/docsas development tooling.
v0.10.1 (2026-06-24): cleanup: unified modality factory kwargs, keyword-only restore(), async SkillAgent parity + import-guard hardening¶
Models¶
- Change the modality factory classes (
ImageClient,AudioClient,SpeechClient,TranscriptionClient,EmbeddingClient) now take provider construction kwargs directly as**kwargs, matchingModelClient(model, base_url=...)and the top-levelaimu.image_client(model, variant="fp16")helpers:ImageClient(HuggingFaceImageModel.SDXL_BASE, variant="fp16"). The oldmodel_kwargs={...}argument is removed (pass the kwargs directly instead). The concrete provider clients (HuggingFaceImageClient, etc.) are unchanged and still takemodel_kwargs=. - Fix optional-provider import guards (
aimu.models,ModelClient, and theiraimu.aiomirrors) now catchImportErrorinstead of bareException. A real error inside a provider module (aSyntaxError, anAttributeError, a broken transitive dependency) was previously swallowed and the provider silently reported as "dependency not installed," surfacing later as a confusing "no client for …" message; the real cause now propagates at import time.
Agents and workflows¶
- Change the composite-runner
restore()selectors are now keyword-only and give clear errors on a bad selector (sync +aimu.aio):Chain.restore(messages, *, step=0),Parallel.restore(messages, *, worker=0),Router.restore(messages, *, route=None).step/workerout of range now raiseIndexErrorwith a descriptive message (Router already raisedKeyErroron an unknown route). Existing keyword calls are unaffected; only positional selector calls (e.g.chain.restore(msgs, 1)) need updating tostep=1. The semantic names are kept rather than collapsed to a generictarget=. - Fix async
SkillAgent.run()(aimu.aio) ignoreddeps=andschema=, which its sync twin andaio.Agent.run()both accept; async skill users silently lostToolContextdependency injection and structured output. The async override now mirrorsaio.Agent.run()in full:deps=,schema=(mutually exclusive withstream=True), and thefinal_answer_promptforced-wrap-up on both the streamed and non-streamed paths.
v0.10.0 (2026-06-23): A2A interop + resilience (fallback, timeout/retry), Anthropic prompt caching, streaming usage, uniform restore¶
Models¶
- New streaming token usage:
client.last_usagenow populates after a fully-consumedchat(stream=True)/generate(stream=True), where before it was reset toNone. OpenAI-compat clients request it viastream_options={"include_usage": True}and read the terminal usage chunk; Ollama reads the final streamed part's eval counts; Anthropic readsstream.get_final_message().usage(which also carries the P1-A cache-token fields). Usage is set once the stream is drained (reading mid-stream still yieldsNone), and matches the non-streaming semantics (final turn's counts). Hardened the OpenAI-compat stream loop against empty-choiceschunks. In-process providers (HuggingFace, LlamaCpp) expose no streaming counts and still leave itNone. - New opt-in Anthropic prompt caching:
AnthropicClient/AsyncAnthropicClientacceptcache_prompt=True(threads throughaimu.client("anthropic:...", cache_prompt=True)), which marks the system prompt and the tool definitions withcache_control: {"type":"ephemeral"}breakpoints at request time (the large, unchanging prefix an agent resends every turn). Markers are injected in the two format adapters, so all request paths (chat, tool-follow-up, streaming, structured) are covered. Below Anthropic's minimum cacheable size the API silently skips caching, so the flag is safe to leave on.usage_from_anthropicnow also surfacescache_creation_input_tokens/cache_read_input_tokensinclient.last_usagewhen the response reports them, so cache creation/hits are observable (the baseinput/output/total_tokenskeys are unchanged). Pure passthrough; no AIMU-side caching layer. - New
FallbackClient(sync) /aio.AsyncFallbackClient(async): wrap an ordered list ofBaseModelClients and fail over to the next on error. The first client that answers wins; a raising client (by default anyException, narrowable viaretry_on=) hands off to the next with the same conversation state, so multi-turn history is preserved across a failover; when all fail,FallbackExhaustedErroris raised with the last error chained as__cause__(and all errors on.errors). Because it is aBaseModelClient, it drops intoAgent, workflows,Benchmark, andagent.as_model_client()with no failover-specific wiring. Streaming fails over only before the first chunk is emitted. Pure policy layer (no backoff/sleep); pair with per-clienttimeout/max_retriesfor in-SDK retry plus cross-provider failover. Exported fromaimu,aimu.models, andaimu.aio. - New
timeoutandmax_retrieson the networked model clients (sync +aimu.aio), forwarded verbatim to the underlying SDK so requests get a bounded timeout and automatic retry on transient failures:aimu.client("anthropic:claude-sonnet-4-6", timeout=30, max_retries=5). Supported by Anthropic, OpenAI, Gemini, and every local OpenAI-compat server (LM Studio, vLLM, llama-server, SGLang, Ollama-OpenAI, HF-Serve) via theanthropic/openaiSDKs' native support. Ollama's native client supportstimeout(the syncOllamaClientnow holds anollama.Clientinstance rather than calling module-level functions) but has no request-retry, so passingmax_retriesto it raisesValueErrorpointing at theollama-openaiprovider. In-process providers (HuggingFace, LlamaCpp) are not networked and don't accept these kwargs. No retry/backoff machinery is implemented in AIMU; this is pure passthrough to the SDKs.
Tools¶
- New runtime tool-argument validation. Model-supplied tool-call arguments are now validated and lax-coerced against each
@toolfunction's type hints before the tool runs (sync,aimu.aio, and the streaming / concurrent dispatch paths alike, via the shared_ChatStateMixin._tool_call_kwargs). A coercible mismatch is coerced ("5"→5for anintparam); an uncoercible value, a missing required argument, or an unknown argument raises the newToolArgumentError, which the dispatcher reports back to the model as a tool result so it can self-correct (distinct from a tool that runs and crashes). A PydanticTypeAdapterper parameter is built once at decoration time, so dispatch stays cheap. The validator is exposed asaimu.tools.coerce_tool_arguments(fn, arguments). MCPas_tools()wrappers carry no local type hints and pass through unchanged (their server validates).pydantic>=2, previously a transitive dependency, is now a declared core dependency.
Agents and workflows¶
- New
restore()on every composite runner and fullaimu.aioparity. The save/restore pattern (persist a failed run'slist[dict], reload, resume) now coversRouter.restore(messages, route=None)(route key selects a handler;Nonerestores the routing classifier),Parallel.restore(messages, worker=0)(index selects a worker), andOrchestratorAgent.restore(messages)(delegates to the inner orchestrator agent), in addition to the existingAgent/Chain/EvaluatorOptimizer. The async surface previously had norestore(); all six aio runners now mirror their sync twins.restore()stays per-class (signatures vary by selector), not on theRunnerABC. - New
Runner.as_tool(*, name=None, description=None)(sync andaimu.aio): wraps any agent or workflow as a@tool-style callable (tool(task: str) -> str) that delegates torun(). This is the seam that lets an autonomousAgentcall anyRunner(including aChain/Router/Parallelworkflow or a remote A2A agent), not just other agents. The name defaults to the runner'sname(sanitised), the description to the first line of itssystem_message(or a generic fallback for workflows). - Change
OrchestratorAgent.assemble(workers=...)now acceptslist[Runner](waslist[Agent]) on both surfaces, wrapping each worker viaRunner.as_tool(). Worker dispatch can now target a workflow or a remote agent, not only anAgent. ExistingAgent-only call sites are unaffected; the internal_wrap_worker_as_toolhelper is removed in favour ofas_tool().
A2A interop (new optional a2a extra)¶
- New
aimu.agents.a2a: Agent2Agent protocol interop, the agent-level analog of the MCP tool surface (aimu.tools.MCPClient/python -m aimu.tools.mcp). Install withpip install 'aimu[a2a]';aimu.agents.HAS_A2Areports availability. A2A types never leak intoRunner/Agentcore; they adapt at the boundary. - Consume:
RemoteAgent.connect(url)resolves a remote agent card and returns a localRunner. Because it is aRunner, a remote A2A agent composes like any local one (intoChain/Router/Parallel/OrchestratorAgent.assemble(workers=[...]), or into anAgent's tool list viaremote.as_tool()), with no A2A-specific wiring. The sync client drives the asynca2a-sdkthrough an anyio portal (mirroringMCPClient);aimu.aio.a2a.RemoteAgentuses it natively and supports incrementalmessage/streamstreaming. - Expose:
serve_a2a(runner)(blocking) /build_a2a_app(runner)(returns a Starlette ASGI app) wrap anyRunneras an A2A server with an agent card at/.well-known/agent-card.json. CLI:python -m aimu.agents.a2a --model ... --system ... --port 9000. - Pinned to the
a2a-sdk0.3.xline (pydantic-native API matching the A2A ecosystem); the protobuf1.xline is a tracked future migration. Connection / call failures raiseA2AConnectionError.
Documentation¶
- New notebook
23 - Composing Agents (A2A), explanation page A2A vs MCP, and how-to Connect agents (A2A).
v0.9.1 (2026-06-16): EvaluatorOptimizer revision-prompt fix¶
Agents and workflows¶
- Fix
EvaluatorOptimizer(sync andaimu.aio) lost the draft it was revising. The revision prompt carried only the evaluator's feedback and the original task, so when the generator was anAgentwith a system prompt (which resets its conversation on everyrun()) it could not see its prior response and effectively regenerated from scratch each round instead of revising. The revision prompt now re-supplies the previous output alongside the task and feedback.
v0.9.0 (2026-06-16): Tool dependency injection, structured-output agents, configurable evaluator & pretty_print¶
Tools¶
- New
aimu.ToolContext: dependency injection for tools. A tool parameter annotatedToolContext(orToolContext[Deps]) is filled by the agent at call time and excluded from the model-facing JSON schema, so the model never supplies it. This lets a tool reach shared state (a document store, cache, configuration) without module-level globals.@aimu.toolrecords the injected parameter names onfunc.__tool_injected__; both sync and async dispatch fill them via_tool_call_kwargs()from the client'stool_context_deps. Exported fromaimuandaimu.tools.
Agents and workflows¶
- New
Agent.depsfield + per-runAgent.run(..., deps=...)override (sync andaimu.aio): supplies the value injected asctx.depsinto tools that declare aToolContextparameter. The per-rundeps=takes precedence over the agent'sdeps=field;_prepare_run()publishes the effective value to the model client before each run.None(bareclient.chat()) meansctx.depsisNone. Forwarded bySkillAgent. - New
Agent.run(..., schema=...)(sync andaimu.aio): pass a dataclass or Pydantic v2 model to make the run a single structured-output turn that returns a validated instance instead of running the tool-calling loop. Useful for an agent whose job is to return a typed object (e.g. a critic's verdict). Mutually exclusive withstream=True. - New
EvaluatorOptimizertyped-verdict acceptance, replacing brittle substring matching. Acceptance is now decided by one of three mechanisms in priority order:stop_when(a predicate over the evaluator's output, either the raw text or the typed verdict whenverdict_schemais set),verdict_schema(a dataclass / Pydantic model the evaluator must return via structured output; acceptance reads itspassedbool and revision uses itsfeedbackstr,passed_attr/feedback_attrare configurable, and a malformed verdict raises rather than silently continuing), orpass_keyword(the default, unchanged; accept when the substring appears in the evaluator's text). Leaving the new fields unset preserves prior behaviour exactly.
Console output¶
- New
aimu.pretty_print(stream, *, file=None, show_thinking=False, show_tools=True): render theStreamChunkiterator fromclient.chat(stream=True),Agent.run(stream=True), or any workflow run to a readable transcript (tool calls flagged, generated text streamed inline, thinking optional), and return the concatenated generated text. Saves callers from re-implementing thechunk.is_tool_call()/chunk.is_text()dispatch loop. Exported fromaimu.
Documentation¶
- New README "Agents and workflows", "Tools", "Output and utilities", and quick-start sections cover
ToolContextinjection, the configurableEvaluatorOptimizeracceptance (pass_keyword/stop_when/verdict_schema), andpretty_print, with a runnable example combining all three.
Examples¶
- Change Consolidated the loose
scripts/directory and thedata/skills/demo skills into a single top-levelexamples/tree, organized by theme:examples/text-refinement/(theepic_*family),examples/image-refinement/(thehotdog_*family),examples/news-summarizer/, andexamples/skills/(haiku-poet,unit-converter). Each example directory has its ownREADME.md, andexamples/README.mdindexes them. Files were moved withgit mv(history preserved);scripts/anddata/are removed. - New
aimu.paths.examplesconstant pointing at theexamples/directory.aimu.paths.skillsnow resolves toexamples/skills(wasdata/skills); the unusedaimu.paths.dataconstant is removed. - Change The example test suites (
test_epic_scripts.py,test_hotdog_scripts.py) are now scoped out of the defaultpytestrun viatestpaths = ["tests"]. Run them explicitly withpytest examples/. The two refinement directories are onpythonpathso their shared-helper imports resolve. - New Examples are surfaced from the README (
## Examplessection), the docs site (docs/examples.md+ nav entry), and cross-linked from notebooks 07, 08, and 09. The two iterative-refinement how-to guides andgenerate-images.mdnow reference theexamples/paths.
Models¶
- Fix
HuggingFaceModel.QWEN_3_6_27B(and the Qwen 3.5/3.6 family) crashed at generation withRuntimeError: expected mat1 and mat2 to have the same dtype, but got: c10::BFloat16 != c10::Float8_e4m3fn. These are unified multimodal FP8 checkpoints whosequantization_config.modules_to_not_convertskip-list is written against the multimodal module tree (model.language_model.*/model.visual.*). The text-only entries loaded viaAutoModelForCausalLM, which builds a text-only tree (model.layers.*) the skip-list can't match, so layers meant to stay bf16 (routermlp.gate,lm_head,linear_attnprojections) mis-quantized. Qwen 3.5/3.6 now always load viaAutoModelForImageTextToText. - Change Merged the Qwen 3.5/3.6 text-only and
_VLenum members into singlevision=Trueentries (QWEN_3_6_27B,QWEN_3_5_9B); removedQWEN_3_6_27B_VLandQWEN_3_5_9B_VL. The two variants loaded the identical checkpoint via the identical loader (vision tower included either way), so the split no longer backed any loader or VRAM difference. - Fix
HuggingFaceClient's module-level weight cache could collide: two enum members sharing a repo id andmodel_kwargsbut loading via different classes (AutoModelForCausalLMvsAutoModelForImageTextToText) produced the same cache key, so the second silently received the first's model object._make_cache_keynow folds in a load-profile tag (mirroring how the image/audio/speech clients key onpipeline_class/pipeline_type).
v0.8.0 (2026-06-12): Embeddings, transcription, structured output, RAG & audio input¶
Models¶
- New
audio: bool = Falsefield onModelSpec. Audio-capable text models exposesupports_audioon their enum members,is_audio_modelon their client instances, and anAUDIO_MODELSclassproperty (parallel toTOOL_MODELS,THINKING_MODELS,VISION_MODELS). - New Audio-capable models added to the catalog:
OpenAIModelGPT-4o, GPT-4o-mini, GPT-4.1, GPT-4.1-mini, GPT-4.1-nano;GeminiModel2.0 Flash, 2.0 Flash Lite, 2.5 Pro, 2.5 Flash;HuggingFaceModel.GEMMA_4_E4B,GEMMA_4_12B,NEMOTRON_H_8B. Ollama models remainaudio=Falsewith inline comments noting where the underlying weights support audio (upgrade path once the Ollama API adds audio input).
ModelClient.chat() and ModelClient.generate()¶
- New
audio=parameter on bothchat()(stateful; turn persists inself.messages) andgenerate()(stateless one-shot; no history touched). Accepts any mix of: file path strings,pathlib.Path, raw bytes (WAV assumed),https://URLs (fetched eagerly), anddata:audio/...;base64,...data URLs. Supported format strings:wav,mp3,ogg,flac,m4a,webm, inferred from file extension or MIME type. - New Passing
audio=to a model withsupports_audio=FalseraisesValueErrorbefore any API call. - New
images=andaudio=are mutually exclusive per turn; passing both raisesValueError. - Internally normalised to OpenAI
input_audiocontent blocks ({"type": "input_audio", "input_audio": {"data": "<b64>", "format": "wav"}}). Provider adaptation happens at request time: OpenAI/Gemini/OpenAI-compat pass through; Anthropic converts to{"type": "audio", "source": {"type": "base64", ...}}; HuggingFace decodes to float32 numpy arrays viasoundfileand passes them to theAutoProcessor; Ollama raises with a clear message (API does not yet support audio). - Mirrored on the async surface (
aimu.aio): same signature onaio.chat()andaio.generate(). - Fix
ModelClient._generate(and the asyncAsyncModelClient._generate/_chat) now accept and forwardaudio=. They were missing the parameter while the basegenerate()/chat()always pass it, so everyaimu.client().generate()/aimu.chat(...)call through the factory raisedTypeError: _generate() got an unexpected keyword argument 'audio'. (Concrete provider clients were unaffected, which is why the live test suite, which constructs them directly, didn't surface it.)
Documentation¶
- New
docs/how-to/handle-audio-input.md: accepted input forms, model selection, stateful vs. stateless, async surface, per-provider adaptation. - New
notebooks/05 - Audio Input.ipynb: capability flags, all input forms, multiple clips per turn, stateful/stateless split, multi-turn conversations, capability check, mutual-exclusion demo, Gemini and HuggingFace sections, async surface.
Transcription (speech-to-text)¶
- New
aimu.transcription_client()/aimu.transcribe()+TranscriptionClientfactory +BaseTranscriptionClientABC: a dedicated speech-to-text surface, parallel to TTS (BaseSpeechClient). Disjoint from theaudio=parameter on text models, which handles audio analysis/QA by audio-capable chat models; this surface uses dedicated ASR models (Whisper family, gpt-4o-transcribe) optimised for transcription. - New
OpenAITranscriptionClient+OpenAITranscriptionModel: cloud ASR backed byopenai.audio.transcriptions.create(). Models:WHISPER_1,GPT_4O_TRANSCRIBE,GPT_4O_MINI_TRANSCRIBE. Auth viaOPENAI_API_KEY. Uses the sameopenaiSDK already required by the[openai_compat]extra. - New
HuggingFaceTranscriptionClient+HuggingFaceTranscriptionModel: local ASR backed bytransformers.pipeline("automatic-speech-recognition"). Models:WHISPER_TINY,WHISPER_BASE,WHISPER_SMALL,WHISPER_MEDIUM,WHISPER_LARGE_V3,DISTIL_WHISPER_LARGE_V3. Weight caching via module-level registry (same pattern as other HF clients). - New
transcribe(audio, language=None, response_format="text", prompt=None, temperature=None) -> str | dict. Accepted audio forms: file path, raw bytes,https://URL,data:audio/...URL, the same set asaudio=onchat().response_format="verbose_json"returns a dict withtext,segments(start/end/text),language,duration.response_formatdefaults to"text"(plain string). - New
AIMU_TRANSCRIPTION_MODELenv var: sets the default model foraimu.transcription_client()andaimu.transcribe()whenmodel=is omitted. - New Async mirror under
aimu.aio:AsyncTranscriptionClient,aio.transcription_client(sync_client),await aio.transcribe(audio, *, model, ...). Wraps sync viaasyncio.to_thread(Decision 7, same as every other aio modality). - New Built-in
transcribe_audio(audio_path: str) -> str@toolinaimu.tools.builtin;builtin.transcriptionsubgroup; included inALL_TOOLS. Backed by a lazy_transcription_clientsingleton viaAIMU_TRANSCRIPTION_MODEL.make_transcription_tool(client)binds a fresh tool to a caller-supplied client. - New
docs/how-to/transcribe-audio.mdandnotebooks/21 - Transcription.ipynb.
Embeddings (text-to-vector)¶
- New
aimu.embedding_client()/aimu.embed()+EmbeddingClientfactory +BaseEmbeddingClientABC: a dedicated text-embedding surface, parallel to the other modality clients.embed()takes one string (returnslist[float]) or a list (returnslist[list[float]], order preserved); an empty list returns[]without a provider call.client.dimensionsreports the spec's vector width. - New
OpenAIEmbeddingClient+OpenAIEmbeddingModel(text-embedding-3-small/large,text-embedding-ada-002) viaopenai.embeddings.create(); auth viaOPENAI_API_KEY. - New
OllamaEmbeddingClient+OllamaEmbeddingModel(nomic-embed-text,mxbai-embed-large,bge-m3,all-minilm) viaollama.embed(). - New
HuggingFaceEmbeddingClient+HuggingFaceEmbeddingModel(MiniLM-L6-v2, BGE small/base/large-en-v1.5, GTE-large, E5-large-v2, mxbai-embed-large-v1) backed bysentence-transformersso each model's own pooling/normalization config is honoured; lazy load + module-level weight cache (freed byaimu.clear_hf_cache()). Addssentence-transformers>=3to the[hf]extra. - New
SemanticMemoryStore(embedding_client=...): pluggable embedding model; defaultNonekeeps ChromaDB's built-in embedder (unchanged behaviour). - New
AIMU_EMBEDDING_MODELenv var sets the default model foraimu.embedding_client()/aimu.embed()whenmodel=is omitted (raises if unset; no implicit download). - New Async mirror:
aio.embedding_client(sync_client)/aio.embed()wrap a sync client viaasyncio.to_thread. - Docs
docs/how-to/use-embeddings.md,notebooks/11 - Embeddings.ipynb, API reference, and env-var reference.
Structured output¶
- New
schema=onchat()andgenerate()(sync and async). Pass a dataclass type or a Pydantic v2 model; the call returns a validated instance of that type instead of a string. Mutually exclusive withstream=True. - New
ModelSpec.structured_outputflag →client.supports_structured_outputproperty and aSTRUCTURED_MODELSclassproperty (parallel totools/thinking/vision/audio). Set on the OpenAI, Gemini, Ollama (all models), and Anthropic catalogs. - Auto-escalate semantics: native provider enforcement when
supports_structured_output=True(OpenAIresponse_formatjson_schema; Ollamaformat=; Anthropic forced-tool), otherwise the schema is appended to the prompt and the response is parsed. The branch is on the static capability flag, not on catching a runtime error, so a genuine provider failure surfaces rather than silently downgrading; parse failure raisesValueError. self.messagesstays plain strings; the typed object is a return value only, so conversation history remains provider-portable.- Composition:
schema=works alongsidetools=on OpenAI-compatible and parse-path providers. On Anthropic (native structured output is a forced tool) combiningschema=with active tools raisesValueError. - New
schema_to_json_schema()(internal) converts a dataclass/Pydantic model to a JSON Schema, reusing the@tooldecorator's Python-type → JSON-Schema mapping. - Docs
docs/how-to/use-structured-output.md. - Deferred:
Agent.run(schema=...), astrict=True(native-or-raise) knob, and native HuggingFace/llama-cpp enforcement (those use the parse path).
RAG primitives (retrieval-augmented generation)¶
- New
aimu.rag: chunk/retrieve/rerank helpers as plain functions over theMemoryStoreinterface (no retriever/splitter/loader class hierarchy). - New
split_text(text, *, chunk_size=1000, chunk_overlap=200, separators=None, length_function=len): recursive separator-based chunking (paragraphs → lines → sentences → words → characters) with overlap.length_functiondefaults to character count; pass a tokenizer's counter for token-aware chunking. Oversized unsplittable text hard-cuts atchunk_size. - New
ingest(store, documents, *, chunk_size, chunk_overlap, separators, length_function) -> int: splits one or many documents and stores each chunk viastore.store(); returns the chunk count.retrieve(store, query, *, n_results=5, **search_kwargs) -> list[str]is a RAG-named pass-through tostore.search()(forwards e.g.max_distance=).format_context(chunks, *, separator="\n\n", numbered=False) -> strjoins chunks for prompt augmentation. - New
rerank(query, documents, *, model="cross-encoder/ms-marco-MiniLM-L-6-v2", top_n=None): cross-encoder reranking viasentence-transformers(the[hf]extra); lazy-loaded and cached. Empty input returns[]without loading the model. - New
make_retrieval_tool(store, *, n_results=5)inaimu.tools.builtin: wrapsretrieve+format_contextas aretrieve_context(query)agent tool (returns numbered context). - Docs
docs/how-to/use-rag.mdand theaimu.ragAPI reference. - Loaders and per-chunk metadata are intentionally out of scope: ingestion sources are covered by
read_file/get_webpage(or any text-returning library), and chunks are stored as plain strings per theMemoryStorecontract.
Token usage surfacing¶
- New
client.last_usage: token counts for the most recent non-streamingchat()/generate(), as{"input_tokens", "output_tokens", "total_tokens"}(orNonewhen the provider/server omits usage). Captured for Anthropic, OpenAI-compat (incl. OpenAI/Gemini/local servers), and Ollama, on both sync and async surfaces, and delegated through theModelClient/AsyncModelClientwrappers. Reset toNoneon streaming calls (streaming usage capture is a separate follow-up) and byreset(). Token counts only; dollar cost is derivable but intentionally not computed (no maintained price table).
Anthropic models & adaptive thinking¶
- New
AnthropicModelmembers:CLAUDE_FABLE_5(claude-fable-5),CLAUDE_OPUS_4_8(claude-opus-4-8),CLAUDE_OPUS_4_7(claude-opus-4-7), alltools=True, thinking=True, vision=True. - New
ThinkingStyleenum (ENABLED/ADAPTIVE) carried as a per-member extra onAnthropicModel(analogous to HuggingFace'sToolCallFormat).AnthropicClient._thinking_kwargs()builds the request accordingly:ENABLED→{"type": "enabled", "budget_tokens": N};ADAPTIVE→{"type": "adaptive", "display": "summarized"}withtemperature/top_p/top_kdropped. Opus 4.7+ and Fable 5 are adaptive-only (theenabledform 400s on them); Opus 4.6, Sonnet 4.6, and Haiku 4.5 use the budget form. - Fix
CLAUDE_HAIKU_4_5now correctly hasthinking=True. Haiku 4.5 supports extended thinking via theenabled/budget_tokensform (previously omitted, so thinking tests silently skipped). - Adaptive models decide per request whether to think and may emit none on simple prompts; the thinking tests use a multi-step reasoning prompt and assert thinking emission rather than an exact answer.
- Docs Updated the model matrix, provider matrix, add a new model, and CLAUDE.md (Thinking Models, AnthropicClient notes) to cover the two thinking styles and the new models.
- Docs Added an "Adaptive vs. budget thinking" section to
notebooks/01 - Model Client.ipynb(section C) demonstratingThinkingStyleand adaptive models skipping thinking on trivial prompts.
Dependencies¶
- Fix Pinned the
[hf]extra'skernelsto>=0.12,<0.13. It was unconstrained and resolved tokernels 0.15.2, which is outside the rangetransformerssupports (<0.13);transformersconstructskernels.LayerRepository(...)at import time and 0.13+ maderevision/versionmandatory, sofrom transformers import AutoProcessorraisedValueError, silently flippingHAS_HFtoFalse(HuggingFace clients unavailable) and erroring every HF test on import. - Pinned the
[hf]extra'stransformersto>=5,<6(the major the model catalog targets: Qwen 3.6, Gemma 4, GPT-OSS) so a future major can't reintroduce this class of import-time breakage on resolve.
Async surface¶
- New Async→sync tool bridging for wrapped in-process clients.
AsyncHuggingFaceClient/AsyncLlamaCppClientrun their sync client's_chattool-dispatch loop in a worker thread (viaasyncio.to_thread), and that sync dispatcher refusesasync deftools, but the async surface routinely attaches them (e.g.await aio.MCPClient.as_tools()). Each async tool is now wrapped as a sync callable that drives the coroutine back on the main event loop (run_coroutine_threadsafe) and blocks only the worker thread (no deadlock, the main loop is free, awaiting theto_threadfuture). Async-generator (streaming) tools bridge to sync generators; the OpenAI tool spec and dispatch name are preserved. Async agents using in-process models can now mix sync and async tools (including MCP) transparently.
Fixes¶
- Change HuggingFace default
max_new_tokensraised from1024to4096. The previous default truncated reasoning models (e.g. Qwen 3.5/3.6) mid-thinking, before the closing</think>, which left no room for the answer; the higher default leaves headroom for thinking plus a response. Override per call withgenerate_kwargs={"max_tokens": N}. - Fix Streamed
chat()on a HuggingFace thinking model no longer raisesRuntimeError: generator raised StopIterationwhen a turn produces reasoning but is truncated before emitting an answer. The streaming path now mirrors the non-streaming one: it surfaces the buffered thinking and finishes with empty generated content instead of doing an unguardednext()on an empty token stream. - Fix (tests) Mock-only audio/speech/image API tests previously replaced
transformers/soundfile/diffusersinsys.moduleswith bare stubs at collection time and never restored them, breaking any live model test that ran later in the same session (ModuleNotFoundError: Could not import module 'Qwen3_5ForCausalLM'). The stubs are now installed via auto-restoring,monkeypatch-scoped fixtures, and the permanent install is skipped whenever the real dependency is importable.
v0.7.0 (2026-06-08): MCP tool unification, model resolvers, and agent improvements¶
Breaking changes¶
- Breaking Removed the
model_client.mcp_clientattribute. MCP tools now integrate through the singlemodel_client.toolsregistry: callMCPClient(...).as_tools()(sync) orawait aio.MCPClient.connect(...).as_tools()(async) to turn a server's tools into@tool-style callables, then add them totools(constructorAgent(tools=...),client.tools = ..., or the per-callchat(tools=...)/run(tools=...)override). Migration: replaceclient.mcp_client = mcpwithclient.tools = mcp.as_tools()(concatenate with@toolfunctions as needed, e.g.builtin.web + mcp.as_tools()). Two consequences: dispatch is now one by-name lookup overtools, so on a name collision the last entry wins (previously Python@toolalways beat a same-named MCP tool; to preserve that, append the Python tool aftermcp.as_tools()); andMCPClient.get_tools()is no longer called on everychat()(the tool list is snapshotted byas_tools()), so callas_tools()again to pick up server-side tool changes.SkillAgentand the internal dispatch (_handle_tool_calls(tool_calls),_call_plain_tool(tc, tc_id), both of which lost theirtoolsparameter) were updated accordingly. TheMCPClientclass, itsget_tools()/call_tool()/ping(), and theaio.MCPClientparallel are unchanged. - Breaking
system_messageis no longer immutable after the firstchat(). The setter is now always live: assigning it mid-conversation rewrites the{"role": "system"}entry inmessagesin place (re-conditioning the model on the new prompt while preserving history), inserts one if absent, or removes it onNone. Before the first chat it still just seeds the value. The previous behaviour raisedRuntimeError; code that caught that error to gate areset()can now assign directly. To change the prompt and drop history, usereset(system_message="new"). Two consequences are accepted by design: the transcript becomes counterfactual (prior assistant turns predate the new prompt), and there is no longer a guard against silently re-conditioning aModelClientshared by another agent's in-flight conversation, so don't share a live-conversation client across agents that each setsystem_message. The_system_message_lockedflag has been removed. See System message lifecycle.
Models¶
- New
aimu.resolve_model_enum(model)andaimu.resolve_image_model_enum(model): resolve a model to itsModel/ImageModelenum member from any of three input forms: an enum member (returned unchanged), a"provider:model_id"string (delegates toresolve_model_string/resolve_image_model_string), or a bare enum-member name (e.g."QWEN_3_8B","FLUX_2_KLEIN_4B","NANO_BANANA") looked up across every installed provider enum. Useful for CLIs/scripts that accept "enum, name, or string" uniformly. For text, an ambiguous bare name (the same id ships under many providers) is disambiguated the way the omitted-modeldefault is: prefer a provider where the model is actually available locally (running Ollama → cached HuggingFace → reachable local OpenAI-compat server, tool-capable first), logged at WARNING; if it isn't available under any provider,ValueErrorlists the"provider:model_id"options. This availability probe runs only on the ambiguous path.resolve_image_model_enumhas no local-availability notion (image catalogs don't collide) and raises on the rare ambiguity. Exported fromaimu.modelsand top-levelaimu. - New
aimu.available_text_models(*, include_hf_cache=True)for discovery: return locally available text models asModelenum members (running Ollama → cached HuggingFace → reachable local OpenAI-compat servers), in provider-priority order. Download-free and cloud-free.aimu.resolve_default_text_model_enum(*, include_hf_cache=True)returns the single auto-pick (env var → first available, tool-capable preferred) as an enum member, the enum-returning twin of the internal default resolver that backsclient()/chat()/agent()whenmodel=is omitted. - New Gemma 4 12B added to every provider that can run it:
OllamaModel.GEMMA_4_12B(gemma4:12b, tools/thinking/vision, with the shared Gemma sampling kwargs),HuggingFaceModel.GEMMA_4_12B(the instruction-tunedgoogle/gemma-4-12b-it, tools/vision, processorparse_responsepath), and aGEMMA_4_12Bmember on every OpenAI-compat server enum (OllamaOpenAIModel,LMStudioOpenAIModel,VLLMOpenAIModel,HFOpenAIModel,LlamaServerOpenAIModel,SGLangOpenAIModel) plusLlamaCppModel. The server/llama.cpp entries aretools=True, matching the establishedGEMMA_3_12Bconvention for those catalogs. Resolvable via the usual"provider:model_id"strings (e.g."ollama:gemma4:12b","hf:google/gemma-4-12b-it","vllm:google/gemma-4-12b-it").
Tools¶
- New The
tooldecorator is re-exported at the top level asaimu.tool.@aimu.toolis now the single recommended/documented form across the README, tutorials, how-tos, and notebook examples. It's namespaced, so it can't be silently shadowed by another library's same-namedtooldecorator (LangChain, smolagents, etc.).from aimu.tools import toolremains valid and unchanged (same object); it's the natural form for code already insideaimu.tools. TheToolSignatureErrormessage prefix is now@aimu.tool:to match. No behaviour change to decoration or dispatch. - New
MCPClient.as_tools()(sync) andaio.MCPClient.as_tools()(async) return a server's tools as@tool-style callables, each closing over the client, invokingcall_tool()cross-process, and carrying__tool_spec__/__tool_is_async__/__tool_is_streaming__. Drop them straight intotools(client.tools = mcp.as_tools(),Agent(tools=builtin.web + mcp.as_tools())). This unifies MCP and in-process tools onto the singleself.toolsregistry and one dispatch path; see the breaking-change note above for the migration frommodel_client.mcp_client. New shared helperaimu.tools.mcp_format.mcp_content_to_text(tool_response)flattens acall_toolresult to a string. - New Per-call tool override:
chat(..., tools=None)andAgent.run(..., tools=None)(both sync andaimu.aio) accept atools=list that replaces the client's configuredself.toolsfor a single call/run, restored afterward.tools=None(default) keeps the existing behaviour;tools=[]disables tools for the call (MCP tools, being callables inself.toolsviaas_tools(), are included in the swap). On anAgent, the override applies to every turn of the agentic loop. Implemented as a scopedself.toolsswap (_ChatStateMixin._tools_override) covering both request-spec building and dispatch; the agent threads it through each loopchat()call so no new agent state is introduced. Not safe across concurrentchat()calls on a shared client; same contract asself.messages. Not added to theRunnerABC / workflow classes.
Agents and workflows¶
- New
Agent.final_answer_prompt(opt-in, defaultNone; sync andaimu.aio): guarantees a final answer when the agentic loop exhaustsmax_iterationswhile the model is still calling tools. Instead of returning whatever the last (possibly tool-only) turn produced (an empty or stub result), the agent sends this prompt once with tools disabled (chat(..., tools=[])), forcing the model to synthesize an answer from the context it has gathered. The trigger is the post-loop_last_turn_called_tools()check (no new counter); it fires only on the cap-with-pending-tools path (a natural finish, a turn with no tool calls, is unaffected) and the wrap-up turn is not counted againstmax_iterations.OrchestratorAgent._init_orchestrator()andOrchestratorAgent.assemble(..., final_answer_prompt=...)(sync +aio) forward it to the inner orchestrator agent, and it is accepted as afrom_configkey. Leaving itNonepreserves prior behaviour exactly.
Fixes¶
- Fix
SkillAgentskill injection no longer wipes conversation history when applied to an already-used client. It previously calledreset()to unlock the setter (clearingmessages); it now assignssystem_messagedirectly, which swaps the system entry in place.
v0.6.0 (2026-06-04): Output utilities, model weight caching, and experiment checkpointing¶
Breaking changes¶
- Breaking Renamed
HuggingFaceImageModel.FLUX_DEV→FLUX_1_DEVandFLUX_SCHNELL→FLUX_1_SCHNELLfor naming consistency with theFLUX_2_KLEIN_4B/FLUX_2_KLEIN_9Bmembers. The underlying model id strings (black-forest-labs/FLUX.1-dev,black-forest-labs/FLUX.1-schnell) are unchanged. Update enum references;"hf:black-forest-labs/FLUX.1-dev"string-form usage is unaffected. - Behavior change
builtin.computenow includesexecute_pythonalongsidecalculate. If you were passingtools=builtin.computeand want to exclude the sandboxed REPL, switch totools=[builtin.calculate]explicitly.ALL_TOOLSandmake_tools()are unchanged (opt-in only viapython_sandbox=True).
Output utilities¶
- New
aimu.parse_json_response(text, schema=None): extract JSON from any LLM response string using three extraction strategies (raw parse, fenced code block,{…}substring). Pass a dataclass class or Pydantic v2BaseModelasschemato coerce the parsed dict into a typed object. RaisesValueErroron all-strategy failure with the first 200 characters of the response included. Exported fromaimu.models._json,aimu.models, and top-levelaimu. - New
aimu.generate_json(client, prompt, schema=None, *, retries=2, generate_kwargs=None): callclient.generate()and parse the result as JSON, retrying up toretriestimes on parse failure. Convenience wrapper aroundparse_json_response. - New
aimu.extract_tool_calls(messages): convert an OpenAI-format message list (e.g.agent.model_client.messages) into a flatlist[dict]of{iteration, tool, arguments, result}records. Handles bothargumentsandparameterskey names for cross-model compatibility. Replaces manual reconstruction boilerplate common in agentic scripts.
Model weight caching¶
- New All four in-process HuggingFace clients (
HuggingFaceClient,HuggingFaceImageClient,HuggingFaceAudioClient,HuggingFaceSpeechClient) now maintain a module-level weight registry keyed on(spec.id, *sorted_model_kwargs). A second client instance with the same model and construction kwargs reuses already-loaded weights rather than callingfrom_pretrained()again. The text client checks on construction; the lazy-loading modality clients check on first load.LlamaCppClienthas the same pattern with key(model_path, n_ctx, n_gpu_layers, chat_format). - New
aimu.clear_hf_cache(model=None): evict HuggingFace weight entries from all four modality registries and callgc.collect()+cuda.empty_cache(). Pass a model enum member to clear just that model; passNoneto clear all. - New
aimu.clear_llamacpp_cache(model=None): same forLlamaCppClient.
Tools¶
- New
execute_python(code)built-in tool inbuiltin.compute. Executes sandboxed Python in a fresh namespace per call, captures stdout, and returns the last expression value. Allowed imports:math,statistics,json,re,itertools,functools,datetime, andnumpy/pandas/scipy/matplotlibwhen installed. Filesystem (open,os,pathlib) and subprocess access are blocked. Not included inALL_TOOLS; opt in viatools=builtin.computeormake_tools(python_sandbox=True). - New
make_tools(..., python_sandbox=False): newpython_sandbox=kwarg appendsexecute_pythonwhenTrue. - New
make_memory_tools(store)inaimu.tools.builtin: wraps anyMemoryStoreinstance as three@tool-decorated functions (store_memory,search_memories,list_memories) for direct in-process agent use. Unlike the image/audio/speech built-in tools, there is no lazy singleton: the store is always explicit because persistence semantics (persist_path, backend, collection name) are meaningful caller choices. Works withSemanticMemoryStore,DocumentStore, or anyMemoryStoresubclass. For cross-process or multi-agent memory, the existing FastMCP servers (aimu.memory.mcp/aimu.memory.document_mcp) remain the recommended path. - New
builtin.make_tools(..., memory_store=None): newmemory_store=kwarg appendsmake_memory_tools(store)to the assembled tool list when provided.
Agents and workflows¶
- New
Agent.restore(messages): restore an agent from a savedlist[dict](OpenAI message format) for resuming after failure. Callsmodel_client.reset(), strips the leading system message to prevent duplication on the nextchat(), and setsmodel_client.messages. The live partial state after a failed run is onagent.model_client.messages(not the post-run snapshot fromagent.messages). - New
EvaluatorOptimizer.restore(messages): delegates togenerator.restore(). - New
Chain.restore(messages, step=0): restores the specified step's agent client.
Documentation¶
- New
docs/how-to/using-llms-inside-tools.md: covers the history pollution problem,generate()for stateless in-tool LLM calls, the HuggingFace weight caching model (includingclear_hf_cache()/clear_llamacpp_cache()), and the save/restore checkpointing pattern with a full try/except example.
v0.5.1 (2026-06-01): Image-to-image, FLUX.2 Klein, and curated model catalog¶
Image generation¶
- New Image-to-image (img2img) support: pass
reference_image=toBaseImageClient.generate()(and all subclasses). Accepts a file path string,pathlib.Path, raw bytes, data URL, http(s) URL, or PIL Image. HuggingFace derives the img2img pipeline from the loaded txt2img pipeline viafrom_pipe()(shared weights, no extra VRAM).strength=(default0.75) controls deviation from the reference for FLUX.1-style pipelines.width/heightare ignored; output size is derived from the reference image. Gemini passes the reference as inline PNG data in a multipart request, enabling image editing. - New
HuggingFaceImageModel.FLUX_2_KLEIN_4BandFLUX_2_KLEIN_9B: FLUX.2 Klein by Black Forest Labs. 4-step distilled model with improved text rendering, better hand/face quality, and higher resolution support. UsesFlux2KleinPipeline(diffusers 0.37+), a unified pipeline that handles both txt2img and img2img natively (image=parameter, nostrength).img2img_uses_strength=Falseon the spec distinguishes it from FLUX.1-style img2img. - New
HuggingFaceImageSpec.img2img_pipeline_class: diffusers class name for the img2img variant (e.g."StableDiffusionImg2ImgPipeline");Nonefor ad-hoc"hf:<repo>"strings. - New
HuggingFaceImageSpec.img2img_uses_strength:True(default) for strength-based pipelines;Falsefor unified pipelines like FLUX.2 Klein that condition on the reference image directly. - New
aimu.models._images._reference_image_to_pil(): shared helper used by both HF and Gemini image clients to normalise any reference image input form to a PIL Image. - Changed
scripts/hotdog_loop.pyabsorbshotdog_climbing.py: the two scripts shared identical structure and differed only in their acceptance policy. Pass--strategy climbingfor hill-climbing behaviour (keep best, revert on non-improvement);--strategy greedy(default) preserves the original loop behaviour.hotdog_climbing.pyis removed. - New
scripts/hotdog_img2img.py: iterative hotdog refinement via img2img + strength annealing. Hill-climbs in image space (always refines from the best image, not the most recent) while annealingstrengthfrom high (explore) to low (polish). Detects and warns when the active model does not supportstrength(e.g. FLUX.2 Klein).
Negative prompts¶
- New
ImageSpec.supports_negative_promptcapability flag.Trueby default;Falsefor guidance-distilled / conversational models that have no negative-prompt parameter, such asHuggingFaceImageModel.FLUX_2_KLEIN_4B/_9Band the entire Gemini image family (GeminiImageSpecdefaults it toFalse). - Behavior
BaseImageClient.generate()now raisesValueErrorifnegative_prompt=is passed to a model whose spec setssupports_negative_prompt=False, instead of crashing deep in the pipeline (HuggingFace) or silently ignoring it (Gemini). Callers branch onspec.supports_negative_promptand fold avoidance into the prose prompt for unsupporting models. The hotdog scripts do this via a newnegative_prompt_plan()helper (native kwarg → summarizer-folded positive constraints → prompt suffix, by model).
Curated model catalog (breaking for unknown ids)¶
- Breaking Model id strings must name a model AIMU ships a spec for. Passing an arbitrary
"hf:<unknown-repo>"/"gemini:<unknown-id>"/"openai:<unknown-id>"to an image, audio, or speech client now raisesValueError(listing available ids) instead of fabricating a spec with guessed capabilities. Text was always strict (resolve_model_stringraises); this brings the other modalities in line. For a one-off custom model, construct the provider spec and pass the object (e.g.ImageClient(HuggingFaceImageSpec(...))), the explicit escape hatch. - Fixed A
"provider:model_id"string for a known model now resolves to the same spec object as the equivalent enum member, so capabilities are identical regardless of construction path. Previously the string form fabricated a default spec; e.g."hf:black-forest-labs/FLUX.2-klein-4B"lostsupports_negative_prompt=False/img2img_uses_strength=False, and"hf:suno/bark"lost BARK'sdefault_voice. - Removed The
_REPO_PIPELINE_HINTSrepo-prefix capability-guessing heuristics in the HuggingFace audio and speech clients (dead once unknown ids raise).
v0.5.0 (2026-05-31): Async, audio, speech, and default models¶
A feature release on top of the v0.4 redesign: a full async surface, two new output modalities (audio and speech), a cloud image provider, automatic default-model resolution, and streaming tools. No breaking changes to the v0.4 sync API.
Async surface (aimu.aio)¶
- New
aimu.aiomirrors the entire public sync API one-for-one, with the same class names in a different namespace. Switch paradigms with one import line plusawait. Exportschat,client,Agent,SkillAgent,Chain,Router,Parallel,EvaluatorOptimizer,PlanExecuteEvaluator,OrchestratorAgent,MCPClient. Imported by default, sofrom aimu import aioneeds no separate install. - New
aio.Parallelandconcurrent_tool_calls=Trueuseasyncio.TaskGroupfor structured concurrency: sibling cancellation on first failure,ExceptionGroupaggregation. - New Native async providers: Anthropic, OpenAI, Gemini, Ollama, and every OpenAI-compatible endpoint. In-process providers (HuggingFace, LlamaCpp) wrap an existing sync client so weights load only once (
aio.client(sync_client)). - New async
MCPClientbuilt on FastMCP's native asyncClient(no anyio portal); construct viaawait MCPClient.connect(...). The syncMCPClientremains first-class. - New
@toolasync detection (__tool_is_async__):async deftools are awaited directly; sync CPU-bound tools are routed throughasyncio.to_threadso the event loop stays free. - Note Streaming on the async surface returns
AsyncIterator[StreamChunk](consume withasync for); the sync surface returnsIterator[StreamChunk]. TheStreamChunktype itself is identical on both. - Requirement Python 3.11+ is now required (the async surface uses
asyncio.TaskGroup,asyncio.timeout, and nativeExceptionGroup).
Audio generation¶
- New
aimu.audio_client()/aimu.generate_audio()+AudioClientfactory +BaseAudioClientABC, parallel to the text and image surfaces. - New HuggingFace audio models: MusicGen small/medium/large (32 kHz, token-autoregressive), AudioLDM2 (16 kHz, diffusion), Stable Audio Open (44.1 kHz stereo, diffusion).
- New
AUDIO_GENERATINGStreamChunkphase +StreamChunk.is_audio_progress(). Streaming progress for diffusers-backed models. - New
encode_audio()output formats:numpy(default),bytes,data_url,path(WAV viasoundfile). - New Built-in
generate_audiostreaming tool +make_audio_tool(client, duration_s=);builtin.audiosubgroup.
Speech (text-to-speech)¶
- New
aimu.speech_client()/aimu.generate_speech()+SpeechClientfactory +BaseSpeechClientABC. - New Providers: HuggingFace local (SpeechT5, MMS-TTS, BARK) and OpenAI cloud (
tts-1,tts-1-hd). - New
SPEECH_GENERATINGStreamChunkphase +StreamChunk.is_speech_progress(); OpenAI byte-chunk streaming. - New Built-in
generate_speechstreaming tool +make_speech_tool(client, voice=, speed=);builtin.speechsubgroup.
Image generation¶
- New Google Gemini "Nano Banana" cloud provider (
GeminiImageClient,gemini-2.5-flash-image) under the[google]extra, dispatched viaaimu.image_client("gemini:..."). - New
aimu.image_client()accepts ad-hoc"hf:<repo_id>"and"gemini:<id>"strings in addition to enum members. - New Streaming image generation:
IMAGE_GENERATINGchunks during denoising, with optional per-step latent previews viapreview_every=N(HuggingFace diffusers). - New Built-in
generate_imagestreaming tool,make_image_tool(client, preview_every=), andmake_describe_image_tool(client)(binds vision Q&A to a vision-capable chat client);builtin.imagesubgroup.
Default-model resolution¶
- New
model=is now optional onaimu.chat()/aimu.client()/aimu.agent(). When omitted, AIMU resolves a text default:AIMU_LANGUAGE_MODEL("provider:model_id") first, otherwise an already-available local model (running Ollama → cached HuggingFace model → running local OpenAI-compatible server), restricted to enum-known ids and preferring tool-capable ones. A cloud provider is never auto-selected and weights are never downloaded implicitly. - New
AIMU_IMAGE_MODEL/AIMU_AUDIO_MODEL/AIMU_SPEECH_MODELprovide defaults for the image/audio/speech entry points (env-var only; an unset var raises a clearValueError).
Tools and vision¶
- New Streaming tools: a generator-function
@toolmayyieldStreamChunkobjects mid-execution (flag__tool_is_streaming__); the agent forwards them throughagent.run(stream=True). The tool's recorded response resolves from itsreturnvalue, the last chunk'sresult, orstr(last_chunk.content). - New
images=is now accepted on statelessgenerate()(one-shot vision Q&A that does not touchself.messages), in addition to statefulchat(). - New
builtin.make_tools(base_client, image_client=None, audio_client=None, speech_client=None)assembles the full built-in tool list with automatic image/vision/audio/speech wiring.
Examples¶
- Changed The full-featured Streamlit chatbot (
web/streamlit_chatbot.py) gains image, audio, and speech generation, plus optional TTS narration of completed responses.
v0.4 (2026-05-26): API redesign¶
Breaking changes across four areas, plus the new documentation site.
Top-level API¶
- New
aimu.chat(user_message, *, model, ...): one-shot chat with a model string or enum. - New
aimu.client(model, *, system=None, **kwargs): one-lineModelClientfactory. - New
aimu.resolve_model_string("provider:model_id"): model-string parser. - New
ModelClientnow accepts a"provider:model_id"string in addition to enum members.
Model clients¶
- New
ModelSpecfrozen dataclass replaces positional enum tuples. AllModelenums migrated. - New
client.reset(system_message="__keep__")clears history and unlocks the system-message setter. - Breaking
system_messageis immutable after the firstchat()call. The setter raisesRuntimeError; callreset()to unlock. - New
include=[...]stream filter onchat()andgenerate()selects phases ("thinking","tool_calling","generating","done"). - Internal Abstract methods renamed
chat → _chat,generate → _generate. Concretechat/generateon the base class apply theincludefilter and delegate. - New Memory-aware GPU placement for
HuggingFaceImageClient: on load it measures the pipeline size and each GPU's free VRAM (accounting for other processes), then pins to the freest GPU or falls back to model / sequential CPU offload so large models (SD3, FLUX) load without OOM. Override withmodel_kwargs={"device": "cuda:1"}or{"device_map": ...}. Audio/speech clients take the same{"device": ...}hint. Sharedaimu/models/_hf_device.pyhelpers back all three. - New
ImageSpec.max_prompt_tokensrecords the model's text-encoder prompt budget (77 for CLIP, 256/512 for T5 models like SD3/FLUX,Nonefor uncapped cloud models), exposed onBaseImageClient. Use it to size prompts to the model. - Changed
HuggingFaceImageClientnow defaultstorch_dtypeper device (bf16 on CUDA, fp16 on MPS, fp32 on CPU) instead of"auto", which could silently load in fp32 and double VRAM. Passmodel_kwargs={"torch_dtype": ...}to override.
Agents¶
- Breaking
Agentconstructor signature changed:Agent(model_client, system_message=None, name=None, tools=None, ...).system_messageis the second positional argument;nameis optional (auto-derived). - Breaking
AgenticModelClientremoved from the public API. Useagent.as_model_client()instead. - Breaking
OrchestratorAgent._setup_orchestratorrenamed to_init_orchestrator. - New
OrchestratorAgent.assemble(client, system_message, workers=[...])factory builds an orchestrator without subclassing. - New Workflow factories:
Chain.from_client(client, prompts),Router.from_client(client, classifier_prompt, handlers),Parallel.from_client(client, worker_prompts, aggregator_prompt=),PlanExecuteEvaluator.from_client(client, ...). - Breaking
BaseAgentandWorkflowABCs removed. All concrete agents and workflows inherit directly fromRunner. The agent-vs-workflow split survives as a conceptual category in the docs. - Breaking
AgentChunkandChainChunkcollapsed intoStreamChunk, with no back-compat aliases.chunk.agent_name → chunk.agent;chunk.step → chunk.iteration.
Tools¶
- New
@toolraisesToolSignatureErrorat decoration time on unsupported signatures (*args/**kwargs, params with no type hint and no default). - New
Optional[T]andT | Noneunwrap to the inner type in tool specs. - New Built-in tool subgroups:
builtin.web,builtin.fs,builtin.compute,builtin.misc. - New
MCPClientraisesMCPConnectionError(rather than silently failing) on construction or call failure. Added.ping()method.
Skills¶
- Breaking
SkillManagerraisesSkillLoadErroron malformedSKILL.md(instead of silently skipping). - Breaking
SkillManager.get_skill_body()raisesSkillNotFoundErroron unknown skill name (instead of returning a sentinel string). - New Skill catalogue prompt includes script-derived tool names inline.
- Breaking
Skillrenamed toAgentSkill(no back-compat alias). - New Skills logged at
INFOon discovery.
Documentation¶
- New documentation site built with MkDocs Material and hosted on GitHub Pages.
- Diátaxis structure: tutorials, how-to guides, reference, explanation.
- README slimmed to landing-page size.
Earlier versions¶
This is the first formal changelog entry. Prior versions tracked changes via git history; consult git log on GitHub for v0.3.x and earlier.