Praval 0.7.22 had the agent system I wanted. Agents had names and responsibilities, Reef carried Spores between them, and tools, memory, storage, traces, and persisted human approval supported complete workflows. The model layer was less coherent. OpenAI, Anthropic, and Cohere adapters had accumulated their own request handling, streaming, tool execution, and approval logic, so the selected provider affected too much application code.

The 0.8 work corrected that boundary. It added common model contracts, Gemini, OpenAI-compatible servers, MCP tool clients, multimodal input, request-based voice, a separate embedding runtime, stronger lifecycle handling, and a larger set of executable notebooks. Across 320 changed files, the purpose was consistent: make one Agent useful on its own, then let the same Agent participate in a larger system without changing how its model work is executed.

I uploaded 0.8.0 before its matching Git tag and GitHub release were complete, then withdrew it. PyPI does not allow a deleted distribution filename to be reused, so I continued with version 0.8.1. The mistake led to a firmer rule: a release now means one source commit, one wheel, matching documentation, and one release record.

The Praval 0.8.1 release notes provide the feature summary, installation command, and links to the documentation.

Rebuilding the model execution path

I moved shared execution into ModelRuntime and defined its public contracts in praval.models. The runtime builds and validates requests, resolves model capabilities, normalizes responses and streams, executes tools, applies approval, records usage, and creates spans. Provider adapters now translate between that contract and their native APIs.

The direct Agent interface follows this division. chat() still returns a string for existing applications. generate() and agenerate() return a ModelResponse with content, provider, finish, usage, and tool details, while stream() and astream() expose a common sequence of events. The decorated Agent API, start_agents(), Reef, and established imports remain supported, so applications can adopt the richer response and async APIs when they need them.

OpenAI, Anthropic, Cohere, Gemini, and OpenAI-compatible endpoints now sit behind this contract. A capability registry records whether a model supports tools, structured output, reasoning controls, streaming, and media. Praval rejects unsupported requests instead of silently weakening them, and local-server presets begin conservatively because an OpenAI-compatible HTTP shape says little about the model behind it. Structured output remains precise: the provider constrains generation and returns JSON in ModelResponse.content; the application parses and validates it when a local schema is authoritative.

Typed content parts carry images, files, audio, and video where the model supports them. OpenAI-backed Agents add transcribe() and speak() for bounded speech requests, while persistent realtime audio remains a separate future problem. EmbeddingRuntime also stays separate from chat because a vector collection depends on its embedding model and dimensions. It supports SentenceTransformers, OpenAI, Gemini, and compatible endpoints, and reports when a collection must be reindexed.

Keeping tools, approval, and collaboration explicit

A model can propose an action, but the application must validate and authorize it. Praval now routes decorated tools, shared tools, external JSON schemas, and provider tool calls through one Agent registry. Agent.add_tool_spec() attaches schemas that originate outside Praval without bypassing argument handling, errors, tracing, async rules, or approval.

HITL uses that provider-neutral path. Before a protected handler runs, Praval stores the tool request and interrupted runtime state in SQLite. A reviewer can approve the original arguments, edit them, or reject the action, and another process can resume the continuation. If approval is required and HITL is not configured, execution stops. This gives the model room to prepare an action without giving it the authority to perform that action.

MCP fits the same design. The optional client discovers tools from stdio or Streamable HTTP servers, namespaces them, and registers async handlers with the Agent. Those handlers inherit approval by default, timeouts, size limits, tracing, redaction, and cleanup. The first release handles text and structured tool results. Resources, prompts, hosting, managed OAuth, binary results, automatic reconnect, and synchronous session bridging remain outside its scope.

ModelRuntime operates inside an Agent; Reef connects Agents. Spores carry structured messages and correlation data, async handlers can perform network work, and wait_for_completion() replaces fixed sleeps. An in-process Reef serves one application, while RabbitMQ supports Agents in separate processes or machines. This separation keeps deterministic workflows independent of model execution and lets a research or release system assign work to specialists with different evidence and permissions.

Memory, storage, observability, and lifecycle support both single-Agent and team designs. Praval separates short-term, episodic, semantic, and long-term memory, and its async storage layer supports the filesystem, PostgreSQL, Redis, S3-compatible storage, and Qdrant. Spans now finalize before one-time storage and can go to the console, SQLite, or OTLP HTTP. PravalApp closes the Agents and Reef it creates, without pretending to be a general dependency container.

The four capstones test these boundaries as systems. Research specialists gather and challenge evidence, a support team combines policy with customer history, release reviewers run checks in a temporary workspace, and a live marketing studio uses OpenAI, screenshot input, structured assets, a protected claim, persisted approval, and campaign memory. Each case exposes the message trail and application state instead of hiding the framework behind notebook helpers.

Testing the wheel developers install

I did not want the new surface area supported by tests that passed only inside the source tree. The 0.8 cycle fixed the causes of expected failures, made unexpected passes fail, removed broad coverage omissions, added focused coverage floors, and made formatting, linting, typing, documentation, and package checks fatal. Provider contract tests now exercise the same requests, streams, tools, usage, errors, and capability rules across adapters. MCP, Reef, storage, observability, continuation, media, and shutdown paths have their own failure tests.

Every Python demo and notebook is registered with its dependencies, services, provider needs, timeout, and certification mode. The runner creates a clean environment, installs the supplied wheel, clears PYTHONPATH, verifies the package path and wheel hash, and runs outside the repository. Thirteen course notebooks explain the framework in sequence, and four capstones show complete systems rather than isolated calls.

Paid provider checks remain manual because a fake cannot prove that a service accepted media, generated a protected tool request, or returned usable audio. The live voice path sends a fixture through STT, an Agent, TTS, and a second transcription. Other checks cover real streaming, tools, structured output, media, embeddings, and HITL with credentials and model names supplied by the developer. These calls do not run on every push.

Publication now begins with the exact CI wheel. Documentation is built against that installed package with source imports disabled, release evidence stays outside dist/, and GitHub receives the same wheel uploaded to PyPI. That discipline ties the runtime work, examples, documentation, and public artifact to the same code.

Praval 0.8.1 is available on PyPI and in the matching GitHub release. The notebook course shows the framework in execution, and the reference documentation covers the public API.